78f1e964f4126db1dea3be92766919bd33ca0743
[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   11 => { map {$_=>1} 11 }, #remembrance day
25   12 => { map {$_=>1} 25 }, #christmas
26   12 => { map {$_=>1} 26 }, #boxing day
27 );
28 my %holiday = (
29   2016 => {  2 => { map {$_=>1} 15 }, #family day
30              3 => { map {$_=>1} 25 }, #good friday
31              3 => { map {$_=>1} 28 }, #easter monday
32              5 => { map {$_=>1} 23 }, #victoria day
33              7 => { map {$_=>1}  1 }, #canada 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              7 => { map {$_=>1}  1 }, #canada day
43              8 => { map {$_=>1}  7 }, #First Monday of August Civic Holiday
44              9 => { map {$_=>1}  4 }, #labour day
45             10 => { map {$_=>1}  9 }, #thanksgiving
46           },
47 );
48
49 sub is_holiday {
50   my $dt = shift;
51   return 1 if exists( $holiday_yearly{$dt->month} )
52           and exists( $holiday_yearly{$dt->month}{$dt->day} );
53   return 1 if exists( $holiday{$dt->year} )
54           and exists( $holiday{$dt->year}{$dt->month} )
55           and exists( $holiday{$dt->year}{$dt->month}{$dt->day} );
56   return 0;
57 }
58
59 %export_info = (
60
61   init => sub {
62     my $conf = shift;
63     my $agentnum = shift;
64     my @config;
65     if ( $conf->exists('batch-spoolagent') ) {
66       @config = $conf->config('batchconfig-eft_canada', $agentnum);
67     } else {
68       @config = $conf->config('batchconfig-eft_canada');
69     }
70     # SFTP login, password, business and personal trans codes, delay time
71     ($business_trans_code) = $config[2];
72     ($personal_trans_code) = $config[3];
73
74     my ($process_date) = process_dates($conf, $agentnum);
75     $req_date = $process_date->strftime('%D');
76   },
77
78   delimiter => '', # avoid blank lines for header/footer
79
80   # EFT Upload Specification for .CSV Files, Rev. 2.0
81   # not a true CSV format--strings aren't quoted, so be careful
82   row => sub {
83     my ($cust_pay_batch, $pay_batch) = @_;
84     my @fields;
85     # company + empty or first + last
86     my $company = sprintf('%.64s', $cust_pay_batch->cust_main->company);
87     if ( $company ) {
88       push @fields, 'Business';
89       push @fields, $company, '';
90       $trans_code = $business_trans_code;
91     }
92     else {
93       push @fields, 'Personal';
94       push @fields, map { sprintf('%.64s', $_) } 
95         $cust_pay_batch->first, $cust_pay_batch->last;
96         $trans_code = $personal_trans_code;
97     }
98     my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
99     my($bankno, $branch);
100     if ( $aba =~ /^0(\d{3})(\d{5})$/ ) { # standard format for Canadian bank ID
101       ($bankno, $branch) = ( $1, $2 );
102     } elsif ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #how we store branches
103       ($branch, $bankno) = ( $1, $2 );
104     } else {
105       die "invalid branch/routing number '$aba'\n";
106     }
107     push @fields, sprintf('%05s', $branch),
108                   sprintf('%03s', $bankno),
109                   $account,
110                   sprintf('%.02f', $cust_pay_batch->amount);
111     # DB = debit
112     push @fields, 'DB', $trans_code, $req_date;
113     push @fields, $cust_pay_batch->paybatchnum; # reference
114     # strip illegal characters that might occur in customer name
115     s/[,|']//g foreach @fields; # better substitution for these?
116     return join(',', @fields) . "\n";
117   },
118
119 );
120
121 sub download_note { # is a class method
122   my $class = shift;
123   my $pay_batch = shift;
124   my $conf = FS::Conf->new;
125   my $agentnum = $pay_batch->agentnum;
126   my ($process_date, $upload_date) = process_dates($conf, $agentnum);
127   my $date_format = $conf->config('date_format') || '%D';
128   my $days_until_upload = $upload_date->delta_days(DateTime->now);
129
130   my $note = '';
131   if ( $days_until_upload->days == 0 ) {
132     $note = 'Upload this file before 11:00 AM today'. 
133             ' (' . $upload_date->strftime($date_format) . '). ';
134   } elsif ( $days_until_upload->days == 1 ) {
135     $note = 'Upload this file before 11:00 AM tomorrow'. 
136             ' (' . $upload_date->strftime($date_format) . '). ';
137   } else {
138     $note = 'Upload this file before 11:00 AM on '.
139       $upload_date->strftime($date_format) . '. ';
140   }
141   $note .= 'Payments will be processed on '.
142     $process_date->strftime($date_format) . '.';
143
144   $note;
145 }
146
147 sub process_dates { # returns both process and upload dates
148   my ($conf, $agentnum) = @_;
149   my @config;
150   if ( $conf->exists('batch-spoolagent') ) {
151     @config = $conf->config('batchconfig-eft_canada', $agentnum);
152   } else {
153     @config = $conf->config('batchconfig-eft_canada');
154   }
155   
156   my $process_delay = $config[4] || 1;
157
158   my $ut = DateTime->now; # the latest time we assume the user
159                           # could upload the file
160   $ut->truncate(to => 'day')->set_hour(10); # is 10 AM on whatever day
161   if ( $ut < DateTime->now ) {
162     # then we would submit the file today but it's already too late
163     $ut->add(days => 1);
164   }
165   while (    $ut->day_of_week == 6 # Saturday
166           or $ut->day_of_week == 7 # Sunday
167           or is_holiday($ut)
168         )
169   {
170     $ut->add(days => 1);
171   }
172   # $ut is now the latest time that the user can upload the file.
173
174   # that time, plus the process delay, is the _earliest_ process date we can
175   # request. if that's on a weekend or holiday, the process date has to be
176   # later.
177
178   my $pt = $ut->clone();
179   $pt->add(days => $process_delay);
180   while (    $pt->day_of_week == 6
181           or $pt->day_of_week == 7
182           or is_holiday($pt)
183         )
184   {
185     $pt->add(days => 1);
186   }
187
188   ($pt, $ut);
189 }
190
191 1;