From cd1a490338a5da42ab94b62b24231aa14dd5d048 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 12 Mar 2015 19:30:08 -0500 Subject: [PATCH] RT#33582: RBC return batch processing failure [handling for non-chronological file] --- FS/FS/Conf.pm | 2 +- FS/FS/cust_pay_batch.pm | 6 ----- FS/FS/pay_batch.pm | 16 ++++++------ FS/FS/pay_batch/RBC.pm | 68 +++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index d5e8960b2..a37e5a6ef 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3960,7 +3960,7 @@ and customer address. Include units.', { 'key' => 'batchconfig-RBC', 'section' => 'billing', - 'description' => 'Configuration for Royal Bank of Canada PDS batching, four lines: 1. Client number, 2. Short name, 3. Long name, 4. Transaction code.', + 'description' => 'Configuration for Royal Bank of Canada PDS batching, five lines: 1. Client number, 2. Short name, 3. Long name, 4. Transaction code 5. (optional) set to TEST to turn on test mode.', 'type' => 'textarea', }, diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index da003d85e..13b2eefe5 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -344,14 +344,8 @@ sub decline { } $cust_pay->void($reason); } - elsif ( lc($old->status) eq 'declined' ) { - # batch files from RBC can have multiple lines for one decline - # if this causes problems elsewhere, try hacking pay_batch/RBC.pm instead - return ''; - } else { # normal case: refuse to do anything - # should never happen...only statuses are approved or declined return "cannot decline paybatchnum $paybatchnum, already resolved ('".$old->status."')"; } } # !$old->status diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm index f41b3e38b..449ea2221 100644 --- a/FS/FS/pay_batch.pm +++ b/FS/FS/pay_batch.pm @@ -222,17 +222,17 @@ takes precedence over I. Supported format keys (defined in the specified FS::pay_batch module) are: -I - CSV, fixed, variable, XML +I - required, can be CSV, fixed, variable, XML -I - list of field names for each row/line +I - required list of field names for each row/line I - regular expression for fixed filetype -I - for variable filetype +I - required for variable filetype -I - for XML filetype +I - required for XML filetype -I - for XML filetype +I - required for XML filetype I - sub, ignore all lines before this returns true @@ -242,11 +242,11 @@ I - sub, runs immediately after end_condition returns true I - sub, skip lines when this returns true -I - sub, runs before approved/declined conditions are checked +I - required, sub, runs before approved/declined conditions are checked -I - sub, returns true when approved +I - required, sub, returns true when approved -I - sub, returns true when declined +I - required, sub, returns true when declined I - sub, decide whether or not to close the batch diff --git a/FS/FS/pay_batch/RBC.pm b/FS/FS/pay_batch/RBC.pm index a99d057fa..45e888dc3 100644 --- a/FS/FS/pay_batch/RBC.pm +++ b/FS/FS/pay_batch/RBC.pm @@ -6,7 +6,7 @@ use Date::Format 'time2str'; use FS::Conf; 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 @@ -24,11 +24,12 @@ $name = 'RBC'; # 4 - Foreign Currency Information Records # We skip all subtypes except 0 # -# additional info available at https://www.rbcroyalbank.com/ach/file-451806.pdf +# 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' => - '^([0134]).{18}(.{4}).{3}(.).{11}(.{19}).{6}(.{30}).{17}(.{9})(.{18}).{6}(.{14}).{23}(.).{9}\r?$', + '^(.).{18}(.{4}).{3}(.).{11}(.{19}).{6}(.{30}).{17}(.{9})(.{18}).{6}(.{14}).{23}(.).{9}\r?$', 'fields' => [ qw( recordtype batchnum @@ -53,30 +54,67 @@ $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'); + $totaloffset = sprintf("%.2f", $totaloffset / 100 ); + $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->{'recordtype'} eq '3' || - $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 += $hash->{'paid'} + if $hash->{'status'} eq ' '; #false laziness with 'approved' above + 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 }, ); @@ -87,18 +125,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'. -- 2.11.0