1 package FS::cust_main::Billing_ThirdParty;
4 use vars qw( $DEBUG $me );
5 use FS::Record qw( qsearch qsearchs dbh );
7 use FS::cust_pay_pending;
10 $me = '[FS::cust_main::Billing_ThirdParty]';
11 # arguably doesn't even belong under cust_main...
17 =item create_payment OPTIONS
19 Create a pending payment for a third-party gateway. OPTIONS must include:
20 - method: a Business::OnlineThirdPartyPayment method argument. Currently
22 - amount: a decimal amount. Unlike in Billing_Realtime, there is NO default.
23 - session_id: the customer's self-service session ID.
25 and may optionally include:
26 - invnum: the invoice that this payment will apply to
27 - pkgnum: the package balance that this payment will apply to.
28 - description: the transaction description for the gateway.
29 - payip: the IP address the payment is initiated from
31 On failure, returns a simple string error message. On success, returns
32 a hashref of 'url' => the URL to redirect the user to to complete payment,
33 and optionally 'post_params' => a hashref of name/value pairs to be POSTed
38 my @methods = qw(PAYPAL CC);
39 my %method2payby = ( 'PAYPAL' => 'PPAL',
40 'CC' => 'MCRD', #? but doesn't MCRD mean _offline_
41 #card, not third-party card? but no
42 #one is doing non-paypal right now
49 # avoid duplicating this--we just need description and invnum
51 $self->_bop_defaults($defaults);
53 my $method = $opt{'method'} or return 'method required';
54 my $amount = $opt{'amount'} or return 'amount required';
55 return "unknown method '$method'" unless grep {$_ eq $method} @methods;
56 return "amount must be > 0" unless $amount > 0;
57 return "session_id required" unless length($opt{'session_id'});
59 my $gateway = $self->agent->payment_gateway(
64 return "no third-party gateway enabled for method $method" if !$gateway;
66 # create pending record
67 $self->select_for_update;
68 my @pending = qsearch('cust_pay_pending', {
69 'custnum' => $self->custnum,
70 'status' => { op=>'!=', value=>'done' }
73 # if there are pending payments in the 'thirdparty' state,
74 # we can safely remove them
76 if ( $_->status eq 'thirdparty' ) {
77 my $error = $_->delete;
78 return "Error deleting unfinished payment #".
79 $_->paypendingnum . ": $error\n" if $error;
81 return "A payment is already being processed for this customer.";
85 my $cpp = FS::cust_pay_pending->new({
86 'custnum' => $self->custnum,
88 'gatewaynum' => $gateway->gatewaynum,
89 'paid' => sprintf('%.2f',$opt{'amount'}),
90 'payby' => $method2payby{ $opt{'method'} },
91 'pkgnum' => $opt{'pkgnum'},
92 'invnum' => $opt{'invnum'} || $defaults->{'invnum'},
93 'session_id' => $opt{'session_id'},
96 my $error = $cpp->insert;
97 return $error if $error;
99 my $transaction = $gateway->processor;
100 # Not included in this content hash:
101 # payinfo, paydate, paycvv, any kind of recurring billing indicator,
102 # paystate, paytype (account type), stateid, ss, payname
104 # Also, unlike bop_realtime, we don't allow the magical %options hash
105 # to override the customer's information. If they need to enter a
106 # different address or something for the billing provider, they can do
107 # that after the redirect.
109 'action' => 'create',
110 'description' => $opt{'description'} || $defaults->{'description'},
112 'customer_id' => $self->custnum,
113 'email' => $self->invoicing_list_emailonly_scalar,
114 'customer_ip' => $opt{'payip'},
115 'first_name' => $self->first,
116 'last_name' => $self->last,
117 'address1' => $self->address1,
118 'address2' => $self->address2,
119 'city' => $self->city,
120 'state' => $self->state,
122 'country' => $self->country,
123 'phone' => ($self->daytime || $self->night),
128 eval { $transaction->create(%content) };
130 warn "ERROR: Executing third-party payment:\n$@\n";
131 return { error => $@ };
135 if ($transaction->is_success) {
136 $cpp->status('thirdparty');
137 # for whatever is most identifiable as the "transaction ID"
138 $cpp->payinfo($transaction->token);
139 # for anything else the transaction needs to remember
140 $cpp->statustext($transaction->statustext);
141 $error = $cpp->replace;
142 return $error if $error;
144 return {url => $transaction->redirect,
145 post_params => $transaction->post_params};
148 $cpp->status('done');
149 $cpp->statustext($transaction->error_message);
150 $error = $cpp->replace;
151 return $error if $error;
153 return $transaction->error_message;
158 =item execute_payment SESSION_ID, PARAMS
160 Complete the payment and get the status. Triggered from the return_url
161 handler; PARAMS are all of the CGI parameters we received in the redirect.
162 On failure, returns an error message. On success, returns a hashref of
163 'paynum', 'paid', 'order_number', and 'auth'.
167 sub execute_payment {
169 my $session_id = shift;
172 my $cpp = qsearchs('cust_pay_pending', {
173 'session_id' => uc($session_id),
174 'custnum' => $self->custnum,
175 'status' => 'thirdparty',
177 or return 'no payment in process for this session';
179 my $gateway = FS::payment_gateway->by_key( $cpp->gatewaynum );
180 my $transaction = $gateway->processor;
181 $transaction->token($cpp->payinfo);
182 $transaction->statustext($cpp->statustext);
186 eval { $transaction->execute(%params) };
188 warn "ERROR: Executing third-party payment:\n$@\n";
189 return { error => $@ };
195 if ( $transaction->is_success ) {
197 $error = $cpp->approve(
198 'processor' => $gateway->gateway_module,
199 'order_number' => $transaction->order_number,
200 'auth' => $transaction->authorization,
204 return $error if $error;
207 'paynum' => $cpp->paynum,
208 'paid' => $cpp->paid,
209 'order_number' => $transaction->order_number,
210 'auth' => $transaction->authorization,
215 my $error = $gateway->gateway_module. " error: ".
216 $transaction->error_message;
218 my $jobnum = $cpp->jobnum;
220 my $placeholder = FS::queue->by_key($jobnum);
222 if ( $placeholder ) {
223 my $e = $placeholder->depended_delete || $placeholder->delete;
224 warn "error removing provisioning jobs after declined paypendingnum ".
225 $cpp->paypendingnum. ": $e\n\n"
228 warn "error finding job $jobnum for declined paypendingnum ".
229 $cpp->paypendingnum. "\n\n";
234 # the raw HTTP response thing when there's no error message
235 # decline notices (the customer has already seen the decline message)
237 # set the pending status
238 my $e = $cpp->decline($error);
240 $e = "WARNING: payment declined but pending payment not resolved - ".
241 "error updating status for pendingnum :".$cpp->paypendingnum.
244 $error = "$e ($error)";
252 =item cancel_payment SESSION_ID
254 Cancel a pending payment attempt. This just cleans up the cust_pay_pending
261 my $session_id = shift;
262 my $cust_pay_pending = qsearchs('cust_pay_pending', {
263 'session_id' => uc($session_id),
264 'status' => 'thirdparty',
266 return { 'error' => $cust_pay_pending->delete };