1 package FS::payinfo_Mixin;
4 use Business::CreditCard;
6 use FS::Record qw(qsearch);
8 use vars qw($ignore_masked_payinfo);
12 FS::payinfo_Mixin - Mixin class for records in tables that contain payinfo.
16 package FS::some_table;
18 @ISA = qw( FS::payinfo_Mixin FS::Record );
22 This is a mixin class for records that contain payinfo.
30 The following payment types (payby) are supported:
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>)
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)
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
56 Payment information (payinfo) can be one of the following types:
58 Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username)
59 prepayment identifier (see L<FS::prepay_credit>), PayPal transaction ID
64 my($self,$payinfo) = @_;
66 if ( defined($payinfo) ) {
67 $self->setfield('payinfo', $payinfo);
68 $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #token
70 $self->getfield('payinfo');
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
80 #this prevents encrypting empty values on insert?
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);
88 $self->getfield('paycvv');
97 my($self, $paymask) = @_;
99 if ( defined($paymask) ) {
100 $self->setfield('paymask', $paymask);
102 $self->getfield('paymask') || $self->mask_payinfo;
112 =item mask_payinfo [ PAYBY, PAYINFO ]
114 This method converts the payment info (credit card, bank account, etc.) into a
117 Optionally, an arbitrary payby and payinfo can be passed.
123 my $payby = scalar(@_) ? shift : $self->payby;
124 my $payinfo = scalar(@_) ? shift : $self->payinfo;
126 # Check to see if it's encrypted...
127 if ( ref($self) && $self->is_encrypted($payinfo) ) {
129 } elsif ( $payinfo =~ /^99\d{14}$/ || $payinfo eq 'N/A' ) { #token
130 return 'N/A (tokenized)'; #?
131 } else { # if not, mask it...
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_
141 # special handling for Local Isracards: always show last 4
142 if ( $payinfo =~ /^(\d{8,9})$/ ) {
144 return 'x'x(length($payinfo)-4).
145 substr($payinfo,(length($payinfo)-4));
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);
155 return substr($payinfo,0,$first).
156 'x'x(length($payinfo)-$first-$last).
157 substr($payinfo,(length($payinfo)-$last));
159 } elsif ($payby eq 'CHEK' || $payby eq 'DCHK' ) {
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 : '');
167 } elsif ($payby eq '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
172 return 'x' x (length($payinfo) - 4) . substr($payinfo, -4);
174 } else { # Tie up loose ends
178 #die "shouldn't be reached";
183 Checks payby and payinfo.
190 FS::payby->can_payby($self->table, $self->payby)
191 or return "Illegal payby: ". $self->payby;
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 ) {
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)";
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";
208 $self->payinfo('N/A'); #???
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;
217 my $error = $self->ut_textn('payinfo');
218 return $error if $error;
224 =item payby_payinfo_pretty [ LOCALE ]
226 Returns payment method and information (suitably masked, if applicable) as
227 a human-readable string, such as:
229 Card #54xxxxxxxxxxxx32
237 sub payby_payinfo_pretty {
240 my $lh = FS::L10N->get_handle($locale);
241 if ( $self->payby eq 'CARD' ) {
242 if ($self->paymask =~ /tokenized/) {
243 $lh->maketext('Tokenized Card');
245 $lh->maketext('Card #') . $self->paymask;
247 } elsif ( $self->payby eq 'CHEK' ) {
249 #false laziness w/view/cust_main/payment_history.html::translate_payinfo
250 my( $account, $aba ) = split('@', $self->paymask );
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);
257 $lh->maketext("Routing [_1], Acct [_2]", $aba, $account);
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;
278 $self->payby. ' '. $self->payinfo;
282 =item payinfo_used [ PAYINFO ]
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.
291 my $payinfo = shift || $self->payinfo;
293 'custnum' => $self->custnum,
298 if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
299 || qsearch('cust_pay',
300 { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) } )
308 For transactions that have both 'status' and 'failure_status', shows the
309 status in a single, display-friendly string.
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',
326 if ( $self->failure_status ) {
327 return $status{$self->failure_status};
329 return $status{$self->status};
339 L<FS::payby>, L<FS::Record>