81e04a42916acc58911ed7ea874402045df4aca1
[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);
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     my $payinfo = $self->payinfo;
202     my $cardtype = cardtype($payinfo);
203     $self->set('cardtype', $cardtype);
204     if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
205       # allow it
206     } else {
207       $payinfo =~ s/\D//g;
208       $self->payinfo($payinfo);
209       if ( $self->payinfo ) {
210         $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
211           or return "Illegal (mistyped?) credit card number (payinfo)";
212         $self->payinfo($1);
213         validate($self->payinfo) or return "Illegal credit card number";
214         return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
215                                    && $cardtype eq "Unknown";
216       } else {
217         $self->payinfo('N/A'); #???
218       }
219     }
220   } else {
221     if ( $self->payby eq 'CARD' and $self->paymask ) {
222       # if we can't decrypt the card, at least detect the cardtype
223       $self->set('cardtype', cardtype($self->paymask));
224     } else {
225       $self->set('cardtype', '');
226     }
227     if ( $self->is_encrypted($self->payinfo) ) {
228       #something better?  all it would cause is a decryption error anyway?
229       my $error = $self->ut_anything('payinfo');
230       return $error if $error;
231     } else {
232       my $error = $self->ut_textn('payinfo');
233       return $error if $error;
234     }
235   }
236
237 }
238
239 =item payby_payinfo_pretty [ LOCALE ]
240
241 Returns payment method and information (suitably masked, if applicable) as
242 a human-readable string, such as:
243
244   Card #54xxxxxxxxxxxx32
245
246 or
247
248   Check #119006
249
250 =cut
251
252 sub payby_payinfo_pretty {
253   my $self = shift;
254   my $locale = shift;
255   my $lh = FS::L10N->get_handle($locale);
256   if ( $self->payby eq 'CARD' ) {
257     if ($self->paymask =~ /tokenized/) {
258       $lh->maketext('Tokenized Card');
259     } else {
260       $lh->maketext('Card #') . $self->paymask;
261     }
262   } elsif ( $self->payby eq 'CHEK' ) {
263
264     #false laziness w/view/cust_main/payment_history.html::translate_payinfo
265     my( $account, $aba ) = split('@', $self->paymask );
266
267     if ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #blame canada
268       my($branch, $routing) = ($1, $2);
269       $lh->maketext("Routing [_1], Branch [_2], Acct [_3]",
270                      $routing, $branch, $account);
271     } else {
272       $lh->maketext("Routing [_1], Acct [_2]", $aba, $account);
273     }
274
275   } elsif ( $self->payby eq 'BILL' ) {
276     $lh->maketext('Check #') . $self->payinfo;
277   } elsif ( $self->payby eq 'PREP' ) {
278     $lh->maketext('Prepaid card #') . $self->payinfo;
279   } elsif ( $self->payby eq 'CASH' ) {
280     $lh->maketext('Cash') . ' ' . $self->payinfo;
281   } elsif ( $self->payby eq 'WEST' ) {
282     # does Western Union localize their name?
283     $lh->maketext('Western Union');
284   } elsif ( $self->payby eq 'MCRD' ) {
285     $lh->maketext('Manual credit card');
286   } elsif ( $self->payby eq 'MCHK' ) {
287     $lh->maketext('Manual electronic check');
288   } elsif ( $self->payby eq 'EDI' ) {
289     $lh->maketext('EDI') . ' ' . $self->paymask;
290   } elsif ( $self->payby eq 'PPAL' ) {
291     $lh->maketext('PayPal transaction#') . $self->order_number;
292   } else {
293     $self->payby. ' '. $self->payinfo;
294   }
295 }
296
297 =item payinfo_used [ PAYINFO ]
298
299 Returns 1 if there's an existing payment using this payinfo.  This can be 
300 used to set the 'recurring payment' flag required by some processors.
301
302 =cut
303
304 sub payinfo_used {
305   my $self = shift;
306   my $payinfo = shift || $self->payinfo;
307   my %hash = (
308     'custnum' => $self->custnum,
309     'payby'   => 'CARD',
310   );
311
312   return 1
313   if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
314   || qsearch('cust_pay', 
315     { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) }  )
316   ;
317
318   return 0;
319 }
320
321 =item upgrade_set_cardtype
322
323 Find all records with a credit card payment type and no cardtype, and
324 replace them in order to set their cardtype.
325
326 =cut
327
328 sub upgrade_set_cardtype {
329   my $class = shift;
330   # assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype
331   # will do this. ignore any problems with the cards.
332   local $ignore_masked_payinfo = 1;
333   my $search = FS::Cursor->new({
334     table     => $class->table,
335     extra_sql => q[ WHERE payby IN('CARD','DCRD') AND cardtype IS NULL ],
336   });
337   while (my $record = $search->fetch) {
338     my $error = $record->replace;
339     die $error if $error;
340   }
341 }
342
343 =back
344
345 =head1 BUGS
346
347 =head1 SEE ALSO
348
349 L<FS::payby>, L<FS::Record>
350
351 =cut
352
353 1;
354