summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2013-06-28 16:42:17 -0700
committerMark Wells <mark@freeside.biz>2013-06-28 16:42:17 -0700
commit3be3194b8429383cf47fde8ea662327a0a60d8b0 (patch)
treedbf3b9d2bf42ce661387ec7a4530e84f543d96b1 /FS
parenta281af4caba53bfd4219bab5d07ce4153a83125a (diff)
new thirdparty payment framework, #23752, #23579, #22395
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm48
-rw-r--r--FS/FS/Conf.pm6
-rw-r--r--FS/FS/Schema.pm3
-rw-r--r--FS/FS/agent.pm30
-rw-r--r--FS/FS/cust_main.pm1
-rw-r--r--FS/FS/cust_pay.pm9
-rw-r--r--FS/FS/cust_pay_pending.pm131
-rw-r--r--FS/FS/payment_gateway.pm47
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.