<?php defined( 'BASEPATH' ) or die( 'Restricted' );
/*
This code is copyright 2009-2010 by TMLA INC. ALL RIGHTS RESERVED.
Please view license.txt in /tgsf_core/legal/license.txt or
http://tgWebSolutions.com/opensource/tgsf/license.txt
for complete licensing information.
*/
/* File Format:
FILE HEADER
record_type 1
transmit_id Transmitter ID
pg_password Empty
creation_date YYYYMMDD
creation_time ?
file_format_code CSV
file_reference_code Blank
"1",?????,"",20091023, 44843,"CSV",""
BATCH HEADER
0 record_type 2
1 transaction_type PPD
2 merchant_id Merchant ID
3 merchant_name Merchant Name
4 batch_entry_description ACH
5 batch_reference_code blank
6 batch_number Sequentially assigned Batch#
DETAIL RECORDS EFT RESPONSE Detail Record
0 record_type E = EFT Detail Record
1 response_type See Response Type Table (F,R)
A Approved Verification Used with ATMVerify processing only
B Batch Confirm Transaction received for processing
D Declined Verification Used with ATMVerify processing only
F Funded Transaction has been funded
R Rejected Transaction rejected/declined
U Uncollectible Transaction has rejected as uncollectible
Z Z Reject Previously funded transaction has rejected
2 response_code See Response Code Table
A01 Approved This transaction has been approved for processing
S01 Funded-1st attempt This transaction has funded on the 1st attempt
S02 Funded-2nd attempt This transaction has funded on the 2nd attempt
S03 Funded-3rd attempt This transaction has funded on the 3rd attempt
X02 Voided This transaction has been voided
R01 Insufficient Funds Balance is not sufficient to cover value of transaction
R02 Account Closed Previously open account has been closed
R03 No Account Account is closed or doesn't match name submitted
R04 Invalid Account Number Account number structure is invalid
R05 Prenote Not Received Prenotification was not received
R06 Returned Per ODFI ODFI has requested RDFI to return this item
R07 Authorization Revoked Account holder has revoked company's authorization
R08 Payment Stopped Account holder has stopped payment on this single transaction
R09 Uncollected Funds Balance is sufficient but can't be released until uncollected items are collected
R10 No Authorization Account holder advises that transaction is not authorized
R11 Check Safekeeping Return Return of a check safekeeping entry return
R12 Branch Sold Account now at a branch sold to another financial institution
R13 RDFI Not Qualified RDFI not qualified to participate
R14 Deceased The account holder is deceased
R15 Beneficiary Deceased Beneficiary entitled to benefits is deceased
R16 Account Frozen Funds unavailable due to action by RDFI or other legal action
R20 Non Transaction Account Policies/regulations restrict activity to this account
R23 Payment Refused Account holder refuses transaction because amount is inaccurate or other legal
R24 Duplicate Entry Transaction appears to be a duplicate item
R26 Mandatory Error Transaction is missing data from a mandatory field
R28 Invalid TRN Transit Routing Number is Invalid
R29 Corporate Not Authorized Corporate receiver has notified RDFI that Corp entry is not authorized
R31 ODFI Permits Late Return ODFI agrees to accept a return
R50 Invalid Company ID The OwnerCompany ID field is not valid
R56 Invalid Transaction Date Date specified is invalid
R57 Stale Date Transaction is too old for processing
R95 Over Limit This transaction is over your authorized limit
R96 Account on Hold This company account is on hold
R97 RDFI Does not Participate RDFI does not allow this type of transaction
R98 Invalid Password The password supplied was invalid
R99 Declined Unpaid Items This account or ID has been declined due to unpaid items
3 trace_code Unique transaction code
4 authorization_code Authorization Code
5 response_date Response or "Effective" date
6 debit_credit 'D'ebit or 'C'redit
7 checking_savings 'C'hecking or 'S'avings Account
8 customer_name Name of the Account Holder
9 transit_routing_number Bank's TRN# or ABA#
10 account_number Account Number
11 total_amount Total amount of the transaction
12 addenda_indicator Addenda records present for this detail item
13 item_description Description of this transaction (very specific, such as this monthÂ’s invoice#, etc.)
14 external_customer_id Merchant assignable field to identify customer
15 external_transaction_id Merchant assignable field to identify this transaction
16 external_transaction_id2 2nd Merchant assignable data field
17 additional_info Additional response information about transaction
18 customer_address
19 customer_address2
20 customer_city
21 customer_stateprov
22 customer_postalcode
23 customer_country_code
24 customer_phone_number
25 customer_email_address
26 transaction_indicator (S)ingle Tran, (F)irst Tran in Set, (R)ecurring Scheduled Item
27 transaction_source (L)ive or Realtim Connection, (B)atch Connection
ADDENDA RECORD
record_type A - Addenda Record
addenda_type_code 3 = EFT Addend
payment_info Addenda data
addenda_sequence Each addenda record is sequentially numbered
BATCH FOOTER
0 record_type 8
1 transaction_type PPD
2 merchant_id Merchant ID
3 batch_entry_count # of Detail Records
4 batch_debit_amount Value of Debit Items in this batch
5 batch_credit_amount Value of Credit Items in this batch
6 batch_debit_count Number of Debit Items in this batch
7 batch_credit_count Number of Credit Items in this batch
8 batch_reference_code Blank
9 batch_number Same batch number assigned in batch header
FOOTER
0 record_type 9
1 transmit_id Transmitter ID
2 batch_count Total number of batches in this file
3 file_debit_amount Total value of debit entries in this file
4 file_credit_amount Total value of credit entries in this file
5 file_debit_count Total number of Debit Items in this file
6 file_credit_count Total number of Credit Items in this file
7 file_reference_code Blank
"9",14872,1,2,0,2,0,""
FIELD NAMES AND MAPPING
Real-Time Name FileSpec 3.10 Name
pg_consumer_id external_customer_id
ecom_consumerorderid external_transaction_id
ecom_walletid external_transaction_id2
ecom_payment_check_checkno item_description
pg_merchant_data_1 Addenda Record #1
pg_merchant_data_2 Addenda Record #2
pg_merchant_data_9 Addenda Record #9
*/
function translateResponseCode( $code )
{
$codes = array(
'A01' => 'Approved This transaction has been approved for processing',
'S01' => 'Funded-1st attempt This transaction has funded on the 1st attempt',
'S02' => 'Funded-2nd attempt This transaction has funded on the 2nd attempt',
'S03' => 'Funded-3rd attempt This transaction has funded on the 3rd attempt',
'X02' => 'Voided This transaction has been voided',
'R01' => 'Insufficient Funds Balance is not sufficient to cover value of transaction',
'R02' => 'Account Closed Previously open account has been closed',
'R03' => 'No Account Account is closed or does not match name submitted',
'R04' => 'Invalid Account Number Account number structure is invalid',
'R05' => 'Prenote Not Received Prenotification was not received',
'R06' => 'Returned Per ODFI ODFI has requested RDFI to return this item',
'R07' => 'Authorization Revoked Account holder has revoked company-s authorization',
'R08' => 'Payment Stopped Account holder has stopped payment on this single transaction',
'R09' => 'Uncollected Funds Balance is sufficient but cannott be released until uncollected items are collecte',
'R10' => 'No Authorization Account holder advises that transaction is not authorized',
'R11' => 'Check Safekeeping Return Return of a check safekeeping entry return',
'R12' => 'Branch Sold Account now at a branch sold to another financial institution',
'R13' => 'RDFI Not Qualified RDFI not qualified to participate',
'R14' => 'Deceased The account holder is deceased',
'R15' => 'Beneficiary Deceased Beneficiary entitled to benefits is deceased',
'R16' => 'Account Frozen Funds unavailable due to action by RDFI or other legal action',
'R20' => 'Non Transaction Account Policies/regulations restrict activity to this account',
'R23' => 'Payment Refused Account holder refuses transaction because amount is inaccurate or other legal',
'R24' => 'Duplicate Entry Transaction appears to be a duplicate item',
'R26' => 'Mandatory Error Transaction is missing data from a mandatory field',
'R28' => 'Invalid TRN Transit Routing Number is Invalid',
'R29' => 'Corporate Not Authorized Corporate receiver has notified RDFI that Corp entry is not authorized',
'R31' => 'ODFI Permits Late Return ODFI agrees to accept a return',
'R50' => 'Invalid Company ID The OwnerCompany ID field is not valid',
'R56' => 'Invalid Transaction Date Date specified is invalid',
'R57' => 'Stale Date Transaction is too old for processing',
'R95' => 'Over Limit This transaction is over your authorized limit',
'R96' => 'Account on Hold This company account is on hold',
'R97' => 'RDFI Does not Participate RDFI does not allow this type of transaction',
'R98' => 'Invalid Password The password supplied was invalid',
'R99' => 'Declined Unpaid Items This account or ID has been declined due to unpaid items'
);
if ( array_key_exists( $code, $codes ) )
{
return $code . ' - ' . $codes[$code];
}
return $code . ' - Unknown Code';
}
class ACHDirectFileProcessing extends tgsfBase
{
public $demoDate = null;
protected $_txnModel;
protected $_external_accountModel;
protected $_fileName;
protected $_fileHeader;
protected $_fileFooter;
protected $_batches;
protected $_lines;
//--------------------------------------------------------------------------
public function __construct()
{
$this->_txnModel = load_model('txn');
$this->_external_accountModel = load_model( 'external_account' );
}
function checkData( $batch )
{
if ( count($this->_lines) < 4 )
{
throw new Exception( "Invalid Settlement File: Not enough lines - had " . count( $this->_lines ) );
}
if ( $this->_fileHeader[1] != trim( config( 'ftp_user' ), 'u' ) )
{
throw new Exception( "Invalid Settlement File: Invalid Merchant Login" );
}
if ( $this->_fileHeader[5] != 'CSV' )
{
throw new Exception( "Invalid Settlement File: Invalid File Format" );
}
if ( $batch['header'][1] != 'PPD' )
{
echo "Skipped Batch Type: Invalid Transaction Type: " . $batch['header'][1] . PHP_EOL;
return false;
}
if ( $batch['header'][2] != config( 'pg_merchant_id' ) && $batch['header'][2] != config( 'pg_merchant_id_micro_deposit' ) )
{
throw new Exception( "Invalid Settlement File: Invalid Merchant ID" );
}
// 2009-12-17 - TG - Commented out because we don't need it and it was causing errors
/*
if ( $batch['header'][4] != 'ACH' )
{
echo "Invalid Batch: Invalid Batch Entry Description: " . $batch['header'][4] . PHP_EOL;
return false;
}*/
if ( count( $batch['settlements'] ) != intval( $batch['footer'][3] ) )
{
var_dump( $batch );
throw new Exception( "Invalid Settlement File: Batch Line count (" . count( $batch['settlements'] ) . ") does not match batch footer count of settlement records (" . intval( $batch['footer'][3] ) . ')' );
}
$debit = 0;
$debits = array();
$credit = 0;
$credits = array();
foreach( $batch['settlements'] as $s )
{
$data = $s['data'];
if ( strtoupper( $data[6] ) == 'D' )
{
$debit += $data[11];
$debits[] = $s;
}
else
{
$credit += $data[11];
$credits[] = $s;
}
}
if ( (string)$credit != (string)$batch['footer'][5] )
{
throw new Exception( "5 - Invalid Settlement File: Batch Credit Amount (" . floatval( $batch['footer'][5] ) . ") does not match settlement records (" . $credit . ")" );
}
if ( $debit != floatval( $batch['footer'][4] ) )
{
throw new Exception( "4 - Invalid Settlement File: Batch Debit Amount (" . floatval($batch['footer'][4]) . ") does not match settlement records (" . $debit . ")" );
}
if ( count( $credits ) != intval( $batch['footer'][7] ) )
{
throw new Exception( "7 - Invalid Settlement File: Batch Credit Count (" . count( $credits ) . ") does not match batch footer settlement records (" . intval( $batch['footer'][7] ) . ')' );
}
if ( count( $debits ) != intval( $batch['footer'][6] ) )
{
throw new Exception( "6 - Invalid Settlement File: Batch Debit count (" . count( $debits ) . ") does not match batch footer settlement records (" . intval( $batch['footer'][6] ) . ')' );
}
return true;
}
//------------------------------------------------------------------------
function processGoodTransaction( $data, $ds )
{
if ( $ds->txn_status != txnsPROCESSING )
{
echo ' Transaction status is not processing: ' . $ds->txn_id . PHP_EOL;
}
else
{
$this->_txnModel->settle( $ds, $data[2] );
}
}
//------------------------------------------------------------------------
function processBadTransaction( $data, $ds )
{
if ( $ds->_('txn_status') != txnsPROCESSING )
{
echo ' Transaction status is not processing: ' . $ds->txn_id . PHP_EOL;
}
else
{
echo ' Response: ' . translateResponseCode($data[2] ) . PHP_EOL;
$this->_txnModel->reject( $ds, $data[2] );
}
}
//------------------------------------------------------------------------
function processChargebackTransaction( $data, $ds )
{
if ( $ds->_('txn_status') != txnsCOMPLETED )
{
echo ' Transaction status is not completed: ' . $external_transaction_id . PHP_EOL;
}
else
{
echo ' Response: ' . translateResponseCode($data[2] ) . PHP_EOL;
$this->_txnModel->chargeback($ds, $data[2]);
}
}
//------------------------------------------------------------------------
/**
*
*/
public function processMicroDeposit( $external_account_id, $responseType )
{
$externalAccount = $this->_external_accountModel->fetchByIdOnly( $external_account_id );
if ( $externalAccount == false )
{
$message = 'Unable to load an external account record when processing a microdeposit for external account ID: ' . $external_account_id;
echo $message;
LOGGER()->log( $message );
return;
}
switch( $responseType )
{
case 'F':
// the micro deposit was funded. Update external account record and send email
$this->_external_accountModel->updateMicroStatus( $externalAccount, eamsPROCESSING, eamsCOMPLETED );
if ( $externalAccount->external_account_disabled != true )
{
mmBankAccountMicroDepositCompletedEmail( $externalAccount );
}
break;
case 'R':
case 'U':
case 'Z':
// the micro deposit did not go through, update external account record and send email
$this->_external_accountModel->updateMicroStatus( $externalAccount, eamsPROCESSING, eamsREJECTED );
if ( $externalAccount->external_account_disabled != true )
{
mmBankAccountMicroDepositRejectedEmail( $externalAccount );
}
break;
default:
throw new tgsfException( 'Unexpected response type for micro deposits: ' . $responseType . ' - For external_account_id: ' . $external_account_id );
break;
}
}
//------------------------------------------------------------------------
function processSettlements($batch)
{
$ix = 1;
foreach( $batch['settlements'] as $settlement )
{
$data = $settlement['data'];
$adds = $settlement['adds'];
/*
F Funded Transaction has been funded
R Rejected Transaction rejected/declined
U Uncollectible Transaction has rejected as uncollectible
Z Z Reject Previously funded transaction has rejected
*/
//echo 'Process Line ' . $ix . ': Status [' . $data[1] . '] Txn [' . $data[15] . ']: $ [' . $data[11] . ']' . PHP_EOL;
$external_transaction_id = trim($data[15]);
$responseType = trim($data[1]);
if ( strlen($external_transaction_id) == 0 && count($adds) >= 1 )
{
$external_transaction_id = trim($adds[0][2]);
}
if ( strlen($external_transaction_id) == 0 )
{
LOGGER()->log( "Settlement File Transaction Line has no txn_id\n" .
get_dump( $data )
, 'ACHDirectFileProcessing::processSettlements()' );
continue;
}
else if ( starts_with( $external_transaction_id, 'MICRO-' ) )
{
$this->processMicroDeposit( substr( $external_transaction_id, 6 ), $responseType );
}
else
{
if ( starts_with( $external_transaction_id, 'TXN-' ) )
{
$external_transaction_id = substr($external_transaction_id, 4 );
}
$total_amount = floatval($data[11]);
$ds = $this->_txnModel->fetchById( $external_transaction_id );
if ( $ds === false )
{
echo ' Original Transaction not found: ' . $external_transaction_id . PHP_EOL;
}
else
{
if ( $this->demoDate !== null )
{
$ds->setVar( 'txn_datetime', $this->demoDate );
}
else
{
$ds->unsetVar( 'txn_datetime' );
}
switch( $responseType )
{
case 'F':
$this->processGoodTransaction( $data, $ds );
break;
case 'R':
case 'U':
$this->processBadTransaction( $data, $ds );
break;
case 'Z':
$this->processChargebackTransaction( $data, $ds );
break;
default:
LOGGER()->log( 'Unhandled Record in settlement File' , 'processing', $settlement );
break;
}
}
}
$ix++;
}
}
//------------------------------------------------------------------------
/**
* process File into ACHSettlement objects
*/
function processFile( $fileName )
{
$this->_fileName = $fileName;
$this->_lines = array();
$this->_batches = array();
$batch = null;
$last_settlement = null;
$fp = fopen($this->_fileName, 'r');
while ( $line = fgetcsv( $fp, 3000 ) )
{
$this->_lines[] = $line;
switch( $line[0] )
{
case '1':
$this->_fileHeader = $line;
break;
case '2':
$batch = array();
$batch['ix'] = 0;
$batch['header'] = $line;
break;
case '8':
if ( $batch == null )
{
throw exception( "Invalid File Format: Batch footer found before batch header" );
}
$batch['footer'] = $line;
$this->_batches[] = $batch;
break;
case '9':
$this->_fileFooter = $line;
break;
case 'E':
if ( $batch == null )
{
throw exception( "Invalid File Format: Settlement data found before batch header" );
}
$last_settlement = array();
$last_settlement['adds'] = array();
$last_settlement['data'] = $line;
$ix = $batch['ix'];
$batch['settlements'][$ix] = $last_settlement;
$batch['ix'] = $batch['ix'] + 1;
break;
case 'A':
$ix = $batch['ix'] - 1;
$batch['settlements'][$ix]['adds'][] = $line;
break;
case 'C':
// Comment
break;
default:
throw new tgsfException( "Invalid Settlement File: Invalid Transaction Line: Not Supported: " . $line[0] );
break;
}
}
foreach( $this->_batches as $batch )
{
if ( $this->checkData($batch) )
{
$this->processSettlements($batch);
}
else
{
// Batch did not check out - this is OK because some batches
// might be fore Virtual Terminal entered Data.
// If there is a serious issue an exception will be thrown and the whole file will be invalid.
//
}
}
}
}
//------------------------------------------------------------------------
class ACHDirectFiles extends tgsfBase
{
public $demoDate = null;
protected $_ticks;
//------------------------------------------------------------------------
function __construct()
{
$this->_ticks = 0;
}
//------------------------------------------------------------------------
function IsDirEmpty( $dir )
{
$d = new DirectoryIterator( $dir );
foreach( $d as $fileInfo )
{
if ( $fileInfo->isDot())
{
}
else if ( $fileInfo->isDir() )
{
return false;
}
else
{
return false;
}
}
return true;
}
//------------------------------------------------------------------------
function processAll( $dir )
{
$ticks = 0;
$d = new DirectoryIterator( $dir );
foreach( $d as $fileInfo )
{
if ( $fileInfo->isDot())
{
}
else if ( $fileInfo->isDir() )
{
$this->processAll( $fileInfo->getPathname() );
$baseDirName = $fileInfo->getPathname();
if ( $this->isDirEmpty($baseDirName) )
{
//echo 'remove directory: ' . $baseDirName;
rmdir( $baseDirName );
}
}
else
{
$tpsFile = $dir . '/' . $fileInfo->getFilename();
$ach = new ACHDirectFileProcessing();
$ach->demoDate = $this->demoDate;
$ach->processFile( $tpsFile );
$this->_ticks++;
if ( config( 'rename_tps_files' ) === true )
{
rename( $tpsFile, config( 'tps_archive' ) . date('tps-Y-M-D-H-i-s-') . $this->_ticks . '-' . basename( $tpsFile ) );
}
}
}
}
//------------------------------------------------------------------------
}