Location: PHPKode > projects > tgsf > tgsf-0.9.2/tgsf_core/libraries/ecom/ACHDirectFileProcessing.php
<?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 ) );
				}
			}
		}
	}
	//------------------------------------------------------------------------
}
Return current item: tgsf