From 768ab093771b3305a67c9d929b461ef777ecdad8 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 15 Jan 2016 12:41:48 -0600 Subject: [PATCH] RT#38363: use cust_payby when saving cards during payments --- 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(-) diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 33a8e61b9..6b91101d5 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 f6b6862af..ee70deaa6 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) 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 f91c5fbdc..7612df3b1 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 c700cf7f8..20d014513 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 9feaf14cd..b9e79a2be 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 6b96bbe27..56efbc4e8 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 aff9bcae6..1054e6a00 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 f4f0b56dd..7afdfd159 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'; % } @@ -87,7 +94,7 @@ <& /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 @@ - 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; }"> + NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }"> <% mt("Charge future payments to this [_1] automatically",$type{$payby}) |h %> @@ -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 efba9ed9a..5cd5d31b5 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 = ''; -- 2.11.0