package ACH::Builder; use strict; use warnings; use POSIX qw( strftime ); our $VERSION = '0.03'; #------------------------------------------------- # new( $file ? ) #------------------------------------------------- sub new { my ( $class, $vars ) = @_; my $self = {}; bless( $self, $class ); # set default values $self->{__BATCH_COUNT__} = 0; $self->{__BLOCK_COUNT__} = 0; $self->{__ENTRY_COUNT__} = 0; $self->{__ENTRY_HASH__} = 0; $self->{__DEBIT_AMOUNT__} = 0; $self->{__CREDIT_AMOUNT__} = 0; $self->{__BATCH_TOTAL_DEBIT__} = 0; $self->{__BATCH_TOTAL_CREDIT__} = 0; $self->{__BATCH_ENTRY_COUNT__} = 0; $self->{__BATCH_ENTRY_HASH__} = 0; $self->{__SERVICE_CLASS_CODE__} = $vars->{service_class_code} || 200; $self->{__IMMEDIATE_DEST_NAME__} = $vars->{destination_name}; $self->{__IMMEDIATE_ORIGIN_NAME__} = $vars->{origination_name}; $self->{__IMMEDIATE_DEST__} = $vars->{destination}; $self->{__IMMEDIATE_ORIGIN__} = $vars->{origination}; $self->{__ENTRY_CLASS_CODE__} = $vars->{entry_class_code} || 'PPD'; $self->{__ENTRY_DESCRIPTION__} = $vars->{entry_description}; $self->{__COMPANY_ID__} = $vars->{company_id}; $self->{__COMPANY_NAME__} = $vars->{company_name}; $self->{__COMPANY_NOTE__} = $vars->{company_note}; $self->{__FILE_ID_MODIFIER__} = $vars->{file_id_modifier} || 'A'; $self->{__RECORD_SIZE__} = $vars->{record_size} || 94; $self->{__BLOCKING_FACTOR__} = $vars->{blocking_factor} || 10; $self->{__FORMAT_CODE__} = $vars->{format_code} || 1; $self->{__EFFECTIVE_DATE__} = $vars->{effective_date} || strftime( "%y%m%d", localtime( time + 86400 ) ); $self->{__ACH_DATA__} = []; # populate self with data from site return( $self ); } # END new #------------------------------------------------- # to_string() #------------------------------------------------- sub to_string { my $self = shift; return( join( "\n", @{ $self->{__ACH_DATA__} } ) ); } #------------------------------------------------- # set_format_code() setter #------------------------------------------------- sub set_format_code { my ( $self, $p ) = @_; $self->{__FORMAT_CODE__} = $p; } #------------------------------------------------- # set_blocking_factor() setter #------------------------------------------------- sub set_blocking_factor { my ( $self, $p ) = @_; $self->{__BLOCKING_FACTOR__} = $p; } #------------------------------------------------- # set_record_size() setter #------------------------------------------------- sub set_record_size { my ( $self, $p ) = @_; $self->{__RECORD_SIZE__} = $p; } #------------------------------------------------- # set_file_id_modifier() setter #------------------------------------------------- sub set_file_id_modifier { my ( $self, $p ) = @_; $self->{__FILE_ID_MODIFIER__} = $p; } #------------------------------------------------- # set_immediate_origin_name() setter #------------------------------------------------- sub set_immediate_origin_name { my ( $self, $p ) = @_; $self->{__IMMEDIATE_ORIGIN_NAME__} = $p; } #------------------------------------------------- # set_immediate_origin() setter #------------------------------------------------- sub set_immediate_origin { my ( $self, $p ) = @_; $self->{__IMMEDIATE_ORIGIN__} = $p; } #------------------------------------------------- # set_immediate_dest_name() setter #------------------------------------------------- sub set_immediate_dest_name { my ( $self, $p ) = @_; $self->{__IMMEDIATE_DEST_NAME__} = $p; } #------------------------------------------------- # set_immediate_dest() setter #------------------------------------------------- sub set_immediate_dest { my ( $self, $p ) = @_; $self->{__IMMEDIATE_DEST__} = $p; } #------------------------------------------------- # set_entry_desription() setter #------------------------------------------------- sub set_entry_description { my ( $self, $p ) = @_; $self->{__ENTRY_DESCRIPTION__} = $p; } #------------------------------------------------- # set_entry_class_code() setter #------------------------------------------------- sub set_entry_class_code { my ( $self, $p ) = @_; $self->{__ENTRY_CLASS_CODE__} = $p; } #------------------------------------------------- # set_company_id() setter #------------------------------------------------- sub set_company_id { my ( $self, $p ) = @_; $self->{__COMPANY_ID__} = $p; } #------------------------------------------------- # set_company_note() setter #------------------------------------------------- sub set_company_note { my ( $self, $p ) = @_; $self->{__COMPANY_NOTE__} = $p; } #------------------------------------------------- # set_service_class_code() setter #------------------------------------------------- sub set_service_class_code { my ( $self, $p ) = @_; $self->{__SERVICE_CLASS_CODE__} = $p; } #------------------------------------------------- # ach_data() accessor #------------------------------------------------- sub ach_data { my ( $self ) = shift; $self->{__ACH_DATA__}; } #------------------------------------------------- # make_batch( @$records ) #------------------------------------------------- sub make_batch { my( $self, $records ) = @_; return if scalar( @{ $records } ) <= 0; # bump the batch count ++$self->{__BATCH_COUNT__}; # inititalize the batch variables $self->{__BATCH_TOTAL_DEBIT__} = 0; $self->{__BATCH_TOTAL_CREDIT__} = 0; $self->{__BATCH_ENTRY_COUNT__} = 0; $self->{__BATCH_ENTRY_HASH__} = 0; # get batch header $self->_make_batch_header_record(); # loop over the detail records foreach my $record ( @{ $records } ) { # modify batch values $self->{__BATCH_TOTAL_DEBIT__} += $record->{amount}; $self->{__BATCH_ENTRY_HASH__} += $record->{routing_number}; ++$self->{__BATCH_ENTRY_COUNT__}; # modify file values $self->{__ENTRY_HASH__} += $record->{routing_number}; $self->{__TOTAL_DEBIT__} += $record->{amount}; ++$self->{__ENTRY_COUNT__}; # get detail record $self->_make_detail_record( $record ) } # get batch control record $self->_make_batch_control_record(); } #------------------------------------------------- # make_file_control_record( ) #------------------------------------------------- sub make_file_control_record { my( $self ) = @_; my @def = qw( record_type batch_count block_count file_entry_count entry_hash total_debit_amount total_credit_amount bank_39 ); my $data = { record_type => 9, batch_count => $self->{__BATCH_COUNT__}, block_count => $self->{__BLOCK_COUNT__}, file_entry_count => $self->{__ENTRY_COUNT__}, entry_hash => $self->{__ENTRY_HASH__}, total_debit_amount => $self->{__DEBIT_AMOUNT__}, total_credit_amount => $self->{__CREDIT_AMOUNT__}, bank_39 => '', }; # stash line push( @{ $self->ach_data() }, fixedlength( $self->format_rules(), $data, \@def ) ); } #------------------------------------------------- # make_file_header_record() #------------------------------------------------- sub make_file_header_record { my( $self ) = @_; # ach file header definition my @def = qw( record_type priority_code immediate_dest immediate_origin date time file_id_modifier record_size blocking_factor format_code immediate_dest_name immediate_origin_name reference_code ); my $data = { record_type => 1, priority_code => 1, immediate_dest => $self->{__IMMEDIATE_DEST__}, immediate_origin => $self->{__IMMEDIATE_ORIGIN__}, date => strftime( "%y%m%d", localtime(time) ), time => strftime( "%H%M", localtime(time) ), file_id_modifier => $self->{__FILE_ID_MODIFIER__}, record_size => $self->{__RECORD_SIZE__}, blocking_factor => $self->{__BLOCKING_FACTOR__}, format_code => $self->{__FORMAT_CODE__}, immediate_dest_name => $self->{__IMMEDIATE_DEST_NAME__}, immediate_origin_name => $self->{__IMMEDIATE_ORIGIN_NAME__}, reference_code => '', }; push( @{ $self->ach_data() }, fixedlength( $self->format_rules, $data, \@def ) ); } #------------------------------------------------- # sample_detail_records() #------------------------------------------------- sub sample_detail_records { my( $self ) = shift; my @records; push( @records, { customer_name => 'JOHN SMITH', customer_acct => sprintf( "%010d", '6124' ) . sprintf( "%08d", '2882282' ), amount => '2501', routing_number => '010010101', bank_account => '103030030', } ); push( @records, { customer_name => 'JOHN SMITHSTIMTIMSTIMSIMSIMS', customer_acct => sprintf( "%010d", '4124' ) . sprintf( "%08d", '4882282' ), amount => '40801', routing_number => '010010401', bank_account => '440030030', } ); return @records; } #------------------------------------------------- # format_rules() #------------------------------------------------- sub format_rules { my( $self ) = @_; return( { customer_name => '22L', customer_acct => '15L', amount => '10R*D', bank_2 => '2L', transaction_type => '2L', bank_15 => '15L', addenda => '1L', trace_num => '15L', transaction_code => '2L', record_type => '1L', bank_account => '17L', routing_number => '9R*D', record_type => '1L', priority_code => '2R*D', immediate_dest => '10R', immediate_origin => '10R', date => '6L', time => '4L', file_id_modifier => '1L', record_size => '3R*D', blocking_factor => '2R*D', format_code => '1L', immediate_dest_name => '23L', immediate_origin_name => '23L', reference_code => '8L', service_class_code => '3L', company_name => '16L', company_note_data => '20L', company_id => '10L', standard_entry_class_code => '3L', company_entry_descr => '10L', effective_date => '6L', settlement_date => '3L', # for bank origin_status_code => '1L', # for bank origin_dfi_id => '8L', # for bank batch_number => '7R*D', entry_count => '6R*D', entry_hash => '10R*D', total_debit_amount => '12R*D', total_credit_amount => '12R*D', authen_code => '19L', bank_6 => '6L', batch_count => '6R*D', block_count => '6R*D', file_entry_count => '8R*D', bank_39 => '39L', } ); } #------------------------------------------------- # _make_batch_control_record( ) #------------------------------------------------- sub _make_batch_control_record { my( $self ) = @_; my @def = qw( record_type service_class_code entry_count entry_hash total_debit_amount total_credit_amount company_id authen_code bank_6 origin_dfi_id batch_number ); my $data = { record_type => 8, service_class_code => $self->{__SERVICE_CLASS_CODE__}, company_id => $self->{__COMPANY_ID__}, origin_dfi_id => '', batch_number => $self->{__BATCH_COUNT__}, authen_code => '', bank_6 => '', entry_hash => substr( $self->{__BATCH_ENTRY_HASH__}, 0, 9 ), entry_count => $self->{__BATCH_ENTRY_COUNT__}, total_debit_amount => $self->{__BATCH_TOTAL_DEBIT__}, total_credit_amount => $self->{__BATCH_TOTAL_CREDIT__}, }; push( @{ $self->ach_data() }, fixedlength( $self->format_rules(), $data, \@def ) ); } #------------------------------------------------- # _make_detail_record( ) #------------------------------------------------- sub _make_detail_record { my( $self, $record ) = @_; my @def = qw( record_type transaction_code routing_number bank_account amount customer_acct customer_name transaction_type addenda bank_15 ); # add to record unless already defined $record->{record_type} ||= 6; $record->{transaction_code} ||= 27; $record->{transaction_type} ||= 'S'; $record->{bank_15} ||= ''; $record->{addenda} ||= 0; # stash detail record push( @{ $self->ach_data() }, fixedlength( $self->format_rules(), $record, \@def ) ); } #------------------------------------------------- # _make_batch_header_record( ) #------------------------------------------------- sub _make_batch_header_record { my( $self ) = @_; my @def = qw( record_type service_class_code company_name company_note_data company_id standard_entry_class_code company_entry_descr date effective_date settlement_date origin_status_code origin_dfi_id batch_number ); my $data = { record_type => 5, service_class_code => 200, company_name => $self->{__COMPANY_NAME__}, company_note_data => $self->{__COMPANY_NOTE__}, company_id => $self->{__COMPANY_ID__}, standard_entry_class_code => $self->{__ENTRY_CLASS_CODE__}, company_entry_descr => $self->{__ENTRY_DESCRIPTION__}, date => strftime( "%y%m%d", localtime(time) ), effective_date => $self->{__EFFECTIVE_DATE__}, settlement_date => '', origin_status_code => '', origin_dfi_id => '', batch_number => $self->{__BATCH_COUNT__}, authen_code => '', bank_6 => '', }; push( @{ $self->ach_data() }, fixedlength( $self->format_rules(), $data, \@def ) ); } sub fixedlength { my( $format, $data, $order ) = @_; my $int_re = '([*])?(D)'; my $flt_re = '([*])?(F)(\d+)?'; my $numfmt_re = "($int_re|$flt_re)"; my $format_re =<{$field} ) { die( "Format for the field $field was not defined\n" ); } if ( ! defined $data->{$field} ) { warn( "data for $field is not defined" ); $data->{$field} = ""; } if ( $format->{$field} =~ /$format_re/x ) { my $width = $1; my $just = $2 || 'L'; $just = $just eq 'L' ? '-' : ''; my $text = ( $3 || '' ); if ( $text =~ /$int_re/i or $text =~ /$flt_re/ ) { my $zero_fill = $1 ? '0' : ''; my $d_or_f = lc $2; warn "d_of_f: $d_or_f" if $debug; $d_or_f = ".$3$d_or_f" if ($d_or_f eq 'f'); my $fmt = "%${just}${zero_fill}${width}${d_or_f}"; warn "num sprintf :$fmt" if $debug; my $dta = $data->{$field}; # crop text if ( length($dta) > $width ) { $dta = substr( $dta, 0, $width ); } $fmt_string .= sprintf( $fmt, $dta ); } else { my $fmt = "%${just}${width}s"; warn "str sprintf: $fmt" if $debug; my $dta = $data->{$field}; # crop text if ( length($dta) > $width ) { $dta = substr( $dta, 0, $width ); } $fmt_string .= sprintf( $fmt, $dta ); } } # end if match format } # end foreach fields return $fmt_string; } # EOF 1; __END__ =head1 NAME ACH::Builder - Tools for Building ACH (Automated Clearing House) Files =head1 SYNOPSIS use ACH::Builder; my $ach = ACH::Builder->new( { # (required) Company Identification, Fed Tax ID company_id => '11-111111', # (required) This will appear on the receiver's bank statement company_name => 'MY COMPANY', # (required) a brief description of the nature of the # payments this will apper on the receiver's bank statement entry_description => 'TV-TELCOM', # (required) destination => '123123123', destination_name => 'COMMERCE BANK', # (required) origin => '12312311', origin_name => 'MYCOMPANY', # (optional) company_note => 'BILL', # (optional) effective_date => 'yymmdd', } ); # I've included some sample detail records my @samples = $ach->sample_detail_records(); # build file header record $ach->make_file_header_record(); # build batch for web entries $ach->set_entry_class_code( 'WEB' ); $ach->make_batch( \@samples ); # build batch for telephone entries $ach->set_entry_class_code( 'TEL' ); $ach->make_batch( \@samples ); # build file control record $ach->make_file_control_record(); print $ach->to_string; =head1 DESCRIPTION ACH File Structure This module is tool to help construct ACH files, which are fixed width formatted files accpected by most banks. ACH (Automated Clearing House) is an electronic banking network operating system in the United States. ACH processes large volumes of both credit and debit transactions which are originated in batches. Rules and regulations governing the ACH network are established by the National Automated Clearing House Association (NACHA) and the Federal Reserve (Fed). ACH credit transfers include direct deposit payroll payments and payments to contractors and vendors. ACH debit transfers include consumer payments on insurance premiums, mortgage loans, and other kinds of bills. =head1 DETAIL RECORD FORMAT Detail Record Format =over 4 { customer_name => 'JOHN SMITH', customer_acct => '0000-0111111', amount => '2501', routing_number => '010010101' bank_account => '103030030' } =back =head1 METHODS =over 4 =item new (constructor) params: Hash Ref { company_id => '...', company_note ... } ** set methods are also provided for these parameters =over 4 service_class_code destination_name origination_name destination origination entry_class_code entry_description company_id company_name company_note file_id_modifier record_size blocking_factor format_code =back =item make_file_header_record Called to create the File Header record. This should be called before "make_batch". =item make_file_control_record Called to create the File Control Record. This should be called after "make_batch". =item make_batch params: AoH Records Called the create and stash a batch of ACH entries. This method requires an AoH records. See "sample_detail_records" from record specifications. =item format_rules Hash of ACH format rules. =item sample_detail_records AoH of sample detail records Detail Record Format =over 4 { customer_name => 'JOHN SMITH', customer_acct => '0000-0111111', amount => '2501', routing_number => '010010101' bank_account => '103030030' } =back =item to_string returns the built ACH file =back =head1 METHOD Setters =over 4 =item set_service_class_code =item set_destination_name =item set_destination =item set_origination_name =item set_origination =item set_entry_class_code =item set_entry_description =item set_company_id =item set_company_name =item set_company_note =item set_file_id_modifier =item set_record_size =item set_format_code =back =head1 NOTES ACH File structure. File Header Batch Header Entries Batch Control Batch Header Entries Batch Control File Control =head1 LIMITATIONS Only supports the ACH format. =head1 AUTHOR Tim Keefer =head1 COPYRIGHT Tim Keefer =cut