1 package FS::payinfo_Mixin;
4 use Business::CreditCard;
6 use FS::Record qw(qsearch);
7 use FS::UID qw(driver_name);
8 use Time::Local qw(timelocal);
10 use vars qw($ignore_masked_payinfo);
14 FS::payinfo_Mixin - Mixin class for records in tables that contain payinfo.
18 package FS::some_table;
20 @ISA = qw( FS::payinfo_Mixin FS::Record );
24 This is a mixin class for records that contain payinfo.
32 The following payment types (payby) are supported:
34 For Customers (cust_main):
35 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
36 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
37 'LECB' (Phone bill billing), 'BILL' (billing), 'COMP' (free), or
38 'PREPAY' (special billing type: applies a credit and sets billing type to I<BILL> - see L<FS::prepay_credit>)
40 For Refunds (cust_refund):
41 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
42 'LECB' (Phone bill billing), 'BILL' (billing), 'CASH' (cash),
43 'WEST' (Western Union), 'MCRD' (Manual credit card), 'MCHK' (Manual electronic
44 check), 'CBAK' Chargeback, or 'COMP' (free)
47 For Payments (cust_pay):
48 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
49 'LECB' (phone bill billing), 'BILL' (billing), 'PREP' (prepaid card),
50 'CASH' (cash), 'WEST' (Western Union), 'MCRD' (Manual credit card), 'MCHK'
51 (Manual electronic check), 'PPAL' (PayPal)
52 'COMP' (free) is depricated as a payment type in cust_pay
58 Payment information (payinfo) can be one of the following types:
60 Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username)
61 prepayment identifier (see L<FS::prepay_credit>), PayPal transaction ID
66 my($self,$payinfo) = @_;
68 if ( defined($payinfo) ) {
69 $self->setfield('payinfo', $payinfo);
70 $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #token
72 $self->getfield('payinfo');
78 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
82 #this prevents encrypting empty values on insert?
84 my($self,$paycvv) = @_;
85 # This is only allowed in cust_payby (formerly cust_main)
86 # It shouldn't be stored longer than necessary to run the first transaction
87 if ( defined($paycvv) ) {
88 $self->setfield('paycvv', $paycvv);
90 $self->getfield('paycvv');
99 my($self, $paymask) = @_;
101 if ( defined($paymask) ) {
102 $self->setfield('paymask', $paymask);
104 $self->getfield('paymask') || $self->mask_payinfo;
114 =item mask_payinfo [ PAYBY, PAYINFO ]
116 This method converts the payment info (credit card, bank account, etc.) into a
119 Optionally, an arbitrary payby and payinfo can be passed.
125 my $payby = scalar(@_) ? shift : $self->payby;
126 my $payinfo = scalar(@_) ? shift : $self->payinfo;
128 # Check to see if it's encrypted...
129 if ( ref($self) && $self->is_encrypted($payinfo) ) {
131 } elsif ( $payinfo =~ /^99\d{14}$/ || $payinfo eq 'N/A' ) { #token
132 return 'N/A (tokenized)'; #?
133 } else { # if not, mask it...
135 if ($payby eq 'CARD' || $payby eq 'DCRD') {
136 #|| $payby eq 'MCRD') {
137 #MCRD isn't a card in payinfo,
138 #its a record of an _offline_
143 # special handling for Local Isracards: always show last 4
144 if ( $payinfo =~ /^(\d{8,9})$/ ) {
146 return 'x'x(length($payinfo)-4).
147 substr($payinfo,(length($payinfo)-4));
151 my $conf = new FS::Conf;
152 my $mask_method = $conf->config('card_masking_method') || 'first6last4';
153 $mask_method =~ /^first(\d+)last(\d+)$/
154 or die "can't parse card_masking_method $mask_method";
155 my($first, $last) = ($1, $2);
157 return substr($payinfo,0,$first).
158 'x'x(length($payinfo)-$first-$last).
159 substr($payinfo,(length($payinfo)-$last));
161 } elsif ($payby eq 'CHEK' || $payby eq 'DCHK' ) {
163 # Checks (Show last 2 @ bank)
164 my( $account, $aba ) = split('@', $payinfo );
165 return 'x'x(length($account)-2).
166 substr($account,(length($account)-2)).
167 ( length($aba) ? "@".$aba : '');
169 } elsif ($payby eq 'EDI') {
171 # These numbers have been seen anywhere from 8 to 30 digits, and
172 # possibly more. Lacking any better idea I'm going to mask all but
174 return 'x' x (length($payinfo) - 4) . substr($payinfo, -4);
176 } else { # Tie up loose ends
180 #die "shouldn't be reached";
185 Checks payby and payinfo.
192 FS::payby->can_payby($self->table, $self->payby)
193 or return "Illegal payby: ". $self->payby;
195 if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
196 my $payinfo = $self->payinfo;
197 if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
201 $self->payinfo($payinfo);
202 if ( $self->payinfo ) {
203 $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
204 or return "Illegal (mistyped?) credit card number (payinfo)";
206 validate($self->payinfo) or return "Illegal credit card number";
207 return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
208 && cardtype($self->payinfo) eq "Unknown";
210 $self->payinfo('N/A'); #???
214 if ( $self->is_encrypted($self->payinfo) ) {
215 #something better? all it would cause is a decryption error anyway?
216 my $error = $self->ut_anything('payinfo');
217 return $error if $error;
219 my $error = $self->ut_textn('payinfo');
220 return $error if $error;
226 =item payby_payinfo_pretty [ LOCALE ]
228 Returns payment method and information (suitably masked, if applicable) as
229 a human-readable string, such as:
231 Card #54xxxxxxxxxxxx32
239 sub payby_payinfo_pretty {
242 my $lh = FS::L10N->get_handle($locale);
243 if ( $self->payby eq 'CARD' ) {
244 if ($self->paymask =~ /tokenized/) {
245 $lh->maketext('Tokenized Card');
247 $lh->maketext('Card #') . $self->paymask;
249 } elsif ( $self->payby eq 'CHEK' ) {
251 #false laziness w/view/cust_main/payment_history.html::translate_payinfo
252 my( $account, $aba ) = split('@', $self->paymask );
254 if ( $aba =~ /^(\d{5})\.(\d{3})$/ ) { #blame canada
255 my($branch, $routing) = ($1, $2);
256 $lh->maketext("Routing [_1], Branch [_2], Acct [_3]",
257 $routing, $branch, $account);
259 $lh->maketext("Routing [_1], Acct [_2]", $aba, $account);
262 } elsif ( $self->payby eq 'BILL' ) {
263 $lh->maketext('Check #') . $self->payinfo;
264 } elsif ( $self->payby eq 'PREP' ) {
265 $lh->maketext('Prepaid card #') . $self->payinfo;
266 } elsif ( $self->payby eq 'CASH' ) {
267 $lh->maketext('Cash') . ' ' . $self->payinfo;
268 } elsif ( $self->payby eq 'WEST' ) {
269 # does Western Union localize their name?
270 $lh->maketext('Western Union');
271 } elsif ( $self->payby eq 'MCRD' ) {
272 $lh->maketext('Manual credit card');
273 } elsif ( $self->payby eq 'MCHK' ) {
274 $lh->maketext('Manual electronic check');
275 } elsif ( $self->payby eq 'EDI' ) {
276 $lh->maketext('EDI') . ' ' . $self->paymask;
277 } elsif ( $self->payby eq 'PPAL' ) {
278 $lh->maketext('PayPal transaction#') . $self->order_number;
280 $self->payby. ' '. $self->payinfo;
284 =item payinfo_used [ PAYINFO ]
286 Returns 1 if there's an existing payment using this payinfo. This can be
287 used to set the 'recurring payment' flag required by some processors.
293 my $payinfo = shift || $self->payinfo;
295 'custnum' => $self->custnum,
300 if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
301 || qsearch('cust_pay',
302 { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) } )
310 For transactions that have both 'status' and 'failure_status', shows the
311 status in a single, display-friendly string.
318 'done' => 'Approved',
319 'expired' => 'Card Expired',
320 'stolen' => 'Lost/Stolen',
321 'pickup' => 'Pick Up Card',
322 'nsf' => 'Insufficient Funds',
323 'inactive' => 'Inactive Account',
324 'blacklisted' => 'Blacklisted',
325 'declined' => 'Declined',
326 'approved' => 'Approved',
328 if ( $self->failure_status ) {
329 return $status{$self->failure_status};
331 return $status{$self->status};
335 =item paydate_monthyear
337 Returns a two-element list consisting of the month and year of this customer's
338 paydate (credit card expiration date for CARD customers)
342 sub paydate_monthyear {
344 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
346 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
355 Returns the exact time in seconds corresponding to the payment method
356 expiration date. For CARD/DCRD customers this is the end of the month;
357 for others (COMP is the only other payby that uses paydate) it's the start.
358 Returns 0 if the paydate is empty or set to the far future.
364 my ($month, $year) = $self->paydate_monthyear;
365 return 0 if !$year or $year >= 2037;
366 if ( $self->payby eq 'CARD' or $self->payby eq 'DCRD' ) {
368 if ( $month == 13 ) {
372 return timelocal(0,0,0,1,$month-1,$year) - 1;
375 return timelocal(0,0,0,1,$month-1,$year);
379 =item paydate_epoch_sql
381 Class method. Returns an SQL expression to obtain the payment expiration date
382 as a number of seconds.
386 # Special expiration date behavior for non-CARD/DCRD customers has been
387 # carefully preserved. Do we really use that?
388 sub paydate_epoch_sql {
390 my $table = $class->table;
392 if ( driver_name eq 'Pg' ) {
393 $case1 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) + INTERVAL '1 month') - 1";
394 $case2 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) )";
396 elsif ( lc(driver_name) eq 'mysql' ) {
397 $case1 = "UNIX_TIMESTAMP( DATE_ADD( CAST( $table.paydate AS DATETIME ), INTERVAL 1 month ) ) - 1";
398 $case2 = "UNIX_TIMESTAMP( CAST( $table.paydate AS DATETIME ) )";
401 return "CASE WHEN $table.payby IN('CARD','DCRD')
413 L<FS::payby>, L<FS::Record>