[freeside-commits] branch master updated. c5a2d3613acdc3b9ab6f32eaf5316c2834071417

Jonathan Prykop jonathan at 420.am
Tue Oct 11 18:48:01 PDT 2016


The branch, master has been updated
       via  c5a2d3613acdc3b9ab6f32eaf5316c2834071417 (commit)
      from  04f53daab621710db56b075e1aaf56e7c52f9ba9 (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 c5a2d3613acdc3b9ab6f32eaf5316c2834071417
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Tue Oct 11 20:43:13 2016 -0500

    71513: Card tokenization in v4+

diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 2f05af6..11d7763 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -4443,6 +4443,10 @@ CHEK only
 
 CHEK only
 
+=item saved_cust_payby
+
+scalar reference, for returning saved object
+
 =back
 
 =cut
@@ -4639,6 +4643,9 @@ PAYBYLOOP:
     return $error;
   }
 
+  ${$opt{'saved_cust_payby'}} = $new
+    if $opt{'saved_cust_payby'};
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 
diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index 2951360..ced3b23 100644
--- a/FS/FS/cust_main/Billing_Realtime.pm
+++ b/FS/FS/cust_main/Billing_Realtime.pm
@@ -111,6 +111,8 @@ I<depend_jobnum> allows payment capture to unlock export jobs
 
 =cut
 
+# Currently only used by ClientAPI
+# NOT 4.x COMPATIBLE (see below)
 sub realtime_collect {
   my( $self, %options ) = @_;
 
@@ -124,6 +126,7 @@ sub realtime_collect {
   $options{amount} = $self->balance unless exists( $options{amount} );
   return '' unless $options{amount} > 0;
 
+  #### NOT 4.x COMPATIBLE
   $options{method} = FS::payby->payby2bop($self->payby)
     unless exists( $options{method} );
 
@@ -137,16 +140,14 @@ Runs a realtime credit card or ACH (electronic check) transaction
 via a Business::OnlinePayment realtime gateway.  See
 L<http://420.am/business-onlinepayment> for supported gateways.
 
-Required arguments in the hashref are I<method>, and I<amount>
+Required arguments in the hashref are I<amount> and either
+I<cust_payby> or I<method>, I<payinfo> and (as applicable for method)
+I<payname>, I<address1>, I<address2>, I<city>, I<state>, I<zip> and I<paydate>.
 
 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
 
 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
 
-The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
-I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
-if set, will override the value from the customer record.
-
 I<description> is a free-text field passed to the gateway.  It defaults to
 the value defined by the business-onlinepayment-description configuration
 option, or "Internet services" if that is unset.
@@ -279,11 +280,6 @@ sub _bop_defaults {
     }
   }
 
-  unless ( exists( $options->{'payinfo'} ) ) {
-    $options->{'payinfo'} = $self->payinfo;
-    $options->{'paymask'} = $self->paymask;
-  }
-
   # Default invoice number if the customer has exactly one open invoice.
   unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
     $options->{'invnum'} = '';
@@ -291,14 +287,50 @@ sub _bop_defaults {
     $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
   }
 
-  $options->{payname} = $self->payname unless exists( $options->{payname} );
+}
+
+sub _bop_cust_payby_options {
+  my ($self,$options) = @_;
+  my $cust_payby = $options->{'cust_payby'};
+  if ($cust_payby) {
+
+    $options->{'method'} = FS::payby->payby2bop( $cust_payby->payby );
+
+    if ($cust_payby->payby =~ /^(CARD|DCRD)$/) {
+      # false laziness with cust_payby->check
+      #   which might not have been run yet
+      my( $m, $y );
+      if ( $cust_payby->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
+        ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
+      } elsif ( $cust_payby->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
+        ( $m, $y ) = ( $2, "19$1" );
+      } elsif ( $cust_payby->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
+        ( $m, $y ) = ( $3, "20$2" );
+      } else {
+        return "Illegal expiration date: ". $cust_payby->paydate;
+      }
+      $m = sprintf('%02d',$m);
+      $options->{paydate} = "$y-$m-01";
+    } else {
+      $options->{paydate} = '';
+    }
+
+    $options->{$_} = $cust_payby->$_() 
+      for qw( payinfo paycvv paymask paystart_month paystart_year 
+              payissue payname paystate paytype payip );
+
+    if ( $cust_payby->locationnum ) {
+      my $cust_location = $cust_payby->cust_location;
+      $options->{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
+    }
+  }
 }
 
 sub _bop_content {
   my ($self, $options) = @_;
   my %content = ();
 
-  my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
+  my $payip = $options->{'payip'};
   $content{customer_ip} = $payip if length($payip);
 
   $content{invoice_number} = $options->{'invnum'}
@@ -325,26 +357,14 @@ sub _bop_content {
 
   $content{name} = $payname;
 
-  $content{address} = exists($options->{'address1'})
-                        ? $options->{'address1'}
-                        : $self->address1;
-  my $address2 = exists($options->{'address2'})
-                   ? $options->{'address2'}
-                   : $self->address2;
+  $content{address} = $options->{'address1'};
+  my $address2 = $options->{'address2'};
   $content{address} .= ", ". $address2 if length($address2);
 
-  $content{city} = exists($options->{city})
-                     ? $options->{city}
-                     : $self->city;
-  $content{state} = exists($options->{state})
-                      ? $options->{state}
-                      : $self->state;
-  $content{zip} = exists($options->{zip})
-                    ? $options->{'zip'}
-                    : $self->zip;
-  $content{country} = exists($options->{country})
-                        ? $options->{country}
-                        : $self->country;
+  $content{city} = $options->{'city'};
+  $content{state} = $options->{'state'};
+  $content{zip} = $options->{'zip'};
+  $content{country} = $options->{'country'};
 
   $content{phone} = $self->daytime || $self->night;
 
@@ -356,28 +376,24 @@ sub _bop_content {
 }
 
 sub _tokenize_card {
-  my ($self,$transaction,$payinfo,$log) = @_;
+  my ($self,$transaction,$cust_payby,$log,%opt) = @_;
 
-  if ( $transaction->can('card_token') 
+  if ( $cust_payby
+       and $transaction->can('card_token') 
        and $transaction->card_token 
-       and $payinfo !~ /^99\d{14}$/ #not already tokenized
+       and $cust_payby->payinfo !~ /^99\d{14}$/ #not already tokenized
   ) {
 
-    my @cust_payby = $self->cust_payby('CARD','DCRD');
-    @cust_payby = grep { $payinfo == $_->payinfo } @cust_payby;
-    if (@cust_payby > 1) {
-      $log->error('Multiple matching card numbers for cust '.$self->custnum.', could not tokenize card');
-    } elsif (@cust_payby) {
-      my $cust_payby = $cust_payby[0];
-      $cust_payby->payinfo($transaction->card_token);
-      my $error = $cust_payby->replace;
-      if ( $error ) {
-        $log->error('Error storing token for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum.': '.$error);
-      } else {
-        $log->debug('Tokenized card for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum);
-      }
+    $cust_payby->payinfo($transaction->card_token);
+
+    my $error;
+    $error = $cust_payby->replace if $opt{'replace'};
+    if ( $error ) {
+      $log->error('Error storing token for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum.': '.$error);
+      return $error;
     } else {
-      $log->debug('No matching card numbers for cust '.$self->custnum.', could not tokenize card');
+      $log->debug('Tokenized card for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum);
+      return '';
     }
 
   }
@@ -411,6 +427,8 @@ sub realtime_bop {
     $options{amount} = $amount;
   }
 
+  # set fields from passed cust_payby
+  $self->_bop_cust_payby_options(\%options);
 
   ### 
   # optional credit card surcharge
@@ -450,6 +468,9 @@ sub realtime_bop {
 
   $self->_bop_defaults(\%options);
 
+  return "Missing payinfo"
+    unless $options{'payinfo'};
+
   ###
   # set trans_is_recur based on invnum if there is one
   ###
@@ -535,29 +556,19 @@ sub realtime_bop {
     if ( $options{method} eq 'CC' ) {
 
       $content{card_number} = $options{payinfo};
-      $paydate = exists($options{'paydate'})
-                      ? $options{'paydate'}
-                      : $self->paydate;
+      $paydate = $options{'paydate'};
       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
       $content{expiration} = "$2/$1";
 
       $content{cvv2} = $options{'paycvv'}
         if length($options{'paycvv'});
 
-      my $paystart_month = exists($options{'paystart_month'})
-                             ? $options{'paystart_month'}
-                             : $self->paystart_month;
-
-      my $paystart_year  = exists($options{'paystart_year'})
-                             ? $options{'paystart_year'}
-                             : $self->paystart_year;
-
+      my $paystart_month = $options{'paystart_month'};
+      my $paystart_year  = $options{'paystart_year'};
       $content{card_start} = "$paystart_month/$paystart_year"
         if $paystart_month && $paystart_year;
 
-      my $payissue       = exists($options{'payissue'})
-                             ? $options{'payissue'}
-                             : $self->payissue;
+      my $payissue       = $options{'payissue'};
       $content{issue_number} = $payissue if $payissue;
 
       if ( $self->_bop_recurring_billing(
@@ -576,13 +587,8 @@ sub realtime_bop {
       ( $content{account_number}, $content{routing_code} ) =
         split('@', $options{payinfo});
       $content{bank_name} = $options{payname};
-      $content{bank_state} = exists($options{'paystate'})
-                               ? $options{'paystate'}
-                               : $self->getfield('paystate');
-      $content{account_type}=
-        (exists($options{'paytype'}) && $options{'paytype'})
-          ? uc($options{'paytype'})
-          : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
+      $content{bank_state} = $options{'paystate'};
+      $content{account_type}= uc($options{'paytype'}) || 'PERSONAL CHECKING';
 
       $content{company} = $self->company if $self->company;
 
@@ -805,7 +811,8 @@ sub realtime_bop {
   # Tokenize
   ###
 
-  $self->_tokenize_card($transaction,$options{'payinfo'},$log);
+  my $error = $self->_tokenize_card($transaction,$options{'cust_payby'},$log,'replace' => 1);
+  return $error if $error;
 
   ###
   # result handling
@@ -1721,21 +1728,14 @@ successful, immediatly reverses the authorization).
 Returns the empty string if the authorization was sucessful, or an error
 message otherwise.
 
-I<payinfo>
+Option I<cust_payby> should be passed, even if it's not yet been inserted.
+Object will be tokenized if possible, but that change will not be
+updated in database (must be inserted/replaced afterwards.)
 
-I<payname>
-
-I<paydate> specifies the expiration date for a credit card overriding the
-value from the customer record or the payment record. Specified as yyyy-mm-dd
-
-#The additional options I<address1>, I<address2>, I<city>, I<state>,
-#I<zip> are also available.  Any of these options,
-#if set, will override the value from the customer record.
+Currently only succeeds for Business::OnlinePayment CC transactions.
 
 =cut
 
-#Available methods are: I<CC> or I<ECHECK>
-
 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
 #it worth merging but some useful small subs should be pulled out
 sub realtime_verify_bop {
@@ -1756,6 +1756,10 @@ sub realtime_verify_bop {
     warn "  $_ => $options{$_}\n" foreach keys %options;
   }
 
+  # set fields from passed cust_payby
+  return "No cust_payby" unless $options{'cust_payby'};
+  $self->_bop_cust_payby_options(\%options);
+
   ###
   # select a gateway
   ###
@@ -1802,43 +1806,33 @@ sub realtime_verify_bop {
     if ( $options{method} eq 'CC' ) {
 
       $content{card_number} = $options{payinfo};
-      $paydate = exists($options{'paydate'})
-                      ? $options{'paydate'}
-                      : $self->paydate;
+      $paydate = $options{'paydate'};
       $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
       $content{expiration} = "$2/$1";
 
       $content{cvv2} = $options{'paycvv'}
         if length($options{'paycvv'});
 
-      my $paystart_month = exists($options{'paystart_month'})
-                             ? $options{'paystart_month'}
-                             : $self->paystart_month;
-
-      my $paystart_year  = exists($options{'paystart_year'})
-                             ? $options{'paystart_year'}
-                             : $self->paystart_year;
+      my $paystart_month = $options{'paystart_month'};
+      my $paystart_year  = $options{'paystart_year'};
 
       $content{card_start} = "$paystart_month/$paystart_year"
         if $paystart_month && $paystart_year;
 
-      my $payissue       = exists($options{'payissue'})
-                             ? $options{'payissue'}
-                             : $self->payissue;
+      my $payissue       = $options{'payissue'};
       $content{issue_number} = $payissue if $payissue;
 
     } elsif ( $options{method} eq 'ECHECK' ){
-
-      #nop for checks (though it shouldn't be called...)
-
+      #cannot verify, move along (though it shouldn't be called...)
+      return '';
     } else {
-      die "unknown method ". $options{method};
+      return "unknown method ". $options{method};
     }
-
   } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
-    #move along
+    #cannot verify, move along
+    return '';
   } else {
-    die "unknown namespace $namespace";
+    return "unknown namespace $namespace";
   }
 
   ###
@@ -1847,6 +1841,7 @@ sub realtime_verify_bop {
 
   my $error;
   my $transaction; #need this back so we can do _tokenize_card
+
   # don't mutex the customer here, because they might be uncommitted. and
   # this is only verification. it doesn't matter if they have other
   # unfinished verifications.
@@ -1859,12 +1854,10 @@ sub realtime_verify_bop {
     'payinfo'           => $options{payinfo},
     'paymask'           => $options{paymask},
     'paydate'           => $paydate,
-    #'recurring_billing' => $content{recurring_billing},
     'pkgnum'            => $options{'pkgnum'},
     'status'            => 'new',
     'gatewaynum'        => $payment_gateway->gatewaynum || '',
     'session_id'        => $options{session_id} || '',
-    #'jobnum'            => $options{depend_jobnum} || '',
   };
   $cust_pay_pending->payunique( $options{payunique} )
     if defined($options{payunique}) && length($options{payunique});
@@ -1905,12 +1898,9 @@ sub realtime_verify_bop {
       'action'         => 'Authorization Only',
       'description'    => $options{'description'},
       'amount'         => '1.00',
-      #'invoice_number' => $options{'invnum'},
       'customer_id'    => $self->custnum,
       %$bop_content,
       'reference'      => $cust_pay_pending->paypendingnum, #for now
-      'callback_url'   => $payment_gateway->gateway_callback_url,
-      'cancel_url'     => $payment_gateway->gateway_cancel_url,
       'email'          => $email,
       %content, #after
     );
@@ -2123,7 +2113,9 @@ sub realtime_verify_bop {
   # Tokenize
   ###
 
-  $self->_tokenize_card($transaction,$options{'payinfo'},$log);
+  #important that we not pass replace option here,
+  #because cust_payby->replace uses realtime_verify_bop!
+  $self->_tokenize_card($transaction,$options{'cust_payby'},$log);
 
   ###
   # result handling
@@ -2135,6 +2127,144 @@ sub realtime_verify_bop {
 
 }
 
+=item realtime_tokenize [ OPTION => VALUE ... ]
+
+If possible, runs a tokenize transaction.
+In order to be possible, a credit card cust_payby record
+must be passed and a Business::OnlinePayment gateway capable
+of Tokenize transactions must be configured for this user.
+
+Returns the empty string if the authorization was sucessful
+or was not possible (thus allowing this to be safely called with
+non-tokenizable records/gateways, without having to perform separate tests),
+or an error message otherwise.
+
+Option I<cust_payby> should be passed, even if it's not yet been inserted.
+Object will be tokenized if possible, but that change will not be
+updated in database (must be inserted/replaced afterwards.)
+
+=cut
+
+sub realtime_tokenize {
+  my $self = shift;
+
+  local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+  my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_tokenize');
+
+  my %options = ();
+  if (ref($_[0]) eq 'HASH') {
+    %options = %{$_[0]};
+  } else {
+    %options = @_;
+  }
+
+  # set fields from passed cust_payby
+  return "No cust_payby" unless $options{'cust_payby'};
+  $self->_bop_cust_payby_options(\%options);
+  return '' unless $options{method} eq 'CC';
+  return '' if $options{payinfo} =~ /^99\d{14}$/; #already tokenized
+
+  ###
+  # select a gateway
+  ###
+
+  $options{'nofatal'} = 1;
+  my $payment_gateway =  $self->_payment_gateway( \%options );
+  return '' unless $payment_gateway;
+  my $namespace = $payment_gateway->gateway_namespace;
+  return '' unless $namespace eq 'Business::OnlinePayment';
+
+  eval "use $namespace";  
+  return $@ if $@;
+
+  ###
+  # check for tokenize ability
+  ###
+
+  # just create transaction now, so it loads gateway_module
+  my $transaction = new $namespace( $payment_gateway->gateway_module,
+                                    $self->_bop_options(\%options),
+                                  );
+
+  my %supported_actions = $transaction->info('supported_actions');
+  return '' unless $supported_actions{'CC'} and grep(/^Tokenize$/,@{$supported_actions{'CC'}});
+
+  ###
+  # check for banned credit card/ACH
+  ###
+
+  my $ban = FS::banned_pay->ban_search(
+    'payby'   => $bop_method2payby{'CC'},
+    'payinfo' => $options{payinfo},
+  );
+  return "Banned credit card" if $ban && $ban->bantype ne 'warn';
+
+  ###
+  # massage data
+  ###
+
+  my $bop_content = $self->_bop_content(\%options);
+  return $bop_content unless ref($bop_content);
+
+  my $paydate = '';
+  my %content = ();
+
+  $content{card_number} = $options{payinfo};
+  $paydate = $options{'paydate'};
+  $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+  $content{expiration} = "$2/$1";
+
+  $content{cvv2} = $options{'paycvv'}
+    if length($options{'paycvv'});
+
+  my $paystart_month = $options{'paystart_month'};
+  my $paystart_year  = $options{'paystart_year'};
+
+  $content{card_start} = "$paystart_month/$paystart_year"
+    if $paystart_month && $paystart_year;
+
+  my $payissue       = $options{'payissue'};
+  $content{issue_number} = $payissue if $payissue;
+
+  ###
+  # run transaction
+  ###
+
+  my $error;
+
+  # no cust_pay_pending---this is not a financial transaction
+
+  $transaction->content(
+    'type'           => 'CC',
+    $self->_bop_auth(\%options),          
+    'action'         => 'Tokenize',
+    'description'    => $options{'description'},
+    'customer_id'    => $self->custnum,
+    %$bop_content,
+    %content, #after
+  );
+
+  # no $BOP_TESTING handling for this
+  $transaction->test_transaction(1)
+    if $conf->exists('business-onlinepayment-test_transaction');
+  $transaction->submit();
+
+  if ( $transaction->card_token() ) { # no is_success flag
+
+    #important that we not pass replace option here, 
+    #because cust_payby->replace uses realtime_tokenize!
+    $self->_tokenize_card($transaction,$options{'cust_payby'},$log);
+
+  } else {
+
+    $error = $transaction->error_message || 'Unknown error';
+
+  }
+
+  return $error;
+
+}
+
 =back
 
 =head1 BUGS
diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm
index e4a1d19..626fc9f 100644
--- a/FS/FS/cust_payby.pm
+++ b/FS/FS/cust_payby.pm
@@ -250,8 +250,11 @@ sub replace {
 
     if ( $conf->exists('business-onlinepayment-verification') ) {
       $error = $self->verify;
-      return $error if $error;
+    } else {
+      $error = $self->tokenize;
     }
+    return $error if $error;
+
   }
 
   local $SIG{HUP} = 'IGNORE';
@@ -521,9 +524,12 @@ sub check {
 
   }
 
-  if ( ! $self->custpaybynum
-       && $conf->exists('business-onlinepayment-verification') ) {
-    $error = $self->verify;
+  if ( ! $self->custpaybynum ) {
+    if ($conf->exists('business-onlinepayment-verification')) {
+      $error = $self->verify;
+    } else {
+      $error = $self->tokenize;
+    }
     return $error if $error;
   }
 
@@ -638,59 +644,48 @@ sub label {
 
 =item realtime_bop
 
+Runs a L<realtime_bop|FS::cust_main::Billing_Realtime::realtime_bop> transaction on this card
+
 =cut
 
 sub realtime_bop {
   my( $self, %opt ) = @_;
 
-  $opt{$_} = $self->$_() for qw( payinfo payname paydate );
-
-  if ( $self->locationnum ) {
-    my $cust_location = $self->cust_location;
-    $opt{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
-  }
-
   $self->cust_main->realtime_bop({
-    'method' => FS::payby->payby2bop( $self->payby ),
     %opt,
+    'cust_payby' => $self,
   });
 
 }
 
-=item verify 
+=item tokenize
+
+Runs a L<realtime_tokenize|FS::cust_main::Billing_Realtime::realtime_tokenize> transaction on this card
 
 =cut
 
-sub verify {
+sub tokenize {
   my $self = shift;
   return '' unless $self->payby =~ /^(CARD|DCRD)$/;
 
-  my %opt = ();
+  $self->cust_main->realtime_tokenize({
+    'cust_payby' => $self,
+  });
 
-  # false laziness with check
-  my( $m, $y );
-  if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
-    ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
-  } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
-    ( $m, $y ) = ( $2, "19$1" );
-  } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
-    ( $m, $y ) = ( $3, "20$2" );
-  } else {
-    return "Illegal expiration date: ". $self->paydate;
-  }
-  $m = sprintf('%02d',$m);
-  $opt{paydate} = "$y-$m-01";
+}
 
-  $opt{$_} = $self->$_() for qw( payinfo payname paycvv );
+=item verify 
 
-  if ( $self->locationnum ) {
-    my $cust_location = $self->cust_location;
-    $opt{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
-  }
+Runs a L<realtime_verify_bop|FS::cust_main::Billing_Realtime/realtime_verify_bop> transaction on this card
+
+=cut
+
+sub verify {
+  my $self = shift;
+  return '' unless $self->payby =~ /^(CARD|DCRD)$/;
 
   $self->cust_main->realtime_verify_bop({
-    'method' => FS::payby->payby2bop( $self->payby ),
-    %opt,
+    'cust_payby' => $self,
   });
 
 }
diff --git a/FS/FS/log_context.pm b/FS/FS/log_context.pm
index ee3e413..809237d 100644
--- a/FS/FS/log_context.pm
+++ b/FS/FS/log_context.pm
@@ -9,6 +9,7 @@ my @contexts = ( qw(
   FS::cust_main::Billing::bill_and_collect
   FS::cust_main::Billing::bill
   FS::cust_main::Billing_Realtime::realtime_bop
+  FS::cust_main::Billing_Realtime::realtime_tokenize
   FS::cust_main::Billing_Realtime::realtime_verify_bop
   FS::pay_batch::import_from_gateway
   FS::part_pkg
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 5f7ce35..3a32ad5 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -67,8 +67,9 @@ sub payinfo {
   my($self,$payinfo) = @_;
 
   if ( defined($payinfo) ) {
+    $self->paymask($self->mask_payinfo) unless $self->payinfo =~ /^99\d{14}$/; #make sure old mask is set
     $self->setfield('payinfo', $payinfo);
-    $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #token
+    $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #remask unless tokenizing
   } else {
     $self->getfield('payinfo');
   }
diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi
index 852becb..74ca734 100644
--- a/httemplate/misc/process/payment.cgi
+++ b/httemplate/misc/process/payment.cgi
@@ -72,7 +72,7 @@ $cgi->param('discount_term') =~ /^(\d*)$/
   or errorpage("illegal discount_term");
 my $discount_term = $1;
 
-my( $payinfo, $paycvv, $month, $year, $payname );
+my( $cust_payby, $payinfo, $paycvv, $month, $year, $payname );
 my $paymask = '';
 if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
 
@@ -80,10 +80,11 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
   # use stored cust_payby info
   ##
 
-  my $cust_payby = qsearchs('cust_payby', { custnum      => $custnum,
+  $cust_payby = qsearchs('cust_payby', { custnum      => $custnum,
                                             custpaybynum => $custpaybynum, } )
     or die "unknown custpaybynum $custpaybynum";
 
+  # not needed for realtime_bop, but still needed for batch_card
   $payinfo = $cust_payby->payinfo;
   $paymask = $cust_payby->paymask;
   $paycvv = $cust_payby->paycvv; # pass it if we got it, running a transaction will clear it
@@ -164,7 +165,7 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
     die "unknown payby $payby";
   }
 
-  # save first, for proper tokenization later
+  # save first, for proper tokenization
   if ( $cgi->param('save') ) {
 
     my %saveopt;
@@ -181,6 +182,7 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
     }
 
     my $error = $cust_main->save_cust_payby(
+      'saved_cust_payby' => \$cust_payby,
       'payment_payby' => $payby,
       'auto'          => scalar($cgi->param('auto')),
       'weight'        => scalar($cgi->param('weight')),
@@ -220,6 +222,7 @@ if ( $cgi->param('batch') ) {
 } else {
 
   $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
+    'cust_payby' => $cust_payby, # if defined, will override passed payinfo, etc 
     'quiet'      => 1,
     'manual'     => 1,
     'balance'    => $balance,

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

Summary of changes:
 FS/FS/cust_main.pm                  |    7 +
 FS/FS/cust_main/Billing_Realtime.pm |  342 ++++++++++++++++++++++++-----------
 FS/FS/cust_payby.pm                 |   65 +++----
 FS/FS/log_context.pm                |    1 +
 FS/FS/payinfo_Mixin.pm              |    3 +-
 httemplate/misc/process/payment.cgi |    9 +-
 6 files changed, 282 insertions(+), 145 deletions(-)




More information about the freeside-commits mailing list