X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main%2FBilling_Realtime.pm;h=f089059aa387f709b1978576a9b81004e592a6e4;hb=2ebfd5c5a550befcd4546edeed8de8300e0c59d2;hp=f331a39acd8ca0a4d01e937be3ecf50ee7faaeca;hpb=fb4d406cbc74ea6abac58770f81570b4f20d6cb9;p=freeside.git diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index f331a39ac..f089059aa 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -416,6 +416,13 @@ sub realtime_bop { # set fields from passed cust_payby _bop_cust_payby_options(\%options); + # check for banned credit card/ACH + my $ban = FS::banned_pay->ban_search( + 'payby' => $bop_method2payby{$options{method}}, + 'payinfo' => $options{payinfo}, + ); + return "Banned credit card" if $ban && $ban->bantype ne 'warn'; + # possibly run a separate transaction to tokenize card number, # so that we never store tokenized card info in cust_pay_pending if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) { @@ -502,16 +509,6 @@ sub realtime_bop { die $@ if $@; ### - # check for banned credit card/ACH - ### - - my $ban = FS::banned_pay->ban_search( - 'payby' => $bop_method2payby{$options{method}}, - 'payinfo' => $options{payinfo}, - ); - return "Banned credit card" if $ban && $ban->bantype ne 'warn'; - - ### # check for term discount validity ### @@ -1454,9 +1451,10 @@ sub realtime_refund_bop { ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 ); } + my $payment_gateway; if ( $gatewaynum ) { #gateway for the payment to be refunded - my $payment_gateway = + $payment_gateway = qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } ); die "payment gateway $gatewaynum not found" unless $payment_gateway; @@ -1470,7 +1468,7 @@ sub realtime_refund_bop { } else { #try the default gateway my $conf_processor; - my $payment_gateway = + $payment_gateway = $self->agent->payment_gateway('method' => $options{method}); ( $conf_processor, $login, $password, $namespace ) = @@ -1480,21 +1478,40 @@ sub realtime_refund_bop { @bop_options = $payment_gateway->gatewaynum ? $payment_gateway->options : @{ $payment_gateway->get('options') }; + my %bop_options = @bop_options; return "processor of payment $options{'paynum'} $processor does not". " match default processor $conf_processor" - unless $processor eq $conf_processor; + unless ($processor eq $conf_processor) + || (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'})); + + $processor = $conf_processor; } + # if gateway has switched to CardFortress but token_check hasn't run yet, + # tokenize just this record now, so that token gets passed/set appropriately + if ($cust_pay->payby eq 'CARD' && !$cust_pay->tokenized) { + my %tokenopts = ( + 'payment_gateway' => $payment_gateway, + 'method' => 'CC', + 'payinfo' => $cust_pay->payinfo, + 'paydate' => $cust_pay->paydate, + ); + my $error = $self->realtime_tokenize(\%tokenopts); # no-op unless gateway can tokenize + if ($self->tokenized($tokenopts{'payinfo'})) { # implies no error + warn " tokenizing cust_pay\n" if $DEBUG > 1; + $cust_pay->payinfo($tokenopts{'payinfo'}); + $error = $cust_pay->replace; + } + return $error if $error; + } } else { # didn't specify a paynum, so look for agent gateway overrides # like a normal transaction my $payment_gateway = - $self->agent->payment_gateway( 'method' => $options{method}, - #'payinfo' => $payinfo, - ); + $self->agent->payment_gateway( 'method' => $options{method} ); my( $processor, $login, $password, $namespace ) = map { my $method = "gateway_$_"; $payment_gateway->$method } qw( module username password namespace ); @@ -1627,18 +1644,22 @@ sub realtime_refund_bop { if length($payip); my $payinfo = ''; + my $paymask = ''; # for refund record if ( $options{method} eq 'CC' ) { if ( $cust_pay ) { $content{card_number} = $payinfo = $cust_pay->payinfo; + $paymask = $cust_pay->paymask; (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate) =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ && ($content{expiration} = "$2/$1"); # where available } else { - $content{card_number} = $payinfo = $self->payinfo; - (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate) - =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; - $content{expiration} = "$2/$1"; + # this really needs a better cleanup + die "Refund without paynum not supported"; +# $content{card_number} = $payinfo = $self->payinfo; +# (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate) +# =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; +# $content{expiration} = "$2/$1"; } } elsif ( $options{method} eq 'ECHECK' ) { @@ -1702,6 +1723,7 @@ sub realtime_refund_bop { '_date' => '', 'payby' => $bop_method2payby{$options{method}}, 'payinfo' => $payinfo, + 'paymask' => $paymask, 'reasonnum' => $options{'reasonnum'}, 'gatewaynum' => $gatewaynum, # may be null 'processor' => $processor, @@ -1768,6 +1790,13 @@ sub realtime_verify_bop { return "No cust_payby" unless $options{'cust_payby'}; _bop_cust_payby_options(\%options); + # check for banned credit card/ACH + my $ban = FS::banned_pay->ban_search( + 'payby' => $bop_method2payby{'CC'}, + 'payinfo' => $options{payinfo}, + ); + return "Banned credit card" if $ban && $ban->bantype ne 'warn'; + # possibly run a separate transaction to tokenize card number, # so that we never store tokenized card info in cust_pay_pending if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) { @@ -1788,16 +1817,6 @@ sub realtime_verify_bop { die $@ if $@; ### - # check for banned credit card/ACH - ### - - my $ban = FS::banned_pay->ban_search( - 'payby' => $bop_method2payby{'CC'}, - 'payinfo' => $options{payinfo}, - ); - return "Banned credit card" if $ban && $ban->bantype ne 'warn'; - - ### # massage data ### @@ -2205,6 +2224,13 @@ sub realtime_tokenize { return '' unless $options{method} eq 'CC'; return '' if $self->tokenized($options{payinfo}); #already tokenized + # check for banned credit card/ACH + my $ban = FS::banned_pay->ban_search( + 'payby' => $bop_method2payby{'CC'}, + 'payinfo' => $options{payinfo}, + ); + return "Banned credit card" if $ban && $ban->bantype ne 'warn'; + ### # select a gateway ### @@ -2233,16 +2259,6 @@ sub realtime_tokenize { && grep /^Tokenize$/, @{$supported_actions{'CC'}}; ### - # check for banned credit card/ACH - ### - - my $ban = FS::banned_pay->ban_search( - 'payby' => $bop_method2payby{'CC'}, - 'payinfo' => $options{payinfo}, - ); - return "Banned credit card" if $ban && $ban->bantype ne 'warn'; - - ### # massage data ### @@ -2383,8 +2399,9 @@ sub token_check { my $cache = {}; #cache for module info - # look for a gateway that can't tokenize + # look for a gateway that can and can't tokenize my $require_tokenized = 1; + my $someone_tokenizing = 0; foreach my $gateway ( FS::payment_gateway->all_gateways( 'method' => 'CC', @@ -2396,18 +2413,26 @@ sub token_check { # no default gateway, no promise to tokenize # can just load other gateways as-needeed below $require_tokenized = 0; - last; + last if $someone_tokenizing; + next; } my $info = _token_check_gateway_info($cache,$gateway); die $info unless ref($info); # means it's an error message - unless ($info->{'can_tokenize'}) { + if ($info->{'can_tokenize'}) { + $someone_tokenizing = 1; + } else { # a configured gateway can't tokenize, that's all we need to know right now # can just load other gateways as-needeed below $require_tokenized = 0; - last; + last if $someone_tokenizing; } } + unless ($someone_tokenizing) { #no need to check, if no one can tokenize + warn "no gateways tokenize\n" if $debug; + return; + } + warn "REQUIRE TOKENIZED" if $require_tokenized && $debug; # upgrade does not call this with autocommit turned on, @@ -2438,12 +2463,12 @@ CUSTLOOP: } if ($require_tokenized && $opt{'daily'}) { - $log->critical("Untokenized card number detected in cust_payby ".$cust_payby->custpaybynum); + $log->info("Untokenized card number detected in cust_payby ".$cust_payby->custpaybynum. '; tokenizing'); $dbh->commit or die $dbh->errstr; # commit log message } # only load gateway if we need to, and only need to load it once - my $payment_gateway ||= $cust_main->_payment_gateway({ + $payment_gateway ||= $cust_main->_payment_gateway({ 'method' => 'CC', 'conf' => $conf, 'nofatal' => 1, # handle lack of gateway smoothly below @@ -2539,19 +2564,80 @@ CUSTLOOP: } if ($require_tokenized && $opt{'daily'}) { - $log->critical("Untokenized card number detected in $table ".$record->get($record->primary_key)); + $log->info("Untokenized card number detected in $table ".$record->get($record->primary_key). ';tokenizing'); $dbh->commit or die $dbh->errstr; # commit log message } - # don't use customer agent gateway here, use the gatewaynum specified by the record - my $gateway = FS::payment_gateway->by_key_or_default( - 'method' => 'CC', - 'conf' => $conf, - 'nofatal' => 1, - 'gatewaynum' => $record->gatewaynum || '', - ); + my $cust_main = $record->cust_main; + if (!$cust_main) { + # might happen for cust_pay_pending from failed verify records, + # in which case we attempt tokenization without cust_main + # everything else should absolutely have a cust_main + if ($table eq 'cust_pay_pending' and !$record->custnum ) { + # override the usual safety check and allow the record to be + # updated even without a custnum. + $record->set('custnum_pending', 1); + } else { + my $error = "Could not load cust_main for $table ".$record->get($record->primary_key); + if ($opt{'queue'}) { + $log->critical($error); + $dbh->commit or die $dbh->errstr; # commit log message + next; + } + $dbh->rollback if $oldAutoCommit; + die $error; + } + } + + my $gateway; + + # use the gatewaynum specified by the record if possible + $gateway = FS::payment_gateway->by_key_with_namespace( + 'gatewaynum' => $record->gatewaynum, + ) if $record->gateway; + + # otherwise use the cust agent gateway if possible (which realtime_refund_bop would do) + # otherwise just use default gateway + unless ($gateway) { + + $gateway = $cust_main + ? $cust_main->agent->payment_gateway + : FS::payment_gateway->default_gateway; + + # check for processor mismatch + unless ($table eq 'cust_pay_pending') { # has no processor table + if (my $processor = $record->processor) { + + my $conf_processor = $gateway->gateway_module; + my %bop_options = $gateway->gatewaynum + ? $gateway->options + : @{ $gateway->get('options') }; + + # this is the same standard used by realtime_refund_bop + unless ( + ($processor eq $conf_processor) || + (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'})) + ) { + + # processors don't match, so refund already cannot be run on this object, + # regardless of what we do now... + # but unless we gotta tokenize everything, just leave well enough alone + unless ($require_tokenized) { + warn "Skipping mismatched processor for $table ".$record->get($record->primary_key) if $debug; + next; + } + ### no error--we'll tokenize using the new gateway, just to remove stored payinfo, + ### because refunds are already impossible for this record, anyway + + } # end processor mismatch + + } # end record has processor + } # end not cust_pay_pending + + } + + # means no default gateway, no promise to tokenize, can skip unless ($gateway) { - # means no default gateway, no promise to tokenize, can skip warn "Skipping missing gateway for $table ".$record->get($record->primary_key) if $debug; next; } @@ -2570,27 +2656,10 @@ CUSTLOOP: next; } - my $cust_main = $record->cust_main; - if (!$cust_main) { - # might happen for cust_pay_pending from failed verify records, - # in which case we attempt tokenization without cust_main - # everything else should absolutely have a cust_main - if ($table eq 'cust_pay_pending' && $record->{'custnum_pending'}) { - warn "ATTEMPTING GATEWAY-ONLY TOKENIZE" if $debug; - } else { - my $error = "Could not load cust_main for $table ".$record->get($record->primary_key); - if ($opt{'queue'}) { - $log->critical($error); - $dbh->commit or die $dbh->errstr; # commit log message - next; - } - $dbh->rollback if $oldAutoCommit; - die $error; - } - } + warn "ATTEMPTING GATEWAY-ONLY TOKENIZE" if $debug && !$cust_main; # if we got this far, time to mutex - $record = $record->select_for_update; + $record->select_for_update; # no clear record of name/address/etc used for transaction, # but will load name/phone/id from customer if run as an object method, @@ -2637,7 +2706,15 @@ sub _token_check_next_recnum { my $recnum = shift @$recnums; return $recnum if $recnum; my $tclass = 'FS::'.$table; - my $sth = $dbh->prepare('SELECT '.$tclass->primary_key.' FROM '.$table.' ORDER BY '.$tclass->primary_key.' LIMIT '.$step.' OFFSET '.$$offset) or die $dbh->errstr; + my $sth = $dbh->prepare( + 'SELECT '.$tclass->primary_key. + ' FROM '.$table. + " WHERE payby IN ( 'CARD', 'DCRD' ) ". + " AND ( length(payinfo) > 80 OR payinfo NOT LIKE '99%' )". + ' ORDER BY '.$tclass->primary_key. + ' LIMIT '.$step. + ' OFFSET '.$$offset + ) or die $dbh->errstr; $sth->execute() or die $sth->errstr; my @recnums; while (my $rec = $sth->fetchrow_hashref) {