1 package FS::pay_batch::eft_canada;
4 use vars qw(@ISA %import_info %export_info $name);
5 use FS::Record 'qsearch';
7 use FS::cust_pay_batch;
15 %import_info = ( filetype => 'NONE' ); # see FS/bin/freeside-eftca-download
17 my ($business_trans_code, $personal_trans_code, $trans_code);
18 my $req_date; # requested process date, in %D format
20 # use Date::Holidays::CA for this?
21 #ref http://gocanada.about.com/od/canadatravelplanner/a/canada_holidays.htm
22 my %holiday_yearly = (
23 1 => { map {$_=>1} 1 }, #new year's
24 7 => { map {$_=>1} 1 }, #canada day
25 11 => { map {$_=>1} 11 }, #remembrance day
26 12 => { map {$_=>1} 25 }, #christmas
27 12 => { map {$_=>1} 26 }, #boxing day
30 2016 => { 2 => { map {$_=>1} 15 }, #family day
31 3 => { map {$_=>1} 25 }, #good friday
32 3 => { map {$_=>1} 28 }, #easter monday
33 5 => { map {$_=>1} 23 }, #victoria day
34 8 => { map {$_=>1} 1 }, #First Monday of August Civic Holiday
35 9 => { map {$_=>1} 5 }, #labour day
36 10 => { map {$_=>1} 10 }, #thanksgiving
38 2017 => { 2 => { map {$_=>1} 20 }, #family day
39 4 => { map {$_=>1} 14 }, #good friday
40 4 => { map {$_=>1} 17 }, #easter monday
41 5 => { map {$_=>1} 22 }, #victoria day
42 8 => { map {$_=>1} 7 }, #First Monday of August Civic Holiday
43 9 => { map {$_=>1} 4 }, #labour day
44 10 => { map {$_=>1} 9 }, #thanksgiving
46 2018 => { 2 => { map {$_=>1} 19 }, #family day
47 3 => { map {$_=>1} 30 }, #good friday
48 4 => { map {$_=>1} 2 }, #easter monday
49 5 => { map {$_=>1} 21 }, #victoria day
50 8 => { map {$_=>1} 6 }, #First Monday of August Civic Holiday
51 9 => { map {$_=>1} 3 }, #labour day
52 10 => { map {$_=>1} 8 }, #thanksgiving
54 2019 => { 2 => { map {$_=>1} 18 }, #family day
55 4 => { map {$_=>1} 19 }, #good friday
56 4 => { map {$_=>1} 22 }, #easter monday
57 5 => { map {$_=>1} 20 }, #victoria day
58 8 => { map {$_=>1} 5 }, #First Monday of August Civic Holiday
59 9 => { map {$_=>1} 2 }, #labour day
60 10 => { map {$_=>1} 14 }, #thanksgiving
62 2020 => { 2 => { map {$_=>1} 17 }, #family day
63 4 => { map {$_=>1} 10 }, #good friday
64 4 => { map {$_=>1} 13 }, #easter monday
65 5 => { map {$_=>1} 18 }, #victoria day
66 8 => { map {$_=>1} 3 }, #First Monday of August Civic Holiday
67 9 => { map {$_=>1} 7 }, #labour day
68 10 => { map {$_=>1} 12 }, #thanksgiving
70 2021 => { 2 => { map {$_=>1} 15 }, #family day
71 4 => { map {$_=>1} 2 }, #good friday
72 4 => { map {$_=>1} 5 }, #easter monday
73 5 => { map {$_=>1} 24 }, #victoria day
74 8 => { map {$_=>1} 2 }, #First Monday of August Civic Holiday
75 9 => { map {$_=>1} 6 }, #labour day
76 10 => { map {$_=>1} 11 }, #thanksgiving
78 2022 => { 2 => { map {$_=>1} 21 }, #family day
79 4 => { map {$_=>1} 15 }, #good friday
80 4 => { map {$_=>1} 18 }, #easter monday
81 5 => { map {$_=>1} 23 }, #victoria day
82 8 => { map {$_=>1} 1 }, #First Monday of August Civic Holiday
83 9 => { map {$_=>1} 5 }, #labour day
84 10 => { map {$_=>1} 10 }, #thanksgiving
86 2023 => { 2 => { map {$_=>1} 20 }, #family day
87 4 => { map {$_=>1} 7 }, #good friday
88 4 => { map {$_=>1} 10 }, #easter monday
89 5 => { map {$_=>1} 22 }, #victoria day
90 8 => { map {$_=>1} 7 }, #First Monday of August Civic Holiday
91 9 => { map {$_=>1} 4 }, #labour day
92 10 => { map {$_=>1} 9 }, #thanksgiving
94 2024 => { 2 => { map {$_=>1} 19 }, #family day
95 3 => { map {$_=>1} 29 }, #good friday
96 4 => { map {$_=>1} 1 }, #easter monday
97 5 => { map {$_=>1} 20 }, #victoria day
98 8 => { map {$_=>1} 5 }, #First Monday of August Civic Holiday
99 9 => { map {$_=>1} 2 }, #labour day
100 10 => { map {$_=>1} 14 }, #thanksgiving
102 2025 => { 2 => { map {$_=>1} 17 }, #family day
103 4 => { map {$_=>1} 18 }, #good friday
104 4 => { map {$_=>1} 21 }, #easter monday
105 5 => { map {$_=>1} 19 }, #victoria day
106 8 => { map {$_=>1} 4 }, #First Monday of August Civic Holiday
107 9 => { map {$_=>1} 1 }, #labour day
108 10 => { map {$_=>1} 13 }, #thanksgiving
114 return 1 if exists( $holiday_yearly{$dt->month} )
115 and exists( $holiday_yearly{$dt->month}{$dt->day} );
116 return 1 if exists( $holiday{$dt->year} )
117 and exists( $holiday{$dt->year}{$dt->month} )
118 and exists( $holiday{$dt->year}{$dt->month}{$dt->day} );
126 my $agentnum = shift;
128 if ( $conf->exists('batch-spoolagent') ) {
129 @config = $conf->config('batchconfig-eft_canada', $agentnum);
131 @config = $conf->config('batchconfig-eft_canada');
133 # SFTP login, password, business and personal trans codes, delay time
134 ($business_trans_code) = $config[2];
135 ($personal_trans_code) = $config[3];
137 my ($process_date) = process_dates($conf, $agentnum);
138 $req_date = $process_date->strftime('%D');
141 delimiter => '', # avoid blank lines for header/footer
143 # EFT Upload Specification for .CSV Files, Rev. 2.0
144 # not a true CSV format--strings aren't quoted, so be careful
146 my ($cust_pay_batch, $pay_batch) = @_;
148 # company + empty or first + last
149 my $company = sprintf('%.64s', $cust_pay_batch->cust_main->company);
151 push @fields, 'Business';
152 push @fields, $company, '';
153 $trans_code = $business_trans_code;
156 push @fields, 'Personal';
157 push @fields, map { sprintf('%.64s', $_) }
158 $cust_pay_batch->first, $cust_pay_batch->last;
159 $trans_code = $personal_trans_code;
161 my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
162 my($bankno, $branch);
163 if ( $aba =~ /^0(\d{3})(\d{5})$/ ) { # standard format for Canadian bank ID
164 ($bankno, $branch) = ( $1, $2 );
165 } elsif ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #how we store branches
166 ($branch, $bankno) = ( $1, $2 );
168 die "invalid branch/routing number '$aba'\n";
170 push @fields, sprintf('%05s', $branch),
171 sprintf('%03s', $bankno),
173 sprintf('%.02f', $cust_pay_batch->amount);
175 push @fields, 'DB', $trans_code, $req_date;
176 push @fields, $cust_pay_batch->paybatchnum; # reference
177 # strip illegal characters that might occur in customer name
178 s/[,|']//g foreach @fields; # better substitution for these?
179 return join(',', @fields) . "\n";
184 sub download_note { # is a class method
186 my $pay_batch = shift;
187 my $conf = FS::Conf->new;
188 my $agentnum = $pay_batch->agentnum;
189 my ($process_date, $upload_date) = process_dates($conf, $agentnum);
190 my $date_format = $conf->config('date_format') || '%D';
191 my $days_until_upload = $upload_date->delta_days(DateTime->now);
194 if ( $days_until_upload->days == 0 ) {
195 $note = 'Upload this file before 11:00 AM today'.
196 ' (' . $upload_date->strftime($date_format) . '). ';
197 } elsif ( $days_until_upload->days == 1 ) {
198 $note = 'Upload this file before 11:00 AM tomorrow'.
199 ' (' . $upload_date->strftime($date_format) . '). ';
201 $note = 'Upload this file before 11:00 AM on '.
202 $upload_date->strftime($date_format) . '. ';
204 $note .= 'Payments will be processed on '.
205 $process_date->strftime($date_format) . '.';
210 sub process_dates { # returns both process and upload dates
211 my ($conf, $agentnum) = @_;
213 if ( $conf->exists('batch-spoolagent') ) {
214 @config = $conf->config('batchconfig-eft_canada', $agentnum);
216 @config = $conf->config('batchconfig-eft_canada');
219 my $process_delay = $config[4] || 1;
221 my $ut = DateTime->now; # the latest time we assume the user
222 # could upload the file
223 $ut->truncate(to => 'day')->set_hour(10); # is 10 AM on whatever day
224 if ( $ut < DateTime->now ) {
225 # then we would submit the file today but it's already too late
228 while ( $ut->day_of_week == 6 # Saturday
229 or $ut->day_of_week == 7 # Sunday
235 # $ut is now the latest time that the user can upload the file.
237 # that time, plus the process delay, is the _earliest_ process date we can
238 # request. if that's on a weekend or holiday, the process date has to be
241 my $pt = $ut->clone();
242 $pt->add(days => $process_delay);
243 while ( $pt->day_of_week == 6
244 or $pt->day_of_week == 7