[freeside-commits] branch master updated. 768ab093771b3305a67c9d929b461ef777ecdad8

Jonathan Prykop jonathan at 420.am
Fri Jan 15 10:42:33 PST 2016


The branch, master has been updated
       via  768ab093771b3305a67c9d929b461ef777ecdad8 (commit)
      from  873c5e2e62ccdbfc0632fdc27151cd040a12c3f6 (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 768ab093771b3305a67c9d929b461ef777ecdad8
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Fri Jan 15 12:41:48 2016 -0600

    RT#38363: use cust_payby when saving cards during payments

diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index 33a8e61..6b91101 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -853,27 +853,33 @@ sub payment_info {
   $return{$_} = $cust_main->bill_location->get($_) 
     for qw(address1 address2 city state zip);
 
-  #XXX look for stored cust_payby info
-  #
-  # $return{payname} = $cust_main->payname
-  #                    || ( $cust_main->first. ' '. $cust_main->get('last') );
-  #
-  #if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
-  #  $return{card_type} = cardtype($cust_main->payinfo);
-  #  $return{payinfo} = $cust_main->paymask;
-  #
-  #  @return{'month', 'year'} = $cust_main->paydate_monthyear;
-  #
-  #}
-  #
-  #if ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) {
-  #  my ($payinfo1, $payinfo2) = split '@', $cust_main->paymask;
-  #  $return{payinfo1} = $payinfo1;
-  #  $return{payinfo2} = $payinfo2;
-  #  $return{paytype}  = $cust_main->paytype;
-  #  $return{paystate} = $cust_main->paystate;
-  #  $return{payname}  = $cust_main->payname;	# override 'first/last name' default from above, if any.  Is instution-name here.  (#15819)
-  #}
+  # look for stored cust_payby info
+  #   only if we've been given a clear payment_payby (to avoid payname conflicts)
+  if ($p->{'payment_payby'} =~ /^(CARD|CHEK)$/) {
+    my @search_payby = ($p->{'payment_payby'} eq 'CARD') ? ('CARD','DCRD') : ('CHEK','DCHK');
+    my ($cust_payby) = $cust_main->cust_payby(@search_payby);
+    if ($cust_payby) {
+      $return{payname} = $cust_payby->payname
+                         || ( $cust_main->first. ' '. $cust_main->get('last') );
+
+      if ( $cust_payby->payby =~ /^(CARD|DCRD)$/ ) {
+        $return{card_type} = cardtype($cust_payby->payinfo);
+        $return{payinfo} = $cust_payby->paymask;
+
+        @return{'month', 'year'} = $cust_payby->paydate_monthyear;
+
+      }
+
+      if ( $cust_payby->payby =~ /^(CHEK|DCHK)$/ ) {
+        my ($payinfo1, $payinfo2) = split '@', $cust_payby->paymask;
+        $return{payinfo1} = $payinfo1;
+        $return{payinfo2} = $payinfo2;
+        $return{paytype}  = $cust_payby->paytype;
+        $return{paystate} = $cust_payby->paystate;
+        $return{payname}  = $cust_payby->payname;	# override 'first/last name' default from above, if any.  Is instution-name here.  (#15819)
+      }
+    }
+  }
 
   if ( $conf->config('prepayment_discounts-credit_type') ) {
     #need to eval?
@@ -961,8 +967,12 @@ sub validate_payment {
     my $payinfo2 = $1;
     $payinfo = $payinfo1. '@'. $payinfo2;
 
-    $payinfo = $cust_main->payinfo
-      if $cust_main->paymask eq $payinfo;
+    foreach my $cust_payby ($cust_main->cust_payby('CHEK','DCHK')) {
+      if ( $cust_payby->paymask eq $payinfo ) {
+        $payinfo = $cust_payby->payinfo;
+        last;
+      }
+    }
    
   } elsif ( $payby eq 'CARD' || $payby eq 'DCRD' ) {
    
@@ -972,9 +982,12 @@ sub validate_payment {
 
     #more intelligent matching will be needed here if you change
     #card_masking_method and don't remove existing paymasks
-    if ( $cust_main->paymask eq $payinfo ) {
-      $payinfo = $cust_main->payinfo;
-      $onfile = 1;
+    foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) {
+      if ( $cust_payby->paymask eq $payinfo ) {
+        $payinfo = $cust_payby->payinfo;
+        $onfile = 1;
+        last;
+      }
     }
 
     $payinfo =~ s/\D//g;
@@ -1092,28 +1105,33 @@ sub do_process_payment {
   my $payby = delete $validate->{'payby'};
 
   if ( $validate->{'save'} ) {
-    my $new = new FS::cust_main { $cust_main->hash };
-    if ($payby eq 'CARD' || $payby eq 'DCRD') {
-      $new->set( $_ => $validate->{$_} )
-        foreach qw( payname paystart_month paystart_year payissue payip );
-      $new->set( 'payby' => $validate->{'auto'} ? 'CARD' : 'DCRD' );
 
+    my %saveopt;
+    foreach my $field ( qw( auto payinfo paymask payname payip ) ) {
+      $saveopt{$field} = $validate->{$field};
+    }
+
+    if ( $payby eq 'CARD' ) {
       my $bill_location = FS::cust_location->new({
           map { $_ => $validate->{$_} } 
           qw(address1 address2 city state country zip)
-      }); # county?
-      $new->set('bill_location' => $bill_location);
-      # but don't allow the service address to change this way.
-
-    } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') {
-      $new->set( $_ => $validate->{$_} )
-        foreach qw( payname payip paytype paystate
-                    stateid stateid_state );
-      $new->set( 'payby' => $validate->{'auto'} ? 'CHEK' : 'DCHK' );
+      });
+      $saveopt{'bill_location'} = $bill_location;
+      foreach my $field ( qw( paydate paystart_month paystart_year payissue ) ) {
+        $saveopt{$field} = $validate->{$field};
+      }
+    } else {
+      # stateid/stateid_state won't be saved, might be broken as of 4.x
+      foreach my $field ( qw( paytype paystate ) ) {
+        $saveopt{$field} = $validate->{$field};
+      }
     }
-    $new->payinfo( $validate->{'payinfo'} ); #to properly set paymask
-    $new->set( 'paydate' => $validate->{'paydate'} );
-    my $error = $new->replace($cust_main);
+
+    my $error = $cust_main->save_cust_payby(
+      'payment_payby' => $payby,
+      %saveopt
+    );
+
     if ( $error ) {
       #no, this causes customers to process their payments again
       #return { 'error' => $error };
@@ -1122,11 +1140,10 @@ sub do_process_payment {
       #address" page but indicate if the payment processed?
       delete($validate->{'payinfo'}); #don't want to log this!
       warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n".
-           "NEW: ". Dumper($new)."\n".
-           "OLD: ". Dumper($cust_main)."\n".
+           "PAYBY: $payby\n".
+           "SAVEOPT: ".Dumper(\%saveopt)."\n".
+           "CUST_MAIN: ". Dumper($cust_main)."\n".
            "PACKET: ". Dumper($validate)."\n";
-    } else {
-      $cust_main = $new;
     }
   }
 
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index f6b6862..ee70dea 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -2169,21 +2169,35 @@ sub cust_contact {
   qsearch('cust_contact', { 'custnum' => $self->custnum } );
 }
 
-=item cust_payby
+=item cust_payby PAYBY
 
 Returns all payment methods (see L<FS::cust_payby>) for this customer.
 
+If one or more PAYBY are specified, returns only payment methods for specified PAYBY.
+Does not validate PAYBY--do not pass tainted values.
+
 =cut
 
 sub cust_payby {
   my $self = shift;
-  qsearch({
+  my @payby = @_;
+  my $search = {
     'table'    => 'cust_payby',
     'hashref'  => { 'custnum' => $self->custnum },
     'order_by' => "ORDER BY payby IN ('CARD','CHEK') DESC, weight ASC",
-  });
+  };
+  $search->{'extra_sql'} = ' AND payby IN ( ' . join(',', map { "'$_'" } @payby) . ' ) '
+    if @payby;
+
+  qsearch($search);
 }
 
+=item has_cust_payby_auto
+
+Returns true if customer has an automatic payment method ('CARD' or 'CHEK')
+
+=cut
+
 sub has_cust_payby_auto {
   my $self = shift;
   scalar( qsearch({ 
@@ -2885,24 +2899,6 @@ sub payment_info {
 
 }
 
-=item paydate_monthyear
-
-Returns a two-element list consisting of the month and year of this customer's
-paydate (credit card expiration date for CARD customers)
-
-=cut
-
-sub paydate_monthyear {
-  my $self = shift;
-  if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
-    ( $2, $1 );
-  } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
-    ( $1, $3 );
-  } else {
-    ('', '');
-  }
-}
-
 =item paydate_epoch
 
 Returns the exact time in seconds corresponding to the payment method 
@@ -4406,6 +4402,246 @@ sub payment_history {
   return @out;
 }
 
+=item save_cust_payby
+
+Saves a new cust_payby for this customer, replacing an existing entry only
+in select circumstances.  Does not validate input.
+
+If auto is specified, marks this as the customer's primary method (weight 1) 
+and changes existing primary methods for that payby to secondary methods (weight 2.)
+If bill_location is specified with auto, also sets location in cust_main.
+
+Will not insert complete duplicates of existing records, or records in which the
+only difference from an existing record is to turn off automatic payment (will
+return without error.)  Will replace existing records in which the only difference 
+is to add a value to a previously empty preserved field and/or turn on automatic payment.
+Fields marked as preserved are optional, and existing values will not be overwritten with 
+blanks when replacing.
+
+Accepts the following named parameters:
+
+payment_payby - either CARD or CHEK
+
+auto - save as an automatic payment type (CARD/CHEK if true, DCRD/DCHK if false)
+
+payinfo - required
+
+paymask - optional, but should be specified for anything that might be tokenized, will be preserved when replacing
+
+payname - required
+
+payip - optional, will be preserved when replacing
+
+paydate - CARD only, required
+
+bill_location - CARD only, required, FS::cust_location object
+
+paystart_month - CARD only, optional, will be preserved when replacing
+
+paystart_year - CARD only, optional, will be preserved when replacing
+
+payissue - CARD only, optional, will be preserved when replacing
+
+paycvv - CARD only, only used if conf cvv-save is set appropriately
+
+paytype - CHEK only
+
+paystate - CHEK only
+
+=cut
+
+#The code for this option is in place, but it's not currently used
+#
+# replace - existing cust_payby object to be replaced (must match custnum)
+
+# stateid/stateid_state/ss are not currently supported in cust_payby,
+# might not even work properly in 4.x, but will need to work here if ever added
+
+sub save_cust_payby {
+  my $self = shift;
+  my %opt = @_;
+
+  my $old = $opt{'replace'};
+  my $new = new FS::cust_payby { $old ? $old->hash : () };
+  return "Customer number does not match" if $new->custnum and $new->custnum != $self->custnum;
+  $new->set( 'custnum' => $self->custnum );
+
+  my $payby = $opt{'payment_payby'};
+  return "Bad payby" unless grep(/^$payby$/,('CARD','CHEK'));
+
+  # don't allow turning off auto when replacing
+  $opt{'auto'} ||= 1 if $old and $old->payby !~ /^D/;
+
+  my @check_existing; # payby relevant to this payment_payby
+
+  # set payby based on auto
+  if ( $payby eq 'CARD' ) { 
+    $new->set( 'payby' => ( $opt{'auto'} ? 'CARD' : 'DCRD' ) );
+    @check_existing = qw( CARD DCRD );
+  } elsif ( $payby eq 'CHEK' ) {
+    $new->set( 'payby' => ( $opt{'auto'} ? 'CHEK' : 'DCHK' ) );
+    @check_existing = qw( CHEK DCHK );
+  }
+
+  # every automatic payment type added here will be marked primary
+  $new->set( 'weight' => $opt{'auto'} ? 1 : '' );
+
+  # basic fields
+  $new->payinfo($opt{'payinfo'}); # sets default paymask, but not if it's already tokenized
+  $new->paymask($opt{'paymask'}) if $opt{'paymask'}; # in case it's been tokenized, override with loaded paymask
+  $new->set( 'payname' => $opt{'payname'} );
+  $new->set( 'payip' => $opt{'payip'} ); # will be preserved below
+
+  my $conf = new FS::Conf;
+
+  # compare to FS::cust_main::realtime_bop - check both to make sure working correctly
+  if ( $payby eq 'CARD' &&
+       grep { $_ eq cardtype($opt{'payinfo'}) } $conf->config('cvv-save') ) {
+    $new->set( 'paycvv' => $opt{'paycvv'} );
+  } else {
+    $new->set( 'paycvv' => '');
+  }
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  # set fields specific to payment_payby
+  if ( $payby eq 'CARD' ) {
+    if ($opt{'bill_location'}) {
+      $opt{'bill_location'}->set('custnum' => $self->custnum);
+      my $error = $opt{'bill_location'}->find_or_insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+      $new->set( 'locationnum' => $opt{'bill_location'}->locationnum );
+    }
+    foreach my $field ( qw( paydate paystart_month paystart_year payissue ) ) {
+      $new->set( $field => $opt{$field} );
+    }
+  } else {
+    foreach my $field ( qw(paytype paystate) ) {
+      $new->set( $field => $opt{$field} );
+    }
+  }
+
+  # other cust_payby to compare this to
+  my @existing = $self->cust_payby(@check_existing);
+
+  # fields that can overwrite blanks with values, but not values with blanks
+  my @preserve = qw( paymask locationnum paystart_month paystart_year payissue payip );
+
+  my $skip_cust_payby = 0; # true if we don't need to save or reweight cust_payby
+  unless ($old) {
+    # generally, we don't want to overwrite existing cust_payby with this,
+    # but we can replace if we're only marking it auto or adding a preserved field
+    # and we can avoid saving a total duplicate or merely turning off auto
+PAYBYLOOP:
+    foreach my $cust_payby (@existing) {
+      # check fields that absolutely should not change
+      foreach my $field ($new->fields) {
+        next if grep(/^$field$/, qw( custpaybynum payby weight ) );
+        next if grep(/^$field$/, @preserve );
+        next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field);
+      }
+      # now check fields that can replace if one value is blank
+      my $replace = 0;
+      foreach my $field (@preserve) {
+        if (
+          ( $new->get($field) and !$cust_payby->get($field) ) or
+          ( $cust_payby->get($field) and !$new->get($field) )
+        ) {
+          # prevention of overwriting values with blanks happens farther below
+          $replace = 1;
+        } elsif ( $new->get($field) ne $cust_payby->get($field) ) {
+          next PAYBYLOOP;
+        }
+      }
+      unless ( $replace ) {
+        # nearly identical, now check weight
+        if ($new->get('weight') eq $cust_payby->get('weight') or !$new->get('weight')) {
+          # ignore identical cust_payby, and ignore attempts to turn off auto
+          # no need to save or re-weight cust_payby (but still need to update/commit $self)
+          $skip_cust_payby = 1;
+          last PAYBYLOOP;
+        }
+        # otherwise, only change is to mark this as primary
+      }
+      # if we got this far, we're definitely replacing
+      $old = $cust_payby;
+      last PAYBYLOOP;
+    }
+  }
+
+  if ($old) {
+    $new->set( 'custpaybynum' => $old->custpaybynum );
+    # don't turn off automatic payment (but allow it to be turned on)
+    if ($new->payby =~ /^D/ and $new->payby ne $old->payby) {
+      $opt{'auto'} = 1;
+      $new->set( 'payby' => $old->payby );
+      $new->set( 'weight' => 1 );
+    }
+    # make sure we're not overwriting values with blanks
+    foreach my $field (@preserve) {
+      if ( $old->get($field) and !$new->get($field) ) {
+        $new->set( $field => $old->get($field) );
+      }
+    }
+  }
+
+  # only overwrite cust_main bill_location if auto
+  if ($opt{'auto'} && $opt{'bill_location'}) {
+    $self->set('bill_location' => $opt{'bill_location'});
+    my $error = $self->replace;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  # done with everything except reweighting and saving cust_payby
+  # still need to commit changes to cust_main and cust_location
+  if ($skip_cust_payby) {
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return '';
+  }
+
+  # re-weight existing primary cust_pay for this payby
+  if ($opt{'auto'}) {
+    foreach my $cust_payby (@existing) {
+      # relies on cust_payby return order
+      last unless $cust_payby->payby !~ /^D/;
+      last if $cust_payby->weight > 1;
+      next if $new->custpaybynum eq $cust_payby->custpaybynum;
+      $cust_payby->set( 'weight' => 2 );
+      my $error = $cust_payby->replace;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "Error reweighting cust_payby: $error";
+      }
+    }
+  }
+
+  # finally, save cust_payby
+  my $error = $old ? $new->replace($old) : $new->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =back
 
 =head1 CLASS METHODS
diff --git a/FS/FS/cust_main/Billing_Batch.pm b/FS/FS/cust_main/Billing_Batch.pm
index f91c5fb..7612df3 100644
--- a/FS/FS/cust_main/Billing_Batch.pm
+++ b/FS/FS/cust_main/Billing_Batch.pm
@@ -65,12 +65,7 @@ sub batch_card {
     && !($options{payby} && $options{payinfo} && $options{paydate} && $options{payname});
 
   #false laziness with Billing_Realtime
-  my @cust_payby = qsearch({
-    'table'     => 'cust_payby',
-    'hashref'   => { 'custnum' => $self->custnum, },
-    'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
-    'order_by'  => 'ORDER BY weight ASC',
-  });
+  my @cust_payby = $self->cust_payby('CARD','CHEK');
 
   # batch can't try out every one like realtime, just use first one
   my $cust_payby = $cust_payby[0];
diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index c700cf7..20d0145 100644
--- a/FS/FS/cust_main/Billing_Realtime.pm
+++ b/FS/FS/cust_main/Billing_Realtime.pm
@@ -56,12 +56,7 @@ sub realtime_cust_payby {
 
   $options{amount} = $self->balance unless exists( $options{amount} );
 
-  my @cust_payby = qsearch({
-    'table'     => 'cust_payby',
-    'hashref'   => { 'custnum' => $self->custnum, },
-    'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
-    'order_by'  => 'ORDER BY weight ASC',
-  });
+  my @cust_payby = $self->cust_payby('CARD','CHEK');
                                                    
   my $error;
   foreach my $cust_payby (@cust_payby) {
@@ -752,8 +747,7 @@ sub realtime_bop {
   # remove paycvv after initial transaction
   ###
 
-  #false laziness w/misc/process/payment.cgi - check both to make sure working
-  # correctly
+  # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
   if ( length($self->paycvv)
        && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
   ) {
diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm
index 9feaf14..b9e79a2 100644
--- a/FS/FS/cust_payby.pm
+++ b/FS/FS/cust_payby.pm
@@ -2,6 +2,7 @@ package FS::cust_payby;
 use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
 
 use strict;
+use Scalar::Util qw( blessed );
 use Digest::SHA qw( sha512_base64 );
 use Business::CreditCard qw( validate cardtype );
 use FS::UID qw( dbh );
@@ -202,8 +203,7 @@ sub replace {
           )
      )
   {
-warn $self->payinfo;
-warn $old->payinfo;
+
     $self->payinfo($old->payinfo);
 
   } elsif ( $self->payby =~ /^(CHEK|DCHK)$/ && $self->payinfo =~ /xx/ ) {
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 6b96bbe..56efbc4 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -330,6 +330,24 @@ sub display_status {
   }
 }
 
+=item paydate_monthyear
+
+Returns a two-element list consisting of the month and year of this customer's
+paydate (credit card expiration date for CARD customers)
+
+=cut
+
+sub paydate_monthyear {
+  my $self = shift;
+  if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
+    ( $2, $1 );
+  } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+    ( $1, $3 );
+  } else {
+    ('', '');
+  }
+}
+
 =back
 
 =head1 BUGS
diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
index aff9bca..1054e6a 100755
--- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
@@ -583,7 +583,7 @@ sub process_order_recharge {
 
 sub make_payment {
 
-  my $payment_info = payment_info( 'session_id' => $session_id );
+  my $payment_info = payment_info( 'session_id' => $session_id, 'payment_payby' => 'CARD' );
 
   my $amount = 
     ($payment_info->{'balance'} && ($payment_info->{'balance'} > 0))
@@ -704,7 +704,7 @@ sub payment_results {
 }
 
 sub make_ach_payment {
-  payment_info( 'session_id' => $session_id );
+  payment_info( 'session_id' => $session_id, 'payment_payby' => 'CHEK' );
 }
 
 sub ach_payment_results {
diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi
index f4f0b56..7afdfd1 100644
--- a/httemplate/misc/payment.cgi
+++ b/httemplate/misc/payment.cgi
@@ -33,15 +33,22 @@
     &>
 % }
 
+% my $auto = 0;
 % if ( $payby eq 'CARD' ) {
 %
 %   my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' );
 %   my $payname = $cust_main->first. ' '. $cust_main->getfield('last');
-%   if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
-%     $payinfo = $cust_main->paymask;
-%     $paycvv = $cust_main->paycvv;
-%     ( $month, $year ) = $cust_main->paydate_monthyear;
-%     $payname = $cust_main->payname if $cust_main->payname;
+%   my $location = $cust_main->bill_location;
+%
+%   #auto-fill with the highest weighted match
+%   my ($cust_payby) = $cust_main->cust_payby('CARD','DCRD');
+%   if ($cust_payby) {
+%     $payinfo = $cust_payby->paymask;
+%     $paycvv  = $cust_payby->paycvv;
+%     ( $month, $year ) = $cust_payby->paydate_monthyear;
+%     $payname = $cust_payby->payname if $cust_payby->payname;
+%     $location = $cust_payby->cust_location || $location;
+%     $auto = 1 if $cust_payby->payby eq 'CARD';
 %   }
 
     <TR>
@@ -87,7 +94,7 @@
     </TR>
 
     <& /elements/location.html,
-                  'object'         => $cust_main->bill_location,
+                  'object'         => $location,
                   'no_asterisks'   => 1,
                   'address1_label' => emt('Card billing address'),
     &>
@@ -97,16 +104,19 @@
 %   my( $account, $aba, $branch, $payname, $ss, $paytype, $paystate,
 %       $stateid, $stateid_state )
 %     = ( '', '', '', '', '', '', '', '', '' );
-%   if ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) {
-%     $cust_main->paymask =~ /^([\dx]+)\@([\d\.x]*)$/i
-%       or die "unparsable payinfo ". $cust_main->payinfo;
+%   my ($cust_payby) = $cust_main->cust_payby('CHEK','DCHK');
+%   if ($cust_payby) {
+%     $cust_payby->paymask =~ /^([\dx]+)\@([\d\.x]*)$/i
+%       or die "unparsable paymask ". $cust_payby->paymask;
 %     ($account, $aba) = ($1, $2);
 %     ($branch,$aba) = split('\.',$aba)
 %       if $conf->config('echeck-country') eq 'CA';
-%     $payname = $cust_main->payname;
+%     $payname = $cust_payby->payname;
+%     $paytype = $cust_payby->getfield('paytype');
+%     $paystate = $cust_payby->getfield('paystate');
+%     $auto = 1 if $cust_payby->payby eq 'CHEK';
+%     # these values aren't in cust_payby, but maybe should be...
 %     $ss = $cust_main->ss;
-%     $paytype = $cust_main->getfield('paytype');
-%     $paystate = $cust_main->getfield('paystate');
 %     $stateid = $cust_main->getfield('stateid');
 %     $stateid_state = $cust_main->getfield('stateid_state');
 %   }
@@ -228,7 +238,7 @@
 
 <TR>
   <TD COLSPAN=2>
-    <INPUT TYPE="checkbox"<% ( ( $payby eq 'CARD' && $cust_main->payby ne 'DCRD' ) || ( $payby eq 'CHEK' && $cust_main->payby eq 'CHEK' ) ) ? ' CHECKED' : '' %> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
+    <INPUT TYPE="checkbox"<% $auto ? ' CHECKED' : '' %> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
     <% mt("Charge future payments to this [_1] automatically",$type{$payby}) |h %> 
   </TD>
 </TR>
@@ -260,10 +270,6 @@ my $custnum = $1;
 my $cust_main = qsearchs( 'cust_main', { 'custnum'=>$custnum } );
 die "unknown custnum $custnum" unless $cust_main;
 
-my $location = $cust_main->bill_location;
-# no proper error handling on this anyway, but when we have it,
-# remember to repopulate fields in $location
-
 my $balance = $cust_main->balance;
 
 my $payinfo = '';
diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi
index efba9ed..5cd5d31 100644
--- a/httemplate/misc/process/payment.cgi
+++ b/httemplate/misc/process/payment.cgi
@@ -76,11 +76,29 @@ my $balance = $1;
 my $payinfo;
 my $paymask; # override only used by loaded cust payinfo, only implemented for realtime processing
 my $paycvv = '';
+my $loaded_cust_payby;
 if ( $payby eq 'CHEK' ) {
 
   if ($cgi->param('payinfo1') =~ /xx/i || $cgi->param('payinfo2') =~ /xx/i ) {
-    $payinfo = $cust_main->payinfo;
-    $paymask = $cust_main->paymask;
+
+    my $search_paymask = $cgi->param('payinfo1') . '@' . $cgi->param('payinfo2');
+    $search_paymask .= '.' . $cgi->param('payinfo3')
+      if $conf->config('echeck-country') eq 'CA';
+
+    #paymask might not be saved in database, need to run paymask method for any potential match
+    foreach my $search_cust_payby ($cust_main->cust_payby('CHEK','DCHK')) {
+      if ($search_paymask eq $search_cust_payby->paymask) {
+        # if there are multiple matches, assume for now that it's the first one returned,
+        # since that's what auto-fills; it's unlikely a masked number would be entered by hand,
+        # but it's very likely users will just click-through what's been auto-filled
+        $loaded_cust_payby = $search_cust_payby;
+        last;
+      }
+    }
+    errorpage("Masked payinfo not found") unless $loaded_cust_payby;
+    $payinfo = $loaded_cust_payby->payinfo;
+    $paymask = $loaded_cust_payby->paymask;
+
   } else {
     $cgi->param('payinfo1') =~ /^(\d+)$/
       or errorpage("Illegal account number ". $cgi->param('payinfo1'));
@@ -99,10 +117,22 @@ if ( $payby eq 'CHEK' ) {
 } elsif ( $payby eq 'CARD' ) {
 
   $payinfo = $cgi->param('payinfo');
-  if ($payinfo eq $cust_main->paymask) {
-    $payinfo = $cust_main->payinfo;
-    $paymask = $cust_main->paymask;
+  if ($payinfo =~ /xx/i) {
+
+    #paymask might not be saved in database, need to run paymask method for any potential match
+    foreach my $search_cust_payby ($cust_main->cust_payby('CARD','DCRD')) {
+      if ($payinfo eq $search_cust_payby->paymask) {
+        $loaded_cust_payby = $search_cust_payby;
+        last;
+      }
+    }
+
+    errorpage("Masked payinfo not found") unless $loaded_cust_payby;
+    $payinfo = $loaded_cust_payby->payinfo;
+    $paymask = $loaded_cust_payby->paymask;
+
   }
+
   $payinfo =~ s/\D//g;
   $payinfo =~ /^(\d{13,16}|\d{8,9})$/
     or errorpage(gettext('invalid_card')); # . ": ". $self->payinfo;
@@ -114,7 +144,7 @@ if ( $payby eq 'CHEK' ) {
     if $payinfo !~ /^99\d{14}$/ #token
     && cardtype($payinfo) eq "Unknown";
 
-  if ( defined $cust_main->dbdef_table->column('paycvv') ) {
+  if ( defined $cust_main->dbdef_table->column('paycvv') ) { #is this test necessary anymore?
     if ( length($cgi->param('paycvv') ) ) {
       if ( cardtype($payinfo) eq 'American Express card' ) {
         $cgi->param('paycvv') =~ /^(\d{4})$/
@@ -140,42 +170,31 @@ my $discount_term = $1;
 
 # save first, for proper tokenization later
 if ( $cgi->param('save') ) {
-  my $new = new FS::cust_main { $cust_main->hash };
-  if ( $payby eq 'CARD' ) { 
-    $new->set( 'payby' => ( $cgi->param('auto') ? 'CARD' : 'DCRD' ) );
-  } elsif ( $payby eq 'CHEK' ) {
-    $new->set( 'payby' => ( $cgi->param('auto') ? 'CHEK' : 'DCHK' ) );
-  } else {
-    die "unknown payby $payby";
-  }
-  $new->payinfo($payinfo);             # sets default paymask, but not if it's already tokenized
-  $new->paymask($paymask) if $paymask; # in case it's been tokenized, override with loaded paymask
-  $new->set( 'paydate' => "$year-$month-01" );
-  $new->set( 'payname' => $payname );
-
-  #false laziness w/FS:;cust_main::realtime_bop - check both to make sure
-  # working correctly
-  if ( $payby eq 'CARD' &&
-       grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save') ) {
-    $new->set( 'paycvv' => $paycvv );
-  } else {
-    $new->set( 'paycvv' => '');
-  }
 
+  my %saveopt;
   if ( $payby eq 'CARD' ) {
     my $bill_location = FS::cust_location->new;
     $bill_location->set( $_ => $cgi->param($_) )
       foreach @{$payby2fields{$payby}};
-    $new->set('bill_location' => $bill_location);
-    # will do nothing if the fields are all unchanged
+    $saveopt{'bill_location'} = $bill_location;
+    $saveopt{'paycvv'} = $paycvv; # save_cust_payby contains conf logic for when to use this
+    $saveopt{'paydate'} = "$year-$month-01";
   } else {
-    $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}};
+    # ss/stateid/stateid_state won't be saved, but should be harmless to pass
+    %saveopt = map { $_ => scalar($cgi->param($_)) } @{$payby2fields{$payby}};
   }
 
-  my $error = $new->replace($cust_main);
+  my $error = $cust_main->save_cust_payby(
+    'payment_payby' => $payby,
+    'auto'          => scalar($cgi->param('auto')),
+    'payinfo'       => $payinfo,
+    'paymask'       => $paymask,
+    'payname'       => $payname,
+    %saveopt
+  );
+
   errorpage("error saving info, payment not processed: $error")
-    if $error;
-  $cust_main = $new;
+    if $error;	
 }
 
 my $error = '';

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

Summary of changes:
 FS/FS/ClientAPI/MyAccount.pm                      |  111 ++++----
 FS/FS/cust_main.pm                                |  278 +++++++++++++++++++--
 FS/FS/cust_main/Billing_Batch.pm                  |    7 +-
 FS/FS/cust_main/Billing_Realtime.pm               |   10 +-
 FS/FS/cust_payby.pm                               |    4 +-
 FS/FS/payinfo_Mixin.pm                            |   18 ++
 fs_selfservice/FS-SelfService/cgi/selfservice.cgi |    4 +-
 httemplate/misc/payment.cgi                       |   40 +--
 httemplate/misc/process/payment.cgi               |   85 ++++---
 9 files changed, 421 insertions(+), 136 deletions(-)




More information about the freeside-commits mailing list