diff options
author | Mark Wells <mark@freeside.biz> | 2013-06-28 16:42:17 -0700 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2013-06-28 16:42:17 -0700 |
commit | 3be3194b8429383cf47fde8ea662327a0a60d8b0 (patch) | |
tree | dbf3b9d2bf42ce661387ec7a4530e84f543d96b1 /FS | |
parent | a281af4caba53bfd4219bab5d07ce4153a83125a (diff) |
new thirdparty payment framework, #23752, #23579, #22395
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/ClientAPI/MyAccount.pm | 48 | ||||
-rw-r--r-- | FS/FS/Conf.pm | 6 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 3 | ||||
-rw-r--r-- | FS/FS/agent.pm | 30 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 1 | ||||
-rw-r--r-- | FS/FS/cust_pay.pm | 9 | ||||
-rw-r--r-- | FS/FS/cust_pay_pending.pm | 131 | ||||
-rw-r--r-- | FS/FS/payment_gateway.pm | 47 |
8 files changed, 261 insertions, 14 deletions
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 01e0ebc..b735958 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -349,6 +349,8 @@ sub access_info { $conf->exists('ticket_system-selfservice_edit_subject') && $cust_main->edit_subject; + $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600; + return { %$info, 'custnum' => $custnum, 'access_pkgnum' => $session->{'pkgnum'}, @@ -845,7 +847,7 @@ sub payment_info { 'save_unchecked' => $conf->exists('selfservice-save_unchecked'), - 'credit_card_surcharge_percentage' => $conf->config('credit-card-surcharge-percentage'), + 'credit_card_surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')), }; } @@ -1267,6 +1269,50 @@ sub realtime_collect { return { 'error' => '', amount => $amount, %$error }; } +sub start_thirdparty { + my $p = shift; + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + my $custnum = $session->{'custnum'}; + my $cust_main = FS::cust_main->by_key($custnum); + + my $amount = $p->{'amount'} + or return { error => 'no amount' }; + + my $result = $cust_main->create_payment( + 'method' => $p->{'method'}, + 'amount' => $p->{'amount'}, + 'pkgnum' => $session->{'pkgnum'}, + 'session_id' => $p->{'session_id'}, + ); + + if ( ref($result) ) { # hashref or error + return $result; + } else { + return { error => $result }; + } +} + +sub finish_thirdparty { + my $p = shift; + my $session_id = delete $p->{'session_id'}; + my $session = _cache->get($session_id) + or return { 'error' => "Can't resume session" }; + my $custnum = $session->{'custnum'}; + my $cust_main = FS::cust_main->by_key($custnum); + + if ( $p->{_cancel} ) { + # customer backed out of making a payment + return $cust_main->cancel_payment( $session_id ); + } + my $result = $cust_main->execute_payment( $session_id, %$p ); + if ( ref($result) ) { + return $result; + } else { + return { error => $result }; + } +} + sub process_payment_order_pkg { my $p = shift; diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index b88aa11..5e70b40 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2272,6 +2272,12 @@ and customer address. Include units.', }, { + 'key' => 'selfservice-timeout', + 'section' => 'self-service', + 'description' => 'Timeout for the self-service login cookie, in seconds. Defaults to 1 hour.', + }, + + { 'key' => 'backend-realtime', 'section' => 'billing', 'description' => 'Run billing for backend signups immediately.', diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index da3ddab..72412c2 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1576,6 +1576,9 @@ sub tables_hashref { #'cust_balance', @money_type, '', '', 'paynum', 'int', 'NULL', '', '', '', 'jobnum', 'bigint', 'NULL', '', '', '', + 'invnum', 'int', 'NULL', '', '', '', + 'manual', 'char', 'NULL', 1, '', '', + 'discount_term','int', 'NULL', '', '', '', ], 'primary_key' => 'paypendingnum', 'unique' => [ [ 'payunique' ] ], diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index 109343a..57093e3 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -230,7 +230,8 @@ sub ticketing_queue { Returns a payment gateway object (see L<FS::payment_gateway>) for this agent. -Currently available options are I<nofatal>, I<invnum>, I<method>, and I<payinfo>. +Currently available options are I<nofatal>, I<invnum>, I<method>, +I<payinfo>, and I<thirdparty>. If I<nofatal> is set, and no gateway is available, then the empty string will be returned instead of throwing a fatal exception. @@ -245,10 +246,34 @@ as well. Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful. When the I<method> is 'CC' then the card number in I<payinfo> can direct this routine to route to a gateway suited for that type of card. +If I<thirdparty> is set, the defined self-service payment gateway will +be returned. + =cut sub payment_gateway { my ( $self, %options ) = @_; + + my $conf = new FS::Conf; + + if ( $options{thirdparty} ) { + # still a kludge, but it gets the job done + # and the 'cardtype' semantics don't really apply to thirdparty + # gateways because we have to choose a gateway without ever + # seeing the card number + my $gatewaynum = + $conf->config('selfservice-payment_gateway', $self->agentnum); + my $gateway = FS::payment_gateway->by_key($gatewaynum) + if $gatewaynum; + + if ( $gateway ) { + return $gateway; + } elsif ( $options{'nofatal'} ) { + return ''; + } else { + die "no third-party gateway configured\n"; + } + } my $taxclass = ''; if ( $options{invnum} ) { @@ -276,8 +301,6 @@ sub payment_gateway { $cardtype = cardtype($options{payinfo}); } elsif ( $options{method} eq 'ECHECK' ) { $cardtype = 'ACH'; - } elsif ( $options{method} eq 'PAYPAL' ) { - $cardtype = 'PayPal'; } else { $cardtype = $options{method} } @@ -298,7 +321,6 @@ sub payment_gateway { taxclass => '', } ); my $payment_gateway; - my $conf = new FS::Conf; if ( $override ) { #use a payment gateway override $payment_gateway = $override->payment_gateway; diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 7e3e26a..04c2e22 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -6,6 +6,7 @@ use base qw( FS::cust_main::Packages FS::cust_main::Status FS::cust_main::NationalID FS::cust_main::Billing FS::cust_main::Billing_Realtime FS::cust_main::Billing_Discount + FS::cust_main::Billing_ThirdParty FS::cust_main::Location FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin FS::geocode_Mixin FS::Quotable_Mixin diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index f6954a4..69f4c39 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -190,6 +190,15 @@ A hash of optional arguments may be passed. Currently "manual" is supported. If true, a payment receipt is sent instead of a statement when 'payment_receipt_email' configuration option is set. +About the "manual" flag: Normally, if the 'payment_receipt' config option +is set, and the customer has an invoice email address, inserting a payment +causes a I<statement> to be emailed to the customer. If the payment is +considered "manual" (or if the customer has no invoices), then it will +instead send a I<payment receipt>. "manual" should be true whenever a +payment is created directly from the web interface, from a user-initiated +realtime payment, or from a third-party payment via self-service. It should +be I<false> when creating a payment from a billing event or from a batch. + =cut sub insert { diff --git a/FS/FS/cust_pay_pending.pm b/FS/FS/cust_pay_pending.pm index f03ed1f..8e29f08 100644 --- a/FS/FS/cust_pay_pending.pm +++ b/FS/FS/cust_pay_pending.pm @@ -128,8 +128,24 @@ Additional status information. L<FS::payment_gateway> id. -=item paynum - +=item paynum +Payment number (L<FS::cust_pay>) of the completed payment. + +=item invnum + +Invoice number (L<FS::cust_bill>) to try to apply this payment to. + +=item manual + +Flag for whether this is a "manual" payment (i.e. initiated through +self-service or the back-office web interface, rather than from an event +or a payment batch). "Manual" payments will cause the customer to be +sent a payment receipt rather than a statement. + +=item discount_term + +Number of months the customer tried to prepay for. =back @@ -203,6 +219,9 @@ sub check { || $self->ut_hexn('session_id') || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' ) || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_foreign_keyn('invnum', 'cust_bill', 'invnum') + || $self->ut_flag('manual') + || $self->ut_numbern('discount_term') || $self->payinfo_check() #payby/payinfo/paymask/paydate ; return $error if $error; @@ -296,6 +315,116 @@ sub insert_cust_pay { } +=item approve OPTIONS + +Sets the status of this pending payment to "done" and creates a completed +payment (L<FS::cust_pay>). This should be called when a realtime or +third-party payment has been approved. + +OPTIONS may include any of 'processor', 'payinfo', 'discount_term', 'auth', +and 'order_number' to set those fields on the completed payment, as well as +'apply' to apply payments for this customer after inserting the new payment. + +=cut + +sub approve { + my $self = shift; + my %opt = @_; + + my $dbh = dbh; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + + my $cust_pay = FS::cust_pay->new({ + 'custnum' => $self->custnum, + 'invnum' => $self->invnum, + 'pkgnum' => $self->pkgnum, + 'paid' => $self->paid, + '_date' => '', + 'payby' => $self->payby, + 'payinfo' => $self->payinfo, + 'gatewaynum' => $self->gatewaynum, + }); + foreach my $opt_field (qw(processor payinfo auth order_number)) + { + $cust_pay->set($opt_field, $opt{$opt_field}) if exists $opt{$opt_field}; + } + + my %insert_opt = ( + 'manual' => $self->manual, + 'discount_term' => $self->discount_term, + ); + my $error = $cust_pay->insert( %insert_opt ); + if ( $error ) { + # try it again without invnum or discount + # (both of those can make payments fail to insert, and at this point + # the payment is a done deal and MUST be recorded) + $self->invnum(''); + my $error2 = $cust_pay->insert('manual' => $self->manual); + if ( $error2 ) { + # attempt to void the payment? + # no, we'll just stop digging at this point. + $dbh->rollback or die $dbh->errstr if $oldAutoCommit; + my $e = "WARNING: payment captured but not recorded - error inserting ". + "payment (". ($opt{processor} || $self->payby) . + ": $error2\n(previously tried insert with invnum#".$self->invnum. + ": $error)\npending payment saved as paypendingnum#". + $self->paypendingnum."\n\n"; + warn $e; + return $e; + } + } + if ( my $jobnum = $self->jobnum ) { + my $placeholder = FS::queue->by_key($jobnum); + my $error; + if (!$placeholder) { + $error = "not found"; + } else { + $error = $placeholder->delete; + } + + if ($error) { + $dbh->rollback or die $dbh->errstr if $oldAutoCommit; + my $e = "WARNING: payment captured but could not delete job $jobnum ". + "for paypendingnum #" . $self->paypendingnum . ": $error\n\n"; + warn $e; + return $e; + } + } + + if ( $opt{'paynum_ref'} ) { + ${ $opt{'paynum_ref'} } = $cust_pay->paynum; + } + + $self->status('done'); + $self->statustext('captured'); + $self->paynum($cust_pay->paynum); + my $cpp_done_err = $self->replace; + + if ( $cpp_done_err ) { + + $dbh->rollback or die $dbh->errstr if $oldAutoCommit; + my $e = "WARNING: payment captured but could not update pending status ". + "for paypendingnum ".$self->paypendingnum.": $cpp_done_err \n\n"; + warn $e; + return $e; + + } else { + + # commit at this stage--we don't want to roll back if applying + # payments fails + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + if ( $opt{'apply'} ) { + my $apply_error = $self->apply_payments_and_credits; + if ( $apply_error ) { + warn "WARNING: error applying payment: $apply_error\n\n"; + } + } + } + ''; +} + =item decline [ STATUSTEXT ] Sets the status of this pending payment to "done" (with statustext diff --git a/FS/FS/payment_gateway.pm b/FS/FS/payment_gateway.pm index e94a62c..68d8418 100644 --- a/FS/FS/payment_gateway.pm +++ b/FS/FS/payment_gateway.pm @@ -53,11 +53,11 @@ currently supported: =item gateway_callback_url - For ThirdPartyPayment only, set to the URL that the user should be redirected to on a successful payment. This will be sent -as a transaction parameter (named "callback_url"). +as a transaction parameter named "return_url". =item gateway_cancel_url - For ThirdPartyPayment only, set to the URL that -the user should be redirected to if they cancel the transaction. PayPal -requires this; other gateways ignore it. +the user should be redirected to if they cancel the transaction. This will +be sent as a transaction parameter named "cancel_url". =item auto_resolve_status - For BatchPayment only, set to 'approve' to auto-approve unresolved payments after some number of days, 'reject' to @@ -277,10 +277,6 @@ sub batch_processor { eval "use Business::BatchPayment;"; die "couldn't load Business::BatchPayment: $@" if $@; - my $conf = new FS::Conf; - my $test_mode = $conf->exists('business-batchpayment-test_transaction'); - $opt{'test_mode'} = 1 if $test_mode; - my $module = $self->gateway_module; my $processor = eval { Business::BatchPayment->create($module, $self->options, %opt) @@ -289,11 +285,46 @@ sub batch_processor { if $@; die "$module does not support test mode" - if $test_mode and not $processor->does('Business::BatchPayment::TestMode'); + if $opt{'test_mode'} + and not $processor->does('Business::BatchPayment::TestMode'); return $processor; } +=item processor OPTIONS + +Loads the module for the processor and returns an instance of it. + +=cut + +sub processor { + local $@; + my $self = shift; + my %opt = @_; + foreach (qw(action username password)) { + if (length($self->get("gateway_$_"))) { + $opt{$_} = $self->get("gateway_$_"); + } + } + $opt{'return_url'} = $self->gateway_callback_url; + $opt{'cancel_url'} = $self->gateway_cancel_url; + + my $conf = new FS::Conf; + my $test_mode = $conf->exists('business-batchpayment-test_transaction'); + $opt{'test_mode'} = 1 if $test_mode; + + my $namespace = $self->gateway_namespace; + eval "use $namespace"; + die "couldn't load $namespace: $@" if $@; + + if ( $namespace eq 'Business::BatchPayment' ) { + # at some point we can merge these, but there's enough special behavior... + return $self->batch_processor(%opt); + } else { + return $namespace->new( $self->gateway_module, $self->options, %opt ); + } +} + # _upgrade_data # # Used by FS::Upgrade to migrate to a new database. |