use vars qw( $conf $DEBUG $me );
use vars qw( $realtime_bop_decline_quiet ); #ugh
use Data::Dumper;
-use Digest::MD5 qw(md5_base64);
use Business::CreditCard 0.28;
use FS::UID qw( dbh );
use FS::Record qw( qsearch qsearchs );
use FS::cust_pay;
use FS::cust_pay_pending;
use FS::cust_refund;
+use FS::banned_pay;
$realtime_bop_decline_quiet = 0;
$DEBUG = 0;
$me = '[FS::cust_main::Billing_Realtime]';
+our $BOP_TESTING = 0;
+our $BOP_TESTING_SUCCESS = 1;
+
install_callback FS::UID sub {
$conf = new FS::Conf;
#yes, need it for stuff below (prolly should be cached)
Required arguments in the hashref are I<method>, and I<amount>
-Available methods are: I<CC>, I<ECHECK> and I<LEC>
+Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
I<depend_jobnum> allows payment capture to unlock export jobs
-I<discount_term> attempts to take a discount by prepaying for discount_term
+I<discount_term> attempts to take a discount by prepaying for discount_term.
+The payment will fail if I<amount> is incorrect for this discount term.
A direct (Business::OnlinePayment) transaction will return nothing on success,
or an error message on failure.
} else {
- my %hash = ( 'custnum' => $self->custnum,
- 'payby' => 'CARD',
- );
-
- return 1
- if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
- || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
- $opt{'payinfo'} )
- } );
+ # return 1 if the payinfo has been used for another payment
+ return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
}
? $options->{country}
: $self->country;
- $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
+ #3.0 is a good a time as any to get rid of this... add a config to pass it
+ # if anyone still needs it
+ #$content{referer} = 'http://cleanwhisker.420.am/';
+
$content{phone} = $self->daytime || $self->night;
+ my $currency = $conf->exists('business-onlinepayment-currency')
+ && $conf->config('business-onlinepayment-currency');
+ $content{currency} = $currency if $currency;
+
\%content;
}
'CC' => 'CARD',
'ECHECK' => 'CHEK',
'LEC' => 'LECB',
+ 'PAYPAL' => 'PPAL',
);
sub realtime_bop {
# check for banned credit card/ACH
###
- my $ban = qsearchs('banned_pay', {
+ my $ban = FS::banned_pay->ban_search(
'payby' => $bop_method2payby{$options{method}},
- 'payinfo' => md5_base64($options{payinfo}),
- } );
- return "Banned credit card" if $ban;
+ 'payinfo' => $options{payinfo},
+ );
+ return "Banned credit card" if $ban && $ban->bantype ne 'warn';
+
+ ###
+ # check for term discount validity
+ ###
+
+ my $discount_term = $options{discount_term};
+ if ( $discount_term ) {
+ my $bill = ($self->cust_bill)[-1]
+ or return "Can't apply a term discount to an unbilled customer";
+ my $plan = FS::discount_plan->new(
+ cust_bill => $bill,
+ months => $discount_term
+ ) or return "No discount available for term '$discount_term'";
+
+ if ( $plan->discounted_total != $options{amount} ) {
+ return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
+ }
+ }
###
# massage data
my $paydate = '';
my %content = ();
- if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
-
- $content{card_number} = $options{payinfo};
- $paydate = exists($options{'paydate'})
- ? $options{'paydate'}
- : $self->paydate;
- $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
- $content{expiration} = "$2/$1";
-
- my $paycvv = exists($options{'paycvv'})
- ? $options{'paycvv'}
- : $self->paycvv;
- $content{cvv2} = $paycvv
- if length($paycvv);
-
- my $paystart_month = exists($options{'paystart_month'})
- ? $options{'paystart_month'}
- : $self->paystart_month;
-
- my $paystart_year = exists($options{'paystart_year'})
- ? $options{'paystart_year'}
- : $self->paystart_year;
-
- $content{card_start} = "$paystart_month/$paystart_year"
- if $paystart_month && $paystart_year;
-
- my $payissue = exists($options{'payissue'})
- ? $options{'payissue'}
- : $self->payissue;
- $content{issue_number} = $payissue if $payissue;
-
- if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
- 'trans_is_recur' => $trans_is_recur,
- )
- )
- {
- $content{recurring_billing} = 'YES';
- $content{acct_code} = 'rebill'
- if $conf->exists('credit_card-recurring_billing_acct_code');
- }
- } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
- ( $content{account_number}, $content{routing_code} ) =
- split('@', $options{payinfo});
- $content{bank_name} = $options{payname};
- $content{bank_state} = exists($options{'paystate'})
- ? $options{'paystate'}
- : $self->getfield('paystate');
- $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
- ? uc($options{'paytype'})
- : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
- $content{account_name} = $self->getfield('first'). ' '.
- $self->getfield('last');
+ if ( $namespace eq 'Business::OnlinePayment' ) {
+
+ if ( $options{method} eq 'CC' ) {
+
+ $content{card_number} = $options{payinfo};
+ $paydate = exists($options{'paydate'})
+ ? $options{'paydate'}
+ : $self->paydate;
+ $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ $content{expiration} = "$2/$1";
+
+ my $paycvv = exists($options{'paycvv'})
+ ? $options{'paycvv'}
+ : $self->paycvv;
+ $content{cvv2} = $paycvv
+ if length($paycvv);
+
+ my $paystart_month = exists($options{'paystart_month'})
+ ? $options{'paystart_month'}
+ : $self->paystart_month;
+
+ my $paystart_year = exists($options{'paystart_year'})
+ ? $options{'paystart_year'}
+ : $self->paystart_year;
+
+ $content{card_start} = "$paystart_month/$paystart_year"
+ if $paystart_month && $paystart_year;
+
+ my $payissue = exists($options{'payissue'})
+ ? $options{'payissue'}
+ : $self->payissue;
+ $content{issue_number} = $payissue if $payissue;
+
+ if ( $self->_bop_recurring_billing(
+ 'payinfo' => $options{'payinfo'},
+ 'trans_is_recur' => $trans_is_recur,
+ )
+ )
+ {
+ $content{recurring_billing} = 'YES';
+ $content{acct_code} = 'rebill'
+ if $conf->exists('credit_card-recurring_billing_acct_code');
+ }
+
+ } elsif ( $options{method} eq 'ECHECK' ){
+
+ ( $content{account_number}, $content{routing_code} ) =
+ split('@', $options{payinfo});
+ $content{bank_name} = $options{payname};
+ $content{bank_state} = exists($options{'paystate'})
+ ? $options{'paystate'}
+ : $self->getfield('paystate');
+ $content{account_type}=
+ (exists($options{'paytype'}) && $options{'paytype'})
+ ? uc($options{'paytype'})
+ : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
+
+ if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
+ $content{account_name} = $self->company;
+ } else {
+ $content{account_name} = $self->getfield('first'). ' '.
+ $self->getfield('last');
+ }
+
+ $content{customer_org} = $self->company ? 'B' : 'I';
+ $content{state_id} = exists($options{'stateid'})
+ ? $options{'stateid'}
+ : $self->getfield('stateid');
+ $content{state_id_state} = exists($options{'stateid_state'})
+ ? $options{'stateid_state'}
+ : $self->getfield('stateid_state');
+ $content{customer_ssn} = exists($options{'ss'})
+ ? $options{'ss'}
+ : $self->ss;
+
+ } elsif ( $options{method} eq 'LEC' ) {
+ $content{phone} = $options{payinfo};
+ } else {
+ die "unknown method ". $options{method};
+ }
- $content{customer_org} = $self->company ? 'B' : 'I';
- $content{state_id} = exists($options{'stateid'})
- ? $options{'stateid'}
- : $self->getfield('stateid');
- $content{state_id_state} = exists($options{'stateid_state'})
- ? $options{'stateid_state'}
- : $self->getfield('stateid_state');
- $content{customer_ssn} = exists($options{'ss'})
- ? $options{'ss'}
- : $self->ss;
- } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
- $content{phone} = $options{payinfo};
} elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
#move along
} else {
- #die an evil death
+ die "unknown namespace $namespace";
}
###
'custnum' => $self->custnum,
'status' => { op=>'!=', value=>'done' }
});
- # This is a problem. A self-service third party payment that fails somehow
- # can't be retried, EVER, until someone manually clears it. Totally
- # arbitrary fix: if the existing payment is more than two minutes old,
- # kill it. This doesn't limit how long it can take the pending payment
- # to complete, only how long it will obstruct new payments.
- my @still_pending;
- foreach (@pending) {
- if ( time - $_->_date > 120 ) {
+
+ #for third-party payments only, remove pending payments if they're in the
+ #'thirdparty' (waiting for customer action) state.
+ if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
+ foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
my $error = $_->delete;
- warn "error deleting stale pending payment ".$_->paypendingnum.": $error"
- if $error; # not fatal, it will fail anyway
- }
- else {
- push @still_pending, $_;
+ warn "error deleting unfinished third-party payment ".
+ $_->paypendingnum . ": $error\n"
+ if $error;
}
+ @pending = grep { $_->status ne 'thirdparty' } @pending;
}
- @pending = @still_pending;
return "A payment is already being processed for this customer (".
join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
%$bop_content,
'reference' => $cust_pay_pending->paypendingnum, #for now
'callback_url' => $payment_gateway->gateway_callback_url,
+ 'cancel_url' => $payment_gateway->gateway_cancel_url,
'email' => $email,
%content, #after
);
my $cpp_pending_err = $cust_pay_pending->replace;
return $cpp_pending_err if $cpp_pending_err;
- #config?
- my $BOP_TESTING = 0;
- my $BOP_TESTING_SUCCESS = 1;
+ warn Dumper($transaction) if $DEBUG > 2;
unless ( $BOP_TESTING ) {
$transaction->test_transaction(1)
if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
+ $cust_pay_pending->status('thirdparty');
+ my $cpp_err = $cust_pay_pending->replace;
+ return { error => $cpp_err } if $cpp_err;
return { reference => $cust_pay_pending->paypendingnum,
map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
return "Error: No error; test failure requested with fake_failure";
}
- #my $paybatch = '';
- #if ( $payment_gateway->gatewaynum ) { # agent override
- # $paybatch = $payment_gateway->gatewaynum. '-';
- #}
- #
- #$paybatch .= "$processor:". $transaction->authorization;
- #
- #$paybatch .= ':'. $transaction->order_number
- # if $transaction->can('order_number')
- # && length($transaction->order_number);
-
- my $paybatch = 'FakeProcessor:54:32';
-
my $cust_pay = new FS::cust_pay ( {
'custnum' => $self->custnum,
'invnum' => $options{'invnum'},
'payby' => $bop_method2payby{$options{method}},
#'payinfo' => $payinfo,
'payinfo' => '4111111111111111',
- 'paybatch' => $paybatch,
#'paydate' => $paydate,
'paydate' => '2012-05-01',
+ 'processor' => 'FakeProcessor',
+ 'auth' => '54',
+ 'order_number' => '32',
} );
$cust_pay->payunique( $options{payunique} ) if length($options{payunique});
if ( $transaction->is_success() ) {
- my $paybatch = '';
- if ( $payment_gateway->gatewaynum ) { # agent override
- $paybatch = $payment_gateway->gatewaynum. '-';
- }
-
- $paybatch .= $payment_gateway->gateway_module. ":".
- $transaction->authorization;
-
- $paybatch .= ':'. $transaction->order_number
- if $transaction->can('order_number')
- && length($transaction->order_number);
+ my $order_number = $transaction->order_number
+ if $transaction->can('order_number');
my $cust_pay = new FS::cust_pay ( {
'custnum' => $self->custnum,
'_date' => '',
'payby' => $cust_pay_pending->payby,
'payinfo' => $options{'payinfo'},
- 'paybatch' => $paybatch,
'paydate' => $cust_pay_pending->paydate,
'pkgnum' => $cust_pay_pending->pkgnum,
- 'discount_term' => $options{'discount_term'},
+ 'discount_term' => $options{'discount_term'},
+ 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
+ 'processor' => $payment_gateway->gateway_module,
+ 'auth' => $transaction->authorization,
+ 'order_number' => $order_number || '',
+
} );
#doesn't hurt to know, even though the dup check is in cust_pay_pending now
$cust_pay->payunique( $options{payunique} )
my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
if ( $error ) {
+ $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
$cust_pay->invnum(''); #try again with no specific invnum
$cust_pay->paynum('');
my $error2 = $cust_pay->insert( $options{'manual'} ?
'amount' => $cust_pay_pending->paid,
#'invoice_number' => $options{'invnum'},
'customer_id' => $self->custnum,
- 'referer' => 'http://cleanwhisker.420.am/',
+
+ #3.0 is a good a time as any to get rid of this... add a config to pass it
+ # if anyone still needs it
+ #'referer' => 'http://cleanwhisker.420.am/',
+
'reference' => $cust_pay_pending->paypendingnum,
'email' => $email,
'phone' => $self->daytime || $self->night,
orignal payment, first an attempt is made to "void" the transaction via
the gateway (to cancel a not-yet settled transaction) and then if that fails,
the normal attempt is made to "refund" ("credit") the transaction via the
-gateway is attempted.
+gateway is attempted. No attempt to "void" the transaction is made if the
+gateway has introspection data and doesn't support void.
#The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
#I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
my( $processor, $login, $password, @bop_options, $namespace ) ;
my( $auth, $order_number ) = ( '', '', '' );
+ my $gatewaynum = '';
if ( $options{'paynum'} ) {
or return "Unknown paynum $options{'paynum'}";
$amount ||= $cust_pay->paid;
- $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
- or return "Can't parse paybatch for paynum $options{'paynum'}: ".
- $cust_pay->paybatch;
- my $gatewaynum = '';
- ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
+ if ( $cust_pay->get('processor') ) {
+ ($gatewaynum, $processor, $auth, $order_number) =
+ (
+ $cust_pay->gatewaynum,
+ $cust_pay->processor,
+ $cust_pay->auth,
+ $cust_pay->order_number,
+ );
+ } else {
+ # this payment wasn't upgraded, which probably means this won't work,
+ # but try it anyway
+ $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
+ or return "Can't parse paybatch for paynum $options{'paynum'}: ".
+ $cust_pay->paybatch;
+ ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
+ }
if ( $gatewaynum ) { #gateway for the payment to be refunded
'password' => $password,
'order_number' => $order_number,
'amount' => $amount,
- 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
+
+ #3.0 is a good a time as any to get rid of this... add a config to pass it
+ # if anyone still needs it
+ #'referer' => 'http://cleanwhisker.420.am/',
);
$content{authorization} = $auth
if length($auth); #echeck/ACH transactions have an order # but no auth
#(at least with authorize.net)
+ my $currency = $conf->exists('business-onlinepayment-currency')
+ && $conf->config('business-onlinepayment-currency');
+ $content{currency} = $currency if $currency;
+
my $disable_void_after;
if ($conf->exists('disable_void_after')
&& $conf->config('disable_void_after') =~ /^(\d+)$/) {
}
#first try void if applicable
+ my $void = new Business::OnlinePayment( $processor, @bop_options );
+
+ my $tryvoid = 1;
+ if ($void->can('info')) {
+ my $paytype = '';
+ $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
+ $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
+ my %supported_actions = $void->info('supported_actions');
+ $tryvoid = 0
+ if ( %supported_actions && $paytype
+ && defined($supported_actions{$paytype})
+ && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
+ }
+
if ( $cust_pay && $cust_pay->paid == $amount
&& (
( not defined($disable_void_after) )
|| ( time < ($cust_pay->_date + $disable_void_after ) )
)
+ && $tryvoid
) {
warn " attempting void\n" if $DEBUG > 1;
- my $void = new Business::OnlinePayment( $processor, @bop_options );
if ( $void->can('info') ) {
if ( $cust_pay->payby eq 'CARD'
&& $void->info('CC_void_requires_card') )
return "$processor error: ". $refund->error_message
unless $refund->is_success();
- my $paybatch = "$processor:". $refund->authorization;
- $paybatch .= ':'. $refund->order_number
- if $refund->can('order_number') && $refund->order_number;
+ $order_number = $refund->order_number if $refund->can('order_number');
while ( $cust_pay && $cust_pay->unapplied < $amount ) {
my @cust_bill_pay = $cust_pay->cust_bill_pay;
'_date' => '',
'payby' => $bop_method2payby{$options{method}},
'payinfo' => $payinfo,
- 'paybatch' => $paybatch,
'reason' => $options{'reason'} || 'card or ACH refund',
+ 'gatewaynum' => $gatewaynum, # may be null
+ 'processor' => $processor,
+ 'auth' => $refund->authorization,
+ 'order_number' => $order_number,
} );
my $error = $cust_refund->insert;
if ( $error ) {