X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main%2FBilling_Realtime.pm;h=90fda5eb2e5394cb8b005bacd2e3831981110fe7;hb=29a7178b002a00aea85973a056cfecb0e23cb99e;hp=5980bf5de365a78cb86ea81bc641c73abcaaf195;hpb=6bf88c075270636dd11150941de53f152e6018a7;p=freeside.git diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 5980bf5de..90fda5eb2 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -11,6 +11,7 @@ use FS::Misc qw( send_email ); use FS::payby; use FS::cust_pay; use FS::cust_pay_pending; +use FS::cust_bill_pay; use FS::cust_refund; use FS::banned_pay; @@ -47,14 +48,14 @@ These methods are available on FS::cust_main objects. =item realtime_collect [ OPTION => VALUE ... ] Attempt to collect the customer's current balance with a realtime credit -card, electronic check, or phone bill transaction (see realtime_bop() below). +card or electronic check transaction (see realtime_bop() below). 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 -I is one of: I, I and I. If none is specified +I is one of: I or I. If none is specified then it is deduced from the customer record. If no I is specified, then the customer balance is used. @@ -96,6 +97,8 @@ sub realtime_collect { } $options{amount} = $self->balance unless exists( $options{amount} ); + return '' unless $options{amount} > 0; + $options{method} = FS::payby->payby2bop($self->payby) unless exists( $options{method} ); @@ -105,13 +108,13 @@ sub realtime_collect { =item realtime_bop { [ ARG => VALUE ... ] } -Runs a realtime credit card, ACH (electronic check) or phone bill transaction +Runs a realtime credit card or ACH (electronic check) transaction via a Business::OnlinePayment realtime gateway. See L for supported gateways. Required arguments in the hashref are I, and I -Available methods are: I, I, I, and I +Available methods are: I, I, or I Available optional arguments are: I, I, I, I, I, I, I @@ -128,7 +131,13 @@ 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. +I can be set to true to prevent that default invnum from being set. + +I can be set to true to run B on success. + +I can be set to true to set that flag on the resulting payment +(prevents payment from being applied by B or B, +but will still be applied if I exists...use with I for intended effect.) I can be set true to surpress email decline notices. @@ -236,10 +245,13 @@ sub _bop_defaults { } } - $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} ); + unless ( exists( $options->{'payinfo'} ) ) { + $options->{'payinfo'} = $self->payinfo; + $options->{'paymask'} = $self->paymask; + } # Default invoice number if the customer has exactly one open invoice. - if( ! $options->{'invnum'} ) { + unless ( $options->{'invnum'} || $options->{'no_invnum'} ) { $options->{'invnum'} = ''; my @open = $self->open_cust_bill; $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1; @@ -316,7 +328,6 @@ sub _bop_content { my %bop_method2payby = ( 'CC' => 'CARD', 'ECHECK' => 'CHEK', - 'LEC' => 'LECB', 'PAYPAL' => 'PPAL', ); @@ -529,8 +540,6 @@ sub realtime_bop { ? $options{'ss'} : $self->ss; - } elsif ( $options{method} eq 'LEC' ) { - $content{phone} = $options{payinfo}; } else { die "unknown method ". $options{method}; } @@ -592,6 +601,7 @@ sub realtime_bop { '_date' => '', 'payby' => $bop_method2payby{$options{method}}, 'payinfo' => $options{payinfo}, + 'paymask' => $options{paymask}, 'paydate' => $paydate, 'recurring_billing' => $content{recurring_billing}, 'pkgnum' => $options{'pkgnum'}, @@ -735,8 +745,6 @@ sub realtime_bop { if ( $transaction->can('card_token') && $transaction->card_token ) { - $self->card_token($transaction->card_token); - if ( $options{'payinfo'} eq $self->payinfo ) { $self->payinfo($transaction->card_token); my $error = $self->replace; @@ -826,8 +834,8 @@ sub fake_bop { # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ] # -# Wraps up processing of a realtime credit card, ACH (electronic check) or -# phone bill transaction. +# Wraps up processing of a realtime credit card or ACH (electronic check) +# transaction. sub _realtime_bop_result { my( $self, $cust_pay_pending, $transaction, %options ) = @_; @@ -859,6 +867,7 @@ sub _realtime_bop_result { '_date' => '', 'payby' => $cust_pay_pending->payby, 'payinfo' => $options{'payinfo'}, + 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask, 'paydate' => $cust_pay_pending->paydate, 'pkgnum' => $cust_pay_pending->pkgnum, 'discount_term' => $options{'discount_term'}, @@ -866,7 +875,7 @@ sub _realtime_bop_result { 'processor' => $payment_gateway->gateway_module, 'auth' => $transaction->authorization, 'order_number' => $order_number || '', - + 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '', } ); #doesn't hurt to know, even though the dup check is in cust_pay_pending now $cust_pay->payunique( $options{payunique} ) @@ -1138,8 +1147,8 @@ sub _realtime_bop_result { =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ] -Verifies successful third party processing of a realtime credit card, -ACH (electronic check) or phone bill transaction via a +Verifies successful third party processing of a realtime credit card or +ACH (electronic check) transaction via a Business::OnlineThirdPartyPayment realtime gateway. See L for supported gateways. @@ -1301,20 +1310,20 @@ sub default_payment_gateway { =item realtime_refund_bop METHOD [ OPTION => VALUE ... ] -Refunds a realtime credit card, ACH (electronic check) or phone bill transaction +Refunds a realtime credit card or ACH (electronic check) transaction via a Business::OnlinePayment realtime gateway. See L for supported gateways. -Available methods are: I, I and I +Available methods are: I or I -Available options are: I, I, I, I +Available options are: I, I, I, I Most gateways require a reference to an original payment transaction to refund, so you probably need to specify a I. I defaults to the original amount of the payment if not specified. -I specifies a reason for the refund. +I specifies a reason for the refund. I specifies the expiration date for a credit card overriding the value from the customer record or the payment record. Specified as yyyy-mm-dd @@ -1352,11 +1361,34 @@ sub realtime_refund_bop { $options{method} = $method; } + my ($reason, $reason_text); + if ( $options{'reasonnum'} ) { + # do this here, because we need the plain text reason string in case we + # void the payment + $reason = FS::reason->by_key($options{'reasonnum'}); + $reason_text = $reason->reason; + } else { + # support old 'reason' string parameter in case it's still used, + # or else set a default + $reason_text = $options{'reason'} || 'card or ACH refund'; + local $@; + $reason = FS::reason->new_or_existing( + reason => $reason_text, + type => 'Refund reason', + class => 'F', + ); + if ($@) { + return "failed to add refund reason: $@"; + } + } + if ( $DEBUG ) { warn "$me realtime_refund_bop (new): $options{method} refund\n"; warn " $_ => $options{$_}\n" foreach keys %options; } + my %content = (); + ### # look up the original payment and optionally a gateway for that payment ### @@ -1375,6 +1407,9 @@ sub realtime_refund_bop { or return "Unknown paynum $options{'paynum'}"; $amount ||= $cust_pay->paid; + my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum }); + $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay; + if ( $cust_pay->get('processor') ) { ($gatewaynum, $processor, $auth, $order_number) = ( @@ -1447,7 +1482,8 @@ sub realtime_refund_bop { eval "use $namespace"; die $@ if $@; - my %content = ( + %content = ( + %content, 'type' => $options{method}, 'login' => $login, 'password' => $password, @@ -1513,7 +1549,7 @@ sub realtime_refund_bop { if $conf->exists('business-onlinepayment-test_transaction'); $void->submit(); if ( $void->is_success ) { - my $error = $cust_pay->void($options{'reason'}); + my $error = $cust_pay->void($reason_text); if ( $error ) { # gah, even with transactions. my $e = 'WARNING: Card/ACH voided but database not updated - '. @@ -1590,8 +1626,7 @@ sub realtime_refund_bop { $content{account_name} = $payname; $content{customer_org} = $self->company ? 'B' : 'I'; $content{customer_ssn} = $self->ss; - } elsif ( $options{method} eq 'LEC' ) { - $content{phone} = $payinfo = $self->payinfo; + } #then try refund @@ -1622,6 +1657,7 @@ sub realtime_refund_bop { $order_number = $refund->order_number if $refund->can('order_number'); + # change this to just use $cust_pay->delete_cust_bill_pay? while ( $cust_pay && $cust_pay->unapplied < $amount ) { my @cust_bill_pay = $cust_pay->cust_bill_pay; last unless @cust_bill_pay; @@ -1633,11 +1669,12 @@ sub realtime_refund_bop { my $cust_refund = new FS::cust_refund ( { 'custnum' => $self->custnum, 'paynum' => $options{'paynum'}, + 'source_paynum' => $options{'paynum'}, 'refund' => $amount, '_date' => '', 'payby' => $bop_method2payby{$options{method}}, 'payinfo' => $payinfo, - 'reason' => $options{'reason'} || 'card or ACH refund', + 'reasonnum' => $reason->reasonnum, 'gatewaynum' => $gatewaynum, # may be null 'processor' => $processor, 'auth' => $refund->authorization, @@ -1646,6 +1683,7 @@ sub realtime_refund_bop { my $error = $cust_refund->insert; if ( $error ) { $cust_refund->paynum(''); #try again with no specific paynum + $cust_refund->source_paynum(''); my $error2 = $cust_refund->insert; if ( $error2 ) { # gah, even with transactions. @@ -1662,6 +1700,301 @@ sub realtime_refund_bop { } +=item realtime_verify_bop [ OPTION => VALUE ... ] + +Runs an authorization-only transaction for $1 against this credit card (if +successful, immediatly reverses the authorization). + +Returns the empty string if the authorization was sucessful, or an error +message otherwise. + +I + +I + +I specifies the expiration date for a credit card overriding the +value from the customer record or the payment record. Specified as yyyy-mm-dd + +#The additional options I, I, I, I, +#I are also available. Any of these options, +#if set, will override the value from the customer record. + +=cut + +#Available methods are: I or I + +#some false laziness w/realtime_bop and realtime_refund_bop, not enough to make +#it worth merging but some useful small subs should be pulled out +sub realtime_verify_bop { + my $self = shift; + + local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG; + + my %options = (); + if (ref($_[0]) eq 'HASH') { + %options = %{$_[0]}; + } else { + %options = @_; + } + + if ( $DEBUG ) { + warn "$me realtime_verify_bop\n"; + warn " $_ => $options{$_}\n" foreach keys %options; + } + + ### + # select a gateway + ### + + my $payment_gateway = $self->_payment_gateway( \%options ); + my $namespace = $payment_gateway->gateway_namespace; + + 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} || $self->payinfo, + ); + return "Banned credit card" if $ban && $ban->bantype ne 'warn'; + + ### + # massage data + ### + + my $bop_content = $self->_bop_content(\%options); + return $bop_content unless ref($bop_content); + + my @invoicing_list = $self->invoicing_list_emailonly; + if ( $conf->exists('emailinvoiceautoalways') + || $conf->exists('emailinvoiceauto') && ! @invoicing_list + || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { + push @invoicing_list, $self->all_emails; + } + + my $email = ($conf->exists('business-onlinepayment-email-override')) + ? $conf->config('business-onlinepayment-email-override') + : $invoicing_list[0]; + + my $paydate = ''; + my %content = (); + + if ( $namespace eq 'Business::OnlinePayment' ) { + + if ( $options{method} eq 'CC' ) { + + $content{card_number} = $options{payinfo} || $self->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; + + } elsif ( $options{method} eq 'ECHECK' ){ + + #nop for checks (though it shouldn't be called...) + + } else { + die "unknown method ". $options{method}; + } + + } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) { + #move along + } else { + die "unknown namespace $namespace"; + } + + ### + # run transaction(s) + ### + + warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1; + $self->select_for_update; #mutex ... just until we get our pending record in + warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1; + + #the checks here are intended to catch concurrent payments + #double-form-submission prevention is taken care of in cust_pay_pending::check + + #also check and make sure there aren't *other* pending payments for this cust + + my @pending = qsearch('cust_pay_pending', { + 'custnum' => $self->custnum, + 'status' => { op=>'!=', value=>'done' } + }); + + return "A payment is already being processed for this customer (". + join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ). + "); verification transaction aborted." + if scalar(@pending); + + #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out + + my $cust_pay_pending = new FS::cust_pay_pending { + 'custnum' => $self->custnum, + 'paid' => '1.00', + '_date' => '', + 'payby' => $bop_method2payby{'CC'}, + 'payinfo' => $options{payinfo} || $self->payinfo, + 'paymask' => $options{paymask} || $self->paymask, + 'paydate' => $paydate, + #'recurring_billing' => $content{recurring_billing}, + 'pkgnum' => $options{'pkgnum'}, + 'status' => 'new', + 'gatewaynum' => $payment_gateway->gatewaynum || '', + 'session_id' => $options{session_id} || '', + #'jobnum' => $options{depend_jobnum} || '', + }; + $cust_pay_pending->payunique( $options{payunique} ) + if defined($options{payunique}) && length($options{payunique}); + + warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n" + if $DEBUG > 1; + my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted + return $cpp_new_err if $cpp_new_err; + + warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n" + if $DEBUG > 1; + warn Dumper($cust_pay_pending) if $DEBUG > 2; + + my $transaction = new $namespace( $payment_gateway->gateway_module, + $self->_bop_options(\%options), + ); + + $transaction->content( + 'type' => 'CC', + $self->_bop_auth(\%options), + 'action' => 'Authorization Only', + 'description' => $options{'description'}, + 'amount' => '1.00', + #'invoice_number' => $options{'invnum'}, + 'customer_id' => $self->custnum, + %$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 + ); + + $cust_pay_pending->status('pending'); + my $cpp_pending_err = $cust_pay_pending->replace; + return $cpp_pending_err if $cpp_pending_err; + + warn Dumper($transaction) if $DEBUG > 2; + + unless ( $BOP_TESTING ) { + $transaction->test_transaction(1) + if $conf->exists('business-onlinepayment-test_transaction'); + $transaction->submit(); + } else { + if ( $BOP_TESTING_SUCCESS ) { + $transaction->is_success(1); + $transaction->authorization('fake auth'); + } else { + $transaction->is_success(0); + $transaction->error_message('fake failure'); + } + } + + if ( $transaction->is_success() ) { + + $cust_pay_pending->status('authorized'); + my $cpp_authorized_err = $cust_pay_pending->replace; + return $cpp_authorized_err if $cpp_authorized_err; + + my $auth = $transaction->authorization; + my $ordernum = $transaction->can('order_number') + ? $transaction->order_number + : ''; + + my $reverse = new $namespace( $payment_gateway->gateway_module, + $self->_bop_options(\%options), + ); + + $reverse->content( 'action' => 'Reverse Authorization', + + # B:OP + 'authorization' => $transaction->authorization, + 'order_number' => $ordernum, + + # vsecure + 'result_code' => $transaction->result_code, + 'txn_date' => $transaction->txn_date, + + %content, + ); + $reverse->test_transaction(1) + if $conf->exists('business-onlinepayment-test_transaction'); + $reverse->submit(); + + if ( $reverse->is_success ) { + + $cust_pay_pending->status('done'); + my $cpp_authorized_err = $cust_pay_pending->replace; + return $cpp_authorized_err if $cpp_authorized_err; + + } else { + + my $e = "Authorization successful but reversal failed, custnum #". + $self->custnum. ': '. $reverse->result_code. + ": ". $reverse->error_message; + warn $e; + return $e; + + } + + } + + ### + # Tokenize + ### + + if ( $transaction->can('card_token') && $transaction->card_token ) { + + if ( $options{'payinfo'} eq $self->payinfo ) { + $self->payinfo($transaction->card_token); + my $error = $self->replace; + if ( $error ) { + warn "WARNING: error storing token: $error, but proceeding anyway\n"; + } + } + + } + + ### + # result handling + ### + + $transaction->is_success() ? '' : $transaction->error_message(); + +} + =back =head1 BUGS