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