use Carp;
use Data::Dumper;
use Business::CreditCard 0.35;
+use Business::OnlinePayment;
use FS::UID qw( dbh myconnect );
use FS::Record qw( qsearch qsearchs );
use FS::payby;
use FS::cust_refund;
use FS::banned_pay;
use FS::payment_gateway;
+use FS::Misc::Savepoint;
$realtime_bop_decline_quiet = 0;
our $BOP_TESTING = 0;
our $BOP_TESTING_SUCCESS = 1;
+our $BOP_TESTING_TIMESTAMP = '';
install_callback FS::UID sub {
$conf = new FS::Conf;
$options{amount} = $self->balance unless exists( $options{amount} );
return '' unless $options{amount} > 0;
+ #huh, in v4, realtime_bop no longer will just process a card without passing
+ # payinfo or cust_payby...
+ if ( ! $options{'payinfo'} && ! $options{'cust_payby'} && $self->has_cust_payby_auto ) {
+ my @cust_payby = $self->cust_payby;
+ $options{'cust_payby'} = $cust_payby[0];
+ }
+
return $self->realtime_bop({%options});
}
$content{name} = $payname if $payname;
- $content{address} = $options->{'address1'};
- my $address2 = $options->{'address2'};
- $content{address} .= ", ". $address2 if length($address2);
+ if ( exists($options->{'address1'}) ) {
+
+ $content{address} = $options->{'address1'};
+ my $address2 = $options->{'address2'};
+ $content{address} .= ", ". $address2 if length($address2);
+
+ $content{$_} = $options->{$_} foreach qw( city state zip country );
+
+ } elsif ( ref($self) ) {
+
+ $content{address} = $self->address1;
+ my $address2 = $self->address2;
+ $content{address} .= ", ". $address2 if length($address2);
- $content{city} = $options->{'city'};
- $content{state} = $options->{'state'};
- $content{zip} = $options->{'zip'};
- $content{country} = $options->{'country'};
+ $content{$_} = $self->$_() foreach qw( city state zip country );
+
+ }
# can't set phone if called as class method
$content{phone} = $self->daytime || $self->night
confess "Can't call realtime_bop within another transaction ".
'($FS::UID::AutoCommit is false)'
- unless $FS::UID::AutoCommit;
+ unless $FS::UID::AutoCommit || $BOP_TESTING;
local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
$options{amount} = $amount;
}
+ return '' unless $options{amount} > 0;
+
# 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'})) {
if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
&& $options{method} eq 'CC';
+ my $cc_surcharge_flat = 0;
+ $cc_surcharge_flat = $conf->config('credit-card-surcharge-flatfee', $self->agentnum)
+ if $conf->config('credit-card-surcharge-flatfee', $self->agentnum)
+ && $options{method} eq 'CC';
+
# always add cc surcharge if called from event
- if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
- $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
+ if($options{'cc_surcharge_from_event'} && ($cc_surcharge_pct > 0 || $cc_surcharge_flat > 0)) {
+ if ($options{'amount'} > 0) {
+ $cc_surcharge = ($options{'amount'} * ($cc_surcharge_pct / 100)) + $cc_surcharge_flat;
$options{'amount'} += $cc_surcharge;
$options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
+ }
}
- elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
- # payment screen), so consider the given
- # amount as post-surcharge
- $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
+ elsif($cc_surcharge_pct > 0 || $cc_surcharge_flat > 0) {
+ # we're called not from event (i.e. from a
+ # payment screen), so consider the given
+ # amount as post-surcharge-processing_fee
+ $cc_surcharge = $options{'amount'} - $options{'processing-fee'} - (($options{'amount'} - ($cc_surcharge_flat + $options{'processing-fee'})) / ( 1 + $cc_surcharge_pct/100 )) if $options{'amount'} > 0;
}
$cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
eval "use $namespace";
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
###
my $cust_pay_pending = new FS::cust_pay_pending {
'custnum' => $self->custnum,
'paid' => $options{amount},
- '_date' => '',
+ '_date' => $BOP_TESTING ? $BOP_TESTING_TIMESTAMP : '',
'payby' => $bop_method2payby{$options{method}},
'payinfo' => $options{payinfo},
'paymask' => $options{paymask},
return { reference => $cust_pay_pending->paypendingnum,
map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
- } elsif ( $transaction->is_success() && $action2 ) {
+ } elsif ( !$BOP_TESTING && $transaction->is_success() && $action2 ) {
$cust_pay_pending->status('authorized');
my $cpp_authorized_err = $cust_pay_pending->replace;
'custnum' => $self->custnum,
'invnum' => $options{'invnum'},
'paid' => $cust_pay_pending->paid,
- '_date' => '',
+ '_date' => $BOP_TESTING ? $BOP_TESTING_TIMESTAMP : '',
'payby' => $cust_pay_pending->payby,
'payinfo' => $options{'payinfo'},
'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ my $savepoint_label = '_realtime_bop_result';
+ savepoint_create( $savepoint_label );
+
#start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
if ( $error ) {
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ savepoint_rollback( $savepoint_label );
+
$cust_pay->invnum(''); #try again with no specific invnum
$cust_pay->paynum('');
my $error2 = $cust_pay->insert( $options{'manual'} ?
if ( $error2 ) {
# gah. but at least we have a record of the state we had to abort in
# from cust_pay_pending now.
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ savepoint_rollback_and_release( $savepoint_label );
+
my $e = "WARNING: $options{method} captured but payment not recorded -".
" error inserting payment (". $payment_gateway->gateway_module.
"): $error2".
my $jobnum = $cust_pay_pending->jobnum;
if ( $jobnum ) {
my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
-
+
unless ( $placeholder ) {
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ savepoint_rollback_and_release( $savepoint_label );
+
my $e = "WARNING: $options{method} captured but job $jobnum not ".
"found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
warn $e;
$error = $placeholder->delete;
if ( $error ) {
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ savepoint_rollback_and_release( $savepoint_label );
+
my $e = "WARNING: $options{method} captured but could not delete ".
"job $jobnum for paypendingnum ".
$cust_pay_pending->paypendingnum. ": $error\n";
my $cpp_done_err = $cust_pay_pending->replace;
if ( $cpp_done_err ) {
+ savepoint_rollback_and_release( $savepoint_label );
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
my $e = "WARNING: $options{method} captured but payment not recorded - ".
"error updating status for paypendingnum ".
$cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
return $e;
} else {
-
+ savepoint_release( $savepoint_label );
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
if ( $options{'apply'} ) {
}
# have a CC surcharge portion --> one-time charge
- if ( $options{'cc_surcharge'} > 0 ) {
+ if ( $options{'cc_surcharge'} > 0 || $options{'processing-fee'} > 0) {
# XXX: this whole block needs to be in a transaction?
my $invnum;
unless ( $invnum ) {
# XXX: unlikely case - pre-paying before any invoices generated
# what it should do is create a new invoice and pick it
- warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
+ warn 'CC SURCHARGE OR PROCESS FEE AND NO INVOICES PICKED TO APPLY IT!';
return '';
}
- my $cust_pkg;
- my $charge_error = $self->charge({
+ if ($options{'cc_surcharge'} > 0) {
+ my $cust_pkg;
+ my $cc_surcharge_text = 'Credit Card Surcharge';
+ $cc_surcharge_text = $conf->config('credit-card-surcharge-text', $self->agentnum) if $conf->exists('credit-card-surcharge-text', $self->agentnum);
+ my $charge_error = $self->charge({
'amount' => $options{'cc_surcharge'},
- 'pkg' => 'Credit Card Surcharge',
+ 'pkg' => $cc_surcharge_text,
'setuptax' => 'Y',
'cust_pkg_ref' => \$cust_pkg,
- });
- if($charge_error) {
- warn 'Unable to add CC surcharge cust_pkg';
- return '';
- }
+ });
+
+ if($charge_error) {
+ warn 'Unable to add CC surcharge cust_pkg';
+ return '';
+ }
+
+ $cust_pkg->setup(time);
+ my $cp_error = $cust_pkg->replace;
+ if($cp_error) {
+ warn 'Unable to set setup time on cust_pkg for cc surcharge';
+ # but keep going...
+ }
- $cust_pkg->setup(time);
- my $cp_error = $cust_pkg->replace;
- if($cp_error) {
- warn 'Unable to set setup time on cust_pkg for cc surcharge';
- # but keep going...
- }
-
- my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
- unless ( $cust_bill ) {
- warn "race condition + invoice deletion just happened";
- return '';
- }
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
+ unless ( $cust_bill ) {
+ warn "race condition + invoice deletion just happened";
+ return '';
+ }
- my $grand_error =
- $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
+ my $grand_error =
+ $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
+
+ warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
+ if $grand_error;
+ } # end if $options{'cc_surcharge'}
+
+ if ($options{'processing-fee'} > 0) {
+ my $pf_cust_pkg;
+ my $processing_fee_text = 'Payment Processing Fee';
+ my $pf_change_error = $self->charge({
+ 'amount' => $options{'processing-fee'},
+ 'pkg' => $processing_fee_text,
+ 'setuptax' => 'Y',
+ 'cust_pkg_ref' => \$pf_cust_pkg,
+ });
+
+ if($pf_change_error) {
+ warn 'Unable to add payment processing fee';
+ return '';
+ }
- warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
- if $grand_error;
+ $pf_cust_pkg->setup(time);
+ my $pf_error = $pf_cust_pkg->replace;
+ if($pf_error) {
+ warn 'Unable to set setup time on cust_pkg for processing fee';
+ # but keep going...
}
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
+ unless ( $cust_bill ) {
+ warn "race condition + invoice deletion just happened";
+ return '';
+ }
+
+ my $grand_pf_error =
+ $cust_bill->add_cc_surcharge($pf_cust_pkg->pkgnum,$options{'processing-fee'});
+
+ warn "cannot add Processing fee to invoice #$invnum: $grand_pf_error"
+ if $grand_pf_error;
+ } #end if $options{'processing-fee'}
+
+ } #end if ( $options{'cc_surcharge'} > 0 || $options{'processing-fee'} > 0)
+
return ''; #no error
}
"resolved - error updating status for paypendingnum ".
$cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
warn $e;
+ #XXX internal system log $e (what's going on?)
$perror = "$e ($perror)";
}
my $payment_gateway =
$self->agent->payment_gateway( 'method' => $options{method} );
- my( $processor, $login, $password, $namespace ) =
+ ( $processor, $login, $password, $namespace ) =
map { my $method = "gateway_$_"; $payment_gateway->$method }
qw( module username password namespace );
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'})) {
eval "use $namespace";
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
###
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
###
return '' unless $supported_actions{'CC'}
&& 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
###
and any other tokenizable records will still be committed.
If the I<daily> flag is also set, detection of existing untokenized records will
-record a critical error in the system log (because they should have never appeared
+record an info message in the system log (because they should have never appeared
in the first place.) Tokenization will still be attempted.
If any configured gateways do NOT have the ability to tokenize, or if a
#acts on all customers
my %opt = @_;
my $debug = !$opt{'quiet'} || $DEBUG;
+ my $hascritical = 0;
warn "token_check called with opts\n".Dumper(\%opt) if $debug;
}
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
}
}
my $error = "No gateway found for custnum ".$cust_main->custnum;
if ($opt{'queue'}) {
+ $hascritical = 1;
$log->critical($error);
$dbh->commit or die $dbh->errstr; # commit error message
next; # not next CUSTLOOP, want to record error for every cust_payby
if ($error) {
$error = "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error;
if ($opt{'queue'}) {
+ $hascritical = 1;
$log->critical($error);
$dbh->commit or die $dbh->errstr; # commit log message, release mutex
next; # not next CUSTLOOP, want to record error for every cust_payby
while (my $recnum = _token_check_next_recnum($dbh,$table,$step,\$offset,\@recnums)) {
my $record = $tclass->by_key($recnum);
+ unless ($record->payby eq 'CARD') {
+ warn "Skipping non-card record for $table ".$record->get($record->primary_key) if $debug;
+ next;
+ }
if (FS::cust_main::Billing_Realtime->tokenized($record->payinfo)) {
warn "Skipping tokenized record for $table ".$record->get($record->primary_key) if $debug;
next;
}
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
}
} else {
my $error = "Could not load cust_main for $table ".$record->get($record->primary_key);
if ($opt{'queue'}) {
+ $hascritical = 1;
$log->critical($error);
$dbh->commit or die $dbh->errstr; # commit log message
next;
if ($error) {
$error = "Error tokenizing $table ".$record->get($record->primary_key).": ".$error;
if ($opt{'queue'}) {
+ $hascritical = 1;
$log->critical($error);
$dbh->commit or die $dbh->errstr; # commit log message, release mutex
next;
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
- return '';
+ return $hascritical ? 'Critical errors occurred on some records, see system log' : '';
}
# not a method!
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 ( is_tokenized IS NULL OR is_tokenized = '' ) ".
+ ' 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) {