[freeside-commits] branch master updated. 06583ca8e9590de4bc44e849791166d1b8be90ce

Jonathan Prykop jonathan at 420.am
Fri Dec 9 10:46:15 PST 2016


The branch, master has been updated
       via  06583ca8e9590de4bc44e849791166d1b8be90ce (commit)
      from  7a33cb6e4c3e33b7399d6574cbd3ee38ddcba5e0 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 06583ca8e9590de4bc44e849791166d1b8be90ce
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Fri Dec 9 12:45:01 2016 -0600

    71513: Card tokenization [refund testing & bug fixes]

diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index 35293f0..59792e7 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 0000000..65202fd
--- /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;
+

-----------------------------------------------------------------------

Summary of changes:
 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




More information about the freeside-commits mailing list