diff options
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/ClientAPI/MyAccount.pm | 111 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 278 | ||||
-rw-r--r-- | FS/FS/cust_main/Billing_Batch.pm | 7 | ||||
-rw-r--r-- | FS/FS/cust_main/Billing_Realtime.pm | 10 | ||||
-rw-r--r-- | FS/FS/cust_payby.pm | 4 | ||||
-rw-r--r-- | FS/FS/payinfo_Mixin.pm | 18 |
6 files changed, 344 insertions, 84 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<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 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 |