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