From 9a8399783bb9d87ef662b4371bebe983d2781dce Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Sat, 3 Dec 2016 10:36:03 -0600 Subject: [PATCH] 71513: Card tokenization [refund gateway choice] --- FS/FS/cust_main/Billing_Realtime.pm | 115 +++++++++++++++++++++++++----------- FS/FS/payinfo_Mixin.pm | 2 - FS/FS/payinfo_transaction_Mixin.pm | 2 - FS/FS/payment_gateway.pm | 25 +++++--- 4 files changed, 98 insertions(+), 46 deletions(-) diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index f331a39ac..3a3947b9c 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -1480,10 +1480,12 @@ sub realtime_refund_bop { @bop_options = $payment_gateway->gatewaynum ? $payment_gateway->options : @{ $payment_gateway->get('options') }; + my %bop_options = @bop_options; return "processor of payment $options{'paynum'} $processor does not". " match default processor $conf_processor" - unless $processor eq $conf_processor; + unless ($processor eq $conf_processor) + || (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'})); } @@ -1492,9 +1494,7 @@ sub realtime_refund_bop { # like a normal transaction my $payment_gateway = - $self->agent->payment_gateway( 'method' => $options{method}, - #'payinfo' => $payinfo, - ); + $self->agent->payment_gateway( 'method' => $options{method} ); my( $processor, $login, $password, $namespace ) = map { my $method = "gateway_$_"; $payment_gateway->$method } qw( module username password namespace ); @@ -1627,18 +1627,22 @@ sub realtime_refund_bop { if length($payip); my $payinfo = ''; + my $paymask = ''; # for refund record if ( $options{method} eq 'CC' ) { if ( $cust_pay ) { $content{card_number} = $payinfo = $cust_pay->payinfo; + $paymask = $cust_pay->paymask; (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate) =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ && ($content{expiration} = "$2/$1"); # where available } else { - $content{card_number} = $payinfo = $self->payinfo; - (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate) - =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; - $content{expiration} = "$2/$1"; + # this really needs a better cleanup + die "Refund without paynum not supported"; +# $content{card_number} = $payinfo = $self->payinfo; +# (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate) +# =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; +# $content{expiration} = "$2/$1"; } } elsif ( $options{method} eq 'ECHECK' ) { @@ -1702,6 +1706,7 @@ sub realtime_refund_bop { '_date' => '', 'payby' => $bop_method2payby{$options{method}}, 'payinfo' => $payinfo, + 'paymask' => $paymask, 'reasonnum' => $options{'reasonnum'}, 'gatewaynum' => $gatewaynum, # may be null 'processor' => $processor, @@ -2443,7 +2448,7 @@ CUSTLOOP: } # only load gateway if we need to, and only need to load it once - my $payment_gateway ||= $cust_main->_payment_gateway({ + $payment_gateway ||= $cust_main->_payment_gateway({ 'method' => 'CC', 'conf' => $conf, 'nofatal' => 1, # handle lack of gateway smoothly below @@ -2543,15 +2548,72 @@ CUSTLOOP: $dbh->commit or die $dbh->errstr; # commit log message } - # don't use customer agent gateway here, use the gatewaynum specified by the record - my $gateway = FS::payment_gateway->by_key_or_default( - 'method' => 'CC', - 'conf' => $conf, - 'nofatal' => 1, - 'gatewaynum' => $record->gatewaynum || '', - ); + my $cust_main = $record->cust_main; + if (!$cust_main) { + # might happen for cust_pay_pending from failed verify records, + # in which case we attempt tokenization without cust_main + # everything else should absolutely have a cust_main + unless ($table eq 'cust_pay_pending' && $record->{'custnum_pending'}) { + my $error = "Could not load cust_main for $table ".$record->get($record->primary_key); + if ($opt{'queue'}) { + $log->critical($error); + $dbh->commit or die $dbh->errstr; # commit log message + next; + } + $dbh->rollback if $oldAutoCommit; + die $error; + } + } + + my $gateway; + + # use the gatewaynum specified by the record if possible + $gateway = FS::payment_gateway->by_key_with_namespace( + 'gatewaynum' => $record->gatewaynum, + ) if $record->gateway; + + # otherwise use the cust agent gateway if possible (which realtime_refund_bop would do) + # otherwise just use default gateway + unless ($gateway) { + + $gateway = $cust_main + ? $cust_main->agent->payment_gateway + : FS::payment_gateway->default_gateway; + + # check for processor mismatch + unless ($table eq 'cust_pay_pending') { # has no processor table + if (my $processor = $record->processor) { + + my $conf_processor = $gateway->gateway_module; + my %bop_options = $gateway->gatewaynum + ? $gateway->options + : @{ $gateway->get('options') }; + + # this is the same standard used by realtime_refund_bop + unless ( + ($processor eq $conf_processor) || + (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'})) + ) { + + # processors don't match, so refund already cannot be run on this object, + # regardless of what we do now... + # but unless we gotta tokenize everything, just leave well enough alone + unless ($require_tokenized) { + warn "Skipping mismatched processor for $table ".$record->get($record->primary_key) if $debug; + next; + } + ### no error--we'll tokenize using the new gateway, just to remove stored payinfo, + ### because refunds are already impossible for this record, anyway + + } # end processor mismatch + + } # end record has processor + } # end not cust_pay_pending + + } + + # means no default gateway, no promise to tokenize, can skip unless ($gateway) { - # means no default gateway, no promise to tokenize, can skip warn "Skipping missing gateway for $table ".$record->get($record->primary_key) if $debug; next; } @@ -2570,24 +2632,7 @@ CUSTLOOP: next; } - my $cust_main = $record->cust_main; - if (!$cust_main) { - # might happen for cust_pay_pending from failed verify records, - # in which case we attempt tokenization without cust_main - # everything else should absolutely have a cust_main - if ($table eq 'cust_pay_pending' && $record->{'custnum_pending'}) { - warn "ATTEMPTING GATEWAY-ONLY TOKENIZE" if $debug; - } else { - my $error = "Could not load cust_main for $table ".$record->get($record->primary_key); - if ($opt{'queue'}) { - $log->critical($error); - $dbh->commit or die $dbh->errstr; # commit log message - next; - } - $dbh->rollback if $oldAutoCommit; - die $error; - } - } + warn "ATTEMPTING GATEWAY-ONLY TOKENIZE" if $debug && !$cust_main; # if we got this far, time to mutex $record = $record->select_for_update; diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index be37568ad..3a51022d3 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -195,8 +195,6 @@ sub payinfo_check { FS::payby->can_payby($self->table, $self->payby) or return "Illegal payby: ". $self->payby; - my $conf = new FS::Conf; - if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) { my $payinfo = $self->payinfo; diff --git a/FS/FS/payinfo_transaction_Mixin.pm b/FS/FS/payinfo_transaction_Mixin.pm index c27d0494b..1b5a0cdff 100644 --- a/FS/FS/payinfo_transaction_Mixin.pm +++ b/FS/FS/payinfo_transaction_Mixin.pm @@ -102,8 +102,6 @@ auth, and order_number) as well as payby and payinfo sub payinfo_check { my $self = shift; - my $conf = new FS::Conf; - $self->SUPER::payinfo_check() || $self->ut_numbern('gatewaynum') # not ut_foreign_keyn, it causes upgrades to fail diff --git a/FS/FS/payment_gateway.pm b/FS/FS/payment_gateway.pm index 170d37af9..3500bf9bc 100644 --- a/FS/FS/payment_gateway.pm +++ b/FS/FS/payment_gateway.pm @@ -385,6 +385,23 @@ sub default_gateway { return $payment_gateway; } +=item by_key_with_namespace GATEWAYNUM + +Like usual by_key, but makes sure namespace is set, +and dies if not found. + +=cut + +sub by_key_with_namespace { + my $self = shift; + my $payment_gateway = $self->by_key(@_); + die "payment_gateway not found" + unless $payment_gateway; + $payment_gateway->gateway_namespace('Business::OnlinePayment') + unless $payment_gateway->gateway_namespace; + return $payment_gateway; +} + =item by_key_or_default OPTIONS Either returns the gateway specified by option gatewaynum, or the default gateway. @@ -399,13 +416,7 @@ sub by_key_or_default { my ($self,%options) = @_; if ($options{'gatewaynum'}) { - my $payment_gateway = $self->by_key($options{'gatewaynum'}); - # regardless of nofatal, which is only meant for handling lack of default gateway - die "payment_gateway ".$options{'gatewaynum'}." not found" - unless $payment_gateway; - $payment_gateway->gateway_namespace('Business::OnlinePayment') - unless $payment_gateway->gateway_namespace; - return $payment_gateway; + return $self->by_key_with_namespace($options{'gatewaynum'}); } else { return $self->default_gateway(%options); } -- 2.11.0