From 06583ca8e9590de4bc44e849791166d1b8be90ce Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 9 Dec 2016 12:45:01 -0600 Subject: [PATCH] 71513: Card tokenization [refund testing & bug fixes] --- FS/FS/cust_main/Billing_Realtime.pm | 30 +++++- FS/t/suite/14-tokenization_refund.t | 200 ++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 2 deletions(-) create mode 100755 FS/t/suite/14-tokenization_refund.t diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 35293f0ea..59792e70f 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -1454,9 +1454,10 @@ sub realtime_refund_bop { ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 ); } + my $payment_gateway; if ( $gatewaynum ) { #gateway for the payment to be refunded - my $payment_gateway = + $payment_gateway = qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } ); die "payment gateway $gatewaynum not found" unless $payment_gateway; @@ -1470,7 +1471,7 @@ sub realtime_refund_bop { } else { #try the default gateway my $conf_processor; - my $payment_gateway = + $payment_gateway = $self->agent->payment_gateway('method' => $options{method}); ( $conf_processor, $login, $password, $namespace ) = @@ -1487,8 +1488,27 @@ sub realtime_refund_bop { unless ($processor eq $conf_processor) || (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'})); + $processor = $conf_processor; + } + # if gateway has switched to CardFortress but token_check hasn't run yet, + # tokenize just this record now, so that token gets passed/set appropriately + if ($cust_pay->payby eq 'CARD' && !$cust_pay->tokenized) { + my %tokenopts = ( + 'payment_gateway' => $payment_gateway, + 'method' => 'CC', + 'payinfo' => $cust_pay->payinfo, + 'paydate' => $cust_pay->paydate, + ); + my $error = $self->realtime_tokenize(\%tokenopts); # no-op unless gateway can tokenize + if ($self->tokenized($tokenopts{'payinfo'})) { # implies no error + warn " tokenizing cust_pay\n" if $DEBUG > 1; + $cust_pay->payinfo($tokenopts{'payinfo'}); + $error = $cust_pay->replace; + } + return $error if $error; + } } else { # didn't specify a paynum, so look for agent gateway overrides # like a normal transaction @@ -1567,6 +1587,12 @@ sub realtime_refund_bop { $content{'name'} = $self->get('first'). ' '. $self->get('last'); } } + if ( $cust_pay->payby eq 'CARD' + && !$content{'card_number'} + && $cust_pay->tokenized + ) { + $content{'card_token'} = $cust_pay->payinfo; + } $void->content( 'action' => 'void', %content ); $void->test_transaction(1) if $conf->exists('business-onlinepayment-test_transaction'); diff --git a/FS/t/suite/14-tokenization_refund.t b/FS/t/suite/14-tokenization_refund.t new file mode 100755 index 000000000..65202fdfd --- /dev/null +++ b/FS/t/suite/14-tokenization_refund.t @@ -0,0 +1,200 @@ +#!/usr/bin/perl + +use strict; +use FS::Test; +use Test::More; +use FS::Conf; +use FS::cust_main; +use Business::CreditCard qw(generate_last_digit); +use DateTime; +if ( stat('/usr/local/etc/freeside/cardfortresstest.txt') ) { + plan tests => 33; +} else { + plan skip_all => 'CardFortress test encryption key is not installed.'; +} + +#local $FS::cust_main::Billing_Realtime::DEBUG = 2; + +my $fs = FS::Test->new( user => 'admin' ); +my $conf = FS::Conf->new; +my $err; +my @bopconf; + +### can only run on test database (company name "Freeside Test") +like( $conf->config('company_name'), qr/^Freeside Test/, 'using test database' ) or BAIL_OUT(''); + +### database might need to be upgraded before this, +### but doesn't matter if existing records are tokenized or not, +### this is all about testing new record creation + +# these will just get in the way for now +foreach my $apg ($fs->qsearch('agent_payment_gateway')) { + $err = $apg->delete; + last if $err; +} +ok( !$err, 'removing agent gateway overrides' ) or BAIL_OUT($err); + +# will need this +my $reason = FS::reason->new_or_existing( + reason => 'Token Test', + type => 'Refund', + class => 'F', +); +isa_ok ( $reason, 'FS::reason', "refund reason" ) or BAIL_OUT(''); + +# non-tokenizing gateway +push @bopconf, +'IPPay +TESTTERMINAL'; + +# tokenizing gateway +push @bopconf, +'CardFortress +cardfortresstest +(TEST54) +Normal Authorization +gateway +IPPay +gateway_login +TESTTERMINAL +gateway_password + +private_key +/usr/local/etc/freeside/cardfortresstest.txt'; + +# for attempting refund post-tokenization +my $n_cust_main; +my $n_cust_pay; + +foreach my $tokenizing (0,1) { + + my $adj = $tokenizing ? 'tokenizable' : 'non-tokenizable'; + + # set payment gateway + $conf->set('business-onlinepayment' => $bopconf[$tokenizing]); + is( join("\n",$conf->config('business-onlinepayment')), $bopconf[$tokenizing], "set $adj default gateway" ) or BAIL_OUT(''); + + if ($tokenizing) { + + my $n_paynum = $n_cust_pay->paynum; + + # refund the previous non-tokenized payment through CF + $err = $n_cust_main->realtime_refund_bop({ + reasonnum => $reason->reasonnum, + paynum => $n_paynum, + method => 'CC', + }); + ok( !$err, "run post-switch refund" ) or BAIL_OUT($err); + + # check for void record + my $n_cust_pay_void = $fs->qsearchs('cust_pay_void',{ paynum => $n_paynum }); + isa_ok( $n_cust_pay_void, 'FS::cust_pay_void', 'post-switch void') or BAIL_OUT("paynum $n_paynum"); + + # check that void tokenized + ok ( $n_cust_pay_void->tokenized, "post-switch void tokenized" ) or BAIL_OUT("paynum $n_paynum"); + + # check for no refund record + ok( !$fs->qsearch('cust_refund',{ source_paynum => $n_paynum }), "post-switch refund did not generate cust_refund" ) or BAIL_OUT("paynum $n_paynum"); + + } + + # create customer + my $cust_main = $fs->new_customer($adj); + isa_ok ( $cust_main, 'FS::cust_main', "$adj customer" ) or BAIL_OUT(''); + + # insert customer + $err = $cust_main->insert; + ok( !$err, "insert $adj customer" ) or BAIL_OUT($err); + + # add card + my $cust_payby; + my %card = random_card(); + $err = $cust_main->save_cust_payby( + %card, + payment_payby => $card{'payby'}, + auto => 1, + saved_cust_payby => \$cust_payby + ); + ok( !$err, "save $adj card" ) or BAIL_OUT($err); + + # retrieve card + isa_ok ( $cust_payby, 'FS::cust_payby', "$adj card" ) or BAIL_OUT(''); + + # check that card tokenized or not + if ($tokenizing) { + ok( $cust_payby->tokenized, 'new cust card tokenized' ) or BAIL_OUT(''); + } else { + ok( !$cust_payby->tokenized, 'new cust card not tokenized' ) or BAIL_OUT(''); + } + + # run a payment + $err = $cust_main->realtime_cust_payby( amount => '1.00' ); + ok( !$err, "run $adj payment" ) or BAIL_OUT($err); + + # get the payment + my $cust_pay = $fs->qsearchs('cust_pay',{ custnum => $cust_main->custnum }); + isa_ok ( $cust_pay, 'FS::cust_pay', "$adj payment" ) or BAIL_OUT(''); + + # refund the payment + $err = $cust_main->realtime_refund_bop({ + reasonnum => $reason->reasonnum, + paynum => $cust_pay->paynum, + method => 'CC', + }); + ok( !$err, "run $adj refund" ) or BAIL_OUT($err); + + unless ($tokenizing) { + + # run a second payment, to refund after switch + $err = $cust_main->realtime_cust_payby( amount => '2.00' ); + ok( !$err, "run $adj second payment" ) or BAIL_OUT($err); + + # get the second payment + $n_cust_pay = $fs->qsearchs('cust_pay',{ custnum => $cust_main->custnum, paid => '2.00' }); + isa_ok ( $n_cust_pay, 'FS::cust_pay', "$adj second payment" ) or BAIL_OUT(''); + + $n_cust_main = $cust_main; + + } + + #check that all transactions tokenized or not + foreach my $table (qw(cust_pay_pending cust_pay cust_pay_void)) { + foreach my $record ($fs->qsearch($table,{ custnum => $cust_main->custnum })) { + if ($tokenizing) { + $err = "record not tokenized: $table ".$record->get($record->primary_key) + unless $record->tokenized; + } else { + $err = "record tokenized: $table ".$record->get($record->primary_key) + if $record->tokenized; + } + last if $err; + } + } + ok( !$err, "$adj transaction token check" ) or BAIL_OUT($err); + + #make sure we voided + ok( $fs->qsearch('cust_pay_void',{ custnum => $cust_main->custnum}), "$adj refund voided" ) or BAIL_OUT(''); + + #make sure we didn't generate refund records + ok( !$fs->qsearch('cust_refund',{ custnum => $cust_main->custnum}), "$adj refund did not generate cust_refund" ) or BAIL_OUT(''); + +}; + +exit; + +sub random_card { + my $payinfo = '4111' . join('', map { int(rand(10)) } 1 .. 11); + $payinfo .= generate_last_digit($payinfo); + my $paydate = DateTime->now + ->add('years' => 1) + ->truncate(to => 'month') + ->strftime('%F'); + return ( 'payby' => 'CARD', + 'payinfo' => $payinfo, + 'paydate' => $paydate, + 'payname' => 'Tokenize Me', + ); +} + +1; + -- 2.11.0