Billing_ThirdParty.pm, missing file
authorMark Wells <mark@freeside.biz>
Sat, 29 Jun 2013 16:24:00 +0000 (09:24 -0700)
committerMark Wells <mark@freeside.biz>
Sat, 29 Jun 2013 16:24:00 +0000 (09:24 -0700)
FS/FS/cust_main/Billing_ThirdParty.pm [new file with mode: 0644]

diff --git a/FS/FS/cust_main/Billing_ThirdParty.pm b/FS/FS/cust_main/Billing_ThirdParty.pm
new file mode 100644 (file)
index 0000000..faced8f
--- /dev/null
@@ -0,0 +1,266 @@
+package FS::cust_main::Billing_ThirdParty;
+
+use strict;
+use vars qw( $DEBUG $me );
+use FS::Record qw( qsearch qsearchs dbh );
+use FS::cust_pay;
+use FS::cust_pay_pending;
+
+$DEBUG = 0;
+$me = '[FS::cust_main::Billing_ThirdParty]';
+# arguably doesn't even belong under cust_main...
+
+=head1 METHODS
+
+=over 4
+
+=item create_payment OPTIONS
+
+Create a pending payment for a third-party gateway.  OPTIONS must include:
+- method: a Business::OnlineThirdPartyPayment method argument.  Currently 
+  only supports PAYPAL.
+- amount: a decimal amount.  Unlike in Billing_Realtime, there is NO default.
+- session_id: the customer's self-service session ID.
+
+and may optionally include:
+- invnum: the invoice that this payment will apply to
+- pkgnum: the package balance that this payment will apply to.
+- description: the transaction description for the gateway.
+- payip: the IP address the payment is initiated from
+
+On failure, returns a simple string error message.  On success, returns 
+a hashref of 'url' => the URL to redirect the user to to complete payment,
+and optionally 'post_params' => a hashref of name/value pairs to be POSTed
+to that URL.
+
+=cut
+
+my @methods = qw(PAYPAL CC);
+my %method2payby = ( 'PAYPAL' => 'PPAL', 'CC' => 'MCRD' );
+
+sub create_payment {
+  my $self = shift;
+  my %opt = @_;
+
+  # avoid duplicating this--we just need description and invnum
+  my $defaults;
+  $self->_bop_defaults($defaults);
+  
+  my $method = $opt{'method'} or return 'method required';
+  my $amount = $opt{'amount'} or return 'amount required';
+  return "unknown method '$method'" unless grep {$_ eq $method} @methods;
+  return "amount must be > 0" unless $amount > 0;
+  return "session_id required" unless length($opt{'session_id'});
+
+  my $gateway = $self->agent->payment_gateway(
+    method      => $method,
+    nofatal     => 1,
+    thirdparty  => 1,
+  );
+  return "no third-party gateway enabled for method $method" if !$gateway;
+
+  # create pending record
+  $self->select_for_update;
+  my @pending = qsearch('cust_pay_pending', {
+      'custnum' => $self->custnum,
+      'status'  => { op=>'!=', value=>'done' }
+  });
+
+  # if there are pending payments in the 'thirdparty' state,
+  # we can safely remove them
+  foreach (@pending) {
+    if ( $_->status eq 'thirdparty' ) {
+      my $error = $_->delete;
+      return "Error deleting unfinished payment #".
+        $_->paypendingnum . ": $error\n" if $error;
+    } else {
+      return "A payment is already being processed for this customer.";
+    }
+  }
+
+  my $cpp = FS::cust_pay_pending->new({
+      'custnum'         => $self->custnum,
+      'status'          => 'new',
+      'gatewaynum'      => $gateway->gatewaynum,
+      'paid'            => sprintf('%.2f',$opt{'amount'}),
+      'payby'           => $method2payby{ $opt{'method'} },
+      'pkgnum'          => $opt{'pkgnum'},
+      'invnum'          => $opt{'invnum'} || $defaults->{'invnum'},
+      'session_id'      => $opt{'session_id'},
+  });
+
+  my $error = $cpp->insert;
+  return $error if $error;
+
+  my $transaction = $gateway->processor;
+  # Not included in this content hash:
+  # payinfo, paydate, paycvv, any kind of recurring billing indicator,
+  # paystate, paytype (account type), stateid, ss, payname
+  #
+  # Also, unlike bop_realtime, we don't allow the magical %options hash
+  # to override the customer's information.  If they need to enter a 
+  # different address or something for the billing provider, they can do 
+  # that after the redirect.
+  my %content = (
+    'action'      => 'create',
+    'description' => $opt{'description'} || $defaults->{'description'},
+    'amount'      => $amount,
+    'customer_id' => $self->custnum,
+    'email'       => $self->invoicing_list_emailonly_scalar,
+    'customer_ip' => $opt{'payip'},
+    'first_name'  => $self->first,
+    'last_name'   => $self->last,
+    'address1'    => $self->address1,
+    'address2'    => $self->address2,
+    'city'        => $self->city,
+    'state'       => $self->state,
+    'zip'         => $self->zip,
+    'country'     => $self->country,
+    'phone'       => ($self->daytime || $self->night),
+  );
+
+  {
+    local $@;
+    eval { $transaction->create(%content) };
+    if ( $@ ) {
+      warn "ERROR: Executing third-party payment:\n$@\n";
+      return { error => $@ };
+    }
+  }
+
+  if ($transaction->is_success) {
+    $cpp->status('thirdparty');
+    # for whatever is most identifiable as the "transaction ID"
+    $cpp->payinfo($transaction->token);
+    # for anything else the transaction needs to remember
+    $cpp->statustext($transaction->statustext);
+    $error = $cpp->replace;
+    return $error if $error;
+
+    return {url => $transaction->redirect,
+            post_params => $transaction->post_params};
+
+  } else {
+    $cpp->status('done');
+    $cpp->statustext($transaction->error_message);
+    $error = $cpp->replace;
+    return $error if $error;
+
+    return $transaction->error_message;
+  }
+
+}
+
+=item execute_payment SESSION_ID, PARAMS
+
+Complete the payment and get the status.  Triggered from the return_url
+handler; PARAMS are all of the CGI parameters we received in the redirect.
+On failure, returns an error message.  On success, returns a hashref of 
+'paynum', 'paid', 'order_number', and 'auth'.
+
+=cut
+
+sub execute_payment {
+  my $self = shift;
+  my $session_id = shift;
+  my %params = @_;
+
+  my $cpp = qsearchs('cust_pay_pending', {
+      'session_id'  => uc($session_id),
+      'custnum'     => $self->custnum,
+      'status'      => 'thirdparty',
+  })
+    or return 'no payment in process for this session';
+
+  my $gateway = FS::payment_gateway->by_key( $cpp->gatewaynum );
+  my $transaction = $gateway->processor;
+  $transaction->token($cpp->payinfo);
+  $transaction->statustext($cpp->statustext);
+
+  {
+    local $@;
+    eval { $transaction->execute(%params) };
+    if ( $@ ) {
+      warn "ERROR: Executing third-party payment:\n$@\n";
+      return { error => $@ };
+    }
+  }
+
+  my $error;
+
+  if ( $transaction->is_success ) {
+
+    $error = $cpp->approve(
+                    'processor'     => $gateway->gateway_module,
+                    'order_number'  => $transaction->order_number,
+                    'auth'          => $transaction->authorization,
+                    'payinfo'       => '',
+                    'apply'         => 1,
+                  );
+    return $error if $error;
+
+    return {
+      'paynum'        => $cpp->paynum,
+      'paid'          => $cpp->paid,
+      'order_number'  => $transaction->order_number,
+      'auth'          => $transaction->authorization,
+    }
+
+  } else {
+
+    my $error = $gateway->gateway_module. " error: ".
+      $transaction->error_message;
+
+    my $jobnum = $cpp->jobnum;
+    if ( $jobnum ) {
+      my $placeholder = FS::queue->by_key($jobnum);
+
+      if ( $placeholder ) {
+        my $e = $placeholder->depended_delete || $placeholder->delete;
+        warn "error removing provisioning jobs after declined paypendingnum ".
+          $cpp->paypendingnum. ": $e\n\n"
+          if $e;
+      } else {
+        warn "error finding job $jobnum for declined paypendingnum ".
+          $cpp->paypendingnum. "\n\n";
+      }
+    }
+
+    # not needed here:
+    # the raw HTTP response thing when there's no error message
+    # decline notices (the customer has already seen the decline message)
+
+    # set the pending status
+    my $e = $cpp->decline($error);
+    if ( $e ) {
+      $e = "WARNING: payment declined but pending payment not resolved - ".
+           "error updating status for pendingnum :".$cpp->paypendingnum.
+           ": $e\n\n";
+      warn $e;
+      $error = "$e ($error)";
+    }
+
+    return $error;
+  }
+
+}
+
+=item cancel_payment SESSION_ID
+
+Cancel a pending payment attempt.  This just cleans up the cust_pay_pending
+record.
+
+=cut
+
+sub cancel_payment {
+  my $self = shift;
+  my $session_id = shift;
+  my $cust_pay_pending = qsearchs('cust_pay_pending', {
+      'session_id'  => uc($session_id),
+      'status'      => 'thirdparty',
+  });
+  return { 'error' => $cust_pay_pending->delete };
+}
+
+1;
+