71513: Card tokenization [refund gateway choice]
authorJonathan Prykop <jonathan@freeside.biz>
Sat, 3 Dec 2016 16:36:03 +0000 (10:36 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Sat, 3 Dec 2016 16:36:03 +0000 (10:36 -0600)
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/payinfo_Mixin.pm
FS/FS/payinfo_transaction_Mixin.pm
FS/FS/payment_gateway.pm

index f331a39..3a3947b 100644 (file)
@@ -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;
index be37568..3a51022 100644 (file)
@@ -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;
index c27d049..1b5a0cd 100644 (file)
@@ -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
index 170d37a..3500bf9 100644 (file)
@@ -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);
   }