+ 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 );
+ }
+
+ $new->set( 'weight' => $opt{'auto'} ? $opt{'weight'} : '' );
+
+ # 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;
+ } #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) );
+ }
+ }