71513: Card tokenization [refund testing & bug fixes]
authorJonathan Prykop <jonathan@freeside.biz>
Fri, 9 Dec 2016 18:45:01 +0000 (12:45 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Fri, 9 Dec 2016 18:45:01 +0000 (12:45 -0600)
FS/FS/cust_main/Billing_Realtime.pm
FS/t/suite/14-tokenization_refund.t [new file with mode: 0755]

index 35293f0..59792e7 100644 (file)
@@ -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 (executable)
index 0000000..65202fd
--- /dev/null
@@ -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;
+