deal with the data we have as best we can without erroring out, RT#71513
[freeside.git] / FS / FS / payinfo_Mixin.pm
1 package FS::payinfo_Mixin;
2
3 use strict;
4 use Business::CreditCard;
5 use FS::payby;
6 use FS::Record qw(qsearch);
7 use FS::UID qw(driver_name);
8 use FS::Cursor;
9 use Time::Local qw(timelocal);
10
11 use vars qw( $ignore_masked_payinfo $allow_closed_replace );
12
13 =head1 NAME
14
15 FS::payinfo_Mixin - Mixin class for records in tables that contain payinfo.  
16
17 =head1 SYNOPSIS
18
19 package FS::some_table;
20 use vars qw(@ISA);
21 @ISA = qw( FS::payinfo_Mixin FS::Record );
22
23 =head1 DESCRIPTION
24
25 This is a mixin class for records that contain payinfo. 
26
27 =head1 FIELDS
28
29 =over 4
30
31 =item payby
32
33 The following payment types (payby) are supported:
34
35 For Customers (cust_main):
36 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
37 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
38 'LECB' (Phone bill billing), 'BILL' (billing), 'COMP' (free), or
39 'PREPAY' (special billing type: applies a credit and sets billing type to I<BILL> - see L<FS::prepay_credit>)
40
41 For Refunds (cust_refund):
42 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
43 'LECB' (Phone bill billing), 'BILL' (billing), 'CASH' (cash),
44 'WEST' (Western Union), 'MCRD' (Manual credit card), 'MCHK' (Manual electronic
45 check), 'CBAK' Chargeback, or 'COMP' (free)
46
47
48 For Payments (cust_pay):
49 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
50 'LECB' (phone bill billing), 'BILL' (billing), 'PREP' (prepaid card),
51 'CASH' (cash), 'WEST' (Western Union), 'MCRD' (Manual credit card), 'MCHK'
52 (Manual electronic check), 'PPAL' (PayPal)
53 'COMP' (free) is depricated as a payment type in cust_pay
54
55 =cut 
56
57 =item payinfo
58
59 Payment information (payinfo) can be one of the following types:
60
61 Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) 
62 prepayment identifier (see L<FS::prepay_credit>), PayPal transaction ID
63
64 =cut
65
66 sub payinfo {
67   my($self,$payinfo) = @_;
68
69   if ( defined($payinfo) ) {
70     $self->setfield('payinfo', $payinfo);
71     $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #token
72   } else {
73     $self->getfield('payinfo');
74   }
75 }
76
77 =item paycvv
78
79 Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
80
81 =cut
82
83 sub paycvv {
84   my($self,$paycvv) = @_;
85   # This is only allowed in cust_main... Even then it really shouldn't be stored...
86   if ($self->table eq 'cust_main') {
87     if ( defined($paycvv) ) {
88       $self->setfield('paycvv', $paycvv); # This is okay since we are the 'setter'
89     } else {
90       $paycvv = $self->getfield('paycvv'); # This is okay since we are the 'getter'
91       return $paycvv;
92     }
93   } else {
94 #    warn "This doesn't work for other tables besides cust_main
95     '';
96   } 
97 }
98
99 =item paymask
100
101 =cut
102
103 sub paymask {
104   my($self, $paymask) = @_;
105
106   if ( defined($paymask) ) {
107     $self->setfield('paymask', $paymask);
108   } else {
109     $self->getfield('paymask') || $self->mask_payinfo;
110   }
111 }
112
113 =back
114
115 =head1 METHODS
116
117 =over 4
118
119 =item mask_payinfo [ PAYBY, PAYINFO ]
120
121 This method converts the payment info (credit card, bank account, etc.) into a
122 masked string.
123
124 Optionally, an arbitrary payby and payinfo can be passed.
125
126 =cut
127
128 sub mask_payinfo {
129   my $self = shift;
130   my $payby   = scalar(@_) ? shift : $self->payby;
131   my $payinfo = scalar(@_) ? shift : $self->payinfo;
132
133   # Check to see if it's encrypted...
134   if ( ref($self) && $self->is_encrypted($payinfo) ) {
135     return 'N/A';
136   } elsif ( $payinfo =~ /^99\d{14}$/ || $payinfo eq 'N/A' ) { #token
137     return 'N/A (tokenized)'; #?
138   } else { # if not, mask it...
139
140     if ($payby eq 'CARD' || $payby eq 'DCRD') {
141                                                 #|| $payby eq 'MCRD') {
142                                                 #MCRD isn't a card in payinfo,
143                                                 #its a record of an _offline_
144                                                 #card
145
146       # Credit Cards
147
148       # special handling for Local Isracards: always show last 4 
149       if ( $payinfo =~ /^(\d{8,9})$/ ) {
150
151         return 'x'x(length($payinfo)-4).
152                substr($payinfo,(length($payinfo)-4));
153
154       }
155
156       my $conf = new FS::Conf;
157       my $mask_method = $conf->config('card_masking_method') || 'first6last4';
158       $mask_method =~ /^first(\d+)last(\d+)$/
159         or die "can't parse card_masking_method $mask_method";
160       my($first, $last) = ($1, $2);
161
162       return substr($payinfo,0,$first).
163              'x'x(length($payinfo)-$first-$last).
164              substr($payinfo,(length($payinfo)-$last));
165
166     } elsif ($payby eq 'CHEK' || $payby eq 'DCHK' ) {
167
168       # Checks (Show last 2 @ bank)
169       my( $account, $aba ) = split('@', $payinfo );
170       return 'x'x(length($account)-2).
171              substr($account,(length($account)-2)).
172              ( length($aba) ? "@".$aba : '');
173
174     } elsif ($payby eq 'EDI') {
175       # EDI.
176       # These numbers have been seen anywhere from 8 to 30 digits, and 
177       # possibly more.  Lacking any better idea I'm going to mask all but
178       # the last 4 digits.
179       return 'x' x (length($payinfo) - 4) . substr($payinfo, -4);
180
181     } else { # Tie up loose ends
182       return $payinfo;
183     }
184   }
185   #die "shouldn't be reached";
186 }
187
188 =item payinfo_check
189
190 Checks payby and payinfo.
191
192 =cut
193
194 sub payinfo_check {
195   my $self = shift;
196
197   FS::payby->can_payby($self->table, $self->payby)
198     or return "Illegal payby: ". $self->payby;
199
200   if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
201
202     unless ( $self->paycardtype ) {
203
204       if ( $self->payinfo =~ /^99\d{14}$/ ) {
205         if ( $self->paymask =~ /^\d+x/ ) {
206           $self->set('paycardtype', cardtype($self->paymask));
207         } else {
208           $self->set('paycardtype', '');
209          # return "paycardtype required ".
210          #        "(can't derive from a token and no paymask w/prefix provided)";
211         }
212       } else {
213         $self->set('paycardtype', cardtype($self->payinfo));
214       }
215
216      }
217
218     if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
219       # allow it
220     } else {
221       my $payinfo = $self->payinfo;
222       $payinfo =~ s/\D//g;
223       $self->payinfo($payinfo);
224       if ( $self->payinfo ) {
225         $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
226           or return "Illegal (mistyped?) credit card number (payinfo)";
227         $self->payinfo($1);
228         validate($self->payinfo) or return "Illegal credit card number";
229         return "Unknown card type" if $self->paycardtype eq "Unknown";
230       } else {
231         $self->payinfo('N/A'); #???
232       }
233     }
234
235   } else {
236
237     unless ( $self->paycardtype ) {
238
239       if ( $self->payby eq 'CARD' && $self->paymask =~ /^\d+x/  ) {
240         # if we can't decrypt the card, at least detect the cardtype
241         $self->set('paycardtype', cardtype($self->paymask));
242       } else {
243         $self->set('paycardtype', '');
244         # return "paycardtype required ".
245         #        "(can't derive from a token and no paymask w/prefix provided)";
246       }
247
248     }
249
250     if ( $self->is_encrypted($self->payinfo) ) {
251       #something better?  all it would cause is a decryption error anyway?
252       my $error = $self->ut_anything('payinfo');
253       return $error if $error;
254     } else {
255       my $error = $self->ut_textn('payinfo');
256       return $error if $error;
257     }
258   }
259
260   return '';
261 }
262
263 =item payby_payinfo_pretty [ LOCALE ]
264
265 Returns payment method and information (suitably masked, if applicable) as
266 a human-readable string, such as:
267
268   Card #54xxxxxxxxxxxx32
269
270 or
271
272   Check #119006
273
274 =cut
275
276 sub payby_payinfo_pretty {
277   my $self = shift;
278   my $locale = shift;
279   my $lh = FS::L10N->get_handle($locale);
280   if ( $self->payby eq 'CARD' ) {
281     if ($self->paymask =~ /tokenized/) {
282       $lh->maketext('Tokenized Card');
283     } else {
284       $lh->maketext('Card #') . $self->paymask;
285     }
286   } elsif ( $self->payby eq 'CHEK' ) {
287
288     #false laziness w/view/cust_main/payment_history.html::translate_payinfo
289     my( $account, $aba ) = split('@', $self->paymask );
290
291     if ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #blame canada
292       my($branch, $routing) = ($1, $2);
293       $lh->maketext("Routing [_1], Branch [_2], Acct [_3]",
294                      $routing, $branch, $account);
295     } else {
296       $lh->maketext("Routing [_1], Acct [_2]", $aba, $account);
297     }
298
299   } elsif ( $self->payby eq 'BILL' ) {
300     $lh->maketext('Check #') . $self->payinfo;
301   } elsif ( $self->payby eq 'PREP' ) {
302     $lh->maketext('Prepaid card #') . $self->payinfo;
303   } elsif ( $self->payby eq 'CASH' ) {
304     $lh->maketext('Cash') . ' ' . $self->payinfo;
305   } elsif ( $self->payby eq 'WEST' ) {
306     # does Western Union localize their name?
307     $lh->maketext('Western Union');
308   } elsif ( $self->payby eq 'MCRD' ) {
309     $lh->maketext('Manual credit card');
310   } elsif ( $self->payby eq 'MCHK' ) {
311     $lh->maketext('Manual electronic check');
312   } elsif ( $self->payby eq 'EDI' ) {
313     $lh->maketext('EDI') . ' ' . $self->paymask;
314   } elsif ( $self->payby eq 'PPAL' ) {
315     $lh->maketext('PayPal transaction#') . $self->order_number;
316   } else {
317     $self->payby. ' '. $self->payinfo;
318   }
319 }
320
321 =item payinfo_used [ PAYINFO ]
322
323 Returns 1 if there's an existing payment using this payinfo.  This can be 
324 used to set the 'recurring payment' flag required by some processors.
325
326 =cut
327
328 sub payinfo_used {
329   my $self = shift;
330   my $payinfo = shift || $self->payinfo;
331   my %hash = (
332     'custnum' => $self->custnum,
333     'payby'   => $self->payby,
334   );
335
336   return 1
337   if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
338   || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo } )
339   ;
340
341   return 0;
342 }
343
344 =item upgrade_set_cardtype
345
346 Find all records with a credit card payment type and no paycardtype, and
347 replace them in order to set their paycardtype.
348
349 =cut
350
351 sub upgrade_set_cardtype {
352   my $class = shift;
353   # assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype
354   # will do this. ignore any problems with the cards.
355   local $ignore_masked_payinfo = 1;
356   my $search = FS::Cursor->new({
357     table     => $class->table,
358     extra_sql => q[ WHERE payby IN('CARD','DCRD') AND paycardtype IS NULL ],
359   });
360   while (my $record = $search->fetch) {
361     my $error = $record->replace;
362     die $error if $error;
363   }
364 }
365
366 =back
367
368 =head1 BUGS
369
370 =head1 SEE ALSO
371
372 L<FS::payby>, L<FS::Record>
373
374 =cut
375
376 1;
377