Katie Horn has submitted this change and it was merged.

Change subject: WorldPay auditing ported to SmashPig
......................................................................


WorldPay auditing ported to SmashPig

All the parts that work are written by khorn.

Change-Id: Ice48735a9c899baf012c55ad1c90deb57b5d5bea
---
A PaymentProviders/WorldPay/Audit/ReferenceData.php
A PaymentProviders/WorldPay/Audit/TransactionReconciliationFile.php
A PaymentProviders/WorldPay/Audit/WorldPayAudit.php
3 files changed, 514 insertions(+), 0 deletions(-)

Approvals:
  Katie Horn: Looks good to me, approved
  Awight: Looks good to me, but someone else must approve
  jenkins-bot: Verified



diff --git a/PaymentProviders/WorldPay/Audit/ReferenceData.php 
b/PaymentProviders/WorldPay/Audit/ReferenceData.php
new file mode 100644
index 0000000..ac83870
--- /dev/null
+++ b/PaymentProviders/WorldPay/Audit/ReferenceData.php
@@ -0,0 +1,109 @@
+<?php
+namespace Smashpig\PaymentProviders\WorldPay\Audit;
+
+class ReferenceData {
+       static function decode_card_type( $code ) {
+               $mapping = array(
+                       'visa' => array(
+                               'BC000',
+                               'XV000',
+                               'BCVIY',
+                               'DE000',
+                               'XD000',
+                               'XVVIY',
+                               'PE000',
+                               'XE000',
+                               'VP001',
+                               'VPVIB',
+                               'VPVID',
+                               'VPVIR',
+                               'VPVIS',
+                               'VPVIX',
+                               'XP001',
+                               'XPVIB',
+                               'XPVID',
+                               'XPVIR',
+                               'XPVIS',
+                               'XPVIX',
+                       ),
+
+                       'mc' => array(
+                               'AC000',
+                               'ACMCW',
+                               'ACMNW',
+                               'XA000',
+                               'XAMCW',
+                               'XAMNW',
+                               'ACMCY',
+                               'DM000',
+                               'XAMCY',
+                               'XN000',
+                               'VP002',
+                               'VPMCB',
+                               'VPMCO',
+                               'VPMCP',
+                               'VPMCF',
+                               'VPMCX',
+                               'XP002',
+                               'XPMCB',
+                               'XPMCO',
+                               'XPMCP',
+                               'XPMCF',
+                               'XPMCX',
+                       ),
+
+                       'maestro' => array(
+                               'PM000',
+                               'XM000',
+                               'PMDOM',
+                               'XSDOM',
+                       ),
+
+                       'jcb' => array(
+                               'KF000',
+                       ),
+
+                       'laser' => array(
+                               'PT000',
+                       ),
+
+                       'other' => array(
+                               'AS000',
+                               'AX000',
+                               'BP000',
+                               'CM000',
+                               'CO000',
+                               'CY000',
+                               'DC000',
+                               'DL000',
+                               'EF000',
+                               'FS000',
+                               'GE000',
+                               'JC000',
+                               'LC000',
+                               'LE000',
+                               'LY000',
+                               'OD000',
+                               'PL000',
+                               'SB000',
+                               'SC000',
+                               'SE000',
+                               'SG000',
+                               'SH000',
+                               'SP000',
+                               'SY000',
+                               'TE000',
+                               'VC000',
+                               'VE000',
+                       ),
+               );
+
+               foreach ( $mapping as $type_name => $codes ) {
+                       if ( in_array( $code, $codes ) ) {
+                               return $type_name;
+                       }
+               }
+
+               throw new \Exception( "Unknown card type code: {$code}" );
+       }
+}
diff --git a/PaymentProviders/WorldPay/Audit/TransactionReconciliationFile.php 
b/PaymentProviders/WorldPay/Audit/TransactionReconciliationFile.php
new file mode 100644
index 0000000..afe9e90
--- /dev/null
+++ b/PaymentProviders/WorldPay/Audit/TransactionReconciliationFile.php
@@ -0,0 +1,378 @@
+<?php
+
+namespace SmashPig\PaymentProviders\WorldPay\Audit;
+
+require_once "ReferenceData.php";
+
+/**
+ * See EMIS User Guide V9 January 2014.pdf
+ */
+class TransactionReconciliationFile {
+
+       const filenamePattern = '/^MA\.PISCESSW\.#M\.RECON\..*/';
+
+       protected $recordTypes;
+       protected $fileData = array( );
+
+       function __construct() {
+               $this->row_header_segment = array(
+                       array( "record_type", 2 ),
+                       array( "sequence_no", 8 ),
+               );
+
+               $credit_debit_summary_segment = array(
+                       array( "accepted_debits", 11 ),
+                       array( "rejected_debits", 11 ),
+                       array( "pending_debits", 11 ),
+                       array( "accepted_credits", 11 ),
+                       array( "rejected_credits", 11 ),
+                       array( "pending_credits", 11 ),
+                       array( "accepted_debits_count", 7 ),
+                       array( "rejected_debits_count", 7 ),
+                       array( "pending_debits_count", 7 ),
+                       array( "accepted_credits_count", 7 ),
+                       array( "rejected_credits_count", 7 ),
+                       array( "pending_credits_count", 7 ),
+               );
+
+               $file_header = new FixedLengthRecordSpecification( "00", array(
+                       array(
+                               array( "record_type", 2 ),
+                               array( "sequence_no", 8 ),
+                               array( "file_id", 13 ),
+                               array( "count", 7 ),
+                       ),
+                       $credit_debit_summary_segment,
+                       array(
+                               array( "file_creation_date", 6 ),
+                               array( "file_sequence_number", 3 ),
+                               array( "site_id", 3 ),
+                       )
+                       ) );
+
+               $reconciliation_merchant_company_header = new 
FixedLengthRecordSpecification( "05", array(
+                       array(
+                               array( "record_type", 2 ),
+                               array( "sequence_no", 8 ),
+                               array( "company_number", 13 ),
+                               array( "count", 7 ),
+                       ),
+                       $credit_debit_summary_segment
+                       ) );
+
+               $reconciliation_merchant_outlet = new 
FixedLengthRecordSpecification( "10", array(
+                       array(
+                               array( "record_type", 2 ),
+                               array( "sequence_no", 8 ),
+                               array( "merchant_id", 13 ),
+                               array( "trading_day", 6 ),
+                               array( "processing_date", 6 ),
+                       ),
+                       $credit_debit_summary_segment
+                       ) );
+
+               $this->reconciliation_transaction_data = new 
FixedLengthRecordSpecification( "15", array( array(
+                               array( "record_type", 2 ),
+                               array( "sequence_no", 8 ),
+                               array( "pan", 19 ),
+                               array( "expiry_date", 4 ),
+                               array( "transaction_value", 11 ),
+                               array( "transaction_date", 6 ),
+                               array( "transaction_time", 6 ),
+                               array( "transaction_type", 1 ),
+                               array( "transaction_source", 1 ),
+                               array( "receipt_number", 6 ),
+                               array( "status", 1 ),
+                               array( "reserved1", 2 ),
+                               array( "local_value", 9 ),
+                               array( "local_currency_code", 3 ),
+                               array( "local_exponent", 1 ),
+                               array( "settlement_value", 9 ),
+                               array( "settlement_currency_code", 3 ),
+                               array( "settlement_exponent", 1 ),
+                               array( "acquired_processed_indicator", 1 ),
+                               array( "card_type", 5 ),
+                       ) ) );
+
+               $this->reconciliation_transaction_supplementary_data = new 
FixedLengthRecordSpecification( "16", array( array(
+                               array( "record_type", 2 ),
+                               array( "sequence_no", 8 ),
+                               array( "auth_code", 6 ),
+                               array( "auth_method", 1 ),
+                               array( "card_issue_number", 2 ),
+                               array( "card_start_date", 4 ),
+                               array( "cash_amount", 7 ),
+                               array( "originators_transaction_reference", 20 
),
+                               array( "ticket_number", 14 ),
+                       ) ) );
+
+               $this->recordTypes = array(
+                       $file_header,
+                       $reconciliation_merchant_company_header,
+                       $reconciliation_merchant_outlet,
+                       $this->reconciliation_transaction_data,
+                       $this->reconciliation_transaction_supplementary_data,
+               );
+       }
+
+       static function isMine( $path ) {
+               $filename = basename( $path );
+               return preg_match( self::filenamePattern, $filename );
+       }
+
+       function parse( $path ) {
+               $this->pending_data = null;
+               $this->pending_supplemental_data = null;
+
+               $this->path = $path;
+               $this->file = fopen( $path, 'r' );
+
+               while ( $line = fgets( $this->file ) ) {
+                       $this->parse_line( $line );
+               }
+               fclose( $this->file );
+
+               $this->flush_data();
+               return $this->fileData;
+       }
+
+       /**
+        * Parse one line.
+        *
+        * There is a crazy thing happening here where we need to coordinate
+        * sequential lines, and merge supplemental records into the main 
transaction
+        * data.  See add_transaction_data and add_supplementary_data.
+        */
+       protected function parse_line( $line ) {
+               # Peek at row header to determine its type
+               $row_info = FixedLengthRecordParser::parse( 
$this->row_header_segment, substr( $line, 0, 10 ) );
+
+               # Find the corresponding line format and parse the contents
+               $record = null;
+               foreach ( $this->recordTypes as $type ) {
+                       if ( $type->getPrefix() === $row_info['record_type'] ) {
+                               $record = FixedLengthRecordParser::parse( 
$type->fields, $line );
+                       }
+               }
+
+               if ( !$record ) {
+                       // FIXME: correct Exception type, and get context 
(path) from log context
+                       // SUPERFIXME: This introduces an extreme degree of 
fragility into the system. One weird line can't blow up the whole thing; We'd 
get killed by the schrapnel on the daily.
+//                     throw new \Exception( "Unknown record type 
{$row_info['record_type']} while processing {$this->path}, aborting!" );
+                       //for now...
+                       error_log( "Unknown record type 
{$row_info['record_type']} while processing {$this->path}. Rolling over it." );
+               }
+
+               # Dispatch to a handler
+               $record_type = false;
+               if ( array_key_exists( 'record_type', $record ) ) {
+                       $record_type = $record['record_type'];
+               }
+               if ( $record_type === 
$this->reconciliation_transaction_data->getPrefix() ) {
+                       $this->add_transaction_data( $record );
+               } elseif ( $record_type === 
$this->reconciliation_transaction_supplementary_data->getPrefix() ) {
+                       $this->add_supplementary_data( $record );
+               } else {
+                       # ignore other crap.
+                       # TODO: assertions for checksummy things built into the 
file
+               }
+       }
+
+       protected function add_transaction_data( $record ) {
+               $this->flush_data();
+               $this->pending_data = $record;
+       }
+
+       protected function add_supplementary_data( $record ) {
+               if ( !$this->pending_data ) {
+                       // TODO: get line number from the context
+                       throw new \Exception( "Cannot eat supplementary 
transaction data because there no unconsumed base data. Line 
{$record["sequence_no"]}" );
+               }
+
+               if ( $this->pending_supplemental_data ) {
+                       throw new \Exception( "Cannot eat supplementary data 
because there is already unconsumed supplemental data. Line 
{$record["sequence_no"]}" );
+               }
+
+               $this->pending_supplemental_data = $record;
+               $this->flush_data();
+       }
+
+       protected function flush_data() {
+               if ( $this->pending_data ) {
+                       $this->send_transaction();
+               }
+       }
+
+       protected function send_transaction() {
+               $record = $this->pending_data;
+
+               # Verify that the data and supplemental data are a pair
+               if ( $this->pending_supplemental_data ) {
+                       if ( ( int ) 
($this->pending_supplemental_data['sequence_no']) !== ( 
$this->pending_data['sequence_no'] + 1 ) ) {
+                               throw new \Exception( "Mismatched data and 
supplemental data! " . print_r( $this->pending_supplemental_data, true ) . " " 
. print_r( $this->pending_data, true ) );
+                       }
+                       $record = array_merge( $record, 
$this->pending_supplemental_data );
+               }
+
+               $msg = $this->normalize( $record );
+
+               $this->pending_data = null;
+               $this->pending_supplemental_data = null;
+
+               $this->fileData[] = $msg;
+       }
+
+       /**
+        * Normalize the pieces of the message that exist, according to the
+        * definition of a standard WMF queue message.
+        *
+        * Defaults should always be left up to the relevant queue consumer.
+        *
+        * See https://wikitech.wikimedia.org/wiki/Fundraising/Queue_messages
+        */
+       protected function normalize( $record ) {
+               $msg = array( );
+
+               if ( $record["transaction_type"] == "0" ) {
+                       $queue = "donations";
+               } elseif ( $record["transaction_type"] == "5" ) {
+                       $queue = "refund";
+               } else {
+                       //FIXME: This also seems incredibly fragile.
+                       throw new \Exception( "Don't know how to handle 
transaction type {$record["transaction_type"]}" );
+               }
+
+               //ARGH.
+               //transaction_date looks like ddmmyy.
+               //@TODO: Verify that the transaction_time is in fact UTC, and 
not some other weirdo country time. 
+               $datestring = '20' . substr( $record["transaction_date"], 4, 2 
) . substr( $record["transaction_date"], 2, 2 ) . substr( 
$record["transaction_date"], 0, 2 );
+               $datestring .= ' ' . $record["transaction_time"] . '-0000';
+
+               $msg["date"] = strtotime( $datestring );
+
+               $msg["gateway"] = "worldpay";
+
+               if ( array_key_exists( "originators_transaction_reference", 
$record ) ) {
+                       $msg["gateway_txn_id"] = trim( 
$record["originators_transaction_reference"] );
+               } else {
+                       throw new \Exception( "We're gonna die: no 
gateway_txn_id available." );
+               }
+
+               # The default currency is GBP, don't make me explain why the 
amount
+               # comes from a different field when currency != GBP :(
+               if ( trim( $record["local_currency_code"] ) ) {
+                       $msg["currency"] = $record["local_currency_code"];
+                       $msg["gross"] = ( int ) ( $record["local_value"] ) * 
self::exponent_to_multiplier( $record["local_exponent"] );
+               } else {
+                       $msg["currency"] = "GBP";
+                       $msg["gross"] = ( int ) ( $record["transaction_value"] 
) * self::exponent_to_multiplier( 2 );
+               }
+
+               if ( $queue === "refund" ) {
+                       $msg["gross_currency"] = $msg["currency"];
+                       $msg["gateway_parent_id"] = $msg["gateway_txn_id"];
+                       # Note that we do not have a new txn id for the refund
+                       $msg["gateway_refund_id"] = $msg["gateway_txn_id"];
+                       # FIXME: chargeback vs refund info is not available in 
this file.
+                       $msg["type"] = "refund";
+                       
#log.info("+Sending\t{id}\t{date}\t{type}".format(id=msg["gateway_parent_id"], 
date=iso_date, type=msg["type"]))
+                       #self.send(queue, msg)
+                       return;
+               }
+
+               /*
+                 if 
self.crm.transaction_exists(gateway_txn_id=msg["gateway_txn_id"], 
gateway="worldpay"):
+                 
log.info("-Duplicate\t{id}\t{date}\t{type}".format(id=msg["gateway_txn_id"], 
date=iso_date, type=queue))
+                 return
+                */
+
+               # Switch behavior depending on the status.  We only like 
"accepted" transactions.
+               $status = trim( $record["status"] );
+               if ( $status == "P" ) {
+                       
#log.info("-Pending\t{id}\t{date}\t{type}".format(id=msg["gateway_txn_id"], 
date=iso_date, type=queue))
+                       return;
+               } elseif ( $status == "R" ) {
+                       
#log.info("-Rejection\t{id}\t{date}\t{type}".format(id=msg["gateway_txn_id"], 
date=iso_date, type=queue))
+                       return;
+               } elseif ( $status != "A" ) {
+                       throw new \Exception( "Unknown gateway status: 
{$status}" );
+               }
+
+               # Include settlement details if they are available.
+               if ( trim( $record["settlement_value"] ) ) {
+                       if ( trim( $record["settlement_currency_code"] ) ) {
+                               $msg["settlement_currency"] = 
$record["settlement_currency_code"];
+                       } else {
+                               $msg["settlement_currency"] = "GBP";
+                       }
+                       $msg["settlement_amount"] = ( int ) ( 
$record["settlement_value"] ) * self::exponent_to_multiplier( 
$record["settlement_exponent"] );
+               }
+
+               $msg["payment_method"] = "cc"; //this one is okay, because WP 
only does cc at this point. Maybe forever?
+               $msg["payment_submethod"] = ReferenceData::decode_card_type( 
trim( $record["card_type"] ) );
+
+               # custom values
+               $msg["raw_card_type"] = trim( $record["card_type"] );
+
+               return $msg;
+       }
+
+       /**
+        * Convert an exponent to a multiplier
+        *
+        * The exponent defines how many digits are "minor units" of the 
currency, so USD
+        * for example has an exponent of 2.  Our queue consumer assumes that 
amounts are
+        * always given in major units, so we produce a multiplier here which 
will
+        * convert from the WorldPay amount, formatted in minor units, to the 
expected amount.
+        *
+        * For example, a currency with 3 digits of minor units should be 
multipled by 0.001
+        * before sending over the queue.
+        */
+       static function exponent_to_multiplier( $exponent ) {
+               if ( !trim( $exponent ) ) {
+                       # The default is GBP, which have an exponent of 2.
+                       $exponent = 2;
+               }
+
+               return pow( 10, 0 - ( int ) ( $exponent ) );
+       }
+
+}
+
+class FixedLengthRecordSpecification {
+
+       public $prefix;
+       public $fields = array( );
+
+       function __construct( $prefix, $fieldSegments ) {
+               $this->prefix = $prefix;
+
+               foreach ( $fieldSegments as $segment ) {
+                       $this->fields = array_merge( $this->fields, $segment );
+               }
+       }
+
+       public function getPrefix() {
+               return $this->prefix;
+       }
+
+}
+
+/**
+ * FIXME: not compatible with Wr1 field specs, yet
+ */
+class FixedLengthRecordParser {
+
+       static function parse( $fields, $line ) {
+               $pos = 0;
+               $out = array( );
+               foreach ( $fields as $field ) {
+                       list( $fieldName, $length ) = $field;
+                       $out[$fieldName] = substr( $line, $pos, $length );
+                       $pos += $length;
+               }
+               $pos = 0;
+               return $out;
+       }
+
+}
diff --git a/PaymentProviders/WorldPay/Audit/WorldPayAudit.php 
b/PaymentProviders/WorldPay/Audit/WorldPayAudit.php
new file mode 100644
index 0000000..d0efaca
--- /dev/null
+++ b/PaymentProviders/WorldPay/Audit/WorldPayAudit.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace SmashPig\PaymentProviders\WorldPay\Audit;
+
+class WorldPayAudit {
+
+       function retrieveFiles() {
+               AuditRetriever::retrieveAll( 'worldpay' );
+       }
+
+       function parseFile( $file ) {
+               // FIXME: this should be specified in configuration
+               $fileTypes = array(
+                       
'SmashPig\PaymentProviders\WorldPay\Audit\TransactionReconciliationFile',
+               );
+
+               foreach ( $fileTypes as $type ) {
+                       if ( $type::isMine( $file ) ) {
+                               $parser = new $type();
+                               $data = $parser->parse( $file );
+                       }
+               }
+
+               return $data;
+       }
+
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/157303
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ice48735a9c899baf012c55ad1c90deb57b5d5bea
Gerrit-PatchSet: 2
Gerrit-Project: wikimedia/fundraising/SmashPig
Gerrit-Branch: master
Gerrit-Owner: Awight <[email protected]>
Gerrit-Reviewer: Awight <[email protected]>
Gerrit-Reviewer: Katie Horn <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to