allow services with a tower but no sector to appear in search results, #33056
[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     $lh->maketext('Card #') . $self->paymask;
243   } elsif ( $self->payby eq 'CHEK' ) {
244
245     #false laziness w/view/cust_main/payment_history.html::translate_payinfo
246     my( $account, $aba ) = split('@', $self->paymask );
247
248     if ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #blame canada
249       my($branch, $routing) = ($1, $2);
250       $lh->maketext("Routing [_1], Branch [_2], Acct [_3]",
251                      $routing, $branch, $account);
252     } else {
253       $lh->maketext("Routing [_1], Acct [_2]", $aba, $account);
254     }
255
256   } elsif ( $self->payby eq 'BILL' ) {
257     $lh->maketext('Check #') . $self->payinfo;
258   } elsif ( $self->payby eq 'PREP' ) {
259     $lh->maketext('Prepaid card #') . $self->payinfo;
260   } elsif ( $self->payby eq 'CASH' ) {
261     $lh->maketext('Cash') . ' ' . $self->payinfo;
262   } elsif ( $self->payby eq 'WEST' ) {
263     # does Western Union localize their name?
264     $lh->maketext('Western Union');
265   } elsif ( $self->payby eq 'MCRD' ) {
266     $lh->maketext('Manual credit card');
267   } elsif ( $self->payby eq 'MCHK' ) {
268     $lh->maketext('Manual electronic check');
269   } elsif ( $self->payby eq 'EDI' ) {
270     $lh->maketext('EDI') . ' ' . $self->paymask;
271   } elsif ( $self->payby eq 'PPAL' ) {
272     $lh->maketext('PayPal transaction#') . $self->order_number;
273   } else {
274     $self->payby. ' '. $self->payinfo;
275   }
276 }
277
278 =item payinfo_used [ PAYINFO ]
279
280 Returns 1 if there's an existing payment using this payinfo.  This can be 
281 used to set the 'recurring payment' flag required by some processors.
282
283 =cut
284
285 sub payinfo_used {
286   my $self = shift;
287   my $payinfo = shift || $self->payinfo;
288   my %hash = (
289     'custnum' => $self->custnum,
290     'payby'   => 'CARD',
291   );
292
293   return 1
294   if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
295   || qsearch('cust_pay', 
296     { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) }  )
297   ;
298
299   return 0;
300 }
301
302 =item display_status
303
304 For transactions that have both 'status' and 'failure_status', shows the
305 status in a single, display-friendly string.
306
307 =cut
308
309 sub display_status {
310   my $self = shift;
311   my %status = (
312     'done'        => 'Approved',
313     'expired'     => 'Card Expired',
314     'stolen'      => 'Lost/Stolen',
315     'pickup'      => 'Pick Up Card',
316     'nsf'         => 'Insufficient Funds',
317     'inactive'    => 'Inactive Account',
318     'blacklisted' => 'Blacklisted',
319     'declined'    => 'Declined',
320     'approved'    => 'Approved',
321   );
322   if ( $self->failure_status ) {
323     return $status{$self->failure_status};
324   } else {
325     return $status{$self->status};
326   }
327 }
328
329 =back
330
331 =head1 BUGS
332
333 =head1 SEE ALSO
334
335 L<FS::payby>, L<FS::Record>
336
337 =cut
338
339 1;
340