1 package FS::payinfo_Mixin;
4 use Business::CreditCard;
6 use FS::Record qw(qsearch);
7 use FS::UID qw(driver_name);
9 use Time::Local qw(timelocal);
11 # allow_closed_replace only relevant to cust_pay/cust_refund, for upgrade tokenizing
12 use vars qw( $ignore_masked_payinfo $allow_closed_replace );
16 FS::payinfo_Mixin - Mixin class for records in tables that contain payinfo.
20 package FS::some_table;
22 @ISA = qw( FS::payinfo_Mixin FS::Record );
26 This is a mixin class for records that contain payinfo.
34 The following payment types (payby) are supported:
36 For Customers (cust_main):
37 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
38 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
39 'LECB' (Phone bill billing), 'BILL' (billing), 'COMP' (free), or
40 'PREPAY' (special billing type: applies a credit and sets billing type to I<BILL> - see L<FS::prepay_credit>)
42 For Refunds (cust_refund):
43 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
44 'LECB' (Phone bill billing), 'BILL' (billing), 'CASH' (cash),
45 'WEST' (Western Union), 'MCRD' (Manual credit card), 'MCHK' (Manual electronic
46 check), 'CBAK' Chargeback, or 'COMP' (free)
49 For Payments (cust_pay):
50 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
51 'LECB' (phone bill billing), 'BILL' (billing), 'PREP' (prepaid card),
52 'CASH' (cash), 'WEST' (Western Union), 'MCRD' (Manual credit card), 'MCHK'
53 (Manual electronic check), 'PPAL' (PayPal)
54 'COMP' (free) is depricated as a payment type in cust_pay
60 Payment information (payinfo) can be one of the following types:
62 Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username)
63 prepayment identifier (see L<FS::prepay_credit>), PayPal transaction ID
68 my($self,$payinfo) = @_;
70 if ( defined($payinfo) ) {
71 $self->paymask($self->mask_payinfo) unless $self->getfield('paymask') || $self->tokenized; #make sure old mask is set
72 $self->setfield('payinfo', $payinfo);
73 $self->paymask($self->mask_payinfo) unless $self->tokenized($payinfo); #remask unless tokenizing
75 $self->getfield('payinfo');
81 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
86 my($self,$paycvv) = @_;
87 # This is only allowed in cust_main... Even then it really shouldn't be stored...
88 if ($self->table eq 'cust_main') {
89 if ( defined($paycvv) ) {
90 $self->setfield('paycvv', $paycvv); # This is okay since we are the 'setter'
92 $paycvv = $self->getfield('paycvv'); # This is okay since we are the 'getter'
96 # warn "This doesn't work for other tables besides cust_main
106 my($self, $paymask) = @_;
108 if ( defined($paymask) ) {
109 $self->setfield('paymask', $paymask);
111 $self->getfield('paymask') || $self->mask_payinfo;
121 =item mask_payinfo [ PAYBY, PAYINFO ]
123 This method converts the payment info (credit card, bank account, etc.) into a
126 Optionally, an arbitrary payby and payinfo can be passed.
132 my $payby = scalar(@_) ? shift : $self->payby;
133 my $payinfo = scalar(@_) ? shift : $self->payinfo;
135 # Check to see if it's encrypted...
136 if ( ref($self) && $self->is_encrypted($payinfo) ) {
138 } elsif ( $self->tokenized($payinfo) || $payinfo eq 'N/A' ) { #token
139 return 'N/A (tokenized)'; #?
140 } else { # if not, mask it...
142 if ($payby eq 'CARD' || $payby eq 'DCRD') {
143 #|| $payby eq 'MCRD') {
144 #MCRD isn't a card in payinfo,
145 #its a record of an _offline_
150 # special handling for Local Isracards: always show last 4
151 if ( $payinfo =~ /^(\d{8,9})$/ ) {
153 return 'x'x(length($payinfo)-4).
154 substr($payinfo,(length($payinfo)-4));
158 my $conf = new FS::Conf;
159 my $mask_method = $conf->config('card_masking_method') || 'first6last4';
160 $mask_method =~ /^first(\d+)last(\d+)$/
161 or die "can't parse card_masking_method $mask_method";
162 my($first, $last) = ($1, $2);
164 return substr($payinfo,0,$first).
165 'x'x(length($payinfo)-$first-$last).
166 substr($payinfo,(length($payinfo)-$last));
168 } elsif ($payby eq 'CHEK' || $payby eq 'DCHK' ) {
170 # Checks (Show last 2 @ bank)
171 my( $account, $aba ) = split('@', $payinfo );
172 return 'x'x(length($account)-2).
173 substr($account,(length($account)-2)).
174 ( length($aba) ? "@".$aba : '');
176 } elsif ($payby eq 'EDI') {
178 # These numbers have been seen anywhere from 8 to 30 digits, and
179 # possibly more. Lacking any better idea I'm going to mask all but
181 return 'x' x (length($payinfo) - 4) . substr($payinfo, -4);
183 } else { # Tie up loose ends
187 #die "shouldn't be reached";
192 Checks payby and payinfo.
199 FS::payby->can_payby($self->table, $self->payby)
200 or return "Illegal payby: ". $self->payby;
202 if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
204 my $payinfo = $self->payinfo;
205 my $cardtype = cardtype($payinfo);
206 $cardtype = 'Tokenized' if $self->tokenized;
207 $self->set('paycardtype', $cardtype);
209 if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
213 $self->payinfo($payinfo);
214 if ( $self->payinfo ) {
215 $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
216 or return "Illegal (mistyped?) credit card number (payinfo)";
218 validate($self->payinfo) or return "Illegal credit card number";
219 return "Unknown card type" if $cardtype eq "Unknown";
221 $self->payinfo('N/A'); #??? re-masks card
225 if ( $self->payby eq 'CARD' and $self->paymask ) {
226 # if we can't decrypt the card, at least detect the cardtype
227 $self->set('paycardtype', cardtype($self->paymask));
229 $self->set('paycardtype', '');
231 if ( $self->is_encrypted($self->payinfo) ) {
232 #something better? all it would cause is a decryption error anyway?
233 my $error = $self->ut_anything('payinfo');
234 return $error if $error;
236 my $error = $self->ut_textn('payinfo');
237 return $error if $error;
244 =item payby_payinfo_pretty [ LOCALE ]
246 Returns payment method and information (suitably masked, if applicable) as
247 a human-readable string, such as:
249 Card #54xxxxxxxxxxxx32
257 sub payby_payinfo_pretty {
260 my $lh = FS::L10N->get_handle($locale);
261 if ( $self->payby eq 'CARD' ) {
262 if ($self->paymask =~ /tokenized/) {
263 $lh->maketext('Tokenized Card');
265 $lh->maketext('Card #') . $self->paymask;
267 } elsif ( $self->payby eq 'CHEK' ) {
269 #false laziness w/view/cust_main/payment_history.html::translate_payinfo
270 my( $account, $aba ) = split('@', $self->paymask );
272 if ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #blame canada
273 my($branch, $routing) = ($1, $2);
274 $lh->maketext("Routing [_1], Branch [_2], Acct [_3]",
275 $routing, $branch, $account);
277 $lh->maketext("Routing [_1], Acct [_2]", $aba, $account);
280 } elsif ( $self->payby eq 'BILL' ) {
281 $lh->maketext('Check #') . $self->payinfo;
282 } elsif ( $self->payby eq 'PREP' ) {
283 $lh->maketext('Prepaid card #') . $self->payinfo;
284 } elsif ( $self->payby eq 'CASH' ) {
285 $lh->maketext('Cash') . ' ' . $self->payinfo;
286 } elsif ( $self->payby eq 'WEST' ) {
287 # does Western Union localize their name?
288 $lh->maketext('Western Union');
289 } elsif ( $self->payby eq 'MCRD' ) {
290 $lh->maketext('Manual credit card');
291 } elsif ( $self->payby eq 'MCHK' ) {
292 $lh->maketext('Manual electronic check');
293 } elsif ( $self->payby eq 'EDI' ) {
294 $lh->maketext('EDI') . ' ' . $self->paymask;
295 } elsif ( $self->payby eq 'PPAL' ) {
296 $lh->maketext('PayPal transaction#') . $self->order_number;
298 $self->payby. ' '. $self->payinfo;
302 =item payinfo_used [ PAYINFO ]
304 Returns 1 if there's an existing payment using this payinfo. This can be
305 used to set the 'recurring payment' flag required by some processors.
311 my $payinfo = shift || $self->payinfo;
313 'custnum' => $self->custnum,
318 if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
319 || qsearch('cust_pay',
320 { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) } )
326 =item upgrade_set_cardtype
328 Find all records with a credit card payment type and no paycardtype, and
329 replace them in order to set their paycardtype.
333 sub upgrade_set_cardtype {
335 # assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype
336 # will do this. ignore any problems with the cards.
337 local $ignore_masked_payinfo = 1;
338 my $search = FS::Cursor->new({
339 table => $class->table,
340 extra_sql => q[ WHERE payby IN('CARD','DCRD') AND paycardtype IS NULL ],
342 while (my $record = $search->fetch) {
343 my $error = $record->replace;
344 die $error if $error;
348 =item tokenized [ PAYINFO ]
350 Returns true if object payinfo is tokenized
352 Optionally, an arbitrary payby and payinfo can be passed.
358 my $payinfo = scalar(@_) ? shift : $self->payinfo;
359 return 0 unless $payinfo; #avoid uninitialized value error
360 $payinfo =~ /^99\d{14}$/;
369 L<FS::payby>, L<FS::Record>