X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fpay_batch%2FRBC.pm;h=53f81085216dc389bd8fe0232198d81f5759cde5;hb=501fc8a7d7a77bb5dde26db85aeff99f7a5cf98c;hp=daf6548da02d444eef1fab4f60f474f73085d251;hpb=20b563c398c5c9264a998d31ae0d3d95454d526e;p=freeside.git diff --git a/FS/FS/pay_batch/RBC.pm b/FS/FS/pay_batch/RBC.pm index daf6548da..53f810852 100644 --- a/FS/FS/pay_batch/RBC.pm +++ b/FS/FS/pay_batch/RBC.pm @@ -4,17 +4,33 @@ use strict; use vars qw(@ISA %import_info %export_info $name); use Date::Format 'time2str'; use FS::Conf; +use Encode 'encode'; my $conf; -my ($client_num, $shortname, $longname, $trans_code, $i); +my ($client_num, $shortname, $longname, $trans_code, $testmode, $i, $declined, $totaloffset); $name = 'RBC'; # Royal Bank of Canada ACH Direct Payments Service +# Meaning of initial characters in records: +# 0 - header row, skipped by begin_condition +# 1 - Debit Detail Record (only when subtype is 0) +# 2 - Credit Detail Record, we die with a parse error (shouldn't appear in freeside-generated batches) +# 3 - Account Trailer Record (appears after Returned items, we skip) +# 4 - Client Trailer Record, indicates end of batch in end_condition +# +# Subtypes (27th char) indicate different kinds of Debit/Credit records +# 0 - Credit/Debit Detail Record +# 3 - Error Message Record +# 4 - Foreign Currency Information Records +# We skip all subtypes except 0 +# +# additional info available at https://www.rbcroyalbank.com/ach/cid-213166.html %import_info = ( 'filetype' => 'fixed', + #this only really applies to Debit Detail, but we otherwise only need first char 'formatre' => - '^(.).{18}(.{4}).{3}(.).{11}(.{19}).{6}(.{30}).{17}(.{9})(.{18}).{6}(.{14}).{23}(.).{9}$', + '^(.).{18}(.{4}).{3}(.).{11}(.{19}).{6}(.{30}).{17}(.{9})(.{18}).{6}(.{14}).{23}(.).{9}\r?$', 'fields' => [ qw( recordtype batchnum @@ -39,29 +55,72 @@ $name = 'RBC'; }, 'declined' => sub { my $hash = shift; - grep { $hash->{'status'} eq $_ } ('E', 'R', 'U', 'T'); + my $status = $hash->{'status'}; + my $message = ''; + if ($status eq 'E') { + $message = 'Reversed payment'; + } elsif ($status eq 'R') { + $message = 'Rejected payment'; + } elsif ($status eq 'U') { + $message = 'Returned payment'; + } elsif ($status eq 'T') { + $message = 'Error'; + } else { + return 0; + } + $hash->{'error_message'} = $message; + $declined->{$hash->{'paybatchnum'}} = 1; + return 1; }, 'begin_condition' => sub { my $hash = shift; - $hash->{recordtype} eq '1'; # Detail Record + # Debit Detail Record + if ($hash->{recordtype} eq '1') { + $declined = {}; + $totaloffset = 0; + return 1; + # Credit Detail Record, will immediately trigger end condition & error + } elsif ($hash->{recordtype} eq '2') { + return 1; + } else { + return 0; + } }, 'end_hook' => sub { my( $hash, $total, $line ) = @_; + return "Can't process Credit Detail Record, aborting import" + if ($hash->{'recordtype'} eq '2'); + $total += $totaloffset; $total = sprintf("%.2f", $total); - # We assume here that this is an 'All Records' or 'Input Records' - # report. + # We assume here that this is an 'All Records' or 'Input Records' report. my $batch_total = sprintf("%.2f", substr($line, 59, 18) / 100); return "Our total $total does not match bank total $batch_total!" if $total != $batch_total; - ''; + return ''; }, 'end_condition' => sub { my $hash = shift; - $hash->{recordtype} eq '4'; # Client Trailer Record + return ($hash->{recordtype} eq '4') # Client Trailer Record + || ($hash->{recordtype} eq '2'); # Credit Detail Record, will throw error in end_hook }, 'skip_condition' => sub { my $hash = shift; - $hash->{'subtype'} ne '0'; + #we already declined it this run, no takebacks + if ($declined->{$hash->{'paybatchnum'}}) { + #file counts this as part of total, but we skip + $totaloffset += sprintf("%.2f", $hash->{'paid'} / 100 ) + if $hash->{'status'} eq ' '; #false laziness with 'approved' above + return 1; + } + #skipping W for now (maybe it should be declined?) + if ($hash->{'status'} eq 'W') { + #file counts this as part of total, but we skip + $totaloffset += sprintf("%.2f", $hash->{'paid'} / 100 ); + return 1; + } + return + ($hash->{'recordtype'} eq '3') || #Account Trailer Record, concludes returned items + ($hash->{'subtype'} ne '0'); #error messages, etc, too late to apply to previous entry }, ); @@ -72,18 +131,22 @@ $name = 'RBC'; $shortname, $longname, $trans_code, + $testmode ) = $conf->config("batchconfig-RBC"); + $testmode = '' unless $testmode eq 'TEST'; $i = 1; }, header => sub { my $pay_batch = shift; - '$$AAPASTD0152[PROD[NL$$'."\n". + my $mode = $testmode ? 'TEST' : 'PROD'; + my $filenum = $testmode ? 'TEST' : sprintf("%04u", $pay_batch->batchnum); + '$$AAPASTD0152['.$mode.'[NL$$'."\n". '000001'. 'A'. 'HDR'. sprintf("%10s", $client_num). sprintf("%-30s", $longname). - sprintf("%04u", $pay_batch->batchnum). + $filenum. time2str("%Y%j", $pay_batch->download). 'CAD'. '1'. @@ -93,6 +156,15 @@ $name = 'RBC'; row => sub { my ($cust_pay_batch, $pay_batch) = @_; my ($account, $aba) = split('@', $cust_pay_batch->payinfo); + my($bankno, $branch); + if ( $aba =~ /^0(\d{3})(\d{5})$/ ) { # standard format for Canadian bank ID + ($bankno, $branch) = ( $1, $2 ); + } elsif ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #how we store branches + ($branch, $bankno) = ( $1, $2 ); + } else { + die "invalid branch/routing number '$aba'\n"; + } + $i++; sprintf("%06u", $i). 'D'. @@ -101,14 +173,15 @@ $name = 'RBC'; ' '. sprintf("%-19s", $cust_pay_batch->paybatchnum). '00'. - sprintf("%09u", $aba). + sprintf("%04s", $bankno). + sprintf("%05s", $branch). sprintf("%-18s", $account). ' '. - sprintf("%010u",$cust_pay_batch->amount*100). + sprintf("%010.0f",$cust_pay_batch->amount*100). ' '. - time2str("%Y%j", $pay_batch->download). - sprintf("%-30s", $cust_pay_batch->cust_main->first . ' ' . - $cust_pay_batch->cust_main->last). + time2str("%Y%j", time + 86400). + sprintf("%-30.30s", encode('utf8', $cust_pay_batch->cust_main->first . ' ' . + $cust_pay_batch->cust_main->last)). 'E'. # English ' '. sprintf("%-15s", $shortname). @@ -129,9 +202,9 @@ $name = 'RBC'; 'Z'. 'TRL'. sprintf("%10s", $client_num). - ' ' x 20 . + '0' x 20 . sprintf("%06u", $batchcount). - sprintf("%014u", $batchtotal*100). + sprintf("%014.0f", $batchtotal*100). '00' . '000000' . # total number of customer information records ' ' x 84