diff options
-rw-r--r-- | FS/FS/cust_main.pm | 3 | ||||
-rw-r--r-- | FS/FS/cust_main/Billing_Realtime.pm | 15 | ||||
-rw-r--r-- | FS/FS/cust_payby.pm | 22 | ||||
-rw-r--r-- | FS/FS/payinfo_Mixin.pm | 39 | ||||
-rwxr-xr-x | FS/t/suite/13-tokenization.t | 30 | ||||
-rwxr-xr-x | FS/t/suite/14-tokenization_refund.t | 7 |
6 files changed, 84 insertions, 32 deletions
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 355940971..9e0fda438 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -5871,11 +5871,12 @@ sub _upgrade_next_recnum { my $recnum = shift @$recnums; return $recnum if $recnum; my $tclass = 'FS::'.$table; + my $paycardtypecheck = ($table ne 'cust_pay_pending') ? q( OR paycardtype = 'Tokenized') : ''; my $sql = 'SELECT '.$tclass->primary_key. ' FROM '.$table. ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum. " AND payby IN ( 'CARD', 'DCRD', 'CHEK', 'DCHK' ) ". - " AND ( length(payinfo) < 80 OR paycardtype = 'Tokenized' ) ". + " AND ( length(payinfo) < 80$paycardtypecheck ) ". ' ORDER BY '.$tclass->primary_key.' LIMIT 500'; my $sth = $dbh->prepare($sql) or die $dbh->errstr; $sth->execute() or die $sth->errstr; diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index ab2e39c9e..12f19794d 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -2372,7 +2372,7 @@ is set, this will instead cause a critical error to be recorded in the log, and any other tokenizable records will still be committed. If the I<daily> flag is also set, detection of existing untokenized records will -record a critical error in the system log (because they should have never appeared +record an info message in the system log (because they should have never appeared in the first place.) Tokenization will still be attempted. If any configured gateways do NOT have the ability to tokenize, or if a @@ -2385,6 +2385,7 @@ sub token_check { #acts on all customers my %opt = @_; my $debug = !$opt{'quiet'} || $DEBUG; + my $hascritical = 0; warn "token_check called with opts\n".Dumper(\%opt) if $debug; @@ -2481,6 +2482,7 @@ CUSTLOOP: } my $error = "No gateway found for custnum ".$cust_main->custnum; if ($opt{'queue'}) { + $hascritical = 1; $log->critical($error); $dbh->commit or die $dbh->errstr; # commit error message next; # not next CUSTLOOP, want to record error for every cust_payby @@ -2517,6 +2519,7 @@ CUSTLOOP: if ($error) { $error = "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error; if ($opt{'queue'}) { + $hascritical = 1; $log->critical($error); $dbh->commit or die $dbh->errstr; # commit log message, release mutex next; # not next CUSTLOOP, want to record error for every cust_payby @@ -2549,6 +2552,10 @@ CUSTLOOP: while (my $recnum = _token_check_next_recnum($dbh,$table,$step,\$offset,\@recnums)) { my $record = $tclass->by_key($recnum); + unless ($record->payby eq 'CARD') { + warn "Skipping non-card record for $table ".$record->get($record->primary_key) if $debug; + next; + } if (FS::cust_main::Billing_Realtime->tokenized($record->payinfo)) { warn "Skipping tokenized record for $table ".$record->get($record->primary_key) if $debug; next; @@ -2579,6 +2586,7 @@ CUSTLOOP: } else { my $error = "Could not load cust_main for $table ".$record->get($record->primary_key); if ($opt{'queue'}) { + $hascritical = 1; $log->critical($error); $dbh->commit or die $dbh->errstr; # commit log message next; @@ -2681,6 +2689,7 @@ CUSTLOOP: if ($error) { $error = "Error tokenizing $table ".$record->get($record->primary_key).": ".$error; if ($opt{'queue'}) { + $hascritical = 1; $log->critical($error); $dbh->commit or die $dbh->errstr; # commit log message, release mutex next; @@ -2696,7 +2705,7 @@ CUSTLOOP: $dbh->commit or die $dbh->errstr if $oldAutoCommit; - return ''; + return $hascritical ? 'Critical errors occurred on some records, see system log' : ''; } # not a method! @@ -2708,8 +2717,6 @@ sub _token_check_next_recnum { my $sth = $dbh->prepare( 'SELECT '.$tclass->primary_key. ' FROM '.$table. - " WHERE payby IN ( 'CARD', 'DCRD' ) ". - " AND ( length(payinfo) > 80 OR payinfo NOT LIKE '99%' )". ' ORDER BY '.$tclass->primary_key. ' LIMIT '.$step. ' OFFSET '.$$offset diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm index 36f90dfb5..136acf1b6 100644 --- a/FS/FS/cust_payby.pm +++ b/FS/FS/cust_payby.pm @@ -356,7 +356,19 @@ sub check { validate($payinfo) or return gettext('invalid_card'); # . ": ". $self->payinfo; - my $cardtype = $self->paycardtype || cardtype($payinfo); + # see parallel checks in check_payinfo_cardtype & payinfo_Mixin::payinfo_check + my $cardtype = $self->paycardtype; + if ( $self->tokenized ) { + if ( $self->paymask =~ /^\d+x/ ) { + $cardtype = cardtype($self->paymask); + } else { + #return "paycardtype required ". + # "(can't derive from a token and no paymask w/prefix provided)" + # unless $cardtype; + } + } else { + $cardtype = cardtype($self->payinfo); + } return gettext('unknown_card_type') if $cardtype eq "Unknown"; @@ -545,7 +557,15 @@ sub check_payinfo_cardtype { my $payinfo = $self->payinfo; $payinfo =~ s/\D//g; + # see parallel checks in cust_payby::check & payinfo_Mixin::payinfo_check if ( $self->tokenized($payinfo) ) { + if ( $self->paymask =~ /^\d+x/ ) { + $self->set('paycardtype', cardtype($self->paymask)); + } else { + $self->set('paycardtype', ''); + #return "paycardtype required ". + # "(can't derive from a token and no paymask w/prefix provided)"; + } return ''; } diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index 7401eb9c8..84759cc11 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -197,20 +197,17 @@ sub payinfo_check { if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) { - unless ( $self->paycardtype ) { - - if ( $self->tokenized ) { - if ( $self->paymask =~ /^\d+x/ ) { - $self->set('paycardtype', cardtype($self->paymask)); - } else { - $self->set('paycardtype', ''); - # return "paycardtype required ". - # "(can't derive from a token and no paymask w/prefix provided)"; - } + # see parallel checks in cust_payby::check & cust_payby::check_payinfo_cardtype + if ( $self->tokenized ) { + if ( $self->paymask =~ /^\d+x/ ) { + $self->set('paycardtype', cardtype($self->paymask)); } else { - $self->set('paycardtype', cardtype($self->payinfo)); + $self->set('paycardtype', '') unless $self->paycardtype; + #return "paycardtype required ". + # "(can't derive from a token and no paymask w/prefix provided)"; } - + } else { + $self->set('paycardtype', cardtype($self->payinfo)); } if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) { @@ -232,17 +229,13 @@ sub payinfo_check { } else { - unless ( $self->paycardtype ) { - - if ( $self->payby eq 'CARD' && $self->paymask =~ /^\d+x/ ) { - # if we can't decrypt the card, at least detect the cardtype - $self->set('paycardtype', cardtype($self->paymask)); - } else { - $self->set('paycardtype', ''); - # return "paycardtype required ". - # "(can't derive from a token and no paymask w/prefix provided)"; - } - + if ( $self->payby eq 'CARD' && $self->paymask =~ /^\d+x/ ) { + # if we can't decrypt the card, at least detect the cardtype + $self->set('paycardtype', cardtype($self->paymask)); + } else { + $self->set('paycardtype', '') unless $self->paycardtype; + # return "paycardtype required ". + # "(can't derive from a token and no paymask w/prefix provided)"; } if ( $self->is_encrypted($self->payinfo) ) { diff --git a/FS/t/suite/13-tokenization.t b/FS/t/suite/13-tokenization.t index edb0f3896..ebded873e 100755 --- a/FS/t/suite/13-tokenization.t +++ b/FS/t/suite/13-tokenization.t @@ -8,11 +8,13 @@ use FS::cust_main; use Business::CreditCard qw(generate_last_digit); use DateTime; if ( stat('/usr/local/etc/freeside/cardfortresstest.txt') ) { - plan tests => 20; + plan tests => 27; } else { plan skip_all => 'CardFortress test encryption key is not installed.'; } +#local $FS::cust_main::Billing_Realtime::DEBUG = 2; + ### can only run on test database (company name "Freeside Test") ### will run upgrade, which uses lots of prints & warns beyond regular test output @@ -101,6 +103,32 @@ my $pending_failed = FS::cust_pay_pending->new({ $err = $pending_failed->insert; ok( !$err, "create a failed payment attempt" ) or BAIL_OUT($err); +# create two customers with an AmEx card & paycvv, +# then run a payment with one, just to generate some test AmEx data + +my $amex_cust; +foreach my $i (0,1) { + my $cust_main = $fs->new_customer("AmEx $i"); + isa_ok ( $cust_main, 'FS::cust_main', "AmEx $i customer" ) or BAIL_OUT(''); + $err = $cust_main->insert; + ok( !$err, "insert AmEx $i customer" ) or BAIL_OUT($err); + # add card + my $cust_payby; + my %card = random_card(); + $card{'payinfo'} = '347594362484937'; + $card{'paycvv'} = '1234'; + $err = $cust_main->save_cust_payby( + %card, + payment_payby => $card{'payby'}, + auto => 1, + saved_cust_payby => \$cust_payby + ); + ok( !$err, "save AmEx $i card" ) or BAIL_OUT($err); + $amex_cust = $cust_main; +} +$err = $amex_cust->realtime_cust_payby( amount => '1.00' ); +ok( !$err, "run AmEx payment" ) or BAIL_OUT($err); + # find two stored credit cards. my @cust = map { FS::cust_main->by_key($_) } (10, 12); my @payby = map { ($_->cust_payby)[0] } @cust; diff --git a/FS/t/suite/14-tokenization_refund.t b/FS/t/suite/14-tokenization_refund.t index 1a0f8405e..a7d9fa8c5 100755 --- a/FS/t/suite/14-tokenization_refund.t +++ b/FS/t/suite/14-tokenization_refund.t @@ -229,8 +229,10 @@ foreach my $voiding (0,1) { exit; sub random_card { - my $payinfo = '4111' . join('', map { int(rand(10)) } 1 .. 11); - $payinfo .= generate_last_digit($payinfo); +# my $payinfo = '4111' . join('', map { int(rand(10)) } 1 .. 11); +# $payinfo .= generate_last_digit($payinfo); +# Use AmEx for everything, to make sure cardtype gets set correctly + my $payinfo = '347594362484937'; #American Express my $paydate = DateTime->now ->add('years' => 1) ->truncate(to => 'month') @@ -239,6 +241,7 @@ sub random_card { 'payinfo' => $payinfo, 'paydate' => $paydate, 'payname' => 'Tokenize Me', + 'paycvv' => '1234', #American Express ); } |