merge webpay support in with autoselection of old realtime_bop and realtime_refund_bop
authorjeff <jeff>
Tue, 10 Mar 2009 16:14:11 +0000 (16:14 +0000)
committerjeff <jeff>
Tue, 10 Mar 2009 16:14:11 +0000 (16:14 +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..02aa580 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'} };
@@ -295,8 +320,6 @@ sub signup_info {
       ];
     warn "$me done setting agent-specific adv. source list\n" if $DEBUG > 1;
 
-    my $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
-                           
     $signup_info->{'agent_name'} = $agent->agent;
 
     $signup_info->{'company_name'} = $conf->config('company_name', $agentnum);
@@ -436,6 +459,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 +587,26 @@ sub new_customer {
     #     " new customer: $bill_error"
     #  if $bill_error;
 
-    $bill_error = $cust_main->collect('realtime' => 1);
+    if ($cust_main->_new_bop_required()) {
+      $bill_error = $cust_main->realtime_collect(
+         method        => FS::payby->payby2bop( $packet->{payby} ),
+         depend_jobnum => $placeholder->jobnum,
+      );
+    } else {
+      $bill_error = $cust_main->collect('realtime' => 1);
+    }
     #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 +656,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..2bad5ec 100644 (file)
@@ -3368,6 +3368,10 @@ sub retry_realtime {
 
 }
 
+# some horrid false laziness here to avoid refactor fallout
+# eventually realtime realtime_bop and realtime_refund_bop should go
+# away and be replaced by _new_realtime_bop and _new_realtime_refund_bop
+
 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
 
 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
@@ -3401,7 +3405,12 @@ I<payunique> is a unique identifier for this payment.
 =cut
 
 sub realtime_bop {
-  my( $self, $method, $amount, %options ) = @_;
+  my $self = shift;
+
+  return $self->_new_realtime_bop(@_)
+    if $self->_new_bop_required();
+
+  my( $method, $amount, %options ) = @_;
   if ( $DEBUG ) {
     warn "$me realtime_bop: $method $amount\n";
     warn "  $_ => $options{$_}\n" foreach keys %options;
@@ -3942,119 +3951,6 @@ sub realtime_bop {
 
 }
 
-=item fake_bop
-
-=cut
-
-sub fake_bop {
-  my( $self, $method, $amount, %options ) = @_;
-
-  if ( $options{'fake_failure'} ) {
-     return "Error: No error; test failure requested with fake_failure";
-  }
-
-  my %method2payby = (
-    'CC'     => 'CARD',
-    'ECHECK' => 'CHEK',
-    'LEC'    => 'LECB',
-  );
-
-  #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);
-
-  my $paybatch = 'FakeProcessor:54:32';
-
-  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});
-
-  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 default_payment_gateway
-
-=cut
-
-sub default_payment_gateway {
-  my( $self, $method ) = @_;
-
-  die "Real-time processing not enabled\n"
-    unless $conf->exists('business-onlinepayment');
-
-  #load up config
-  my $bop_config = 'business-onlinepayment';
-  $bop_config .= '-ach'
-    if $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;
-
-  ( $processor, $login, $password, $action, @bop_options )
-}
-
-=item remove_cvv
-
-Removes the I<paycvv> field from the database directly.
-
-If there is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub remove_cvv {
-  my $self = shift;
-  my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
-    or return dbh->errstr;
-  $sth->execute($self->custnum)
-    or return $sth->errstr;
-  $self->paycvv('');
-  '';
-}
-
 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
 
 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
@@ -4094,7 +3990,12 @@ 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;
+
+  return $self->_new_realtime_refund_bop(@_)
+    if $self->_new_bop_required();
+
+  my( $method, %options ) = @_;
   if ( $DEBUG ) {
     warn "$me realtime_refund_bop: $method refund\n";
     warn "  $_ => $options{$_}\n" foreach keys %options;
@@ -4373,6 +4274,1285 @@ sub realtime_refund_bop {
 
 }
 
+# does the configuration indicate the new bop routines are required?
+
+sub _new_bop_required {
+  my $self = shift;
+
+  my $botpp = 'Business::OnlineThirdPartyPayment';
+
+  return 1
+    if ( $conf->config('business-onlinepayment-namespace') eq $botpp ||
+         scalar( grep { $_->gateway_namespace eq $botpp } 
+                 qsearch( 'payment_gateway', { 'disabled' => '' } )
+               )
+       )
+  ;
+
+  '';
+}
+  
+
+=item realtime_collect [ OPTION => VALUE ... ]
+
+Runs a realtime credit card, ACH (electronic check) or phone bill transaction
+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.
+
+On failure returns an error message.
+
+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,
+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
+
+=cut
+
+sub realtime_collect {
+  my( $self, %options ) = @_;
+
+  if ( $DEBUG ) {
+    warn "$me realtime_collect:\n";
+    warn "  $_ => $options{$_}\n" foreach keys %options;
+  }
+
+  $options{amount} = $self->balance unless exists( $options{amount} );
+  $options{method} = FS::payby->payby2bop($self->payby)
+    unless exists( $options{method} );
+
+  return $self->realtime_bop({%options});
+
+}
+
+=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>
+
+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
+
+# 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,
+  );
+}
+
+sub _bop_options {
+  my ($self, $options) = @_;
+
+  $options->{payment_gateway}->gatewaynum
+    ? $options->{payment_gateway}->options
+    : @{ $options->{payment_gateway}->get('options') };
+}
+
+sub _bop_defaults {
+  my ($self, $options) = @_;
+
+  $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);
+
+  $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 _new_realtime_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 ( $DEBUG ) {
+    warn "$me realtime_bop (new): $options{method} $options{amount}\n";
+    warn "  $_ => $options{$_}\n" foreach keys %options;
+  }
+
+  return $self->fake_bop(%options) if $options{'fake'};
+
+  $self->_bop_defaults(\%options);
+
+  ###
+  # select a gateway
+  ###
+
+  my $payment_gateway =  $self->_payment_gateway( \%options );
+  my $namespace = $payment_gateway->gateway_namespace;
+
+  eval "use $namespace";  
+  die $@ if $@;
+
+  ###
+  # 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 (%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;
+  if ( $conf->exists('emailinvoiceautoalways')
+       || $conf->exists('emailinvoiceauto') && ! @invoicing_list
+       || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
+    push @invoicing_list, $self->all_emails;
+  }
+
+  my $email = ($conf->exists('business-onlinepayment-email-override'))
+              ? $conf->config('business-onlinepayment-email-override')
+              : $invoicing_list[0];
+
+  my $paydate = '';
+  my %content = ();
+  if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
+
+    $content{card_number} = $options{payinfo};
+    $paydate = exists($options{'paydate'})
+                    ? $options{'paydate'}
+                    : $self->paydate;
+    $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+    $content{expiration} = "$2/$1";
+
+    my $paycvv = exists($options{'paycvv'})
+                   ? $options{'paycvv'}
+                   : $self->paycvv;
+    $content{cvv2} = $paycvv
+      if length($paycvv);
+
+    my $paystart_month = exists($options{'paystart_month'})
+                           ? $options{'paystart_month'}
+                           : $self->paystart_month;
+
+    my $paystart_year  = exists($options{'paystart_year'})
+                           ? $options{'paystart_year'}
+                           : $self->paystart_year;
+
+    $content{card_start} = "$paystart_month/$paystart_year"
+      if $paystart_month && $paystart_year;
+
+    my $payissue       = exists($options{'payissue'})
+                           ? $options{'payissue'}
+                           : $self->payissue;
+    $content{issue_number} = $payissue if $payissue;
+
+    $content{recurring_billing} = 'YES'
+      if qsearch('cust_pay', { 'custnum' => $self->custnum,
+                               'payby'   => 'CARD',
+                               'payinfo' => $options{payinfo},
+                             } )
+      || qsearch('cust_pay', { 'custnum' => $self->custnum,
+                               'payby'   => 'CARD',
+                               'paymask' => $self->mask_payinfo('CARD', $options{payinfo}),
+                             } );
+
+
+  } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
+    ( $content{account_number}, $content{routing_code} ) =
+      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{customer_org} = $self->company ? 'B' : 'I';
+    $content{state_id}       = exists($options{'stateid'})
+                                 ? $options{'stateid'}
+                                 : $self->getfield('stateid');
+    $content{state_id_state} = exists($options{'stateid_state'})
+                                 ? $options{'stateid_state'}
+                                 : $self->getfield('stateid_state');
+    $content{customer_ssn} = exists($options{'ss'})
+                               ? $options{'ss'}
+                               : $self->ss;
+  } 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
+  }
+
+  ###
+  # run transaction(s)
+  ###
+
+  my $balance = exists( $options{'balance'} )
+                  ? $options{'balance'}
+                  : $self->balance;
+
+  $self->select_for_update; #mutex ... just until we get our pending record in
+
+  #the checks here are intended to catch concurrent payments
+  #double-form-submission prevention is taken care of in cust_pay_pending::check
+
+  #check the balance
+  return "The customer's balance has changed; $options{method} transaction aborted."
+    if $self->balance < $balance;
+    #&& $self->balance < $options{amount}; #might as well anyway?
+
+  #also check and make sure there aren't *other* pending payments for this cust
+
+  my @pending = qsearch('cust_pay_pending', {
+    'custnum' => $self->custnum,
+    'status'  => { op=>'!=', value=>'done' } 
+  });
+  return "A payment is already being processed for this customer (".
+         join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
+         "); $options{method} transaction aborted."
+    if scalar(@pending);
+
+  #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
+
+  my $cust_pay_pending = new FS::cust_pay_pending {
+    'custnum'    => $self->custnum,
+    #'invnum'     => $options{'invnum'},
+    'paid'       => $options{amount},
+    '_date'      => '',
+    'payby'      => $bop_method2payby{$options{method}},
+    'payinfo'    => $options{payinfo},
+    'paydate'    => $paydate,
+    'status'     => 'new',
+    '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*/, $payment_gateway->gateway_action );
+
+  my $transaction = new $namespace( $payment_gateway->gateway_module,
+                                    $self->_bop_options(\%options),
+                                  );
+
+  $transaction->content(
+    'type'           => $options{method},
+    $self->_bop_auth(\%options),          
+    'action'         => $action1,
+    'description'    => $options{'description'},
+    'amount'         => $options{amount},
+    #'invoice_number' => $options{'invnum'},
+    'customer_id'    => $self->custnum,
+    %bop_content,
+    'reference'      => $cust_pay_pending->paypendingnum, #for now
+    'email'          => $email,
+    %content, #after
+  );
+
+  $cust_pay_pending->status('pending');
+  my $cpp_pending_err = $cust_pay_pending->replace;
+  return $cpp_pending_err if $cpp_pending_err;
+
+  #config?
+  my $BOP_TESTING = 0;
+  my $BOP_TESTING_SUCCESS = 1;
+
+  unless ( $BOP_TESTING ) {
+    $transaction->submit();
+  } else {
+    if ( $BOP_TESTING_SUCCESS ) {
+      $transaction->is_success(1);
+      $transaction->authorization('fake auth');
+    } else {
+      $transaction->is_success(0);
+      $transaction->error_message('fake failure');
+    }
+  }
+
+  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;
+    return $cpp_authorized_err if $cpp_authorized_err;
+
+    my $auth = $transaction->authorization;
+    my $ordernum = $transaction->can('order_number')
+                   ? $transaction->order_number
+                   : '';
+
+    my $capture =
+      new Business::OnlinePayment( $payment_gateway->gateway_module,
+                                   $self->_bop_options(\%options),
+                                 );
+
+    my %capture = (
+      %content,
+      type           => $options{method},
+      action         => $action2,
+      $self->_bop_auth(\%options),          
+      order_number   => $ordernum,
+      amount         => $options{amount},
+      authorization  => $auth,
+      description    => $options{'description'},
+    );
+
+    foreach my $field (qw( authorization_source_code returned_ACI
+                           transaction_identifier validation_code           
+                           transaction_sequence_num local_transaction_date    
+                           local_transaction_time AVS_result_code          )) {
+      $capture{$field} = $transaction->$field() if $transaction->can($field);
+    }
+
+    $capture->content( %capture );
+
+    $capture->submit();
+
+    unless ( $capture->is_success ) {
+      my $e = "Authorization successful but capture failed, custnum #".
+              $self->custnum. ': '.  $capture->result_code.
+              ": ". $capture->error_message;
+      warn $e;
+      return $e;
+    }
+
+  }
+
+  ###
+  # remove paycvv after initial transaction
+  ###
+
+  #false laziness w/misc/process/payment.cgi - check both to make sure working
+  # correctly
+  if ( defined $self->dbdef_table->column('paycvv')
+       && length($self->paycvv)
+       && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
+  ) {
+    my $error = $self->remove_cvv;
+    if ( $error ) {
+      warn "WARNING: error removing cvv: $error\n";
+    }
+  }
+
+  ###
+  # 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->gatewaynum ) { # agent override
+      $paybatch = $payment_gateway->gatewaynum. '-';
+    }
+
+    $paybatch .= $payment_gateway->gateway_module. ":".
+      $transaction->authorization;
+
+    $paybatch .= ':'. $transaction->order_number
+      if $transaction->can('order_number')
+      && length($transaction->order_number);
+
+    my $cust_pay = new FS::cust_pay ( {
+       'custnum'  => $self->custnum,
+       'invnum'   => $options{'invnum'},
+       'paid'     => $cust_pay_pending->paid,
+       '_date'    => '',
+       'payby'    => $cust_pay_pending->payby,
+       #'payinfo'  => $payinfo,
+       'paybatch' => $paybatch,
+       '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} )
+      if defined($options{payunique}) && length($options{payunique});
+
+    my $oldAutoCommit = $FS::UID::AutoCommit;
+    local $FS::UID::AutoCommit = 0;
+    my $dbh = dbh;
+
+    #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
+
+    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.  but at least we have a record of the state we had to abort in
+        # from cust_pay_pending now.
+        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";
+        warn $e;
+        return $e;
+      }
+    }
+
+    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;
+    }
+
+    $cust_pay_pending->status('done');
+    $cust_pay_pending->statustext('captured');
+    $cust_pay_pending->paynum($cust_pay->paynum);
+    my $cpp_done_err = $cust_pay_pending->replace;
+
+    if ( $cpp_done_err ) {
+
+      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+      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;
+      return $e;
+
+    } else {
+
+      $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+      return ''; #no error
+
+    }
+
+  } else {
+
+    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;
+      if ( $transaction->can('response_page') ) {
+        $t_response = {
+                        'page'    => ( $transaction->can('response_page')
+                                         ? $transaction->response_page
+                                         : ''
+                                     ),
+                        'code'    => ( $transaction->can('response_code')
+                                         ? $transaction->response_code
+                                         : ''
+                                     ),
+                        'headers' => ( $transaction->can('response_headers')
+                                         ? $transaction->response_headers
+                                         : ''
+                                     ),
+                      };
+      } else {
+        $t_response .=
+          "No additional debugging information available for ".
+            $payment_gateway->gateway_module;
+      }
+
+      $perror .= "No error_message returned from ".
+                   $payment_gateway->gateway_module. " -- ".
+                 ( ref($t_response) ? Dumper($t_response) : $t_response );
+
+    }
+
+    if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
+         && $conf->exists('emaildecline')
+         && grep { $_ ne 'POST' } $self->invoicing_list
+         && ! grep { $transaction->error_message =~ /$_/ }
+                   $conf->config('emaildecline-exclude')
+    ) {
+      my @templ = $conf->config('declinetemplate');
+      my $template = new Text::Template (
+        TYPE   => 'ARRAY',
+        SOURCE => [ map "$_\n", @templ ],
+      ) or return "($perror) can't create template: $Text::Template::ERROR";
+      $template->compile()
+        or return "($perror) can't compile template: $Text::Template::ERROR";
+
+      my $templ_hash = { error => $transaction->error_message };
+
+      my $error = send_email(
+        'from'    => $conf->config('invoice_from', $self->agentnum ),
+        'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
+        'subject' => 'Your payment could not be processed',
+        'body'    => [ $template->fill_in(HASH => $templ_hash) ],
+      );
+
+      $perror .= " (also received error sending decline notification: $error)"
+        if $error;
+
+    }
+
+    $cust_pay_pending->status('done');
+    $cust_pay_pending->statustext("declined: $perror");
+    my $cpp_done_err = $cust_pay_pending->replace;
+    if ( $cpp_done_err ) {
+      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)";
+    }
+
+    return $perror;
+  }
+
+}
+
+=item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
+
+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.
+
+Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
+
+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.
+
+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.
+
+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;
+  }
+
+  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;
+  }
+
+  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 DEPRECATED -- use agent->payment_gateway
+
+=cut
+
+sub default_payment_gateway {
+  my( $self, $method ) = @_;
+
+  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'
+    if $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;
+
+  ( $processor, $login, $password, $action, @bop_options )
+}
+
+=item remove_cvv
+
+Removes the I<paycvv> field from the database directly.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub remove_cvv {
+  my $self = shift;
+  my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
+    or return dbh->errstr;
+  $sth->execute($self->custnum)
+    or return $sth->errstr;
+  $self->paycvv('');
+  '';
+}
+
+=item _new_realtime_refund_bop METHOD [ OPTION => VALUE ... ]
+
+Refunds 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.
+
+Available methods are: I<CC>, I<ECHECK> and I<LEC>
+
+Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
+
+Most gateways require a reference to an original payment transaction to refund,
+so you probably need to specify a I<paynum>.
+
+I<amount> defaults to the original amount of the payment if not specified.
+
+I<reason> specifies a reason for the refund.
+
+I<paydate> specifies the expiration date for a credit card overriding the
+value from the customer record or the payment record. Specified as yyyy-mm-dd
+
+Implementation note: If I<amount> is unspecified or equal to the amount of the
+orignal payment, first an attempt is made to "void" the transaction via
+the gateway (to cancel a not-yet settled transaction) and then if that fails,
+the normal attempt is made to "refund" ("credit") the transaction via the
+gateway is attempted.
+
+#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.
+
+#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.
+
+=cut
+
+#some false laziness w/realtime_bop, not enough to make it worth merging
+#but some useful small subs should be pulled out
+sub _new_realtime_refund_bop {
+  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 (new): $options{method} refund\n";
+    warn "  $_ => $options{$_}\n" foreach keys %options;
+  }
+
+  ###
+  # look up the original payment and optionally a gateway for that payment
+  ###
+
+  my $cust_pay = '';
+  my $amount = $options{'amount'};
+
+  my( $processor, $login, $password, @bop_options, $namespace ) ;
+  my( $auth, $order_number ) = ( '', '', '' );
+
+  if ( $options{'paynum'} ) {
+
+    warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
+    $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
+      or return "Unknown paynum $options{'paynum'}";
+    $amount ||= $cust_pay->paid;
+
+    $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
+      or return "Can't parse paybatch for paynum $options{'paynum'}: ".
+                $cust_pay->paybatch;
+    my $gatewaynum = '';
+    ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
+
+    if ( $gatewaynum ) { #gateway for the payment to be refunded
+
+      my $payment_gateway =
+        qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
+      die "payment gateway $gatewaynum not found"
+        unless $payment_gateway;
+
+      $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;
+      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"
+        unless $processor eq $conf_processor;
+
+    }
+
+
+  } else { # didn't specify a paynum, so look for agent gateway overrides
+           # like a normal transaction 
+    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 );
+
+    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'           => $options{method},
+    'login'          => $login,
+    'password'       => $password,
+    'order_number'   => $order_number,
+    'amount'         => $amount,
+    'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
+  );
+  $content{authorization} = $auth
+    if length($auth); #echeck/ACH transactions have an order # but no auth
+                      #(at least with authorize.net)
+
+  my $disable_void_after;
+  if ($conf->exists('disable_void_after')
+      && $conf->config('disable_void_after') =~ /^(\d+)$/) {
+    $disable_void_after = $1;
+  }
+
+  #first try void if applicable
+  if ( $cust_pay && $cust_pay->paid == $amount
+    && (
+      ( not defined($disable_void_after) )
+      || ( time < ($cust_pay->_date + $disable_void_after ) )
+    )
+  ) {
+    warn "  attempting void\n" if $DEBUG > 1;
+    my $void = new Business::OnlinePayment( $processor, @bop_options );
+    $void->content( 'action' => 'void', %content );
+    $void->submit();
+    if ( $void->is_success ) {
+      my $error = $cust_pay->void($options{'reason'});
+      if ( $error ) {
+        # gah, even with transactions.
+        my $e = 'WARNING: Card/ACH voided but database not updated - '.
+                "error voiding payment: $error";
+        warn $e;
+        return $e;
+      }
+      warn "  void successful\n" if $DEBUG > 1;
+      return '';
+    }
+  }
+
+  warn "  void unsuccessful, trying refund\n"
+    if $DEBUG > 1;
+
+  #massage data
+  my $address = $self->address1;
+  $address .= ", ". $self->address2 if $self->address2;
+
+  my($payname, $payfirst, $paylast);
+  if ( $self->payname && $options{method} ne 'ECHECK' ) {
+    $payname = $self->payname;
+    $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 @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;
+  }
+
+  my $email = ($conf->exists('business-onlinepayment-email-override'))
+              ? $conf->config('business-onlinepayment-email-override')
+              : $invoicing_list[0];
+
+  my $payip = exists($options{'payip'})
+                ? $options{'payip'}
+                : $self->payip;
+  $content{customer_ip} = $payip
+    if length($payip);
+
+  my $payinfo = '';
+  if ( $options{method} eq 'CC' ) {
+
+    if ( $cust_pay ) {
+      $content{card_number} = $payinfo = $cust_pay->payinfo;
+      (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
+        =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
+        ($content{expiration} = "$2/$1");  # where available
+    } else {
+      $content{card_number} = $payinfo = $self->payinfo;
+      (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
+        =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+      $content{expiration} = "$2/$1";
+    }
+
+  } elsif ( $options{method} eq 'ECHECK' ) {
+
+    if ( $cust_pay ) {
+      $payinfo = $cust_pay->payinfo;
+    } else {
+      $payinfo = $self->payinfo;
+    } 
+    ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
+    $content{bank_name} = $self->payname;
+    $content{account_type} = 'CHECKING';
+    $content{account_name} = $payname;
+    $content{customer_org} = $self->company ? 'B' : 'I';
+    $content{customer_ssn} = $self->ss;
+  } elsif ( $options{method} eq 'LEC' ) {
+    $content{phone} = $payinfo = $self->payinfo;
+  }
+
+  #then try refund
+  my $refund = new Business::OnlinePayment( $processor, @bop_options );
+  my %sub_content = $refund->content(
+    'action'         => 'credit',
+    'customer_id'    => $self->custnum,
+    'last_name'      => $paylast,
+    'first_name'     => $payfirst,
+    'name'           => $payname,
+    'address'        => $address,
+    'city'           => $self->city,
+    'state'          => $self->state,
+    'zip'            => $self->zip,
+    'country'        => $self->country,
+    'email'          => $email,
+    'phone'          => $self->daytime || $self->night,
+    %content, #after
+  );
+  warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
+    if $DEBUG > 1;
+  $refund->submit();
+
+  return "$processor error: ". $refund->error_message
+    unless $refund->is_success();
+
+  my $paybatch = "$processor:". $refund->authorization;
+  $paybatch .= ':'. $refund->order_number
+    if $refund->can('order_number') && $refund->order_number;
+
+  while ( $cust_pay && $cust_pay->unapplied < $amount ) {
+    my @cust_bill_pay = $cust_pay->cust_bill_pay;
+    last unless @cust_bill_pay;
+    my $cust_bill_pay = pop @cust_bill_pay;
+    my $error = $cust_bill_pay->delete;
+    last if $error;
+  }
+
+  my $cust_refund = new FS::cust_refund ( {
+    'custnum'  => $self->custnum,
+    'paynum'   => $options{'paynum'},
+    'refund'   => $amount,
+    '_date'    => '',
+    'payby'    => $bop_method2payby{$options{method}},
+    'payinfo'  => $payinfo,
+    'paybatch' => $paybatch,
+    'reason'   => $options{'reason'} || 'card or ACH refund',
+  } );
+  my $error = $cust_refund->insert;
+  if ( $error ) {
+    $cust_refund->paynum(''); #try again with no specific paynum
+    my $error2 = $cust_refund->insert;
+    if ( $error2 ) {
+      # gah, even with transactions.
+      my $e = 'WARNING: Card/ACH refunded but database not updated - '.
+              "error inserting refund ($processor): $error2".
+              " (previously tried insert with paynum #$options{'paynum'}" .
+              ": $error )";
+      warn $e;
+      return $e;
+    }
+  }
+
+  ''; #no error
+
+}
+
 =item batch_card OPTION => VALUE...
 
 Adds a payment for this invoice to the pending credit card batch (see
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>