revenuewire/ach-file

Create and read ACH Payment files

1.2.3 2020-11-17 18:57 UTC

This package is auto-updated.

Last update: 2024-04-18 02:07:02 UTC


README

Generate an ACH File object from data for submission to an ODFI, or create an ACH File object from a return/reject file provided by the ODFI.

Requirements

  • PHP 7
  • PHPUnit 7.1 for unit testing

Installation

  • Install using composer:
"require": {
    "revenuewire/ach-file": "1.1.*"
}

Run Tests

  • Test using phpunit - with xdebug enabled tests will provide code coverage data
php vendor/bin/phpunit

Usage (Code Examples)

Creating an ACH File From Generated Data

// The code below will generate an ACH File object that contains two batches
// (one credit entry batch and one debit entry batch), each of which contains
// two Entry Detail Records.

$companyID = '0123456789';
$originatingDFIID = '987654321';

// Create a File Header record to use when creating a file
$achFileHeaderRecord = new \RW\ACH\FileHeaderRecord([
    // Required
    \RW\ACH\FileHeaderRecord::IMMEDIATE_DESTINATION => ' 123456789',
    \RW\ACH\FileHeaderRecord::IMMEDIATE_ORIGIN      => $companyID,
    \RW\ACH\FileHeaderRecord::DESTINATION_NAME      => 'A Bank Name',    // Max 23 characters
    \RW\ACH\FileHeaderRecord::ORIGIN_NAME           => 'A Company Name', // Max 23 characters
    // Optional
    \RW\ACH\FileHeaderRecord::FILE_DATE             => new DateTime('2018-06-20 19:18:17'),
    \RW\ACH\FileHeaderRecord::FILE_ID_MODIFIER      => 'A',              // Sequential, A-Z or 0-9
    \RW\ACH\FileHeaderRecord::REFERENCE_CODE        => 'ABCD1234',       // Max 8 characters, originator's internal use
]);

// Create Batch Header records to use when creating batches
$creditBatchHeader = new \RW\ACH\BatchHeaderRecord([
    // Required
    \RW\ACH\BatchHeaderRecord::SERVICE_CLASS_CODE        => \RW\ACH\BatchHeaderRecord::CREDIT_SERVICE_CLASS,
    \RW\ACH\BatchHeaderRecord::COMPANY_NAME              => 'Company Name', // Max 16 characters
    \RW\ACH\BatchHeaderRecord::COMPANY_ID                => $companyID,
    \RW\ACH\BatchHeaderRecord::STANDARD_ENTRY_CLASS_CODE => \RW\ACH\BatchHeaderRecord::SEC_PPD,
    \RW\ACH\BatchHeaderRecord::COMPANY_ENTRY_DESCRIPTION => 'Payroll',      // Transaction type description
    \RW\ACH\BatchHeaderRecord::ORIGINATING_DFI_ID        => $originatingDFIID,
    \RW\ACH\BatchHeaderRecord::BATCH_NUMBER              => '1',            // Sequential
    // Optional
    \RW\ACH\BatchHeaderRecord::EFFECTIVE_ENTRY_DATE      => new DateTime('2018-06-20 19:18:17'),
    \RW\ACH\BatchHeaderRecord::DISCRETIONARY_DATA        => '20 Char Internal Use',
    \RW\ACH\BatchHeaderRecord::COMPANY_DESCRIPTIVE_DATE  => new DateTime('2018-06-20 19:18:17'),
]);
$debitBatchHeader = new \RW\ACH\BatchHeaderRecord([
    // Required
    \RW\ACH\BatchHeaderRecord::SERVICE_CLASS_CODE        => \RW\ACH\BatchHeaderRecord::DEBIT_SERVICE_CLASS,
    \RW\ACH\BatchHeaderRecord::COMPANY_NAME              => 'Company Name', // Max 16 characters
    \RW\ACH\BatchHeaderRecord::COMPANY_ID                => $companyID,
    \RW\ACH\BatchHeaderRecord::STANDARD_ENTRY_CLASS_CODE => \RW\ACH\BatchHeaderRecord::SEC_PPD,
    \RW\ACH\BatchHeaderRecord::COMPANY_ENTRY_DESCRIPTION => 'Payroll',      // Transaction type description
    \RW\ACH\BatchHeaderRecord::ORIGINATING_DFI_ID        => $originatingDFIID,
    \RW\ACH\BatchHeaderRecord::BATCH_NUMBER              => '2',            // Sequential
    // Optional
    \RW\ACH\BatchHeaderRecord::EFFECTIVE_ENTRY_DATE      => new DateTime('2018-06-20 19:18:17'),
    \RW\ACH\BatchHeaderRecord::DISCRETIONARY_DATA        => '20 Char Internal Use',
    \RW\ACH\BatchHeaderRecord::COMPANY_DESCRIPTIVE_DATE  => new DateTime('2018-06-20 19:18:17'),
]);

// Create Entry Detail Records to add to batches
$creditEntryDetailOne = new \RW\ACH\EntryDetailRecord([
    // Required
    \RW\ACH\EntryDetailRecord::TRANSACTION_CODE   => \RW\ACH\EntryDetailRecord::CHECKING_CREDIT_DEPOSIT,
    \RW\ACH\EntryDetailRecord::TRANSIT_ABA_NUMBER => '987654321',       // Customer's bank transit number
    \RW\ACH\EntryDetailRecord::DFI_ACCOUNT_NUMBER => '0123456789',      // Customer's bank account number
    \RW\ACH\EntryDetailRecord::AMOUNT             => '25.55',           // Decimal format
    \RW\ACH\EntryDetailRecord::INDIVIDUAL_NAME    => 'Customer Name',   // Max 22 characters
    \RW\ACH\EntryDetailRecord::TRACE_NUMBER       => $originatingDFIID,
    // Optional
    \RW\ACH\EntryDetailRecord::ID_NUMBER          => '9876543210',      // Customer's internal account number
    \RW\ACH\EntryDetailRecord::DRAFT_INDICATOR    => '  ',
    \RW\ACH\EntryDetailRecord::ADDENDA_INDICATOR  => '0',
], 1);
$creditEntryDetailTwo = new \RW\ACH\EntryDetailRecord([
    // Required
    \RW\ACH\EntryDetailRecord::TRANSACTION_CODE   => \RW\ACH\EntryDetailRecord::SAVINGS_CREDIT_DEPOSIT,
    \RW\ACH\EntryDetailRecord::TRANSIT_ABA_NUMBER => '987654321',
    \RW\ACH\EntryDetailRecord::DFI_ACCOUNT_NUMBER => '0123456789',
    \RW\ACH\EntryDetailRecord::AMOUNT             => '32.45',
    \RW\ACH\EntryDetailRecord::INDIVIDUAL_NAME    => 'Another Customer',
    \RW\ACH\EntryDetailRecord::TRACE_NUMBER       => $originatingDFIID,
    // Optional
    \RW\ACH\EntryDetailRecord::ID_NUMBER          => '9876543210',
    \RW\ACH\EntryDetailRecord::DRAFT_INDICATOR    => '  ',
    \RW\ACH\EntryDetailRecord::ADDENDA_INDICATOR  => '0',
], 2);
$debitEntryDetailOne = new \RW\ACH\EntryDetailRecord([
    // Required
    \RW\ACH\EntryDetailRecord::TRANSACTION_CODE   => \RW\ACH\EntryDetailRecord::CHECKING_DEBIT_PAYMENT,
    \RW\ACH\EntryDetailRecord::TRANSIT_ABA_NUMBER => '987654321',
    \RW\ACH\EntryDetailRecord::DFI_ACCOUNT_NUMBER => '0123456789',
    \RW\ACH\EntryDetailRecord::AMOUNT             => '11.00',
    \RW\ACH\EntryDetailRecord::INDIVIDUAL_NAME    => 'Yet Another Customer',
    \RW\ACH\EntryDetailRecord::TRACE_NUMBER       => $originatingDFIID,
    // Optional
    \RW\ACH\EntryDetailRecord::ID_NUMBER          => '9876543210',
    \RW\ACH\EntryDetailRecord::DRAFT_INDICATOR    => '  ',
    \RW\ACH\EntryDetailRecord::ADDENDA_INDICATOR  => '0',
], 3);
$debitEntryDetailTwo = new \RW\ACH\EntryDetailRecord([
    // Required
    \RW\ACH\EntryDetailRecord::TRANSACTION_CODE   => \RW\ACH\EntryDetailRecord::SAVINGS_DEBIT_PAYMENT,
    \RW\ACH\EntryDetailRecord::TRANSIT_ABA_NUMBER => '987654321',
    \RW\ACH\EntryDetailRecord::DFI_ACCOUNT_NUMBER => '0123456789',
    \RW\ACH\EntryDetailRecord::AMOUNT             => '5.64',
    \RW\ACH\EntryDetailRecord::INDIVIDUAL_NAME    => 'One More Customer',
    \RW\ACH\EntryDetailRecord::TRACE_NUMBER       => $originatingDFIID,
    // Optional
    \RW\ACH\EntryDetailRecord::ID_NUMBER          => '9876543210',
    \RW\ACH\EntryDetailRecord::DRAFT_INDICATOR    => '  ',
    \RW\ACH\EntryDetailRecord::ADDENDA_INDICATOR  => '0',
], 4);

$batchOne = new \RW\ACH\Batch($creditBatchHeader);
$batchOne->addEntryDetailRecord($creditEntryDetailOne)
    ->addEntryDetailRecord($creditEntryDetailTwo)
    ->close();
$batchTwo = new \RW\ACH\Batch($debitBatchHeader);
$batchTwo->addEntryDetailRecord($debitEntryDetailOne)
    ->addEntryDetailRecord($debitEntryDetailTwo)
    ->close();

$achPaymentFile = new \RW\ACH\File($achFileHeaderRecord);
$achPaymentFile->addBatch($batchOne)
    ->addBatch($batchTwo)
    ->close();

Uploading an ACH File Object to a Destination SFTP Server

// This example uses the php ssh2 library to manage the sftp connection

$host = 'url.to.destination.com';
$port = 22;
$userName = 'userName';
$publicKey = '/path/to/public/key.pub';
$privateKey = '/path/to/private/key';
$inputDirectory = '/path/to/inbound/directory/'; // leading and trailing slashes
$date = new DateTime();
$fileName = 'ACHPaymentFile_' . $date->format('Ymd-His') . '.txt';

$connection = ssh2_connect($host, $port);
$auth = ssh2_auth_pubkey_file($connection, $userName, $publicKey, $privateKey);
$sftp = ssh2_sftp($connection);

$destination = fopen('ssh2.sftp://' . intval($sftp) . $inputDirectory . $fileName, 'w');
fwrite($destination, $achPaymentFile->toString());
fclose($destination);

Downloading ACH Files From a Destination SFTP Server

// This example uses the php ssh2 library to manage the connection
// Set up transmission details

$host = 'url.to.destination.com';
$port = 22;
$userName = 'userName';
$publicKey = '/path/to/public/key.pub';
$privateKey = '/path/to/private/key';
$outboundDirectory = '/path/to/outbound/directory/'; // leading and trailing slashes

// Establish connection
if (($connection = ssh2_connect($host, $port)) === false) {
    exit ('Failed to establish connection');
}
if (ssh2_auth_pubkey_file($connection, $userName, $publicKey, $privateKey) === false) {
    exit ('Failed to authorize secure connection');
}
$sftp = ssh2_sftp($connection);

// Get a list of available files for download
$files = array();
$dirHandle = opendir('ssh2.sftp://' . intval($sftp) . $outputDirectory);
while (($file = readdir($dirHandle))) {
    if ($file != '.' && $file != '..') {
        $files[] = $file;
    }
}

// Download and parse the files
foreach ($files as $k => $remoteFileName) {
    // Prep the local file name
    $fileStats = ssh2_sftp_stat($sftp, $outputDirectory . $remoteFileName);
    $fileDateString = (new DateTime())->setTimestamp($fileStats['mtime'])->format('Ymd-His');
    $localFileName = "ACH_RETURN_FILE_{$fileDateString}_{$remoteFileName}";

    // Open the file for processing
    if (!($handle = fopen('ssh2.sftp://' . intval($sftp) . $outputDirectory . $remoteFileName, 'r'))) {
        echo "Failed to open remote file {$remoteFileName} in {$outputDirectory}";
        continue;
    }

    /* Parse the file */

    // Clean up
    fclose($handle)
}

Creating an ACH File Object Using a Provided Return File

$handle = fopen('/path/to/return/file.txt', 'r+');
$achReturnFile = \RW\ACH\File::buildFromResource($handle);

// Get data about the file from the header
$fileHeader = $achReturnFile->getHeaderRecord();
$exampleData = $fileHeader->getField(\RW\ACH\FileHeader::EXAMPLE_FIELD_NAME);

/** @var Batch $batch */
foreach ($achReturnFile->getBatches() as $batch) {
    // Get data about batches from the header
    $batchHeader = $batch->getHeaderRecord();
    $exampleData = $batchHeader->getField(\RW\ACH\BatchHeader::EXAMPLE_FIELD_NAME);

    foreach ($batch->getEntryDetailRecords() as $entryDetailRecord) {
        // Process the entry detail record in some way
        switch (get_class($entryDetailRecord->getAddendaRecord())) {
            case \RW\ACH\NoticeOfChangeAddenda::class:
                // handleNoticeOfChange($entryDetailRecord);
                break;
            case \RW\ACH\ReturnEntryAddenda::class:
                // handleReturnEntry($entryDetailRecord);
                break;
            default:
                // Error
        }
    }
}