webpay support #4103 webpay_support_branch
authorjeff <jeff>
Mon, 9 Mar 2009 03:51:10 +0000 (03:51 +0000)
committerjeff <jeff>
Mon, 9 Mar 2009 03:51:10 +0000 (03:51 +0000)
23 files changed:
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI/Signup.pm
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/agent.pm
FS/FS/cust_main.pm
FS/FS/cust_pay_pending.pm
FS/FS/cust_pkg.pm
FS/FS/payby.pm
FS/FS/payment_gateway.pm
fs_selfservice/FS-SelfService/SelfService.pm
fs_selfservice/FS-SelfService/cgi/change_pay.html
fs_selfservice/FS-SelfService/cgi/make_thirdparty_payment.html [new file with mode: 0755]
fs_selfservice/FS-SelfService/cgi/myaccount.html
fs_selfservice/FS-SelfService/cgi/myaccount_menu.html
fs_selfservice/FS-SelfService/cgi/selfservice.cgi
fs_selfservice/FS-SelfService/cgi/signup.cgi
fs_selfservice/FS-SelfService/cgi/signup.html
fs_selfservice/FS-SelfService/cgi/verify.cgi [new file with mode: 0755]
httemplate/browse/payment_gateway.html
httemplate/edit/payment_gateway.html
httemplate/edit/process/payment_gateway.html
httemplate/elements/tr-textarea.html [new file with mode: 0644]

index c0586af..c6a4e00 100644 (file)
@@ -353,6 +353,9 @@ sub payment_info {
       'paytypes' => [ @FS::cust_main::paytypes ],
 
       'paybys' => [ $conf->config('signup_server-payby') ],
+      'cust_paybys' => [ map { FS::payby->payby2payment($_) }
+                                 $conf->config('signup_server-payby')
+                       ],
 
       'stateid_label' => FS::Msgcat::_gettext('stateid'),
       'stateid_state_label' => FS::Msgcat::_gettext('stateid_state'),
@@ -375,6 +378,18 @@ sub payment_info {
   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
     or return { 'error' => "unknown custnum $custnum" };
 
+  $return{hide_payment_fields} =
+  [
+    map { FS::payby->realtime($_) &&
+          $cust_main
+            ->agent
+            ->payment_gateway( 'method' => FS::payby->payby2bop($_) )
+            ->gateway_namespace
+            eq 'Business::OnlineThirdPartyPayment'
+        }
+    @{ $return{cust_paybys} }
+  ];
+
   $return{balance} = $cust_main->balance;
 
   $return{payname} = $cust_main->payname
@@ -531,6 +546,26 @@ sub process_payment {
 
 }
 
+sub realtime_collect {
+
+  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 = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  my $error = $cust_main->realtime_collect( 'method'     => $p->{'method'},
+                                            'session_id' => $p->{'session_id'},
+                                          );
+  return { 'error' => $error } unless ref( $error );
+
+  return { 'error' => '', amount => $cust_main->balance, %$error };
+}
+
 sub process_payment_order_pkg {
   my $p = shift;
 
index 5569dfb..b54230d 100644 (file)
@@ -6,6 +6,7 @@ use Data::Dumper;
 use Tie::RefHash;
 use FS::Conf;
 use FS::Record qw(qsearch qsearchs dbdef);
+use FS::CGI qw(popurl);
 use FS::Msgcat qw(gettext);
 use FS::Misc qw(card_types);
 use FS::ClientAPI_SessionCache;
@@ -20,6 +21,7 @@ use FS::svc_phone;
 use FS::acct_snarf;
 use FS::queue;
 use FS::reg_code;
+use FS::payby;
 
 $DEBUG = 0;
 $me = '[FS::ClientAPI::Signup]';
@@ -276,6 +278,29 @@ sub signup_info {
 
   if ( $agentnum ) {
 
+    warn "$me setting agent-specific payment flag\n" if $DEBUG > 1;
+    my $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
+    warn "$me has agent $agent\n" if $DEBUG > 1;
+    if ( $agent ) { #else complain loudly?
+      $signup_info->{'hide_payment_fields'} = [];
+      foreach my $payby (@{$signup_info->{payby}}) {
+        warn "$me checking $payby payment fields\n" if $DEBUG > 1;
+        my $hide = 0;
+        if (FS::payby->realtime($payby)) {
+          my $payment_gateway =
+            $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby) );
+          if ($payment_gateway->gateway_namespace eq
+              'Business::OnlineThirdPartyPayment'
+             ) {
+            warn "$me hiding $payby payment fields\n" if $DEBUG > 1;
+            $hide = 1;
+          }
+        }
+        push @{$signup_info->{'hide_payment_fields'}}, $hide;
+      }
+    }
+    warn "$me done setting agent-specific payment flag\n" if $DEBUG > 1;
+
     warn "$me setting agent-specific package list\n" if $DEBUG > 1;
     $signup_info->{'part_pkg'} = $signup_info->{'agentnum2part_pkg'}{$agentnum}
       unless @{ $signup_info->{'part_pkg'} };
@@ -436,6 +461,23 @@ sub new_customer {
     unless grep { $_ eq $packet->{'payby'} }
                 $conf->config('signup_server-payby');
 
+  if (FS::payby->realtime($packet->{payby})) {
+    my $payby = $packet->{payby};
+
+    my $agent = qsearchs('agent', { 'agentnum' => $agentnum });
+    return { 'error' => "Unknown reseller" }
+      unless $agent;
+
+    my $payment_gateway =
+      $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby) );
+
+    if ($payment_gateway->gateway_namespace eq
+        'Business::OnlineThirdPartyPayment'
+       ) {
+      $cust_main->payby('BILL');   # MCRD better?
+    }
+  }
+
   $cust_main->payinfo($cust_main->daytime)
     if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo;
 
@@ -547,10 +589,22 @@ sub new_customer {
     #     " new customer: $bill_error"
     #  if $bill_error;
 
-    $bill_error = $cust_main->collect('realtime' => 1);
+    $bill_error = $cust_main->realtime_collect(
+       method        => FS::payby->payby2bop( $packet->{payby} ),
+       depend_jobnum => $placeholder->jobnum,
+    );
     #warn "[fs_signup_server] error collecting from new customer: $bill_error"
     #  if $bill_error;
 
+    if ($bill_error && ref($bill_error) eq 'HASH') {
+      return { 'error' => '_collect',
+               ( map { $_ => $bill_error->{$_} }
+                 qw(popup_url reference collectitems)
+               ),
+               amount => $cust_main->balance,
+             };
+    }
+
     if ( $cust_main->balance > 0 ) {
 
       #this makes sense.  credit is "un-doing" the invoice
@@ -600,4 +654,83 @@ sub new_customer {
 
 }
 
+sub capture_payment {
+  my $packet = shift;
+
+  warn "$me capture_payment called on $packet\n" if $DEBUG;
+
+  ###
+  # identify processor/gateway from called back URL
+  ###
+
+  my $conf = new FS::Conf;
+
+  my $url = $packet->{url};
+  my $payment_gateway =
+    qsearchs('payment_gateway', { 'gateway_callback_url' => popurl(0, $url) } );
+
+  unless ($payment_gateway) {
+
+    my ( $processor, $login, $password, $action, @bop_options ) =
+      $conf->config('business-onlinepayment');
+    $action ||= 'normal authorization';
+    pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+    die "No real-time processor is enabled - ".
+        "did you set the business-onlinepayment configuration value?\n"
+      unless $processor;
+
+    $payment_gateway = new FS::payment_gateway( {
+      gateway_namespace => $conf->config('business-onlinepayment-namespace'),
+      gateway_module    => $processor,
+      gateway_username  => $login,
+      gateway_password  => $password,
+      gateway_action    => $action,
+      options   => [ ( @bop_options ) ],
+    });
+
+  }
+  die "No real-time third party processor is enabled - ".
+      "did you set the business-onlinepayment configuration value?\n*"
+    unless $payment_gateway->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
+
+  ###
+  # locate pending transaction
+  ###
+
+  eval "use Business::OnlineThirdPartyPayment";
+  die $@ if $@;
+
+  my $transaction =
+    new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
+                                           @{ [ $payment_gateway->options ] },
+                                         );
+
+  my $paypendingnum = $transaction->reference($packet->{data});
+
+  my $cust_pay_pending =
+    qsearchs('cust_pay_pending', { paypendingnum => $paypendingnum } );
+
+  unless ($cust_pay_pending) {
+    my $bill_error = "No payment is being processed with id $paypendingnum".
+                     "; Transaction aborted.";
+    return { error => '_decline', bill_error => $bill_error };
+  }
+
+  if ($cust_pay_pending->status ne 'pending') {
+    my $bill_error = "Payment with id $paypendingnum is not pending, but ".
+                     $cust_pay_pending->status.  "; Transaction aborted.";
+    return { error => '_decline', bill_error => $bill_error };
+  }
+
+  my $cust_main = $cust_pay_pending->cust_main;
+  my $bill_error =
+    $cust_main->realtime_botpp_capture( $cust_pay_pending, %{$packet->{data}} );
+
+  return { 'error'      => ( $bill_error->{bill_error} ? '_decline' : '' ),
+           %$bill_error,
+         };
+
+}
+
 1;
index b869302..3921afd 100644 (file)
@@ -8,6 +8,7 @@ use MIME::Base64;
 use FS::ConfItem;
 use FS::ConfDefaults;
 use FS::Conf_compat17;
+use FS::payby;
 use FS::conf;
 use FS::Record qw(qsearch qsearchs);
 use FS::UID qw(dbh datasrc use_confcompat);
@@ -620,6 +621,17 @@ worry that config_items is freeside-specific and icky.
   },
 
   {
+    'key'         => 'business-onlinepayment-namespace',
+    'section'     => 'billing',
+    'description' => 'Specifies which perl module namespace (which group of collection routines) is used by default.',
+    'type'        => 'select',
+    'select_hash' => [
+                       'Business::OnlinePayment' => 'Direct API (Business::OnlinePayment)',
+                      'Business::OnlineThirdPartyPayment' => 'Web API (Business::ThirdPartyPayment)',
+                     ],
+  },
+
+  {
     'key'         => 'business-onlinepayment-description',
     'section'     => 'billing',
     'description' => 'String passed as the description field to <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a>.  Evaluated as a double-quoted perl string, with the following variables available: <code>$agent</code> (the agent name), and <code>$pkgs</code> (a comma-separated list of packages for which these charges apply)',
index 885eaaa..65f7a7f 100644 (file)
@@ -845,10 +845,12 @@ sub tables_hashref {
         'payunique',    'varchar', 'NULL', $char_d, '', '', #separate paybatch "unique" functions from current usage
 
         'status',       'varchar',     '', $char_d, '', '', 
+        'session_id',   'varchar', 'NULL', $char_d, '', '', #only need 32
         'statustext',   'text',    'NULL',  '', '', '', 
         'gatewaynum',   'int',     'NULL',  '', '', '',
         #'cust_balance', @money_type,            '', '',
         'paynum',       'int',     'NULL',  '', '', '',
+        'jobnum',       'int',     'NULL',  '', '', '', 
       ],
       'primary_key' => 'paypendingnum',
       'unique'      => [ [ 'payunique' ] ],
@@ -1857,10 +1859,12 @@ sub tables_hashref {
     'payment_gateway' => {
       'columns' => [
         'gatewaynum',       'serial',   '',     '', '', '', 
+        'gateway_namespace','varchar',  'NULL', $char_d, '', '', 
         'gateway_module',   'varchar',  '',     $char_d, '', '', 
         'gateway_username', 'varchar',  'NULL', $char_d, '', '', 
         'gateway_password', 'varchar',  'NULL', $char_d, '', '', 
         'gateway_action',   'varchar',  'NULL', $char_d, '', '', 
+        'gateway_callback_url', 'varchar',  'NULL', $char_d, '', '', 
         'disabled',   'char',  'NULL',   1, '', '', 
       ],
       'primary_key' => 'gatewaynum',
index ff0a2b1..e471e04 100644 (file)
@@ -3,12 +3,14 @@ package FS::agent;
 use strict;
 use vars qw( @ISA );
 #use Crypt::YAPassGen;
+use Business::CreditCard 0.28;
 use FS::Record qw( dbh qsearch qsearchs );
 use FS::cust_main;
 use FS::cust_pkg;
 use FS::agent_type;
 use FS::reg_code;
 use FS::TicketSystem;
+use FS::Conf;
 
 @ISA = qw( FS::m2m_Common FS::Record );
 
@@ -200,6 +202,106 @@ sub ticketing_queue {
   FS::TicketSystem->queue($self->ticketing_queueid);
 };
 
+=item payment_gateway [ OPTION => VALUE, ... ]
+
+Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
+
+Currently available options are I<invnum>, I<method>, and I<payinfo>.
+
+If I<invnum> is set to the number of an invoice (see L<FS::cust_bill>) then
+an attempt will be made to select a gateway suited for the taxes paid on 
+the invoice.
+
+The I<method> and I<payinfo> options can be used to influence the choice
+as well.  Presently only 'CC' and 'ECHECK' 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.
+
+=cut
+
+sub payment_gateway {
+  my ( $self, %options ) = @_;
+
+  my $taxclass = '';
+  if ( $options{invnum} ) {
+    my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
+    die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
+    my @taxclasses =
+      map  { $_->part_pkg->taxclass }
+      grep { $_ }
+      map  { $_->cust_pkg }
+      $cust_bill->cust_bill_pkg;
+    unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
+                                                           #different taxclasses      $taxclass = $taxclasses[0];
+    }
+  }
+
+  #look for an agent gateway override first
+  my $cardtype;
+  if ( $options{method} && $options{method} eq 'CC' ) {
+    $cardtype = cardtype($options{payinfo});
+  } elsif ( $options{method} && $options{method} eq 'ECHECK' ) {
+    $cardtype = 'ACH';
+  } else {
+    $cardtype = $options{method} || '';
+  }
+
+  my $override =
+       qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+                                           cardtype => $cardtype,
+                                           taxclass => $taxclass,       } )
+    || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+                                           cardtype => '',
+                                           taxclass => $taxclass,       } )
+    || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+                                           cardtype => $cardtype,
+                                           taxclass => '',              } )
+    || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+                                           cardtype => '',
+                                           taxclass => '',              } );
+
+  my $payment_gateway = new FS::payment_gateway;
+  if ( $override ) { #use a payment gateway override
+
+    $payment_gateway = $override->payment_gateway;
+
+  } else { #use the standard settings from the config
+    # the standard settings from the config could be moved to a null agent
+    # agent_payment_gateway referenced payment_gateway
+
+    my $conf = new FS::Conf;
+    die "Real-time processing not enabled\n"
+      unless $conf->exists('business-onlinepayment');
+
+    #load up config
+    my $bop_config = 'business-onlinepayment';
+    $bop_config .= '-ach'
+      if ( $options{method}
+           && $options{method} =~ /^(ECHECK|CHEK)$/
+           && $conf->exists($bop_config. '-ach')
+         );
+    my ( $processor, $login, $password, $action, @bop_options ) =
+      $conf->config($bop_config);
+    $action ||= 'normal authorization';
+    pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+    die "No real-time processor is enabled - ".
+        "did you set the business-onlinepayment configuration value?\n"
+      unless $processor;
+
+    $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
+                                 'Business::OnlinePayment');
+    $payment_gateway->gateway_module($processor);
+    $payment_gateway->gateway_username($login);
+    $payment_gateway->gateway_password($password);
+    $payment_gateway->gateway_action($action);
+    $payment_gateway->set('options', [ @bop_options ]);
+
+  }
+
+  $payment_gateway;
+}
+
 =item num_prospect_cust_main
 
 Returns the number of prospects (customers with no packages ever ordered) for
index 865632f..6b64388 100644 (file)
@@ -3368,15 +3368,23 @@ sub retry_realtime {
 
 }
 
-=item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
+=item realtime_collect [ OPTION => VALUE ... ]
 
 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
-via a Business::OnlinePayment realtime gateway.  See
-L<http://420.am/business-onlinepayment> for supported gateways.
+via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime
+gateway.  See L<http://420.am/business-onlinepayment> and 
+L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
 
-Available methods are: I<CC>, I<ECHECK> and I<LEC>
+On failure returns an error message.
 
-Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
+Returns false or a hashref upon success.  The hashref contains keys popup_url reference, and collectitems.  The first is a URL to which a browser should be redirected for completion of collection.  The second is a reference id for the transaction suitable for the end user.  The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url.
+
+Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
+
+I<method> is one of: I<CC>, I<ECHECK> and I<LEC>.  If none is specified
+then it is deduced from the customer record.
+
+If no I<amount> is specified, then the customer balance is used.
 
 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
@@ -3396,130 +3404,209 @@ resulting paynum, if any.
 
 I<payunique> is a unique identifier for this payment.
 
-(moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
+I<session_id> is a session identifier associated with this payment.
+
+I<depend_jobnum> allows payment capture to unlock export jobs
 
 =cut
 
-sub realtime_bop {
-  my( $self, $method, $amount, %options ) = @_;
+sub realtime_collect {
+  my( $self, %options ) = @_;
+
   if ( $DEBUG ) {
-    warn "$me realtime_bop: $method $amount\n";
+    warn "$me realtime_collect:\n";
     warn "  $_ => $options{$_}\n" foreach keys %options;
   }
 
-  $options{'description'} ||= 'Internet services';
+  $options{amount} = $self->balance unless exists( $options{amount} );
+  $options{method} = FS::payby->payby2bop($self->payby)
+    unless exists( $options{method} );
 
-  return $self->fake_bop($method, $amount, %options) if $options{'fake'};
+  return $self->realtime_bop({%options});
 
-  eval "use Business::OnlinePayment";  
-  die $@ if $@;
+}
+
+=item realtime_bop { [ ARG => VALUE ... ] }
+
+Runs a realtime credit card, ACH (electronic check) or phone bill transaction
+via a Business::OnlinePayment realtime gateway.  See
+L<http://420.am/business-onlinepayment> for supported gateways.
+
+Required arguments in the hashref are I<method>, and I<amount>
+
+Available methods are: I<CC>, I<ECHECK> and I<LEC>
+
+Available optional arguments are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
 
-  my $payinfo = exists($options{'payinfo'})
-                  ? $options{'payinfo'}
-                  : $self->payinfo;
+The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
+I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
+if set, will override the value from the customer record.
+
+I<description> is a free-text field passed to the gateway.  It defaults to
+"Internet services".
+
+If an I<invnum> is specified, this payment (if successful) is applied to the
+specified invoice.  If you don't specify an I<invnum> you might want to
+call the B<apply_payments> method.
+
+I<quiet> can be set true to surpress email decline notices.
+
+I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
+resulting paynum, if any.
+
+I<payunique> is a unique identifier for this payment.
+
+I<session_id> is a session identifier associated with this payment.
+
+I<depend_jobnum> allows payment capture to unlock export jobs
+
+(moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
+
+=cut
 
-  my %method2payby = (
-    'CC'     => 'CARD',
-    'ECHECK' => 'CHEK',
-    'LEC'    => 'LECB',
+# some helper routines
+sub _payment_gateway {
+  my ($self, $options) = @_;
+
+  $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
+    unless exists($options->{payment_gateway});
+
+  $options->{payment_gateway};
+}
+
+sub _bop_auth {
+  my ($self, $options) = @_;
+
+  (
+    'login'    => $options->{payment_gateway}->gateway_username,
+    'password' => $options->{payment_gateway}->gateway_password,
   );
+}
 
-  ###
-  # check for banned credit card/ACH
-  ###
+sub _bop_options {
+  my ($self, $options) = @_;
 
-  my $ban = qsearchs('banned_pay', {
-    'payby'   => $method2payby{$method},
-    'payinfo' => md5_base64($payinfo),
-  } );
-  return "Banned credit card" if $ban;
+  $options->{payment_gateway}->gatewaynum
+    ? $options->{payment_gateway}->options
+    : @{ $options->{payment_gateway}->get('options') };
+}
 
-  ###
-  # select a gateway
-  ###
+sub _bop_defaults {
+  my ($self, $options) = @_;
 
-  my $taxclass = '';
-  if ( $options{'invnum'} ) {
-    my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
-    die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
-    my @taxclasses =
-      map  { $_->part_pkg->taxclass }
-      grep { $_ }
-      map  { $_->cust_pkg }
-      $cust_bill->cust_bill_pkg;
-    unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
-                                                           #different taxclasses
-      $taxclass = $taxclasses[0];
-    }
-  }
+  $options->{description} ||= 'Internet services';
+  $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
+  $options->{invnum} ||= '';
+  $options->{payname} = $self->payname unless exists( $options->{payname} );
+}
+
+sub _bop_content {
+  my ($self, $options) = @_;
+  my %content = ();
+
+  $content{address} = exists($options->{'address1'})
+                        ? $options->{'address1'}
+                        : $self->address1;
+  my $address2 = exists($options->{'address2'})
+                   ? $options->{'address2'}
+                   : $self->address2;
+  $content{address} .= ", ". $address2 if length($address2);
+
+  my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
+  $content{customer_ip} = $payip if length($payip);
 
-  #look for an agent gateway override first
-  my $cardtype;
-  if ( $method eq 'CC' ) {
-    $cardtype = cardtype($payinfo);
-  } elsif ( $method eq 'ECHECK' ) {
-    $cardtype = 'ACH';
+  $content{invoice_number} = $options->{'invnum'}
+    if exists($options->{'invnum'}) && length($options->{'invnum'});
+
+  $content{email_customer} = 
+    (    $conf->exists('business-onlinepayment-email_customer')
+      || $conf->exists('business-onlinepayment-email-override') );
+      
+  $content{payfirst} = $self->getfield('first');
+  $content{paylast} = $self->getfield('last');
+
+  $content{account_name} = "$content{payfirst} $content{paylast}"
+    if $options->{method} eq 'ECHECK';
+
+  $content{name} = $options->{payname};
+  $content{name} = $content{account_name} if exists($content{account_name});
+
+  $content{city} = exists($options->{city})
+                     ? $options->{city}
+                     : $self->city;
+  $content{state} = exists($options->{state})
+                      ? $options->{state}
+                      : $self->state;
+  $content{zip} = exists($options->{zip})
+                    ? $options->{'zip'}
+                    : $self->zip;
+  $content{country} = exists($options->{country})
+                        ? $options->{country}
+                        : $self->country;
+  $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
+  $content{phone} = $self->daytime || $self->night;
+
+  (%content);
+}
+
+my %bop_method2payby = (
+  'CC'     => 'CARD',
+  'ECHECK' => 'CHEK',
+  'LEC'    => 'LECB',
+);
+
+sub realtime_bop {
+  my $self = shift;
+
+  my %options = ();
+  if (ref($_[0]) eq 'HASH') {
+    %options = %{$_[0]};
   } else {
-    $cardtype = $method;
+    my ( $method, $amount ) = ( shift, shift );
+    %options = @_;
+    $options{method} = $method;
+    $options{amount} = $amount;
+  }
+  
+  if ( $DEBUG ) {
+    warn "$me realtime_bop: $options{method} $options{amount}\n";
+    warn "  $_ => $options{$_}\n" foreach keys %options;
   }
 
-  my $override =
-       qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
-                                           cardtype => $cardtype,
-                                           taxclass => $taxclass,       } )
-    || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
-                                           cardtype => '',
-                                           taxclass => $taxclass,       } )
-    || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
-                                           cardtype => $cardtype,
-                                           taxclass => '',              } )
-    || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
-                                           cardtype => '',
-                                           taxclass => '',              } );
+  return $self->fake_bop(%options) if $options{'fake'};
 
-  my $payment_gateway = '';
-  my( $processor, $login, $password, $action, @bop_options );
-  if ( $override ) { #use a payment gateway override
+  $self->_bop_defaults(\%options);
 
-    $payment_gateway = $override->payment_gateway;
+  ###
+  # select a gateway
+  ###
 
-    $processor   = $payment_gateway->gateway_module;
-    $login       = $payment_gateway->gateway_username;
-    $password    = $payment_gateway->gateway_password;
-    $action      = $payment_gateway->gateway_action;
-    @bop_options = $payment_gateway->options;
+  my $payment_gateway =  $self->_payment_gateway( \%options );
+  my $namespace = $payment_gateway->gateway_namespace;
 
-  } else { #use the standard settings from the config
+  eval "use $namespace";  
+  die $@ if $@;
 
-    ( $processor, $login, $password, $action, @bop_options ) =
-      $self->default_payment_gateway($method);
+  ###
+  # check for banned credit card/ACH
+  ###
 
-  }
+  my $ban = qsearchs('banned_pay', {
+    'payby'   => $bop_method2payby{$options{method}},
+    'payinfo' => md5_base64($options{payinfo}),
+  } );
+  return "Banned credit card" if $ban;
 
   ###
   # massage data
   ###
 
-  my $address = exists($options{'address1'})
-                    ? $options{'address1'}
-                    : $self->address1;
-  my $address2 = exists($options{'address2'})
-                    ? $options{'address2'}
-                    : $self->address2;
-  $address .= ", ". $address2 if length($address2);
-
-  my $o_payname = exists($options{'payname'})
-                    ? $options{'payname'}
-                    : $self->payname;
-  my($payname, $payfirst, $paylast);
-  if ( $o_payname && $method ne 'ECHECK' ) {
-    ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
-      or return "Illegal payname $payname";
-    ($payfirst, $paylast) = ($1, $2);
-  } else {
-    $payfirst = $self->getfield('first');
-    $paylast = $self->getfield('last');
-    $payname =  "$payfirst $paylast";
+  my (%bop_content) = $self->_bop_content(\%options);
+
+  if ( $options{method} ne 'ECHECK' ) {
+    $options{payname} =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
+      or return "Illegal payname $options{payname}";
+    ($bop_content{payfirst}, $bop_content{paylast}) = ($1, $2);
   }
 
   my @invoicing_list = $self->invoicing_list_emailonly;
@@ -3533,25 +3620,11 @@ sub realtime_bop {
               ? $conf->config('business-onlinepayment-email-override')
               : $invoicing_list[0];
 
-  my %content = ();
-
-  my $payip = exists($options{'payip'})
-                ? $options{'payip'}
-                : $self->payip;
-  $content{customer_ip} = $payip
-    if length($payip);
-
-  $content{invoice_number} = $options{'invnum'}
-    if exists($options{'invnum'}) && length($options{'invnum'});
-
-  $content{email_customer} = 
-    (    $conf->exists('business-onlinepayment-email_customer')
-      || $conf->exists('business-onlinepayment-email-override') );
-      
   my $paydate = '';
-  if ( $method eq 'CC' ) { 
+  my %content = ();
+  if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
 
-    $content{card_number} = $payinfo;
+    $content{card_number} = $options{payinfo};
     $paydate = exists($options{'paydate'})
                     ? $options{'paydate'}
                     : $self->paydate;
@@ -3583,25 +3656,24 @@ sub realtime_bop {
     $content{recurring_billing} = 'YES'
       if qsearch('cust_pay', { 'custnum' => $self->custnum,
                                'payby'   => 'CARD',
-                               'payinfo' => $payinfo,
+                               'payinfo' => $options{payinfo},
                              } )
       || qsearch('cust_pay', { 'custnum' => $self->custnum,
                                'payby'   => 'CARD',
-                               'paymask' => $self->mask_payinfo('CARD', $payinfo),
+                               'paymask' => $self->mask_payinfo('CARD', $options{payinfo}),
                              } );
 
 
-  } elsif ( $method eq 'ECHECK' ) {
+  } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
     ( $content{account_number}, $content{routing_code} ) =
-      split('@', $payinfo);
-    $content{bank_name} = $o_payname;
+      split('@', $options{payinfo});
+    $content{bank_name} = $options{payname};
     $content{bank_state} = exists($options{'paystate'})
                              ? $options{'paystate'}
                              : $self->getfield('paystate');
     $content{account_type} = exists($options{'paytype'})
                                ? uc($options{'paytype'}) || 'CHECKING'
                                : uc($self->getfield('paytype')) || 'CHECKING';
-    $content{account_name} = $payname;
     $content{customer_org} = $self->company ? 'B' : 'I';
     $content{state_id}       = exists($options{'stateid'})
                                  ? $options{'stateid'}
@@ -3612,8 +3684,12 @@ sub realtime_bop {
     $content{customer_ssn} = exists($options{'ss'})
                                ? $options{'ss'}
                                : $self->ss;
-  } elsif ( $method eq 'LEC' ) {
-    $content{phone} = $payinfo;
+  } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
+    $content{phone} = $options{payinfo};
+  } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
+    #move along
+  } else {
+    #die an evil death
   }
 
   ###
@@ -3630,9 +3706,9 @@ sub realtime_bop {
   #double-form-submission prevention is taken care of in cust_pay_pending::check
 
   #check the balance
-  return "The customer's balance has changed; $method transaction aborted."
+  return "The customer's balance has changed; $options{method} transaction aborted."
     if $self->balance < $balance;
-    #&& $self->balance < $amount; #might as well anyway?
+    #&& $self->balance < $options{amount}; #might as well anyway?
 
   #also check and make sure there aren't *other* pending payments for this cust
 
@@ -3642,7 +3718,7 @@ sub realtime_bop {
   });
   return "A payment is already being processed for this customer (".
          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
-         "); $method transaction aborted."
+         "); $options{method} transaction aborted."
     if scalar(@pending);
 
   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
@@ -3650,50 +3726,39 @@ sub realtime_bop {
   my $cust_pay_pending = new FS::cust_pay_pending {
     'custnum'    => $self->custnum,
     #'invnum'     => $options{'invnum'},
-    'paid'       => $amount,
+    'paid'       => $options{amount},
     '_date'      => '',
-    'payby'      => $method2payby{$method},
-    'payinfo'    => $payinfo,
+    'payby'      => $bop_method2payby{$options{method}},
+    'payinfo'    => $options{payinfo},
     'paydate'    => $paydate,
     'status'     => 'new',
-    'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
+    '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});
   my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
   return $cpp_new_err if $cpp_new_err;
 
-  my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
+  my( $action1, $action2 ) =
+    split( /\s*\,\s*/, $payment_gateway->gateway_action );
+
+  my $transaction = new $namespace( $payment_gateway->gateway_module,
+                                    $self->_bop_options(\%options),
+                                  );
 
-  my $transaction = new Business::OnlinePayment( $processor, @bop_options );
   $transaction->content(
-    'type'           => $method,
-    'login'          => $login,
-    'password'       => $password,
+    'type'           => $options{method},
+    $self->_bop_auth(\%options),          
     'action'         => $action1,
     'description'    => $options{'description'},
-    'amount'         => $amount,
+    'amount'         => $options{amount},
     #'invoice_number' => $options{'invnum'},
     'customer_id'    => $self->custnum,
-    'last_name'      => $paylast,
-    'first_name'     => $payfirst,
-    'name'           => $payname,
-    'address'        => $address,
-    'city'           => ( exists($options{'city'})
-                            ? $options{'city'}
-                            : $self->city          ),
-    'state'          => ( exists($options{'state'})
-                            ? $options{'state'}
-                            : $self->state          ),
-    'zip'            => ( exists($options{'zip'})
-                            ? $options{'zip'}
-                            : $self->zip          ),
-    'country'        => ( exists($options{'country'})
-                            ? $options{'country'}
-                            : $self->country          ),
-    'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
+    %bop_content,
+    'reference'      => $cust_pay_pending->paypendingnum, #for now
     'email'          => $email,
-    'phone'          => $self->daytime || $self->night,
     %content, #after
   );
 
@@ -3717,7 +3782,12 @@ sub realtime_bop {
     }
   }
 
-  if ( $transaction->is_success() && $action2 ) {
+  if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
+
+    return { reference => $cust_pay_pending->paypendingnum,
+             map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
+
+  } elsif ( $transaction->is_success() && $action2 ) {
 
     $cust_pay_pending->status('authorized');
     my $cpp_authorized_err = $cust_pay_pending->replace;
@@ -3729,16 +3799,17 @@ sub realtime_bop {
                    : '';
 
     my $capture =
-      new Business::OnlinePayment( $processor, @bop_options );
+      new Business::OnlinePayment( $payment_gateway->gateway_module,
+                                   $self->_bop_options(\%options),
+                                 );
 
     my %capture = (
       %content,
-      type           => $method,
+      type           => $options{method},
       action         => $action2,
-      login          => $login,
-      password       => $password,
+      $self->_bop_auth(\%options),          
       order_number   => $ordernum,
-      amount         => $amount,
+      amount         => $options{amount},
       authorization  => $auth,
       description    => $options{'description'},
     );
@@ -3764,10 +3835,6 @@ sub realtime_bop {
 
   }
 
-  $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
-  my $cpp_captured_err = $cust_pay_pending->replace;
-  return $cpp_captured_err if $cpp_captured_err;
-
   ###
   # remove paycvv after initial transaction
   ###
@@ -3776,7 +3843,7 @@ sub realtime_bop {
   # correctly
   if ( defined $self->dbdef_table->column('paycvv')
        && length($self->paycvv)
-       && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
+       && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
   ) {
     my $error = $self->remove_cvv;
     if ( $error ) {
@@ -3788,14 +3855,114 @@ sub realtime_bop {
   # result handling
   ###
 
+  $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
+
+}
+
+=item fake_bop
+
+=cut
+
+sub fake_bop {
+  my $self = shift;
+
+  my %options = ();
+  if (ref($_[0]) eq 'HASH') {
+    %options = %{$_[0]};
+  } else {
+    my ( $method, $amount ) = ( shift, shift );
+    %options = @_;
+    $options{method} = $method;
+    $options{amount} = $amount;
+  }
+  
+  if ( $options{'fake_failure'} ) {
+     return "Error: No error; test failure requested with fake_failure";
+  }
+
+  #my $paybatch = '';
+  #if ( $payment_gateway->gatewaynum ) { # agent override
+  #  $paybatch = $payment_gateway->gatewaynum. '-';
+  #}
+  #
+  #$paybatch .= "$processor:". $transaction->authorization;
+  #
+  #$paybatch .= ':'. $transaction->order_number
+  #  if $transaction->can('order_number')
+  #  && length($transaction->order_number);
+
+  my $paybatch = 'FakeProcessor:54:32';
+
+  my $cust_pay = new FS::cust_pay ( {
+     'custnum'  => $self->custnum,
+     'invnum'   => $options{'invnum'},
+     'paid'     => $options{amount},
+     '_date'    => '',
+     'payby'    => $bop_method2payby{$options{method}},
+     #'payinfo'  => $payinfo,
+     'payinfo'  => '4111111111111111',
+     'paybatch' => $paybatch,
+     #'paydate'  => $paydate,
+     'paydate'  => '2012-05-01',
+  } );
+  $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
+
+  my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
+
+  if ( $error ) {
+    $cust_pay->invnum(''); #try again with no specific invnum
+    my $error2 = $cust_pay->insert( $options{'manual'} ?
+                                    ( 'manual' => 1 ) : ()
+                                  );
+    if ( $error2 ) {
+      # gah, even with transactions.
+      my $e = 'WARNING: Card/ACH debited but database not updated - '.
+              "error inserting (fake!) payment: $error2".
+              " (previously tried insert with invnum #$options{'invnum'}" .
+              ": $error )";
+      warn $e;
+      return $e;
+    }
+  }
+
+  if ( $options{'paynum_ref'} ) {
+    ${ $options{'paynum_ref'} } = $cust_pay->paynum;
+  }
+
+  return ''; #no error
+
+}
+
+
+# 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.
+
+sub _realtime_bop_result {
+  my( $self, $cust_pay_pending, $transaction, %options ) = @_;
+  if ( $DEBUG ) {
+    warn "$me _realtime_bop_result: pending transaction ".
+      $cust_pay_pending->paypendingnum. "\n";
+    warn "  $_ => $options{$_}\n" foreach keys %options;
+  }
+
+  my $payment_gateway = $options{payment_gateway}
+    or return "no payment gateway in arguments to _realtime_bop_result";
+
+  $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
+  my $cpp_captured_err = $cust_pay_pending->replace;
+  return $cpp_captured_err if $cpp_captured_err;
+
   if ( $transaction->is_success() ) {
 
     my $paybatch = '';
-    if ( $payment_gateway ) { # agent override
+    if ( $payment_gateway->gatewaynum ) { # agent override
       $paybatch = $payment_gateway->gatewaynum. '-';
     }
 
-    $paybatch .= "$processor:". $transaction->authorization;
+    $paybatch .= $payment_gateway->gateway_module. ":".
+      $transaction->authorization;
 
     $paybatch .= ':'. $transaction->order_number
       if $transaction->can('order_number')
@@ -3804,12 +3971,12 @@ sub realtime_bop {
     my $cust_pay = new FS::cust_pay ( {
        'custnum'  => $self->custnum,
        'invnum'   => $options{'invnum'},
-       'paid'     => $amount,
+       'paid'     => $cust_pay_pending->paid,
        '_date'    => '',
-       'payby'    => $method2payby{$method},
-       'payinfo'  => $payinfo,
+       'payby'    => $cust_pay_pending->payby,
+       #'payinfo'  => $payinfo,
        'paybatch' => $paybatch,
-       'paydate'  => $paydate,
+       'paydate'  => $cust_pay_pending->paydate,
     } );
     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
     $cust_pay->payunique( $options{payunique} )
@@ -3831,8 +3998,9 @@ sub realtime_bop {
       if ( $error2 ) {
         # gah.  but at least we have a record of the state we had to abort in
         # from cust_pay_pending now.
-        my $e = "WARNING: $method captured but payment not recorded - ".
-                "error inserting payment ($processor): $error2".
+        my $e = "WARNING: $options{method} captured but payment not recorded -".
+                " error inserting payment (". $payment_gateway->gateway_module.
+                "): $error2".
                 " (previously tried insert with invnum #$options{'invnum'}" .
                 ": $error ) - pending payment saved as paypendingnum ".
                 $cust_pay_pending->paypendingnum. "\n";
@@ -3841,6 +4009,31 @@ sub realtime_bop {
       }
     }
 
+    my $jobnum = $cust_pay_pending->jobnum;
+    if ( $jobnum ) {
+       my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
+      
+       unless ( $placeholder ) {
+         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+         my $e = "WARNING: $options{method} captured but job $jobnum not ".
+             "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
+         warn $e;
+         return $e;
+       }
+
+       $error = $placeholder->delete;
+
+       if ( $error ) {
+         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+         my $e = "WARNING: $options{method} captured but could not delete ".
+              "job $jobnum for paypendingnum ".
+              $cust_pay_pending->paypendingnum. ": $error\n";
+         warn $e;
+         return $e;
+       }
+
+    }
+    
     if ( $options{'paynum_ref'} ) {
       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
     }
@@ -3853,7 +4046,7 @@ sub realtime_bop {
     if ( $cpp_done_err ) {
 
       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
-      my $e = "WARNING: $method captured but payment not recorded - ".
+      my $e = "WARNING: $options{method} captured but payment not recorded - ".
               "error updating status for paypendingnum ".
               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
       warn $e;
@@ -3868,8 +4061,26 @@ sub realtime_bop {
 
   } else {
 
-    my $perror = "$processor error: ". $transaction->error_message;
+    my $perror = $payment_gateway->gateway_module. " error: ".
+      $transaction->error_message;
+
+    my $jobnum = $cust_pay_pending->jobnum;
+    if ( $jobnum ) {
+       my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
+      
+       if ( $placeholder ) {
+         my $error = $placeholder->depended_delete;
+         $error ||= $placeholder->delete;
+         warn "error removing provisioning jobs after declined paypendingnum ".
+           $cust_pay_pending->paypendingnum. "\n";
+       } else {
+         my $e = "error finding job $jobnum for declined paypendingnum ".
+              $cust_pay_pending->paypendingnum. "\n";
+         warn $e;
+       }
 
+    }
+    
     unless ( $transaction->error_message ) {
 
       my $t_response;
@@ -3890,10 +4101,12 @@ sub realtime_bop {
                       };
       } else {
         $t_response .=
-          "No additional debugging information available for $processor";
+          "No additional debugging information available for ".
+            $payment_gateway->gateway_module;
       }
 
-      $perror .= "No error_message returned from $processor -- ".
+      $perror .= "No error_message returned from ".
+                   $payment_gateway->gateway_module. " -- ".
                  ( ref($t_response) ? Dumper($t_response) : $t_response );
 
     }
@@ -3930,8 +4143,8 @@ sub realtime_bop {
     $cust_pay_pending->statustext("declined: $perror");
     my $cpp_done_err = $cust_pay_pending->replace;
     if ( $cpp_done_err ) {
-      my $e = "WARNING: $method declined but pending payment not resolved - ".
-              "error updating status for paypendingnum ".
+      my $e = "WARNING: $options{method} declined but pending payment not ".
+              "resolved - error updating status for paypendingnum ".
               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
       warn $e;
       $perror = "$e ($perror)";
@@ -3942,77 +4155,126 @@ sub realtime_bop {
 
 }
 
-=item fake_bop
+=item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
 
-=cut
+Verifies successful third party processing of a realtime credit card,
+ACH (electronic check) or phone bill transaction via a
+Business::OnlineThirdPartyPayment realtime gateway.  See
+L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
 
-sub fake_bop {
-  my( $self, $method, $amount, %options ) = @_;
+Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
 
-  if ( $options{'fake_failure'} ) {
-     return "Error: No error; test failure requested with fake_failure";
-  }
+The additional options I<payname>, I<city>, I<state>,
+I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
+if set, will override the value from the customer record.
 
-  my %method2payby = (
-    'CC'     => 'CARD',
-    'ECHECK' => 'CHEK',
-    'LEC'    => 'LECB',
-  );
+I<description> is a free-text field passed to the gateway.  It defaults to
+"Internet services".
 
-  #my $paybatch = '';
-  #if ( $payment_gateway ) { # agent override
-  #  $paybatch = $payment_gateway->gatewaynum. '-';
-  #}
-  #
-  #$paybatch .= "$processor:". $transaction->authorization;
-  #
-  #$paybatch .= ':'. $transaction->order_number
-  #  if $transaction->can('order_number')
-  #  && length($transaction->order_number);
+If an I<invnum> is specified, this payment (if successful) is applied to the
+specified invoice.  If you don't specify an I<invnum> you might want to
+call the B<apply_payments> method.
 
-  my $paybatch = 'FakeProcessor:54:32';
+I<quiet> can be set true to surpress email decline notices.
 
-  my $cust_pay = new FS::cust_pay ( {
-     'custnum'  => $self->custnum,
-     'invnum'   => $options{'invnum'},
-     'paid'     => $amount,
-     '_date'    => '',
-     'payby'    => $method2payby{$method},
-     #'payinfo'  => $payinfo,
-     'payinfo'  => '4111111111111111',
-     'paybatch' => $paybatch,
-     #'paydate'  => $paydate,
-     'paydate'  => '2012-05-01',
-  } );
-  $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
+I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
+resulting paynum, if any.
 
-  my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
+I<payunique> is a unique identifier for this payment.
 
-  if ( $error ) {
-    $cust_pay->invnum(''); #try again with no specific invnum
-    my $error2 = $cust_pay->insert( $options{'manual'} ?
-                                    ( 'manual' => 1 ) : ()
-                                  );
-    if ( $error2 ) {
-      # gah, even with transactions.
-      my $e = 'WARNING: Card/ACH debited but database not updated - '.
-              "error inserting (fake!) payment: $error2".
-              " (previously tried insert with invnum #$options{'invnum'}" .
-              ": $error )";
-      warn $e;
-      return $e;
-    }
+Returns a hashref containing elements bill_error (which will be undefined
+upon success) and session_id of any associated session.
+
+=cut
+
+sub realtime_botpp_capture {
+  my( $self, $cust_pay_pending, %options ) = @_;
+  if ( $DEBUG ) {
+    warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
+    warn "  $_ => $options{$_}\n" foreach keys %options;
   }
 
-  if ( $options{'paynum_ref'} ) {
-    ${ $options{'paynum_ref'} } = $cust_pay->paynum;
+  eval "use Business::OnlineThirdPartyPayment";  
+  die $@ if $@;
+
+  ###
+  # select the gateway
+  ###
+
+  my $method = FS::payby->payby2bop($cust_pay_pending->payby);
+
+  my $payment_gateway = $cust_pay_pending->gatewaynum
+    ? qsearchs( 'payment_gateway',
+                { gatewaynum => $cust_pay_pending->gatewaynum }
+              )
+    : $self->agent->payment_gateway( 'method' => $method,
+                                     # 'invnum'  => $cust_pay_pending->invnum,
+                                     # 'payinfo' => $cust_pay_pending->payinfo,
+                                   );
+
+  $options{payment_gateway} = $payment_gateway; # for the helper subs
+
+  ###
+  # massage data
+  ###
+
+  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;
   }
 
-  return ''; #no error
+  my $email = ($conf->exists('business-onlinepayment-email-override'))
+              ? $conf->config('business-onlinepayment-email-override')
+              : $invoicing_list[0];
+
+  my %content = ();
+
+  $content{email_customer} = 
+    (    $conf->exists('business-onlinepayment-email_customer')
+      || $conf->exists('business-onlinepayment-email-override') );
+      
+  ###
+  # run transaction(s)
+  ###
+
+  my $transaction =
+    new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
+                                           $self->_bop_options(\%options),
+                                         );
+
+  $transaction->reference({ %options }); 
+
+  $transaction->content(
+    'type'           => $method,
+    $self->_bop_auth(\%options),
+    'action'         => 'Post Authorization',
+    'description'    => $options{'description'},
+    'amount'         => $cust_pay_pending->paid,
+    #'invoice_number' => $options{'invnum'},
+    'customer_id'    => $self->custnum,
+    'referer'        => 'http://cleanwhisker.420.am/',
+    'reference'      => $cust_pay_pending->paypendingnum,
+    'email'          => $email,
+    'phone'          => $self->daytime || $self->night,
+    %content, #after
+    # plus whatever is required for bogus capture avoidance
+  );
+
+  $transaction->submit();
+
+  my $error =
+    $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
+
+  {
+    bill_error => $error,
+    session_id => $cust_pay_pending->session_id,
+  }
 
 }
 
-=item default_payment_gateway
+=item default_payment_gateway DEPRECATED -- use agent->payment_gateway
 
 =cut
 
@@ -4022,6 +4284,8 @@ sub default_payment_gateway {
   die "Real-time processing not enabled\n"
     unless $conf->exists('business-onlinepayment');
 
+  warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
+
   #load up config
   my $bop_config = 'business-onlinepayment';
   $bop_config .= '-ach'
@@ -4094,15 +4358,22 @@ gateway is attempted.
 #some false laziness w/realtime_bop, not enough to make it worth merging
 #but some useful small subs should be pulled out
 sub realtime_refund_bop {
-  my( $self, $method, %options ) = @_;
+  my $self = shift;
+
+  my %options = ();
+  if (ref($_[0]) ne 'HASH') {
+    %options = %{$_[0]};
+  } else {
+    my $method = shift;
+    %options = @_;
+    $options{method} = $method;
+  }
+
   if ( $DEBUG ) {
-    warn "$me realtime_refund_bop: $method refund\n";
+    warn "$me realtime_refund_bop: $options{method} refund\n";
     warn "  $_ => $options{$_}\n" foreach keys %options;
   }
 
-  eval "use Business::OnlinePayment";  
-  die $@ if $@;
-
   ###
   # look up the original payment and optionally a gateway for that payment
   ###
@@ -4110,7 +4381,7 @@ sub realtime_refund_bop {
   my $cust_pay = '';
   my $amount = $options{'amount'};
 
-  my( $processor, $login, $password, @bop_options ) ;
+  my( $processor, $login, $password, @bop_options, $namespace ) ;
   my( $auth, $order_number ) = ( '', '', '' );
 
   if ( $options{'paynum'} ) {
@@ -4136,13 +4407,22 @@ sub realtime_refund_bop {
       $processor   = $payment_gateway->gateway_module;
       $login       = $payment_gateway->gateway_username;
       $password    = $payment_gateway->gateway_password;
+      $namespace   = $payment_gateway->gateway_namespace;
       @bop_options = $payment_gateway->options;
 
     } else { #try the default gateway
 
-      my( $conf_processor, $unused_action );
-      ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
-        $self->default_payment_gateway($method);
+      my $conf_processor;
+      my $payment_gateway =
+        $self->agent->payment_gateway('method' => $options{method});
+
+      ( $conf_processor, $login, $password, $namespace ) =
+        map { my $method = "gateway_$_"; $payment_gateway->$method }
+          qw( module username password namespace );
+
+      @bop_options = $payment_gateway->gatewaynum
+                       ? $payment_gateway->options
+                       : @{ $payment_gateway->get('options') };
 
       return "processor of payment $options{'paynum'} $processor does not".
              " match default processor $conf_processor"
@@ -4153,46 +4433,27 @@ sub realtime_refund_bop {
 
   } else { # didn't specify a paynum, so look for agent gateway overrides
            # like a normal transaction 
-
-    my $cardtype;
-    if ( $method eq 'CC' ) {
-      $cardtype = cardtype($self->payinfo);
-    } elsif ( $method eq 'ECHECK' ) {
-      $cardtype = 'ACH';
-    } else {
-      $cardtype = $method;
-    }
-    my $override =
-           qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
-                                               cardtype => $cardtype,
-                                               taxclass => '',              } )
-        || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
-                                               cardtype => '',
-                                               taxclass => '',              } );
-
-    if ( $override ) { #use a payment gateway override
  
-      my $payment_gateway = $override->payment_gateway;
-
-      $processor   = $payment_gateway->gateway_module;
-      $login       = $payment_gateway->gateway_username;
-      $password    = $payment_gateway->gateway_password;
-      #$action      = $payment_gateway->gateway_action;
-      @bop_options = $payment_gateway->options;
+    my $payment_gateway =
+      $self->agent->payment_gateway( 'method'  => $options{method},
+                                     #'payinfo' => $payinfo,
+                                   );
+    my( $processor, $login, $password, $namespace ) =
+      map { my $method = "gateway_$_"; $payment_gateway->$method }
+        qw( module username password namespace );
 
-    } else { #use the standard settings from the config
-
-      my $unused_action;
-      ( $processor, $login, $password, $unused_action, @bop_options ) =
-        $self->default_payment_gateway($method);
-
-    }
+    my @bop_options = $payment_gateway->gatewaynum
+                        ? $payment_gateway->options
+                        : @{ $payment_gateway->get('options') };
 
   }
   return "neither amount nor paynum specified" unless $amount;
 
+  eval "use $namespace";  
+  die $@ if $@;
+
   my %content = (
-    'type'           => $method,
+    'type'           => $options{method},
     'login'          => $login,
     'password'       => $password,
     'order_number'   => $order_number,
@@ -4242,7 +4503,7 @@ sub realtime_refund_bop {
   $address .= ", ". $self->address2 if $self->address2;
 
   my($payname, $payfirst, $paylast);
-  if ( $self->payname && $method ne 'ECHECK' ) {
+  if ( $self->payname && $options{method} ne 'ECHECK' ) {
     $payname = $self->payname;
     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
       or return "Illegal payname $payname";
@@ -4271,7 +4532,7 @@ sub realtime_refund_bop {
     if length($payip);
 
   my $payinfo = '';
-  if ( $method eq 'CC' ) {
+  if ( $options{method} eq 'CC' ) {
 
     if ( $cust_pay ) {
       $content{card_number} = $payinfo = $cust_pay->payinfo;
@@ -4285,7 +4546,7 @@ sub realtime_refund_bop {
       $content{expiration} = "$2/$1";
     }
 
-  } elsif ( $method eq 'ECHECK' ) {
+  } elsif ( $options{method} eq 'ECHECK' ) {
 
     if ( $cust_pay ) {
       $payinfo = $cust_pay->payinfo;
@@ -4298,7 +4559,7 @@ sub realtime_refund_bop {
     $content{account_name} = $payname;
     $content{customer_org} = $self->company ? 'B' : 'I';
     $content{customer_ssn} = $self->ss;
-  } elsif ( $method eq 'LEC' ) {
+  } elsif ( $options{method} eq 'LEC' ) {
     $content{phone} = $payinfo = $self->payinfo;
   }
 
@@ -4326,12 +4587,6 @@ sub realtime_refund_bop {
   return "$processor error: ". $refund->error_message
     unless $refund->is_success();
 
-  my %method2payby = (
-    'CC'     => 'CARD',
-    'ECHECK' => 'CHEK',
-    'LEC'    => 'LECB',
-  );
-
   my $paybatch = "$processor:". $refund->authorization;
   $paybatch .= ':'. $refund->order_number
     if $refund->can('order_number') && $refund->order_number;
@@ -4349,7 +4604,7 @@ sub realtime_refund_bop {
     'paynum'   => $options{'paynum'},
     'refund'   => $amount,
     '_date'    => '',
-    'payby'    => $method2payby{$method},
+    'payby'    => $bop_method2payby{$options{method}},
     'payinfo'  => $payinfo,
     'paybatch' => $paybatch,
     'reason'   => $options{'reason'} || 'card or ACH refund',
index bbabd24..fba19ea 100644 (file)
@@ -191,6 +191,7 @@ sub check {
     #|| $self->ut_textn('statustext')
     || $self->ut_anything('statustext')
     #|| $self->ut_money('cust_balance')
+    || $self->ut_hexn('session_id')
     || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
     || $self->payinfo_check() #payby/payinfo/paymask/paydate
   ;
@@ -215,6 +216,18 @@ sub check {
   $self->SUPER::check;
 }
 
+=item cust_main
+
+Returns the associated L<FS::cust_main> record if any.  Otherwise returns false.
+
+=cut
+
+sub cust_main {
+  my $self = shift;
+  qsearchs('cust_main', { custnum => $self->custnum } );
+}
+
+
 #these two are kind-of false laziness w/cust_main::realtime_bop
 #(currently only used when resolving pending payments manually)
 
index dd6db1b..7c8656c 100644 (file)
@@ -439,9 +439,7 @@ replace methods.
 sub check {
   my $self = shift;
 
-  $self->locationnum('')
-    if defined($self->locationnum) && length($self->locationnum)
-    && ( $self->locationnum == 0 || $self->locationnum == -1 );
+  $self->locationnum('') if !$self->locationnum || $self->locationnum == -1;
 
   my $error = 
     $self->ut_numbern('pkgnum')
index b54e5d9..30a03dd 100644 (file)
@@ -48,28 +48,33 @@ tie %hash, 'Tie::IxHash',
     tinyname  => 'card',
     shortname => 'Credit card',
     longname  => 'Credit card (automatic)',
+    realtime  => 1,
   },
   'DCRD' => {
     tinyname  => 'card',
     shortname => 'Credit card',
     longname  => 'Credit card (on-demand)',
     cust_pay  => 'CARD', #this is a customer type only, payments are CARD...
+    realtime  => 1,
   },
   'CHEK' => {
     tinyname  => 'check',
     shortname => 'Electronic check',
     longname  => 'Electronic check (automatic)',
+    realtime  => 1,
   },
   'DCHK' => {
     tinyname  => 'check',
     shortname => 'Electronic check',
     longname  => 'Electronic check (on-demand)',
     cust_pay  => 'CHEK', #this is a customer type only, payments are CHEK...
+    realtime  => 1,
   },
   'LECB' => {
     tinyname  => 'phone bill',
     shortname => 'Phone bill billing',
     longname  => 'Phone bill billing',
+    realtime  => 1,
   },
   'BILL' => {
     tinyname  => 'billing',
@@ -131,6 +136,15 @@ sub can_payby {
   return 1;
 }
 
+sub realtime {  # can use realtime payment facilities
+  my( $self, $payby ) = @_;
+
+  return 0 unless $hash{$payby};
+  return 0 unless exists( $hash{$payby}->{realtime} );
+
+  return $hash{$payby}->{realtime};
+}
+
 sub payby2longname {
   my $self = shift;
   map { $_ => $hash{$_}->{longname} } $self->payby;
@@ -157,6 +171,7 @@ sub longname {
 %payby2bop = (
   'CARD' => 'CC',
   'CHEK' => 'ECHECK',
+  'MCRD' => 'CC',
 );
 
 sub payby2bop {
index 35b4f08..bc8b875 100644 (file)
@@ -1,12 +1,14 @@
 package FS::payment_gateway;
 
 use strict;
-use vars qw( @ISA );
+use vars qw( @ISA $me $DEBUG );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::option_Common;
 use FS::agent_payment_gateway;
 
 @ISA = qw( FS::option_Common );
+$me = '[ FS::payment_gateway ]';
+$DEBUG=0;
 
 =head1 NAME
 
@@ -37,6 +39,8 @@ currently supported:
 
 =item gatewaynum - primary key
 
+=item gateway_namespace - Business::OnlinePayment or Business::OnlineThirdPartyPayment
+
 =item gateway_module - Business::OnlinePayment:: module name
 
 =item gateway_username - payment gateway username
@@ -110,8 +114,12 @@ sub check {
   my $error = 
     $self->ut_numbern('gatewaynum')
     || $self->ut_alpha('gateway_module')
+    || $self->ut_enum('gateway_namespace', ['Business::OnlinePayment',
+                                            'Business::OnlineThirdPartyPayment',
+                                           ] )
     || $self->ut_textn('gateway_username')
     || $self->ut_anything('gateway_password')
+    || $self->ut_textn('gateway_callback_url')  # a bit too permissive
     || $self->ut_enum('disabled', [ '', 'Y' ] )
     #|| $self->ut_textn('gateway_action')
   ;
@@ -131,6 +139,10 @@ sub check {
     $self->gateway_action('Normal Authorization');
   }
 
+  # this little kludge mimics FS::CGI::popurl
+  $self->gateway_callback_url($self->gateway_callback_url. '/')
+    if ( $self->gateway_callback_url && $self->gateway_callback_url !~ /\/$/ );
+
   $self->SUPER::check;
 }
 
@@ -186,6 +198,41 @@ sub disable {
 
 }
 
+=item namespace_description
+
+returns a friendly name for the namespace
+
+=cut
+
+my %namespace2description = (
+  '' => 'Direct',
+  'Business::OnlinePayment' => 'Direct',
+  'Business::OnlineThirdPartyPayment' => 'Hosted',
+);
+
+sub namespace_description {
+  $namespace2description{shift->gateway_namespace} || 'Unknown';
+}
+
+# _upgrade_data
+#
+# Used by FS::Upgrade to migrate to a new database.
+#
+#
+
+sub _upgrade_data {
+  my ($class, %opts) = @_;
+  my $dbh = dbh;
+
+  warn "$me upgrading $class\n" if $DEBUG;
+
+  foreach ( qsearch( 'payment_gateway', { 'gateway_namespace' => '' } ) ) {
+    $_->gateway_namespace('Business::OnlinePayment');  #defaulting
+    my $error = $_->replace;
+    die "$class had error during upgrade replacement: $error" if $error;
+  }
+}
+
 =back
 
 =head1 BUGS
index 580ca73..3ede27c 100644 (file)
@@ -39,6 +39,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'process_payment_order_pkg' => 'MyAccount/process_payment_order_pkg',
   'process_payment_order_renew' => 'MyAccount/process_payment_order_renew',
   'process_prepay'            => 'MyAccount/process_prepay',
+  'realtime_collect'          => 'MyAccount/realtime_collect',
   'list_pkgs'                 => 'MyAccount/list_pkgs',     #add to ss (added?)
   'list_svcs'                 => 'MyAccount/list_svcs',     #add to ss (added?)
   'list_svc_usage'            => 'MyAccount/list_svc_usage',   
@@ -58,6 +59,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'signup_info'               => 'Signup/signup_info',
   'domain_select_hash'        => 'Signup/domain_select_hash',  # expose?
   'new_customer'              => 'Signup/new_customer',
+  'capture_payment'           => 'Signup/capture_payment',
   'agent_login'               => 'Agent/agent_login',
   'agent_logout'              => 'Agent/agent_logout',
   'agent_info'                => 'Agent/agent_info',
index 2bea955..7283cb8 100644 (file)
                       'LECB'   => qq/Phone Bill Billing/,
                       'BILL'   => qq/Billing/,
                       'COMP'   => qq/Complimentary/,
+                      'PREP'   => qq/Prepaid Card/,
                       'PREPAY' => qq/Prepaid Card/,
                     );
   tie my %options, 'Tie::IxHash', ();
-  foreach my $payby_option ( @paybys ) {
+  foreach my $payby_option ( grep { exists( $payby_index{$_} ) } @paybys ) {
     $options{$payby_option} = $payby_index{$payby_option};
   }
   $options{$payby} = $payby_index{$payby}
diff --git a/fs_selfservice/FS-SelfService/cgi/make_thirdparty_payment.html b/fs_selfservice/FS-SelfService/cgi/make_thirdparty_payment.html
new file mode 100755 (executable)
index 0000000..042b8b3
--- /dev/null
@@ -0,0 +1,38 @@
+<HTML><HEAD><TITLE>My Account</TITLE></HEAD>
+<BODY BGCOLOR="#eeeeee"><FONT SIZE=5>MyAccount</FONT><BR><BR>
+<SCRIPT TYPE="text/javascript">
+  function popcollect() {
+    overlib( OLiframeContent('<%= $popup_url %>', 336, 550, 'Secure Payment Area', 0, 'auto' ), CAPTION, 'Pay now', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK, BGCOLOR, '#333399', CGCOLOR, '#333399', CLOSETEXT, 'Close' );
+    return false;
+  }
+</SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="overlibmws.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="overlibmws_iframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="overlibmws_draggable.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="overlibmws_crossframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="iframecontentmws.js"></SCRIPT>
+<%= $url = "$selfurl?session=$session_id;action="; ''; %>
+<%= include('myaccount_menu') %>
+<TD VALIGN="top">
+<FONT SIZE=4>Pay now</FONT><BR><BR>
+
+<%= if ( $error ) {
+  $OUT .= qq!<FONT SIZE="+1" COLOR="#ff0000">$error</FONT><BR><BR>!;
+}else{
+  $OUT .= <<EOF;
+    You are about to contact our payment processor to pay $amount.<BR><BR>
+    Your transaction reference number is $reference <BR><BR>
+    <FORM NAME="collect_popper" method="post" action="javascript:void(0)" onSubmit="popcollect()">
+EOF
+
+  my %itemhash = @collectitems;
+  foreach my $input (keys %itemhash) {
+    $OUT .= qq!<INPUT NAME="$input" TYPE="hidden" VALUE="$itemhash{$input}">!;
+  }
+
+  $OUT .= qq!<INPUT NAME="submit" type="submit" value="Pay now">!;
+  $OUT .= qq!</FORM>!;
+}
+%>
+</TD></TR></TABLE>
+</BODY></HTML>
index cb5ed35..c9ca0c5 100644 (file)
@@ -8,7 +8,11 @@ Hello <%= $name %>!<BR><BR>
 <%= $small_custview %>
 <BR>
 <%= if ( $balance > 0 ) {
-  $OUT .= qq! <B><A HREF="${url}make_payment">Make a payment</A></B><BR><BR>!;
+  if (scalar(grep $_, @hide_payment_field)) {
+    $OUT .= qq! <B><A HREF="${url}make_payment">Make a payment</A></B><BR><BR>!;
+  } else {
+    $OUT .= qq! <B><A HREF="${url}make_thirdparty_payment&payby_method=CC">Make a payment</A></B><BR><BR>!;
+  }
 } %>
 <%=
   if ( @open_invoices ) {
index ec5a8fa..cc9f255 100644 (file)
@@ -18,14 +18,34 @@ my @menu = (
 
 if ( 1 ) { #XXXFIXME "enable selfservice prepay features" flag or something, eventually per-pkg or something really fancy
 
-  push @menu, (
-    { title=>'Recharge my account with a credit card',
-      url=>'make_payment', indent=>2 },
-    { title=>'Recharge my account with a check',
-      url=>'make_ach_payment', indent=>2 },
-    { title=>'Recharge my account with a prepaid card',
-      url=>'recharge_prepay', indent=>2 },
-  );
+  #XXXFIXME still a bit sloppy for multi-gateway of differing namespace
+  my $i = 0;
+  while($i < scalar(@cust_paybys)) { last if $cust_paybys[$i] =~ /^CARD/; $i++ }
+  if ( $cust_paybys[$i] =~ /^CARD/ ) {
+    push @menu, { title  => 'Recharge my account with a credit card',
+                  url    => $hide_payment_fields[$i]
+                              ? 'make_thirdparty_payment&payby_method=CC'
+                              : 'make_payment',
+                  indent => 2,
+                 }
+  }
+
+  $i = 0;
+  while($i < scalar(@cust_paybys)) { last if $cust_paybys[$i] =~ /^CHEK/; $i++ }
+  if ( $cust_paybys[$i] =~ /^CHEK/ ) {
+    push @menu, { title  => 'Recharge my account with a check',
+                  url    => $hide_payment_field[$i]
+                              ? 'make_thirdparty_payment&payby_method=ECHECK'
+                              : 'make_ach_payment',
+                  indent => 2,
+                }
+  }
+
+  push @menu, { title  => 'Recharge my account with a prepaid card',
+                url    => 'recharge_prepay',
+                indent => 2,
+              }
+    if grep(/^PREP/, @cust_paybys);
 
 }
 
index 865b5ce..bb3db12 100644 (file)
@@ -10,7 +10,7 @@ use HTML::Entities;
 use Date::Format;
 use Number::Format 1.50;
 use FS::SelfService qw( login_info login customer_info edit_info invoice
-                        payment_info process_payment 
+                        payment_info process_payment realtime_collect
                         process_prepay
                         list_pkgs order_pkg signup_info order_recharge
                         part_svc_info provision_acct provision_external
@@ -72,7 +72,7 @@ $session_id = $cgi->param('session');
 
 #order|pw_list XXX ???
 $cgi->param('action') =~
-    /^(myaccount|view_invoice|make_payment|make_ach_payment|payment_results|ach_payment_results|recharge_prepay|recharge_results|logout|change_bill|change_ship|change_pay|process_change_bill|process_change_ship|process_change_pay|customer_order_pkg|process_order_pkg|customer_change_pkg|process_change_pkg|process_order_recharge|provision|provision_svc|process_svc_acct|process_svc_external|delete_svc|view_usage|view_usage_details|view_support_details|change_password|process_change_password)$/
+    /^(myaccount|view_invoice|make_payment|make_ach_payment|make_thirdparty_payment|payment_results|ach_payment_results|recharge_prepay|recharge_results|logout|change_bill|change_ship|change_pay|process_change_bill|process_change_ship|process_change_pay|customer_order_pkg|process_order_pkg|customer_change_pkg|process_change_pkg|process_order_recharge|provision|provision_svc|process_svc_acct|process_svc_external|delete_svc|view_usage|view_usage_details|view_support_details|change_password|process_change_password)$/
   or die "unknown action ". $cgi->param('action');
 my $action = $1;
 
@@ -98,6 +98,7 @@ warn "processing template $action\n"
 do_template($action, {
   'session_id' => $session_id,
   'action'     => $action, #so the menu knows what tab we're on...
+  %{ payment_info( 'session_id' => $session_id ) },  # cust_paybys for the menu
   %{$result}
 });
 
@@ -472,6 +473,12 @@ sub ach_payment_results {
 
 }
 
+sub make_thirdparty_payment {
+  $cgi->param('payby_method') =~ /^(CC|ECHECK)$/
+    or die "illegal payby method";
+  realtime_collect( 'session_id' => $session_id, 'method' => $1 );
+}
+
 sub recharge_prepay {
   customer_info( 'session_id' => $session_id );
 }
index 47857f0..12452e6 100755 (executable)
@@ -8,11 +8,12 @@ use vars qw( @payby $cgi $init_data
              $ieak_file $ieak_template
              $signup_html $signup_template
              $success_html $success_template
+             $collect_html $collect_template
              $decline_html $decline_template
            );
 
 use subs qw( print_form print_okay print_decline
-             success_default decline_default
+             success_default collect_default decline_default
            );
 use CGI;
 #use CGI::Carp qw(fatalsToBrowser);
@@ -35,6 +36,9 @@ $signup_html = -e 'signup.html'
 $success_html = -e 'success.html'
                   ? 'success.html'
                   : '/usr/local/freeside/success.html';
+$collect_html = -e 'collect.html'
+                  ? 'collect.html'
+                  : '/usr/local/freeside/collect.html';
 $decline_html = -e 'decline.html'
                   ? 'decline.html'
                   : '/usr/local/freeside/decline.html';
@@ -97,6 +101,24 @@ if ( -e $success_html ) {
     or die $Text::Template::ERROR;
 }
 
+if ( -e $collect_html ) {
+  my $collect_txt = Text::Template::_load_text($collect_html)
+    or die $Text::Template::ERROR;
+  $collect_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $collect_txt = $1;
+  $collect_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => $collect_txt,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+} else {
+  $collect_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => &collect_default,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+}
+
 if ( -e $decline_html ) {
   my $decline_txt = Text::Template::_load_text($decline_html)
     or die $Text::Template::ERROR;
@@ -122,9 +144,10 @@ $init_data = signup_info( 'agentnum'   => $agentnum,
                           'reg_code'   => uc(scalar($cgi->param('reg_code'))),
                         );
 
-if (    ( defined($cgi->param('magic')) && $cgi->param('magic') eq 'process' )
-     || ( defined($cgi->param('action')) && $cgi->param('action') eq 'process_signup' )
-   ) {
+my $magic  = $cgi->param('magic') || '';
+my $action = $cgi->param('action') || '';
+
+if ( $magic eq 'process' || $action eq 'process_signup' ) {
 
     $error = '';
 
@@ -218,6 +241,10 @@ if (    ( defined($cgi->param('magic')) && $cgi->param('magic') eq 'process' )
     
     if ( $error eq '_decline' ) {
       print_decline();
+    } elsif ( $error eq '_collect' ) {
+      map { $cgi->param($_, $rv->{$_}) }
+        qw( popup_url reference collectitems amount );
+      print_collect();
     } elsif ( $error ) {
       #fudge the snarf info
       no strict 'refs';
@@ -230,6 +257,16 @@ if (    ( defined($cgi->param('magic')) && $cgi->param('magic') eq 'process' )
       );
     }
 
+} elsif ( $magic eq 'success' || $action eq 'success' ) {
+
+  $cgi->param('username', 'username');  #hmmm temp kludge
+  $cgi->param('_password', 'password');
+  print_okay( map { /^([\w ]+)$/ ? ( $_ => $1 ) : () } $cgi->param ); #hmmm
+
+} elsif ( $magic eq 'decline' || $action eq 'decline' ) {
+
+  print_decline();
+
 } else {
   $error = '';
   print_form;
@@ -258,6 +295,27 @@ sub print_form {
                                  );
 }
 
+sub print_collect {
+
+  $error = "Error: $error" if $error;
+
+  my $r = {
+    $cgi->Vars,
+    %{$init_data},
+    'error' => $error,
+  };
+
+  $r->{pkgpart} ||= $r->{default_pkgpart};
+
+  $r->{referral_custnum} = $r->{'ref'};
+  $r->{self_url} = $cgi->self_url;
+
+  print $cgi->header( '-expires' => 'now' ),
+        $collect_template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi',
+                                    HASH    => $r
+                                  );
+}
+
 sub print_decline {
   print $cgi->header( '-expires' => 'now' ),
         $decline_template->fill_in();
@@ -369,6 +427,37 @@ Package: <%= $pkg %><BR>
 END
 }
 
+sub collect_default { #html to use if there is a collect phase
+  <<'END';
+<HTML><HEAD><TITLE>Pay now</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Pay now</FONT><BR><BR>
+<SCRIPT TYPE="text/javascript">
+  function popcollect() {
+    overlib( OLiframeContent('<%= $popup_url %>', 336, 550, 'Secure Payment Area', 0, 'auto' ), CAPTION, 'Pay now', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK, BGCOLOR, '#333399', CGCOLOR, '#333399', CLOSETEXT, 'Close' );
+    return false;
+  }
+</SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="overlibmws.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="overlibmws_iframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="overlibmws_draggable.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="overlibmws_crossframe.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript" SRC="iframecontentmws.js"></SCRIPT>
+You are about to contact our payment processor to pay <%= $amount %> for
+<%= $pkg %>.<BR><BR>
+Your transaction reference number is <%= $reference %><BR><BR>
+<FORM NAME="collect_popper" method="post" action="javascript:void(0)" onSubmit="popcollect()">
+<%=
+  my %itemhash = @collectitems;
+  foreach my $input (keys %itemhash) {
+    $OUT .= qq!<INPUT NAME="$input" TYPE="hidden" VALUE="$itemhash{$input}">!;
+  }
+%>
+<INPUT NAME="submit" type="submit" value="Pay now">
+</FORM>
+</BODY></HTML>
+END
+}
+
 sub decline_default { #html to use if there is a decline
   <<'END';
 <HTML><HEAD><TITLE>Processing error</TITLE></HEAD>
index 1b97121..ae7b222 100755 (executable)
@@ -245,7 +245,7 @@ HTML::Widgets::SelectLayers->new(
   form_name => 'dummy',
   html_between => '</td></tr></table>',
   form_action => 'dummy.cgi',
-  layer_callback => sub { my $layer = shift; return $paybychecked{$layer}. '</TABLE>'; },
+  layer_callback => sub { my $layer = shift; return ( shift @hide_payment_fields ? '' : $paybychecked{$layer} ) . '</TABLE>'; },
 )->html;
 
 
diff --git a/fs_selfservice/FS-SelfService/cgi/verify.cgi b/fs_selfservice/FS-SelfService/cgi/verify.cgi
new file mode 100755 (executable)
index 0000000..0f8bfcc
--- /dev/null
@@ -0,0 +1,175 @@
+#!/usr/bin/perl -T
+#!/usr/bin/perl -Tw
+
+use strict;
+use vars qw( $cgi $self_url $error
+             $verify_html $verify_template
+             $success_html $success_template
+             $decline_html $decline_template
+           );
+
+use subs qw( print_verify print_okay print_decline
+             verify_default success_default decline_default
+           );
+use CGI;
+use Text::Template;
+use FS::SelfService qw( capture_payment );
+
+$verify_html =  -e 'verify.html'
+                  ? 'verify.html'
+                  : '/usr/local/freeside/verify.html';
+$success_html = -e 'verify_success.html'
+                  ? 'success.html'
+                  : '/usr/local/freeside/success.html';
+$decline_html = -e 'verify_decline.html'
+                  ? 'decline.html'
+                  : '/usr/local/freeside/decline.html';
+
+
+if ( -e $verify_html ) {
+  my $verify_txt = Text::Template::_load_text($verify_html)
+    or die $Text::Template::ERROR;
+  $verify_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $verify_txt = $1;
+  $verify_template = new Text::Template ( TYPE => 'STRING',
+                                          SOURCE => $verify_txt,
+                                          DELIMITERS => [ '<%=', '%>' ],
+                                        )
+    or die $Text::Template::ERROR;
+} else {
+  $verify_template = new Text::Template ( TYPE => 'STRING',
+                                          SOURCE => &verify_default,
+                                          DELIMITERS => [ '<%=', '%>' ],
+                                        )
+    or die $Text::Template::ERROR;
+}
+
+if ( -e $success_html ) {
+  my $success_txt = Text::Template::_load_text($success_html)
+    or die $Text::Template::ERROR;
+  $success_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $success_txt = $1;
+  $success_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => $success_txt,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+} else {
+  $success_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => &success_default,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+}
+
+if ( -e $decline_html ) {
+  my $decline_txt = Text::Template::_load_text($decline_html)
+    or die $Text::Template::ERROR;
+  $decline_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $decline_txt = $1;
+  $decline_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => $decline_txt,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+} else {
+  $decline_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => &decline_default,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+}
+
+$cgi = new CGI;
+
+my $rv = capture_payment(
+           data => { map { $_ => scalar($cgi->param($_)) } $cgi->param },
+           url  => $cgi->self_url,
+);
+
+$error = $rv->{error};
+  
+if ( $error eq '_decline' ) {
+  print_decline();
+} elsif ( $error ) {
+  print_verify();
+} else {
+  print_okay(%$rv);
+}
+
+
+sub print_verify {
+
+  $error = "Error: $error" if $error;
+
+  my $r = { $cgi->Vars, 'error' => $error };
+
+  $r->{self_url} = $cgi->self_url;
+
+  print $cgi->header( '-expires' => 'now' ),
+        $verify_template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi',
+                                   HASH    => $r
+                                 );
+}
+
+sub print_decline {
+  print $cgi->header( '-expires' => 'now' ),
+        $decline_template->fill_in();
+}
+
+sub print_okay {
+  my %param = @_;
+
+  my @success_url = split '/', $cgi->url(-path);
+  pop @success_url;
+
+  my $success_url  = join '/', @success_url;
+  if ($param{session_id}) {
+    my $session_id = lc($param{session_id});
+    $success_url .= "/selfservice.cgi?action=myaccount&session=$session_id";
+  } else {
+    $success_url .= '/signup.cgi?action=success';
+  }
+
+  print $cgi->header( '-expires' => 'now' ),
+        $success_template->fill_in( HASH => { success_url => $success_url } );
+}
+
+sub success_default { #html to use if you don't specify a success file
+  <<'END';
+<HTML><HEAD><TITLE>Signup successful</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Signup successful</FONT><BR><BR>
+Thanks for signing up!
+<BR><BR>
+<SCRIPT TYPE="text/javascript">
+  window.top.location="<%= $success_url %>";
+</SCRIPT>
+</BODY></HTML>
+END
+}
+
+sub verify_default { #html to use for verification response
+  <<'END';
+<HTML><HEAD><TITLE>Processing error</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Processing error</FONT><BR><BR>
+There has been an error processing your account.  Please contact customer
+support.
+</BODY></HTML>
+END
+}
+
+sub decline_default { #html to use if there is a decline
+  <<'END';
+<HTML><HEAD><TITLE>Processing error</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Processing error</FONT><BR><BR>
+There has been an error processing your account.  Please contact customer
+support.
+</BODY></HTML>
+END
+}
+
+# subs for the templates...
+
+package FS::SelfService::_signupcgi;
+use HTML::Entities;
+
index 848c58a..a06e5cf 100644 (file)
                                         },
                 'count_query'        => $count_query,
                 'header'             => [ '#',
+                                          'Type',
                                           'Gateway',
                                           'Username',
                                           'Password',
                                           'Action',
+                                          'URL',
                                           'Options',
                                         ],
                 'fields'             => [ 'gatewaynum',
+                                          'namespace_description',
                                           $gateway_sub,
                                           'gateway_username',
                                           sub { ' - '; },
                                           'gateway_action',
+                                          'gateway_callback_url',
                                           $options_sub,
                                         ],
           )
index e3893cf..2b108f8 100644 (file)
-<% include("/elements/header.html","$action Payment gateway", menubar(
-  'View all payment gateways' => $p. 'browse/payment_gateway.html',
-)) %>
-
-<% include('/elements/error.html') %>
-
-<FORM ACTION="<%popurl(1)%>process/payment_gateway.html" METHOD=POST>
-<INPUT TYPE="hidden" NAME="gatewaynum" VALUE="<% $payment_gateway->gatewaynum %>">
-Gateway #<% $payment_gateway->gatewaynum || "(NEW)" %>
-
-<% ntable('#cccccc', 2, '') %>
-
-<TR>
-  <TH ALIGN="right">Gateway: </TH>
-  <TD>
-% if ( $payment_gateway->gatewaynum ) { 
-
-
-      <% $payment_gateway->gateway_module %>
-      <INPUT TYPE="hidden" NAME="gateway_module" VALUE="<% $payment_gateway->gateway_module %>">
-% } else { 
-
-
-      <SELECT NAME="gateway_module" SIZE=1>
-% foreach my $module ( qw(
-%             2CheckOut
-%             AuthorizeNet
-%             BankOfAmerica
-%             Beanstream
-%             Capstone
-%             Cardstream
-%             CashCow
-%             CyberSource
-%             eSec
-%             eSelectPlus
-%             Exact
-%             iAuthorizer
-%             IPaymentTPG
-%             Jettis
-%             LinkPoint
-%             MerchantCommerce
-%             Network1Financial
-%             OCV
-%             OpenECHO
-%             PayConnect
-%             PayflowPro
-%             PaymentsGateway
-%             PXPost
-%             SecureHostingUPG
-%             Skipjack
-%             StGeorge
-%             SurePay
-%             TCLink
-%             TransactionCentral
-%             TransFirsteLink
-%             VirtualNet
-%           ) ) {
-%        
-
-          <OPTION VALUE="<% $module %>"><% $module %>
-% } 
-
-      </SELECT>
+<% include( 'elements/edit.html',
+            'table'          => 'payment_gateway',
+            'name_singular'  => 'Payment gateway',
+            'viewall_dir'    => 'browse',
+            'fields'         => $fields,
+            'field_callback' => $field_callback,
+            'labels'         => {
+                                  'gatewaynum'           => 'Gateway #',
+                                  'gateway_module'       => 'Gateway',
+                                  'gateway_username'     => 'Username',
+                                  'gateway_password'     => 'Password',
+                                  'gateway_action'       => 'Action',
+                                  'gateway_options'      => 'Options: (Name/Value pairs, one element per line)',
+                                  'gateway_callback_url' => 'Callback URL',
+                                },
+          )
+%>
+
+
+<SCRIPT TYPE="text/javascript">
+  var gatewayNamespace = new Array;
+
+% foreach my $module ( sort { lc($a) cmp lc ($b) } keys %modules ) {
+    gatewayNamespace.push('<% $modules{$module} %>')
 % } 
 
+  // document.getElementById('gateway_namespace').value = gatewayNamespace[0];
+  function setNamespace(what) {
+    document.getElementById('gateway_namespace').value =
+      gatewayNamespace[what.selectedIndex];
+  }
 
-  </TD>
-</TR>
-
-<TR>
-  <TH ALIGN="right">Username: </TH>
-  <TD><INPUT TYPE="text" NAME="gateway_username" VALUE="<% $payment_gateway->gateway_username %>"></TD>
-</TR>
-
-<TR>
-  <TH ALIGN="right">Password: </TH>
-  <TD><INPUT TYPE="text" NAME="gateway_password" VALUE="<% $payment_gateway->gateway_password %>"></TD>
-</TR>
-
-<TR>
-  <TH ALIGN="right">Action: </TH>
-  <TD>
-    <SELECT NAME="gateway_action" SIZE=1>
-% foreach my $action ( 
-%                              'Normal Authorization',
-%                              'Authorization Only',
-%                              'Authorization Only, Post Authorization',
-%                            ) {
-%      
-
-        <OPTION VALUE="<% $action %>"<% $action eq $payment_gateway->gateway_action ? ' SELECTED' : '' %>><% $action %>
-% } 
-
-    </SELECT>
-  </TD>
-</TR>
-
-<TR>
-  <TH ALIGN="right">Options: (Name/Value pairs, one element per line)</TH>
-  <TD>
-    <TEXTAREA ROWS="5" NAME="gateway_options"><% join("\r", $payment_gateway->options ) %></TEXTAREA>
-  </TD>
-</TR>
-
-</TABLE>
-
-<BR><INPUT TYPE="submit" VALUE="<% $payment_gateway->gatewaynum ? "Apply changes" : "Add gateway" %>">
-    </FORM>
-
-<% include('/elements/footer.html') %>
+</SCRIPT>
 
 <%init>
 
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
-my $payment_gateway;
-if ( $cgi->param('error') ) {
-  $payment_gateway = new FS::payment_gateway ( {
-    map { $_, scalar($cgi->param($_)) } fields('payment_gateway')
-  } );
-} elsif ( $cgi->keywords ) {
-  my($query) = $cgi->keywords;
-  $query =~ /^(\d+)$/;
-  $payment_gateway = qsearchs( 'payment_gateway', { 'gatewaynum' => $1 } );
-} else { #adding
-  $payment_gateway = new FS::payment_gateway {};
-}
-my $action = $payment_gateway->gatewaynum ? 'Edit' : 'Add';
-#my $hashref = $payment_gateway->hashref;
+my %modules =  (
+  '2CheckOut'          => 'Business::OnlinePayment',
+  'AuthorizeNet'       => 'Business::OnlinePayment',
+  'BankOfAmerica'      => 'Business::OnlinePayment',
+  'Beanstream'         => 'Business::OnlinePayment',
+  'Capstone'           => 'Business::OnlinePayment',
+  'Cardstream'         => 'Business::OnlinePayment',
+  'CashCow'            => 'Business::OnlinePayment',
+  'CyberSource'        => 'Business::OnlinePayment',
+  'eSec'               => 'Business::OnlinePayment',
+  'eSelectPlus'        => 'Business::OnlinePayment',
+  'Exact'              => 'Business::OnlinePayment',
+  'iAuthorizer'        => 'Business::OnlinePayment',
+  'Interswitchng'      => 'Business::OnlineThirdPartyPayment',
+  'IPaymentTPG'        => 'Business::OnlinePayment',
+  'Jettis'             => 'Business::OnlinePayment',
+  'LinkPoint'          => 'Business::OnlinePayment',
+  'MerchantCommerce'   => 'Business::OnlinePayment',
+  'Network1Financial'  => 'Business::OnlinePayment',
+  'OCV'                => 'Business::OnlinePayment',
+  'OpenECHO'           => 'Business::OnlinePayment',
+  'PayConnect'         => 'Business::OnlinePayment',
+  'PayflowPro'         => 'Business::OnlinePayment',
+  'PaymentsGateway'    => 'Business::OnlinePayment',
+  'PXPost'             => 'Business::OnlinePayment',
+  'SecureHostingUPG'   => 'Business::OnlinePayment',
+  'Skipjack'           => 'Business::OnlinePayment',
+  'StGeorge'           => 'Business::OnlinePayment',
+  'SurePay'            => 'Business::OnlinePayment',
+  'TCLink'             => 'Business::OnlinePayment',
+  'TransactionCentral' => 'Business::OnlinePayment',
+  'TransFirsteLink'    => 'Business::OnlinePayment',
+  'VirtualNet'         => 'Business::OnlinePayment',
+); 
+
+my @actions = (
+                'Normal Authorization',
+                'Authorization Only',
+                'Authorization Only, Post Authorization',
+              );
+
+my $fields = [
+               {
+                 field               => 'gateway_namespace',
+                 type                => 'hidden',
+                 curr_value_callback => sub { my($cgi, $object, $fref) = @_;
+                                              $modules{$object->gateway_module}
+                                              || 'Business::OnlinePayment'
+                                            },
+               },
+               {
+                 field    => 'gateway_module',
+                 type     => 'select',
+                 options  => [ sort { lc($a) cmp lc ($b) } keys %modules ],
+                 onchange => 'setNamespace',
+               },
+               'gateway_username',
+               'gateway_password',
+               {
+                 field    => 'gateway_action',
+                 type     => 'select',
+                 options  => \@actions,
+               },
+               'gateway_callback_url',
+               {
+                 field               => 'gateway_options',
+                 type                => 'textarea',
+                 curr_value_callback => sub { my($cgi, $object, $fref) = @_;
+                                              join("\r", $object->options );
+                                            },
+               },
+             ];
+
+my $field_callback = sub {
+  my ($cgi, $object, $field_hashref ) = @_;
+  if ($object->gatewaynum) {
+    if ( $field_hashref->{field} eq 'gateway_module' ) {
+      $field_hashref->{type} = 'fixed';
+    }
+  }
+};
 
 </%init>
index b16bc3d..812c988 100644 (file)
@@ -1,35 +1,22 @@
-%if ( $error ) {
-%  $cgi->param('error', $error);
-<% $cgi->redirect(popurl(2). "payment_gateway.html?". $cgi->query_string ) %>
-%} else { 
-<% $cgi->redirect(popurl(3). "browse/payment_gateway.html") %>
-%}
+<% include( 'elements/process.html',
+            'table'         => 'payment_gateway',
+            'viewall_dir'   => 'browse',
+            'args_callback' => $args_callback,
+          )
+%>
 <%init>
 
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
-my $gatewaynum = $cgi->param('gatewaynum');
+my $args_callback = sub {
+  my ( $cgi, $new ) = @_;
 
-my $old = qsearchs('payment_gateway',{'gatewaynum'=>$gatewaynum}) if $gatewaynum;
+  my @options = split(/\r?\n/, $cgi->param('gateway_options') );
+  pop @options
+    if scalar(@options) % 2 && $options[-1] =~ /^\s*$/;
+  (@options)
+};
 
-my $new = new FS::payment_gateway ( {
-  map {
-    $_, scalar($cgi->param($_));
-  } fields('payment_gateway')
-} );
-
-my @options = split(/\r?\n/, $cgi->param('gateway_options') );
-pop @options
-  if scalar(@options) % 2 && $options[-1] =~ /^\s*$/;
-my %options = @options;
-
-my $error;
-if ( $gatewaynum ) {
-  $error=$new->replace($old, \%options);
-} else {
-  $error=$new->insert(\%options);
-  $gatewaynum=$new->getfield('gatewaynum');
-}
 
 </%init>
diff --git a/httemplate/elements/tr-textarea.html b/httemplate/elements/tr-textarea.html
new file mode 100644 (file)
index 0000000..fb41ac3
--- /dev/null
@@ -0,0 +1,25 @@
+<% include('tr-td-label.html', @_ ) %>
+
+  <TD <% $cell_style %>>
+
+    <TEXTAREA NAME          = "<% $opt{field} %>"
+              ID            = "<% $opt{id} %>"
+              <% $onchange %>
+    ><% $curr_value |h %></TEXTAREA>
+
+  </TD>
+
+</TR>
+
+<%init>
+
+my %opt = @_;
+
+my $onchange = $opt{'onchange'}
+                 ? 'onChange="'. $opt{'onchange'}. '(this)"'
+                 : '';
+
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+my $curr_value = $opt{'curr_value'};
+
+</%init>