X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main%2FBilling_Realtime.pm;h=545e94339d8f8518d6c78931c69d6faa10d15774;hb=c647fbae23dc64cdecb1c6fa6fee671cca7e8e7a;hp=ea09379bade07d57d1e840a3c21b11b7c37806e6;hpb=0f7643c1af2d909e0c3172e5bec0c01855fca1b9;p=freeside.git diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index ea09379ba..545e94339 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -19,7 +19,7 @@ $realtime_bop_decline_quiet = 0; # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations # 3 is even more information including possibly sensitive data -$DEBUG = 1; +$DEBUG = 0; $me = '[FS::cust_main::Billing_Realtime]'; install_callback FS::UID sub { @@ -43,14 +43,11 @@ These methods are available on FS::cust_main objects. =item realtime_collect [ OPTION => VALUE ... ] -Runs a realtime credit card, ACH (electronic check) or phone bill transaction -via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime -gateway. See L and -L for supported gateways. +Attempt to collect the customer's current balance with a realtime credit +card, electronic check, or phone bill transaction (see realtime_bop() below). -On failure returns an error message. - -Returns false or a hashref upon success. The hashref contains keys popup_url reference, and collectitems. The first is a URL to which a browser should be redirected for completion of collection. The second is a reference id for the transaction suitable for the end user. The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url. +Returns the result of realtime_bop(): nothing, an error message, or a +hashref of state information for a third-party transaction. Available options are: I, I, I, I, I, I, I, I, I @@ -68,12 +65,11 @@ the value defined by the business-onlinepayment-description configuration option, or "Internet services" if that is unset. If an I is specified, this payment (if successful) is applied to the -specified invoice. If you don't specify an I you might want to -call the B method or set the I option. +specified invoice. -I can be set to true to apply a resulting payment. +I will automatically apply a resulting payment. -I can be set true to surpress email decline notices. +I can be set true to suppress email decline notices. I can be set to a scalar reference. It will be filled in with the resulting paynum, if any. @@ -125,8 +121,9 @@ the value defined by the business-onlinepayment-description configuration option, or "Internet services" if that is unset. If an I is specified, this payment (if successful) is applied to the -specified invoice. If you don't specify an I you might want to -call the B method or set the I option. +specified invoice. If the customer has exactly one open invoice, that +invoice number will be assumed. If you don't specify an I you might +want to call the B method or set the I option. I can be set to true to apply a resulting payment. @@ -143,6 +140,16 @@ I allows payment capture to unlock export jobs I attempts to take a discount by prepaying for discount_term +A direct (Business::OnlinePayment) transaction will return nothing on success, +or an error message on failure. + +A third-party transaction will return a hashref containing: + +- popup_url: the URL to which a browser should be redirected to complete + the transaction. +- collectitems: an arrayref of name-value pairs to be posted to popup_url. +- reference: a reference ID for the transaction, to show the customer. + (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too) =cut @@ -233,7 +240,14 @@ sub _bop_defaults { } $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} ); - $options->{invnum} ||= ''; + + # Default invoice number if the customer has exactly one open invoice. + if( ! $options->{'invnum'} ) { + $options->{'invnum'} = ''; + my @open = $self->open_cust_bill; + $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1; + } + $options->{payname} = $self->payname unless exists( $options->{payname} ); } @@ -322,7 +336,9 @@ sub realtime_bop { ### my $cc_surcharge = 0; - my $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage'); + my $cc_surcharge_pct = 0; + $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage') + if $conf->config('credit-card-surcharge-percentage'); # always add cc surcharge if called from event if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) { @@ -335,11 +351,10 @@ sub realtime_bop { # amount as post-surcharge $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 )); } - if ( $cc_surcharge > 0) { - $cc_surcharge = sprintf("%.2f",$cc_surcharge); - $options{'cc_surcharge'} = $cc_surcharge; - } - + + $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0; + $options{'cc_surcharge'} = $cc_surcharge; + if ( $DEBUG ) { warn "$me realtime_bop (new): $options{method} $options{amount}\n"; @@ -460,9 +475,9 @@ sub realtime_bop { $content{bank_state} = exists($options{'paystate'}) ? $options{'paystate'} : $self->getfield('paystate'); - $content{account_type} = exists($options{'paytype'}) - ? uc($options{'paytype'}) || 'CHECKING' - : uc($self->getfield('paytype')) || 'CHECKING'; + $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'); @@ -500,7 +515,6 @@ sub realtime_bop { #check the balance return "The customer's balance has changed; $options{method} transaction aborted." if $self->balance < $balance; - #&& $self->balance < $options{amount}; #might as well anyway? #also check and make sure there aren't *other* pending payments for this cust @@ -508,23 +522,18 @@ sub realtime_bop { '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 ). @@ -535,7 +544,6 @@ sub realtime_bop { my $cust_pay_pending = new FS::cust_pay_pending { 'custnum' => $self->custnum, - #'invnum' => $options{'invnum'}, 'paid' => $options{amount}, '_date' => '', 'payby' => $bop_method2payby{$options{method}}, @@ -599,6 +607,9 @@ sub realtime_bop { 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 ) }; @@ -836,13 +847,16 @@ sub _realtime_bop_result { 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'} ? ( 'manual' => 1 ) : () ); 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; my $e = "WARNING: $options{method} captured but payment not recorded -". " error inserting payment (". $payment_gateway->gateway_module. "): $error2". @@ -912,6 +926,8 @@ sub _realtime_bop_result { # have a CC surcharge portion --> one-time charge if ( $options{'cc_surcharge'} > 0 ) { + # XXX: this whole block needs to be in a transaction? + my $invnum; $invnum = $options{'invnum'} if $options{'invnum'}; unless ( $invnum ) { # probably from a payment screen @@ -942,26 +958,28 @@ sub _realtime_bop_result { 'cust_pkg_ref' => \$cust_pkg, }); if($charge_error) { - warn 'Unable to add CC surcharge'; + 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... + } - my $cust_bill_pkg = new FS::cust_bill_pkg({ - 'invnum' => $invnum, - 'pkgnum' => $cust_pkg->pkgnum, - 'setup' => $options{'cc_surcharge'}, - }); - my $cbp_error = $cust_bill_pkg->insert; - - if ( $cbp_error) { - warn 'Cannot add CC surcharge line item to invoice #'.$invnum; - return ''; - } else { - my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum }); - warn 'invoice for cc surcharge: ' . Dumper($cust_bill) if $DEBUG; - $cust_bill->apply_payments_and_credits; + 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'}); + + warn "cannot add CC surcharge to invoice #$invnum: $grand_error" + if $grand_error; } return ''; #no error @@ -981,7 +999,7 @@ sub _realtime_bop_result { my $error = $placeholder->depended_delete; $error ||= $placeholder->delete; warn "error removing provisioning jobs after declined paypendingnum ". - $cust_pay_pending->paypendingnum. "\n"; + $cust_pay_pending->paypendingnum. ": $error\n"; } else { my $e = "error finding job $jobnum for declined paypendingnum ". $cust_pay_pending->paypendingnum. "\n"; @@ -1267,7 +1285,8 @@ Implementation note: If I is unspecified or equal to the amount of the 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, I, I, I, I, #I, I and I are also available. Any of these options, @@ -1397,14 +1416,28 @@ sub realtime_refund_bop { } #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') )