RT#38363: use cust_payby when saving cards during payments
authorJonathan Prykop <jonathan@freeside.biz>
Fri, 15 Jan 2016 18:41:48 +0000 (12:41 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Fri, 15 Jan 2016 18:41:48 +0000 (12:41 -0600)
FS/FS/ClientAPI/MyAccount.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing_Batch.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/cust_payby.pm
FS/FS/payinfo_Mixin.pm
fs_selfservice/FS-SelfService/cgi/selfservice.cgi
httemplate/misc/payment.cgi
httemplate/misc/process/payment.cgi

index 33a8e61..6b91101 100644 (file)
@@ -853,27 +853,33 @@ sub payment_info {
   $return{$_} = $cust_main->bill_location->get($_) 
     for qw(address1 address2 city state zip);
 
   $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?
 
   if ( $conf->config('prepayment_discounts-credit_type') ) {
     #need to eval?
@@ -961,8 +967,12 @@ sub validate_payment {
     my $payinfo2 = $1;
     $payinfo = $payinfo1. '@'. $payinfo2;
 
     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' ) {
    
    
   } 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
 
     #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;
     }
 
     $payinfo =~ s/\D//g;
@@ -1092,28 +1105,33 @@ sub do_process_payment {
   my $payby = delete $validate->{'payby'};
 
   if ( $validate->{'save'} ) {
   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)
       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 };
     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".
       #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";
            "PACKET: ". Dumper($validate)."\n";
-    } else {
-      $cust_main = $new;
     }
   }
 
     }
   }
 
index f6b6862..ee70dea 100644 (file)
@@ -2169,21 +2169,35 @@ sub cust_contact {
   qsearch('cust_contact', { 'custnum' => $self->custnum } );
 }
 
   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.
 
 
 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;
 =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",
     '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({ 
 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 
 =item paydate_epoch
 
 Returns the exact time in seconds corresponding to the payment method 
@@ -4406,6 +4402,246 @@ sub payment_history {
   return @out;
 }
 
   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
 =back
 
 =head1 CLASS METHODS
index f91c5fb..7612df3 100644 (file)
@@ -65,12 +65,7 @@ sub batch_card {
     && !($options{payby} && $options{payinfo} && $options{paydate} && $options{payname});
 
   #false laziness with Billing_Realtime
     && !($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];
 
   # batch can't try out every one like realtime, just use first one
   my $cust_payby = $cust_payby[0];
index c700cf7..20d0145 100644 (file)
@@ -56,12 +56,7 @@ sub realtime_cust_payby {
 
   $options{amount} = $self->balance unless exists( $options{amount} );
 
 
   $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) {
                                                    
   my $error;
   foreach my $cust_payby (@cust_payby) {
@@ -752,8 +747,7 @@ sub realtime_bop {
   # remove paycvv after initial transaction
   ###
 
   # 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')
   ) {
   if ( length($self->paycvv)
        && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
   ) {
index 9feaf14..b9e79a2 100644 (file)
@@ -2,6 +2,7 @@ package FS::cust_payby;
 use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
 
 use strict;
 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 );
 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/ ) {
     $self->payinfo($old->payinfo);
 
   } elsif ( $self->payby =~ /^(CHEK|DCHK)$/ && $self->payinfo =~ /xx/ ) {
index 6b96bbe..56efbc4 100644 (file)
@@ -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
 =back
 
 =head1 BUGS
index aff9bca..1054e6a 100755 (executable)
@@ -583,7 +583,7 @@ sub process_order_recharge {
 
 sub make_payment {
 
 
 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))
 
   my $amount = 
     ($payment_info->{'balance'} && ($payment_info->{'balance'} > 0))
@@ -704,7 +704,7 @@ sub payment_results {
 }
 
 sub make_ach_payment {
 }
 
 sub make_ach_payment {
-  payment_info( 'session_id' => $session_id );
+  payment_info( 'session_id' => $session_id, 'payment_payby' => 'CHEK' );
 }
 
 sub ach_payment_results {
 }
 
 sub ach_payment_results {
index f4f0b56..7afdfd1 100644 (file)
     &>
 % }
 
     &>
 % }
 
+% my $auto = 0;
 % if ( $payby eq 'CARD' ) {
 %
 %   my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' );
 %   my $payname = $cust_main->first. ' '. $cust_main->getfield('last');
 % 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>
 %   }
 
     <TR>
@@ -87,7 +94,7 @@
     </TR>
 
     <& /elements/location.html,
     </TR>
 
     <& /elements/location.html,
-                  'object'         => $cust_main->bill_location,
+                  'object'         => $location,
                   'no_asterisks'   => 1,
                   'address1_label' => emt('Card billing address'),
     &>
                   'no_asterisks'   => 1,
                   'address1_label' => emt('Card billing address'),
     &>
 %   my( $account, $aba, $branch, $payname, $ss, $paytype, $paystate,
 %       $stateid, $stateid_state )
 %     = ( '', '', '', '', '', '', '', '', '' );
 %   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';
 %     ($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;
 %     $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');
 %   }
 %     $stateid = $cust_main->getfield('stateid');
 %     $stateid_state = $cust_main->getfield('stateid_state');
 %   }
 
 <TR>
   <TD COLSPAN=2>
 
 <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>
     <% 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 $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 = '';
 my $balance = $cust_main->balance;
 
 my $payinfo = '';
index efba9ed..5cd5d31 100644 (file)
@@ -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 $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 ) {
 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'));
   } 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');
 } 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;
   $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 $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})$/
     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') ) {
 
 # 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}};
   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 {
   } 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")
   errorpage("error saving info, payment not processed: $error")
-    if $error;
-  $cust_main = $new;
+    if $error; 
 }
 
 my $error = '';
 }
 
 my $error = '';