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