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 use vars qw($ignore_masked_payinfo);
15 FS::payinfo_Mixin - Mixin class for records in tables that contain payinfo.
19 package FS::some_table;
21 @ISA = qw( FS::payinfo_Mixin FS::Record );
25 This is a mixin class for records that contain payinfo.
33 The following payment types (payby) are supported:
35 For Customers (cust_main):
36 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
37 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
38 'LECB' (Phone bill billing), 'BILL' (billing), 'COMP' (free), or
39 'PREPAY' (special billing type: applies a credit and sets billing type to I<BILL> - see L<FS::prepay_credit>)
41 For Refunds (cust_refund):
42 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
43 'LECB' (Phone bill billing), 'BILL' (billing), 'CASH' (cash),
44 'WEST' (Western Union), 'MCRD' (Manual credit card), 'MCHK' (Manual electronic
45 check), 'CBAK' Chargeback, or 'COMP' (free)
48 For Payments (cust_pay):
49 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
50 'LECB' (phone bill billing), 'BILL' (billing), 'PREP' (prepaid card),
51 'CASH' (cash), 'WEST' (Western Union), 'MCRD' (Manual credit card), 'MCHK'
52 (Manual electronic check), 'PPAL' (PayPal)
53 'COMP' (free) is depricated as a payment type in cust_pay
59 Payment information (payinfo) can be one of the following types:
61 Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username)
62 prepayment identifier (see L<FS::prepay_credit>), PayPal transaction ID
67 my($self,$payinfo) = @_;
69 if ( defined($payinfo) ) {
70 $self->setfield('payinfo', $payinfo);
71 $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #token
73 $self->getfield('payinfo');
79 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
84 my($self,$paycvv) = @_;
85 # This is only allowed in cust_main... Even then it really shouldn't be stored...
86 if ($self->table eq 'cust_main') {
87 if ( defined($paycvv) ) {
88 $self->setfield('paycvv', $paycvv); # This is okay since we are the 'setter'
90 $paycvv = $self->getfield('paycvv'); # This is okay since we are the 'getter'
94 # warn "This doesn't work for other tables besides cust_main
104 my($self, $paymask) = @_;
106 if ( defined($paymask) ) {
107 $self->setfield('paymask', $paymask);
109 $self->getfield('paymask') || $self->mask_payinfo;
119 =item mask_payinfo [ PAYBY, PAYINFO ]
121 This method converts the payment info (credit card, bank account, etc.) into a
124 Optionally, an arbitrary payby and payinfo can be passed.
130 my $payby = scalar(@_) ? shift : $self->payby;
131 my $payinfo = scalar(@_) ? shift : $self->payinfo;
133 # Check to see if it's encrypted...
134 if ( ref($self) && $self->is_encrypted($payinfo) ) {
136 } elsif ( $payinfo =~ /^99\d{14}$/ || $payinfo eq 'N/A' ) { #token
137 return 'N/A (tokenized)'; #?
138 } else { # if not, mask it...
140 if ($payby eq 'CARD' || $payby eq 'DCRD') {
141 #|| $payby eq 'MCRD') {
142 #MCRD isn't a card in payinfo,
143 #its a record of an _offline_
148 # special handling for Local Isracards: always show last 4
149 if ( $payinfo =~ /^(\d{8,9})$/ ) {
151 return 'x'x(length($payinfo)-4).
152 substr($payinfo,(length($payinfo)-4));
156 my $conf = new FS::Conf;
157 my $mask_method = $conf->config('card_masking_method') || 'first6last4';
158 $mask_method =~ /^first(\d+)last(\d+)$/
159 or die "can't parse card_masking_method $mask_method";
160 my($first, $last) = ($1, $2);
162 return substr($payinfo,0,$first).
163 'x'x(length($payinfo)-$first-$last).
164 substr($payinfo,(length($payinfo)-$last));
166 } elsif ($payby eq 'CHEK' || $payby eq 'DCHK' ) {
168 # Checks (Show last 2 @ bank)
169 my( $account, $aba ) = split('@', $payinfo );
170 return 'x'x(length($account)-2).
171 substr($account,(length($account)-2)).
172 ( length($aba) ? "@".$aba : '');
174 } elsif ($payby eq 'EDI') {
176 # These numbers have been seen anywhere from 8 to 30 digits, and
177 # possibly more. Lacking any better idea I'm going to mask all but
179 return 'x' x (length($payinfo) - 4) . substr($payinfo, -4);
181 } else { # Tie up loose ends
185 #die "shouldn't be reached";
190 Checks payby and payinfo.
197 FS::payby->can_payby($self->table, $self->payby)
198 or return "Illegal payby: ". $self->payby;
200 if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
202 my $payinfo = $self->payinfo;
203 my $cardtype = cardtype($payinfo);
204 $cardtype = 'Tokenized' if $payinfo =~ /^99\d{14}$/;
205 $self->set('paycardtype', $cardtype);
207 if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
211 $self->payinfo($payinfo);
212 if ( $self->payinfo ) {
213 $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
214 or return "Illegal (mistyped?) credit card number (payinfo)";
216 validate($self->payinfo) or return "Illegal credit card number";
217 return "Unknown card type" if $cardtype eq "Unknown";
219 $self->payinfo('N/A'); #???
223 if ( $self->payby eq 'CARD' and $self->paymask ) {
224 # if we can't decrypt the card, at least detect the cardtype
225 $self->set('paycardtype', cardtype($self->paymask));
227 $self->set('paycardtype', '');
229 if ( $self->is_encrypted($self->payinfo) ) {
230 #something better? all it would cause is a decryption error anyway?
231 my $error = $self->ut_anything('payinfo');
232 return $error if $error;
234 my $error = $self->ut_textn('payinfo');
235 return $error if $error;
241 =item payby_payinfo_pretty [ LOCALE ]
243 Returns payment method and information (suitably masked, if applicable) as
244 a human-readable string, such as:
246 Card #54xxxxxxxxxxxx32
254 sub payby_payinfo_pretty {
257 my $lh = FS::L10N->get_handle($locale);
258 if ( $self->payby eq 'CARD' ) {
259 if ($self->paymask =~ /tokenized/) {
260 $lh->maketext('Tokenized Card');
262 $lh->maketext('Card #') . $self->paymask;
264 } elsif ( $self->payby eq 'CHEK' ) {
266 #false laziness w/view/cust_main/payment_history.html::translate_payinfo
267 my( $account, $aba ) = split('@', $self->paymask );
269 if ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #blame canada
270 my($branch, $routing) = ($1, $2);
271 $lh->maketext("Routing [_1], Branch [_2], Acct [_3]",
272 $routing, $branch, $account);
274 $lh->maketext("Routing [_1], Acct [_2]", $aba, $account);
277 } elsif ( $self->payby eq 'BILL' ) {
278 $lh->maketext('Check #') . $self->payinfo;
279 } elsif ( $self->payby eq 'PREP' ) {
280 $lh->maketext('Prepaid card #') . $self->payinfo;
281 } elsif ( $self->payby eq 'CASH' ) {
282 $lh->maketext('Cash') . ' ' . $self->payinfo;
283 } elsif ( $self->payby eq 'WEST' ) {
284 # does Western Union localize their name?
285 $lh->maketext('Western Union');
286 } elsif ( $self->payby eq 'MCRD' ) {
287 $lh->maketext('Manual credit card');
288 } elsif ( $self->payby eq 'MCHK' ) {
289 $lh->maketext('Manual electronic check');
290 } elsif ( $self->payby eq 'EDI' ) {
291 $lh->maketext('EDI') . ' ' . $self->paymask;
292 } elsif ( $self->payby eq 'PPAL' ) {
293 $lh->maketext('PayPal transaction#') . $self->order_number;
295 $self->payby. ' '. $self->payinfo;
299 =item payinfo_used [ PAYINFO ]
301 Returns 1 if there's an existing payment using this payinfo. This can be
302 used to set the 'recurring payment' flag required by some processors.
308 my $payinfo = shift || $self->payinfo;
310 'custnum' => $self->custnum,
315 if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
316 || qsearch('cust_pay',
317 { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) } )
323 =item upgrade_set_cardtype
325 Find all records with a credit card payment type and no paycardtype, and
326 replace them in order to set their paycardtype.
330 sub upgrade_set_cardtype {
332 # assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype
333 # will do this. ignore any problems with the cards.
334 local $ignore_masked_payinfo = 1;
335 my $search = FS::Cursor->new({
336 table => $class->table,
337 extra_sql => q[ WHERE payby IN('CARD','DCRD') AND paycardtype IS NULL ],
339 while (my $record = $search->fetch) {
340 my $error = $record->replace;
341 die $error if $error;
351 L<FS::payby>, L<FS::Record>