okay to include URL in comment, RT#33582
[freeside.git] / FS / FS / pay_batch / RBC.pm
1 package FS::pay_batch::RBC;
2
3 use strict;
4 use vars qw(@ISA %import_info %export_info $name);
5 use Date::Format 'time2str';
6 use FS::Conf;
7
8 my $conf;
9 my ($client_num, $shortname, $longname, $trans_code, $i);
10
11 $name = 'RBC';
12 # Royal Bank of Canada ACH Direct Payments Service
13
14 # Meaning of initial characters in records:
15 # 0 - header row, skipped by begin_condition
16 # 1 - Debit Detail Record (only when subtype is 0)
17 # 2 - Credit Detail Record, we die with a parse error (shouldn't appear in freeside-generated batches)
18 # 3 - Account Trailer Record (appears after Returned items, we skip)
19 # 4 - Client Trailer Record, indicates end of batch in end_condition
20 #
21 # Subtypes (27th char) indicate different kinds of Debit/Credit records
22 # 0 - Credit/Debit Detail Record
23 # 3 - Error Message Record
24 # 4 - Foreign Currency Information Records
25 # We skip all subtypes except 0
26 #
27 # additional info available at https://www.rbcroyalbank.com/ach/file-451806.pdf
28 %import_info = (
29   'filetype'    => 'fixed',
30   'formatre'    => 
31   '^([0134]).{18}(.{4}).{3}(.).{11}(.{19}).{6}(.{30}).{17}(.{9})(.{18}).{6}(.{14}).{23}(.).{9}\r?$',
32   'fields' => [ qw(
33     recordtype
34     batchnum
35     subtype
36     paybatchnum
37     custname
38     bank
39     payinfo
40     paid
41     status
42     ) ],
43   'hook' => sub {
44       my $hash = shift;
45       $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
46       $hash->{'_date'} = time;
47       $hash->{'payinfo'} =~ s/^(\S+).*/$1/; # these often have trailing spaces
48       $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'bank'};
49   },
50   'approved'    => sub { 
51       my $hash = shift;
52       $hash->{'status'} eq ' '
53   },
54   'declined'    => sub {
55       my $hash = shift;
56       grep { $hash->{'status'} eq $_ } ('E', 'R', 'U', 'T');
57   },
58   'begin_condition' => sub {
59       my $hash = shift;
60       $hash->{recordtype} eq '1'; # Detail Record
61   },
62   'end_hook'    => sub {
63       my( $hash, $total, $line ) = @_;
64       $total = sprintf("%.2f", $total);
65       # We assume here that this is an 'All Records' or 'Input Records'
66       # report.
67       my $batch_total = sprintf("%.2f", substr($line, 59, 18) / 100);
68       return "Our total $total does not match bank total $batch_total!"
69         if $total != $batch_total;
70       '';
71   },
72   'end_condition' => sub {
73       my $hash = shift;
74       $hash->{recordtype} eq '4'; # Client Trailer Record
75   },
76   'skip_condition' => sub {
77       my $hash = shift;
78       $hash->{'recordtype'} eq '3' ||
79         $hash->{'subtype'} ne '0';
80   },
81 );
82
83 %export_info = (
84   init => sub {
85     $conf = shift;
86     ($client_num,
87      $shortname,
88      $longname,
89      $trans_code, 
90      ) = $conf->config("batchconfig-RBC");
91     $i = 1;
92   },
93   header => sub { 
94     my $pay_batch = shift;
95     '$$AAPASTD0152[PROD[NL$$'."\n".
96     '000001'.
97     'A'.
98     'HDR'.
99     sprintf("%10s", $client_num).
100     sprintf("%-30s", $longname).
101     sprintf("%04u", $pay_batch->batchnum).
102     time2str("%Y%j", $pay_batch->download).
103     'CAD'.
104     '1'.
105     ' ' x 87  # filler/reserved fields
106     ;
107   },
108   row => sub {
109     my ($cust_pay_batch, $pay_batch) = @_;
110     my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
111     my($bankno, $branch);
112     if ( $aba =~ /^0(\d{3})(\d{5})$/ ) { # standard format for Canadian bank ID
113       ($bankno, $branch) = ( $1, $2 );
114     } elsif ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #how we store branches
115       ($branch, $bankno) = ( $1, $2 );
116     } else {
117       die "invalid branch/routing number '$aba'\n";
118     }
119
120     $i++;
121     sprintf("%06u", $i).
122     'D'.
123     sprintf("%3s",$trans_code).
124     sprintf("%10s",$client_num).
125     ' '.
126     sprintf("%-19s", $cust_pay_batch->paybatchnum).
127     '00'.
128     sprintf("%04s", $bankno).
129     sprintf("%05s", $branch).
130     sprintf("%-18s", $account).
131     ' '.
132     sprintf("%010.0f",$cust_pay_batch->amount*100).
133     '      '.
134     time2str("%Y%j", $pay_batch->download).
135     sprintf("%-30s", $cust_pay_batch->cust_main->first . ' ' .
136                      $cust_pay_batch->cust_main->last).
137     'E'. # English
138     ' '.
139     sprintf("%-15s", $shortname).
140     'CAD'.
141     ' '.
142     'CAN'.
143     '    '.
144     'N' # no customer optional information follows
145     ;
146 # Note: IAT Address Information and Remittance records are not 
147 # supported. This means you probably can't process payments 
148 # destined to U.S. bank accounts.  If you need this feature, contact 
149 # Freeside Internet Services.
150   },
151   footer => sub {
152     my ($pay_batch, $batchcount, $batchtotal) = @_;
153     sprintf("%06u", $i + 1).
154     'Z'.
155     'TRL'.
156     sprintf("%10s", $client_num).
157     '0' x 20 .
158     sprintf("%06u", $batchcount).
159     sprintf("%014.0f", $batchtotal*100).
160     '00' .
161     '000000' . # total number of customer information records
162     ' ' x 84
163     ;
164   },
165 );
166
167 1;
168