multiple payment options, RT#23741
[freeside.git] / FS / FS / cust_payby.pm
1 package FS::cust_payby;
2
3 use strict;
4 use base qw( FS::payinfo_Mixin FS::Record );
5 use FS::UID;
6 use FS::Record qw( qsearchs ); #qsearch;
7 use FS::payby;
8 use FS::cust_main;
9
10 use vars qw( $conf $ignore_expired_card $ignore_banned_card  );
11
12 $ignore_expired_card = 0;
13 $ignore_banned_card = 0;
14
15 install_callback FS::UID sub { 
16   $conf = new FS::Conf;
17   #yes, need it for stuff below (prolly should be cached)
18 };
19
20 =head1 NAME
21
22 FS::cust_payby - Object methods for cust_payby records
23
24 =head1 SYNOPSIS
25
26   use FS::cust_payby;
27
28   $record = new FS::cust_payby \%hash;
29   $record = new FS::cust_payby { 'column' => 'value' };
30
31   $error = $record->insert;
32
33   $error = $new_record->replace($old_record);
34
35   $error = $record->delete;
36
37   $error = $record->check;
38
39 =head1 DESCRIPTION
40
41 An FS::cust_payby object represents customer stored payment information.
42 FS::cust_payby inherits from FS::Record.  The following fields are currently
43 supported:
44
45 =over 4
46
47 =item custpaybynum
48
49 primary key
50
51 =item custnum
52
53 custnum
54
55 =item weight
56
57 weight
58
59 =item payby
60
61 payby
62
63 =item payinfo
64
65 payinfo
66
67 =item paycvv
68
69 paycvv
70
71 =item paymask
72
73 paymask
74
75 =item paydate
76
77 paydate
78
79 =item paystart_month
80
81 paystart_month
82
83 =item paystart_year
84
85 paystart_year
86
87 =item payissue
88
89 payissue
90
91 =item payname
92
93 payname
94
95 =item paystate
96
97 paystate
98
99 =item paytype
100
101 paytype
102
103 =item payip
104
105 payip
106
107
108 =back
109
110 =head1 METHODS
111
112 =over 4
113
114 =item new HASHREF
115
116 Creates a new record.  To add the record to the database, see L<"insert">.
117
118 Note that this stores the hash reference, not a distinct copy of the hash it
119 points to.  You can ask the object for a copy with the I<hash> method.
120
121 =cut
122
123 # the new method can be inherited from FS::Record, if a table method is defined
124
125 sub table { 'cust_payby'; }
126
127 =item insert
128
129 Adds this record to the database.  If there is an error, returns the error,
130 otherwise returns false.
131
132 =cut
133
134 # the insert method can be inherited from FS::Record
135
136 =item delete
137
138 Delete this record from the database.
139
140 =cut
141
142 # the delete method can be inherited from FS::Record
143
144 =item replace OLD_RECORD
145
146 Replaces the OLD_RECORD with this one in the database.  If there is an error,
147 returns the error, otherwise returns false.
148
149 =cut
150
151 # the replace method can be inherited from FS::Record
152
153 =item check
154
155 Checks all fields to make sure this is a valid record.  If there is
156 an error, returns the error, otherwise returns false.  Called by the insert
157 and replace methods.
158
159 =cut
160
161 sub check {
162   my $self = shift;
163
164   my $error = 
165     $self->ut_numbern('custpaybynum')
166     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
167     || $self->ut_number('weight')
168     || $self->ut_('payby')
169     || $self->ut_textn('payinfo')
170     || $self->ut_textn('paycvv')
171     || $self->ut_textn('paymask')
172     || $self->ut_textn('paydate')
173     || $self->ut_numbern('paystart_month')
174     || $self->ut_numbern('paystart_year')
175     || $self->ut_textn('payissue')
176     || $self->ut_textn('payname')
177     || $self->ut_textn('paystate')
178     || $self->ut_textn('paytype')
179     || $self->ut_textn('payip')
180   ;
181   return $error if $error;
182
183
184   ### from cust_main
185
186
187   #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
188   #  or return "Illegal payby: ". $self->payby;
189   #$self->payby($1);
190   FS::payby->can_payby($self->table, $self->payby)
191     or return "Illegal payby: ". $self->payby;
192
193   $error =    $self->ut_numbern('paystart_month')
194            || $self->ut_numbern('paystart_year')
195            || $self->ut_numbern('payissue')
196            || $self->ut_textn('paytype')
197   ;
198   return $error if $error;
199
200   if ( $self->payip eq '' ) {
201     $self->payip('');
202   } else {
203     $error = $self->ut_ip('payip');
204     return $error if $error;
205   }
206
207   # If it is encrypted and the private key is not availaible then we can't
208   # check the credit card.
209   my $check_payinfo = ! $self->is_encrypted($self->payinfo);
210
211   # Need some kind of global flag to accept invalid cards, for testing
212   # on scrubbed data.
213   #XXX if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
214   if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
215
216     my $payinfo = $self->payinfo;
217     $payinfo =~ s/\D//g;
218     $payinfo =~ /^(\d{13,16}|\d{8,9})$/
219       or return gettext('invalid_card'); # . ": ". $self->payinfo;
220     $payinfo = $1;
221     $self->payinfo($payinfo);
222     validate($payinfo)
223       or return gettext('invalid_card'); # . ": ". $self->payinfo;
224
225     return gettext('unknown_card_type')
226       if $self->payinfo !~ /^99\d{14}$/ #token
227       && cardtype($self->payinfo) eq "Unknown";
228
229     unless ( $ignore_banned_card ) {
230       my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
231       if ( $ban ) {
232         if ( $ban->bantype eq 'warn' ) {
233           #or others depending on value of $ban->reason ?
234           return '_duplicate_card'.
235                  ': disabled from'. time2str('%a %h %o at %r', $ban->_date).
236                  ' until '.         time2str('%a %h %o at %r', $ban->_end_date).
237                  ' (ban# '. $ban->bannum. ')'
238             unless $self->override_ban_warn;
239         } else {
240           return 'Banned credit card: banned on '.
241                  time2str('%a %h %o at %r', $ban->_date).
242                  ' by '. $ban->otaker.
243                  ' (ban# '. $ban->bannum. ')';
244         }
245       }
246     }
247
248     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
249       if ( cardtype($self->payinfo) eq 'American Express card' ) {
250         $self->paycvv =~ /^(\d{4})$/
251           or return "CVV2 (CID) for American Express cards is four digits.";
252         $self->paycvv($1);
253       } else {
254         $self->paycvv =~ /^(\d{3})$/
255           or return "CVV2 (CVC2/CID) is three digits.";
256         $self->paycvv($1);
257       }
258     } else {
259       $self->paycvv('');
260     }
261
262     my $cardtype = cardtype($payinfo);
263     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
264
265       return "Start date or issue number is required for $cardtype cards"
266         unless $self->paystart_month && $self->paystart_year or $self->payissue;
267
268       return "Start month must be between 1 and 12"
269         if $self->paystart_month
270            and $self->paystart_month < 1 || $self->paystart_month > 12;
271
272       return "Start year must be 1990 or later"
273         if $self->paystart_year
274            and $self->paystart_year < 1990;
275
276       return "Issue number must be beween 1 and 99"
277         if $self->payissue
278           and $self->payissue < 1 || $self->payissue > 99;
279
280     } else {
281       $self->paystart_month('');
282       $self->paystart_year('');
283       $self->payissue('');
284     }
285
286   } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
287
288     my $payinfo = $self->payinfo;
289     $payinfo =~ s/[^\d\@\.]//g;
290     if ( $conf->config('echeck-country') eq 'CA' ) {
291       $payinfo =~ /^(\d+)\@(\d{5})\.(\d{3})$/
292         or return 'invalid echeck account@branch.bank';
293       $payinfo = "$1\@$2.$3";
294     } elsif ( $conf->config('echeck-country') eq 'US' ) {
295       $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
296       $payinfo = "$1\@$2";
297     } else {
298       $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@routing';
299       $payinfo = "$1\@$2";
300     }
301     $self->payinfo($payinfo);
302     $self->paycvv('');
303
304     unless ( $ignore_banned_card ) {
305       my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
306       if ( $ban ) {
307         if ( $ban->bantype eq 'warn' ) {
308           #or others depending on value of $ban->reason ?
309           return '_duplicate_ach' unless $self->override_ban_warn;
310         } else {
311           return 'Banned ACH account: banned on '.
312                  time2str('%a %h %o at %r', $ban->_date).
313                  ' by '. $ban->otaker.
314                  ' (ban# '. $ban->bannum. ')';
315         }
316       }
317     }
318
319   } elsif ( $self->payby eq 'LECB' ) {
320
321     my $payinfo = $self->payinfo;
322     $payinfo =~ s/\D//g;
323     $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
324     $payinfo = $1;
325     $self->payinfo($payinfo);
326     $self->paycvv('');
327
328   } elsif ( $self->payby eq 'BILL' ) {
329
330     $error = $self->ut_textn('payinfo');
331     return "Illegal P.O. number: ". $self->payinfo if $error;
332     $self->paycvv('');
333
334   } elsif ( $self->payby eq 'COMP' ) {
335
336     my $curuser = $FS::CurrentUser::CurrentUser;
337     if (    ! $self->custnum
338          && ! $curuser->access_right('Complimentary customer')
339        )
340     {
341       return "You are not permitted to create complimentary accounts."
342     }
343
344     $error = $self->ut_textn('payinfo');
345     return "Illegal comp account issuer: ". $self->payinfo if $error;
346     $self->paycvv('');
347
348   } elsif ( $self->payby eq 'PREPAY' ) {
349
350     my $payinfo = $self->payinfo;
351     $payinfo =~ s/\W//g; #anything else would just confuse things
352     $self->payinfo($payinfo);
353     $error = $self->ut_alpha('payinfo');
354     return "Illegal prepayment identifier: ". $self->payinfo if $error;
355     return "Unknown prepayment identifier"
356       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
357     $self->paycvv('');
358
359   }
360
361   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
362     return "Expiration date required"
363       # shouldn't payinfo_check do this?
364       unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
365     $self->paydate('');
366   } else {
367     my( $m, $y );
368     if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
369       ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
370     } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
371       ( $m, $y ) = ( $2, "19$1" );
372     } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
373       ( $m, $y ) = ( $3, "20$2" );
374     } else {
375       return "Illegal expiration date: ". $self->paydate;
376     }
377     $m = sprintf('%02d',$m);
378     $self->paydate("$y-$m-01");
379     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
380     return gettext('expired_card')
381       if #XXX !$import
382       #&&
383          !$ignore_expired_card 
384       && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
385   }
386
387   if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
388        ( ! $conf->exists('require_cardname')
389          || $self->payby !~ /^(CARD|DCRD)$/  ) 
390   ) {
391     $self->payname( $self->first. " ". $self->getfield('last') );
392   } else {
393     $self->payname =~ /^([\w \,\.\-\'\&]+)$/
394       or return gettext('illegal_name'). " payname: ". $self->payname;
395     $self->payname($1);
396   }
397
398   ###
399
400   $self->SUPER::check;
401 }
402
403 =back
404
405 =head1 BUGS
406
407 =head1 SEE ALSO
408
409 L<FS::Record>, schema.html from the base documentation.
410
411 =cut
412
413 1;
414