manual echeck payment type, RT#26995
[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 sub paycvv {
81   my($self,$paycvv) = @_;
82   # This is only allowed in cust_main... Even then it really shouldn't be stored...
83   if ($self->table eq 'cust_main') {
84     if ( defined($paycvv) ) {
85       $self->setfield('paycvv', $paycvv); # This is okay since we are the 'setter'
86     } else {
87       $paycvv = $self->getfield('paycvv'); # This is okay since we are the 'getter'
88       return $paycvv;
89     }
90   } else {
91 #    warn "This doesn't work for other tables besides cust_main
92     '';
93   } 
94 }
95
96 =item paymask
97
98 =cut
99
100 sub paymask {
101   my($self, $paymask) = @_;
102
103   if ( defined($paymask) ) {
104     $self->setfield('paymask', $paymask);
105   } else {
106     $self->getfield('paymask') || $self->mask_payinfo;
107   }
108 }
109
110 =back
111
112 =head1 METHODS
113
114 =over 4
115
116 =item mask_payinfo [ PAYBY, PAYINFO ]
117
118 This method converts the payment info (credit card, bank account, etc.) into a
119 masked string.
120
121 Optionally, an arbitrary payby and payinfo can be passed.
122
123 =cut
124
125 sub mask_payinfo {
126   my $self = shift;
127   my $payby   = scalar(@_) ? shift : $self->payby;
128   my $payinfo = scalar(@_) ? shift : $self->payinfo;
129
130   # Check to see if it's encrypted...
131   if ( ref($self) && $self->is_encrypted($payinfo) ) {
132     return 'N/A';
133   } elsif ( $payinfo =~ /^99\d{14}$/ || $payinfo eq 'N/A' ) { #token
134     return 'N/A (tokenized)'; #?
135   } else { # if not, mask it...
136
137     if ($payby eq 'CARD' || $payby eq 'DCRD') {
138                                                 #|| $payby eq 'MCRD') {
139                                                 #MCRD isn't a card in payinfo,
140                                                 #its a record of an _offline_
141                                                 #card
142
143       # Credit Cards
144
145       # special handling for Local Isracards: always show last 4 
146       if ( $payinfo =~ /^(\d{8,9})$/ ) {
147
148         return 'x'x(length($payinfo)-4).
149                substr($payinfo,(length($payinfo)-4));
150
151       }
152
153       my $conf = new FS::Conf;
154       my $mask_method = $conf->config('card_masking_method') || 'first6last4';
155       $mask_method =~ /^first(\d+)last(\d+)$/
156         or die "can't parse card_masking_method $mask_method";
157       my($first, $last) = ($1, $2);
158
159       return substr($payinfo,0,$first).
160              'x'x(length($payinfo)-$first-$last).
161              substr($payinfo,(length($payinfo)-$last));
162
163     } elsif ($payby eq 'CHEK' || $payby eq 'DCHK' ) {
164
165       # Checks (Show last 2 @ bank)
166       my( $account, $aba ) = split('@', $payinfo );
167       return 'x'x(length($account)-2).
168              substr($account,(length($account)-2)).
169              ( length($aba) ? "@".$aba : '');
170
171     } elsif ($payby eq 'EDI') {
172       # EDI.
173       # These numbers have been seen anywhere from 8 to 30 digits, and 
174       # possibly more.  Lacking any better idea I'm going to mask all but
175       # the last 4 digits.
176       return 'x' x (length($payinfo) - 4) . substr($payinfo, -4);
177
178     } else { # Tie up loose ends
179       return $payinfo;
180     }
181   }
182   #die "shouldn't be reached";
183 }
184
185 =item payinfo_check
186
187 Checks payby and payinfo.
188
189 =cut
190
191 sub payinfo_check {
192   my $self = shift;
193
194   FS::payby->can_payby($self->table, $self->payby)
195     or return "Illegal payby: ". $self->payby;
196
197   if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
198     my $payinfo = $self->payinfo;
199     if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
200       # allow it
201     } else {
202       $payinfo =~ s/\D//g;
203       $self->payinfo($payinfo);
204       if ( $self->payinfo ) {
205         $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
206           or return "Illegal (mistyped?) credit card number (payinfo)";
207         $self->payinfo($1);
208         validate($self->payinfo) or return "Illegal credit card number";
209         return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
210                                    && cardtype($self->payinfo) eq "Unknown";
211       } else {
212         $self->payinfo('N/A'); #???
213       }
214     }
215   } else {
216     if ( $self->is_encrypted($self->payinfo) ) {
217       #something better?  all it would cause is a decryption error anyway?
218       my $error = $self->ut_anything('payinfo');
219       return $error if $error;
220     } else {
221       my $error = $self->ut_textn('payinfo');
222       return $error if $error;
223     }
224   }
225
226 }
227
228 =item payby_payinfo_pretty [ LOCALE ]
229
230 Returns payment method and information (suitably masked, if applicable) as
231 a human-readable string, such as:
232
233   Card #54xxxxxxxxxxxx32
234
235 or
236
237   Check #119006
238
239 =cut
240
241 sub payby_payinfo_pretty {
242   my $self = shift;
243   my $locale = shift;
244   my $lh = FS::L10N->get_handle($locale);
245   if ( $self->payby eq 'CARD' ) {
246     $lh->maketext('Card #') . $self->paymask;
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 =back
307
308 =head1 BUGS
309
310 =head1 SEE ALSO
311
312 L<FS::payby>, L<FS::Record>
313
314 =cut
315
316 1;
317