RT#33582: RBC return batch processing failure [handling for non-chronological file]
authorJonathan Prykop <jonathan@freeside.biz>
Fri, 13 Mar 2015 00:30:08 +0000 (19:30 -0500)
committerJonathan Prykop <jonathan@freeside.biz>
Fri, 13 Mar 2015 00:30:08 +0000 (19:30 -0500)
FS/FS/Conf.pm
FS/FS/cust_pay_batch.pm
FS/FS/pay_batch.pm
FS/FS/pay_batch/RBC.pm

index d5e8960..a37e5a6 100644 (file)
@@ -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',
   },
 
index da003d8..13b2eef 100644 (file)
@@ -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
index f41b3e3..449ea22 100644 (file)
@@ -222,17 +222,17 @@ takes precedence over I<format>.
 
 Supported format keys (defined in the specified FS::pay_batch module) are:
 
-I<filetype> - CSV, fixed, variable, XML
+I<filetype> - required, can be CSV, fixed, variable, XML
 
-I<fields> - list of field names for each row/line
+I<fields> - required list of field names for each row/line
 
 I<formatre> - regular expression for fixed filetype
 
-I<parse> - for variable filetype
+I<parse> - required for variable filetype
 
-I<xmlkeys> - for XML filetype
+I<xmlkeys> - required for XML filetype
 
-I<xmlrow> - for XML filetype
+I<xmlrow> - required for XML filetype
 
 I<begin_condition> - sub, ignore all lines before this returns true
 
@@ -242,11 +242,11 @@ I<end_hook> - sub, runs immediately after end_condition returns true
 
 I<skip_condition> - sub, skip lines when this returns true
 
-I<hook> - sub, runs before approved/declined conditions are checked
+I<hook> - required, sub, runs before approved/declined conditions are checked
 
-I<approved> - sub, returns true when approved
+I<approved> - required, sub, returns true when approved
 
-I<declined> - sub, returns true when declined
+I<declined> - required, sub, returns true when declined
 
 I<close_condition> - sub, decide whether or not to close the batch
 
index a99d057..45e888d 100644 (file)
@@ -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'.