Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorJonathan Prykop <jonathan@freeside.biz>
Tue, 15 Nov 2016 09:10:32 +0000 (03:10 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Tue, 15 Nov 2016 09:10:32 +0000 (03:10 -0600)
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI/Signup.pm
FS/FS/Conf.pm
FS/FS/agent.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/payinfo_Mixin.pm
FS/FS/payinfo_transaction_Mixin.pm

index 091d6ac..4a878f8 100644 (file)
@@ -401,20 +401,12 @@ sub payment_gateway {
   my $conf = new FS::Conf;
   my $cust_main = shift;
   my $cust_payby = shift;
-  my $gatewaynum = $conf->config('selfservice-payment_gateway');
-  if ( $gatewaynum ) {
-    my $pg = qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
-    die "configured gatewaynum $gatewaynum not found!" if !$pg;
-    return $pg;
-  }
-  else {
-    return '' if ! FS::payby->realtime($cust_payby);
-    my $pg = $cust_main->agent->payment_gateway(
-      'method'  => FS::payby->payby2bop($cust_payby),
-      'nofatal' => 1
-    );
-    return $pg;
-  }
+  return '' if ! FS::payby->realtime($cust_payby);
+  my $pg = $cust_main->agent->payment_gateway(
+    'method'  => FS::payby->payby2bop($cust_payby),
+    'nofatal' => 1
+  );
+  return $pg;
 }
 
 sub access_info {
index e11a47a..5ced42b 100644 (file)
@@ -344,20 +344,11 @@ sub signup_info {
     my @paybys = @{ $signup_info->{'payby'} };
     $signup_info->{'hide_payment_fields'} = [];
 
-    my $gatewaynum = $conf->config('selfservice-payment_gateway');
-    my $force_gateway;
-    if ( $gatewaynum ) {
-      $force_gateway = qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
-      warn "using forced gateway #$gatewaynum - " .
-        $force_gateway->gateway_username . '@' . $force_gateway->gateway_module
-        if $DEBUG > 1;
-      die "configured gatewaynum $gatewaynum not found!" if !$force_gateway;
-    }
     foreach my $payby (@paybys) {
       warn "$me checking $payby payment fields\n" if $DEBUG > 1;
       my $hide = 0;
       if ( FS::payby->realtime($payby) ) {
-        my $gateway = $force_gateway || 
+        my $gateway = 
           $agent->payment_gateway( 'method'  => FS::payby->payby2bop($payby),
                                    'nofatal' => 1,
                                  );
@@ -627,17 +618,9 @@ sub new_customer {
     return { 'error' => "Unknown reseller" }
       unless $agent;
 
-    my $gw;
-    my $gatewaynum = $conf->config('selfservice-payment_gateway');
-    if ( $gatewaynum ) {
-      $gw = qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
-      die "configured gatewaynum $gatewaynum not found!" if !$gw;
-    }
-    else {
-      $gw = $agent->payment_gateway( 'method'  => FS::payby->payby2bop($payby),
-                                     'nofatal' => 1,
+    my $gw = $agent->payment_gateway( 'method'  => FS::payby->payby2bop($payby),
+                                      'nofatal' => 1,
                                     );
-    }
 
     $cust_main->payby('BILL')   # MCRD better?  no, that's for something else
       if $gw && $gw->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
@@ -1120,36 +1103,28 @@ sub capture_payment {
 
   my $conf = new FS::Conf;
 
-  my $payment_gateway;
-  if ( my $gwnum = $conf->config('selfservice-payment_gateway') ) {
-    $payment_gateway = qsearchs('payment_gateway', { 'gatewaynum' => $gwnum })
-      or die "configured gatewaynum $gwnum not found!";
-  }
-  else {
-    my $url = $packet->{url};
-
-    $payment_gateway = qsearchs('payment_gateway', 
+  my $url = $packet->{url};
+  my $payment_gateway = qsearchs('payment_gateway', 
         { 'gateway_callback_url' => popurl(0, $url) } 
       );
-    if (!$payment_gateway) { 
-
-      my ( $processor, $login, $password, $action, @bop_options ) =
-        $conf->config('business-onlinepayment');
-      $action ||= 'normal authorization';
-      pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
-      die "No real-time processor is enabled - ".
-          "did you set the business-onlinepayment configuration value?\n"
-        unless $processor;
-
-      $payment_gateway = new FS::payment_gateway( {
-        gateway_namespace => $conf->config('business-onlinepayment-namespace'),
-        gateway_module    => $processor,
-        gateway_username  => $login,
-        gateway_password  => $password,
-        gateway_action    => $action,
-        options   => [ ( @bop_options ) ],
-      });
-    }
+  if (!$payment_gateway) { 
+
+    my ( $processor, $login, $password, $action, @bop_options ) =
+      $conf->config('business-onlinepayment');
+    $action ||= 'normal authorization';
+    pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+    die "No real-time processor is enabled - ".
+        "did you set the business-onlinepayment configuration value?\n"
+      unless $processor;
+
+    $payment_gateway = new FS::payment_gateway( {
+      gateway_namespace => $conf->config('business-onlinepayment-namespace'),
+      gateway_module    => $processor,
+      gateway_username  => $login,
+      gateway_password  => $password,
+      gateway_action    => $action,
+      options   => [ ( @bop_options ) ],
+    });
   }
  
   die "No real-time third party processor is enabled - ".
index 51af38b..ea1d391 100644 (file)
@@ -770,6 +770,13 @@ my $validate_email = sub { $_[0] =~
   },
   
   {
+    'key'         => 'no_saved_cardnumbers',
+    'section'     => 'credit_cards',
+    'description' => 'Do not allow credit card numbers to be written to the database.  Prevents realtime processing unless payment gateway supports tokenization.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'credit-card-surcharge-percentage',
     'section'     => 'credit_cards',
     'description' => 'Add a credit card surcharge to invoices, as a % of the invoice total.  WARNING: Although recently permitted to US merchants in general, specific consumer protection laws may prohibit or restrict this practice in California, Colorado, Connecticut, Florda, Kansas, Maine, Massachusetts, New York, Oklahome, and Texas.  Surcharging is also generally prohibited in most countries outside the US, AU and UK.  When allowed, typically not permitted to be above 4%.',
@@ -2185,8 +2192,8 @@ and customer address. Include units.',
 
   {
     'key'         => 'selfservice-payment_gateway',
-    'section'     => 'self-service',
-    'description' => 'Force the use of this payment gateway for self-service.',
+    'section'     => 'deprecated',
+    'description' => '(no longer supported) Force the use of this payment gateway for self-service.',
     %payment_gateway_options,
   },
 
index fc23433..c102e7b 100644 (file)
@@ -265,24 +265,14 @@ sub payment_gateway {
   my $conf = new FS::Conf;
 
   if ( $options{thirdparty} ) {
-    # still a kludge, but it gets the job done
-    # and the 'cardtype' semantics don't really apply to thirdparty
-    # gateways because we have to choose a gateway without ever 
-    # seeing the card number
-    my $gatewaynum =
-      $conf->config('selfservice-payment_gateway', $self->agentnum);
-    my $gateway;
-    $gateway = FS::payment_gateway->by_key($gatewaynum) if $gatewaynum;
-    return $gateway if $gateway;
-
-    # a little less kludgey than the above, and allows PayPal to coexist 
-    # with credit card gateways
+
+    # allows PayPal to coexist with credit card gateways
     my $is_paypal = { op => '!=', value => 'PayPal' };
     if ( uc($options{method}) eq 'PAYPAL' ) {
       $is_paypal = 'PayPal';
     }
 
-    $gateway = qsearchs({
+    my $gateway = qsearchs({
         table     => 'payment_gateway',
         addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ',
         hashref   => {
index 3f3b222..c008c2d 100644 (file)
@@ -226,14 +226,6 @@ sub _bop_recurring_billing {
 sub _payment_gateway {
   my ($self, $options) = @_;
 
-  if ( $options->{'selfservice'} ) {
-    my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
-    if ( $gatewaynum ) {
-      return $options->{payment_gateway} ||= 
-          qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
-    }
-  }
-
   if ( $options->{'fake_gatewaynum'} ) {
        $options->{payment_gateway} =
            qsearchs('payment_gateway',
@@ -421,15 +413,17 @@ sub realtime_bop {
 
   # possibly run a separate transaction to tokenize card number,
   #   so that we never store tokenized card info in cust_pay_pending
-  if (!$self->tokenized($options{'payinfo'})) {
+  if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
     my $token_error = $self->realtime_tokenize(\%options);
     return $token_error if $token_error;
     # in theory, all cust_payby will be tokenized during original save,
     # so we shouldn't get here with opt cust_payby...but just in case...
-    if ($options{'cust_payby'}) {
+    if ($options{'cust_payby'} && $self->tokenized($options{'payinfo'})) {
       $token_error = $options{'cust_payby'}->replace;
       return $token_error if $token_error;
     }
+    return "Cannot tokenize card info"
+      if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'});
   }
 
   ### 
@@ -1773,11 +1767,13 @@ sub realtime_verify_bop {
 
   # possibly run a separate transaction to tokenize card number,
   #   so that we never store tokenized card info in cust_pay_pending
-  if (!$self->tokenized($options{'payinfo'})) {
+  if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
     my $token_error = $self->realtime_tokenize(\%options);
     return $token_error if $token_error;
     #important that we not replace cust_payby here,
     #because cust_payby->replace uses realtime_verify_bop!
+    return "Cannot tokenize card info"
+      if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'});
   }
 
   ###
@@ -2224,6 +2220,8 @@ sub realtime_tokenize {
                                     $self->_bop_options(\%options),
                                   );
 
+  return '' unless $transaction->can('info');
+
   my %supported_actions = $transaction->info('supported_actions');
   return '' unless $supported_actions{'CC'}
                 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
@@ -2326,12 +2324,205 @@ sub tokenized {
   FS::cust_pay->tokenized($payinfo);
 }
 
+=item remove_card_numbers
+
+NOT AN OBJECT METHOD.  Acts on all customers.  Placed here because it makes
+use of module-internal methods, and to keep everything that uses
+Billing::OnlinePayment all in one place.
+
+Removes all stored card numbers from payinfo in cust_payby and 
+CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund.
+Will fail if cust_payby records can't be tokenized.  Transaction records that
+cannot be tokenized will have their payinfo replaced with their paymask.
+
+THIS WILL OVERWRITE STORED PAYINFO ON OLD TRANSACTIONS.
+
+If the gateway originally used for the transaction can't tokenize, this may
+prevent the transaction from being voided or refunded.  Hence, it should
+not (yet) be run as part of a regular upgrade.  This is only intended to be
+run on systems with current gateways that tokenize, after the window has
+passed for voiding/refunding transactions from previous gateways, in order 
+to remove all real card numbers from the system.
+
+Also sets the no_saved_cardnumbers conf, to keep things this way.
+
+=cut
+
+# ??? probably should add MCRD handling to this
+
+sub remove_card_numbers {
+  # no input, always does the same thing
+
+  my $cache = {}; #cache for module info
+
+  eval "use FS::Cursor";  
+  return "Error initializing FS::Cursor: ".$@ if $@;
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  # turn this on
+  $conf->touch('no_saved_cardnumbers');
+
+  ### Tokenize cust_payby
+
+  my $cust_search = FS::Cursor->new({ table => 'cust_main' },$dbh);
+  while (my $cust_main = $cust_search->fetch) {
+    foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) {
+      next if $cust_payby->tokenized;
+      # load gateway first, just so we can cache it
+      my $payment_gateway = $cust_main->_payment_gateway({
+        'payinfo' => $cust_payby->payinfo, # for cardtype agent overrides
+        'nofatal' => 1, # handle error smoothly below
+        # invnum -- XXX need to figure out how to handle taxclass overrides
+      });
+      unless ($payment_gateway) {
+        $cust_search->DESTROY;
+        $dbh->rollback if $oldAutoCommit;
+        return "No gateway found for custnum ".$cust_main->custnum;
+      }
+      my $info = $cust_main->_remove_card_numbers_gateway_info($cache,$payment_gateway);
+      unless (ref($info) && $info->{'can_tokenize'}) {
+        $cust_search->DESTROY;
+        $dbh->rollback if $oldAutoCommit;
+        my $error = ref($info)
+          ? "Gateway ".$payment_gateway->gatewaynum." cannot tokenize, for custnum ".$cust_main->custnum
+          : $info;
+        return $error;
+      }
+      my %tokenopts = (
+        'payment_gateway' => $payment_gateway,
+        'cust_payby'      => $cust_payby,
+      );
+      my $error = $cust_main->realtime_tokenize(\%tokenopts);
+      if ($cust_payby->tokenized) { # implies no error
+        $error = $cust_payby->replace;
+      } else {
+        $error = 'Unknown error';
+      }
+      if ($error) {
+        $cust_search->DESTROY;
+        $dbh->rollback if $oldAutoCommit;
+        return "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error;
+      }
+    }
+  }
+
+  ### Tokenize/mask transaction tables
+
+  # grep assistance:
+  #   $cust_pay_pending->replace, $cust_pay->replace, $cust_pay_void->replace, $cust_refund->replace all run here
+  foreach my $table ( qw(cust_pay_pending cust_pay cust_pay_void cust_refund) ) {
+    my $search = FS::Cursor->new({
+      table     => $table,
+      hashref   => { 'payby' => 'CARD' },
+    },$dbh);
+    while (my $record = $search->fetch) {
+      next if $record->tokenized;
+      next if !$record->payinfo; #shouldn't happen, but just in case, no need to mask
+      next if $record->payinfo =~ /N\/A/; # ??? Not sure what's up with these, but no need to mask
+      next if $record->payinfo eq $record->paymask; #already masked
+      my $old_gateway;
+      if (my $old_gatewaynum = $record->gatewaynum) {
+        $old_gateway = 
+          qsearchs('payment_gateway',{ 'gatewaynum' => $old_gatewaynum, });
+        # not erring out if gateway can't be found, just use paymask
+      }
+      # first try to tokenize
+      my $cust_main = $record->cust_main;
+      if ($cust_main && $old_gateway) {
+        my $info = $cust_main->_remove_card_numbers_gateway_info($cache,$old_gateway);
+        unless (ref($info)) {
+          # only throws error if Business::OnlinePayment won't load,
+          #   which is just cause to abort this whole process
+          $search->DESTROY;
+          $dbh->rollback if $oldAutoCommit;
+          return $info;
+        }
+        if ($info->{'can_tokenize'}) {
+          my %tokenopts = (
+            'payment_gateway' => $old_gateway,
+            'method'          => 'CC',
+            'payinfo'         => $record->payinfo,
+            'paydate'         => $record->paydate,
+          );
+          my $error = $cust_main->realtime_tokenize(\%tokenopts);
+          if ($cust_main->tokenized($tokenopts{'payinfo'})) { # implies no error
+            $record->payinfo($tokenopts{'payinfo'});
+            $error = $record->replace;
+          } else {
+            $error = 'Unknown error';
+          }
+          if ($error) {
+            $search->DESTROY;
+            $dbh->rollback if $oldAutoCommit;
+            return "Error tokenizing $table ".$record->get($record->primary_key).": ".$error;
+          }
+          next;
+        }
+      }
+      # can't tokenize, so just replace with paymask
+      $record->set('payinfo',$record->paymask); #deliberately evade ->payinfo() remasking effects
+      my $error = $record->replace;
+      if ($error) {
+        $search->DESTROY;
+        $dbh->rollback if $oldAutoCommit;
+        return "Error masking payinfo for $table ".$record->get($record->primary_key).": ".$error;
+      }
+    }
+  }
+
+  $dbh->commit if $oldAutoCommit;
+
+  return '';
+}
+
+sub _remove_card_numbers_gateway_info {
+  my ($self,$cache,$payment_gateway) = @_;
+
+  return $cache->{$payment_gateway->gateway_module}
+    if $cache->{$payment_gateway->gateway_module};
+
+  my $info = {};
+  $cache->{$payment_gateway->gateway_module} = $info;
+
+  my $namespace = $payment_gateway->gateway_namespace;
+  return $info unless $namespace eq 'Business::OnlinePayment';
+  $info->{'is_bop'} = 1;
+
+  # only need to load this once,
+  # don't want to load if nothing is_bop
+  unless ($cache->{'Business::OnlinePayment'}) {
+    eval "use $namespace";  
+    return "Error initializing Business:OnlinePayment: ".$@ if $@;
+    $cache->{'Business::OnlinePayment'} = 1;
+  }
+
+  my $transaction = new $namespace( $payment_gateway->gateway_module,
+                                    $self->_bop_options({ 'payment_gateway' => $payment_gateway }),
+                                  );
+
+  return $info unless $transaction->can('info');
+  $info->{'can_info'} = 1;
+
+  my %supported_actions = $transaction->info('supported_actions');
+  $info->{'can_tokenize'} = 1
+    if $supported_actions{'CC'}
+      && grep /^Tokenize$/, @{$supported_actions{'CC'}};
+
+  $info->{'void_requires_card'} = 1
+    if $transaction->info('CC_void_requires_card');
+
+  $cache->{$payment_gateway->gateway_module} = $info;
+
+  return $info;
+}
+
 =back
 
 =head1 BUGS
 
-Not autoloaded.
-
 =head1 SEE ALSO
 
 L<FS::cust_main>, L<FS::cust_main::Billing>
index dfcce2f..7a3dcf0 100644 (file)
@@ -194,6 +194,8 @@ 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;
@@ -212,8 +214,10 @@ sub payinfo_check {
         $self->payinfo($1);
         validate($self->payinfo) or return "Illegal credit card number";
         return "Unknown card type" if $cardtype eq "Unknown";
+        return "Card number not tokenized"
+          if $conf->exists('no_saved_cardnumbers') && !$self->tokenized;
       } else {
-        $self->payinfo('N/A'); #???
+        $self->payinfo('N/A'); #??? re-masks card
       }
     }
   } else {
index 50659ac..6e4b511 100644 (file)
@@ -102,7 +102,11 @@ auth, and order_number) as well as payby and payinfo
 sub payinfo_check {
   my $self = shift;
 
-  # All of these can be null, so in principle this could go in payinfo_Mixin.
+  my $conf = new FS::Conf;
+
+  # allow masked payinfo if we never save card numbers
+  local $FS::payinfo_Mixin::ignore_masked_payinfo = 
+    $conf->exists('no_saved_cardnumbers') ? 1 : $FS::payinfo_Mixin::ignore_masked_payinfo;
 
   $self->SUPER::payinfo_check()
   || $self->ut_numbern('gatewaynum')