RT# - added Holidays thru 2025
[freeside.git] / FS / FS / pay_batch / eft_canada.pm
1 package FS::pay_batch::eft_canada;
2
3 use strict;
4 use vars qw(@ISA %import_info %export_info $name);
5 use FS::Record 'qsearch';
6 use FS::Conf;
7 use FS::cust_pay_batch;
8 use DateTime;
9
10 my $conf;
11 my $origid;
12
13 $name = 'eft_canada';
14
15 %import_info = ( filetype  => 'NONE' ); # see FS/bin/freeside-eftca-download
16
17 my ($business_trans_code, $personal_trans_code, $trans_code);
18 my $req_date; # requested process date, in %D format
19
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
28 );
29 my %holiday = (
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
37           },
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
45           },
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
53           },
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
61           },
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
69           },
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
77           },
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
85           },
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
93           },
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
101           },
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
109           },
110 );
111
112 sub is_holiday {
113   my $dt = shift;
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} );
119   return 0;
120 }
121
122 %export_info = (
123
124   init => sub {
125     my $conf = shift;
126     my $agentnum = shift;
127     my @config;
128     if ( $conf->exists('batch-spoolagent') ) {
129       @config = $conf->config('batchconfig-eft_canada', $agentnum);
130     } else {
131       @config = $conf->config('batchconfig-eft_canada');
132     }
133     # SFTP login, password, business and personal trans codes, delay time
134     ($business_trans_code) = $config[2];
135     ($personal_trans_code) = $config[3];
136
137     my ($process_date) = process_dates($conf, $agentnum);
138     $req_date = $process_date->strftime('%D');
139   },
140
141   delimiter => '', # avoid blank lines for header/footer
142
143   # EFT Upload Specification for .CSV Files, Rev. 2.0
144   # not a true CSV format--strings aren't quoted, so be careful
145   row => sub {
146     my ($cust_pay_batch, $pay_batch) = @_;
147     my @fields;
148     # company + empty or first + last
149     my $company = sprintf('%.64s', $cust_pay_batch->cust_main->company);
150     if ( $company ) {
151       push @fields, 'Business';
152       push @fields, $company, '';
153       $trans_code = $business_trans_code;
154     }
155     else {
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;
160     }
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 );
167     } else {
168       die "invalid branch/routing number '$aba'\n";
169     }
170     push @fields, sprintf('%05s', $branch),
171                   sprintf('%03s', $bankno),
172                   $account,
173                   sprintf('%.02f', $cust_pay_batch->amount);
174     # DB = debit
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";
180   },
181
182 );
183
184 sub download_note { # is a class method
185   my $class = shift;
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);
192
193   my $note = '';
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) . '). ';
200   } else {
201     $note = 'Upload this file before 11:00 AM on '.
202       $upload_date->strftime($date_format) . '. ';
203   }
204   $note .= 'Payments will be processed on '.
205     $process_date->strftime($date_format) . '.';
206
207   $note;
208 }
209
210 sub process_dates { # returns both process and upload dates
211   my ($conf, $agentnum) = @_;
212   my @config;
213   if ( $conf->exists('batch-spoolagent') ) {
214     @config = $conf->config('batchconfig-eft_canada', $agentnum);
215   } else {
216     @config = $conf->config('batchconfig-eft_canada');
217   }
218   
219   my $process_delay = $config[4] || 1;
220
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
226     $ut->add(days => 1);
227   }
228   while (    $ut->day_of_week == 6 # Saturday
229           or $ut->day_of_week == 7 # Sunday
230           or is_holiday($ut)
231         )
232   {
233     $ut->add(days => 1);
234   }
235   # $ut is now the latest time that the user can upload the file.
236
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
239   # later.
240
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
245           or is_holiday($pt)
246         )
247   {
248     $pt->add(days => 1);
249   }
250
251   ($pt, $ut);
252 }
253
254 1;