[freeside-commits] branch master updated. 0ad946f751d3a953c8a41eea1d30ad362ba38ace

Mark Wells mark at 420.am
Fri Jul 13 11:00:57 PDT 2012


The branch, master has been updated
       via  0ad946f751d3a953c8a41eea1d30ad362ba38ace (commit)
      from  26004f55ce70242d07fc8de51e24439e783e9e49 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 0ad946f751d3a953c8a41eea1d30ad362ba38ace
Author: Mark Wells <mark at freeside.biz>
Date:   Fri Jul 13 10:56:10 2012 -0700

    one-way check batches, #17623

diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index e3f8a5a..b4ce0ba 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -3525,6 +3525,13 @@ and customer address. Include units.',
     'select_enum' => [ 'approve', 'decline' ],
   },
 
+  {
+    'key'         => 'batch-errors_to',
+    'section'     => 'billing',
+    'description' => 'Email errors when processing batches to this address.  If unspecified, batch processing will stop immediately on error.',
+    'type'        => 'text',
+  },
+
   #lists could be auto-generated from pay_batch info
   {
     'key'         => 'batch-fixed_format-CARD',
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 797b705..61bd00c 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1407,6 +1407,7 @@ sub tables_hashref {
         'depositor',  'varchar', 'NULL', $char_d, '', '',
         'account',    'varchar', 'NULL', 20,      '', '',
         'teller',     'varchar', 'NULL', 20,      '', '',
+        'batchnum',       'int', 'NULL', '', '', '', #pay_batch foreign key
       ],
       'primary_key' => 'paynum',
       #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it# 'unique' => [ [ 'payunique' ] ],
@@ -1486,10 +1487,11 @@ sub tables_hashref {
       'columns' => [
         'batchnum', 'serial',     '', '', '', '', 
         'agentnum',    'int', 'NULL', '', '', '', 
-	'payby',      'char',     '',  4, '', '', # CARD/CHEK
+        'payby',      'char',     '',  4, '', '', # CARD/CHEK
         'status',     'char', 'NULL',  1, '', '', 
         'download',       @date_type,     '', '', 
         'upload',         @date_type,     '', '', 
+        'title',   'varchar', 'NULL',255, '', '',
       ],
       'primary_key' => 'batchnum',
       'unique' => [],
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 82b09b6..d6a86c7 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -2463,6 +2463,25 @@ Adds a payment for this invoice to the pending credit card batch (see
 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
 runs the payment using a realtime gateway.
 
+Options may include:
+
+B<amount>: the amount to be paid; defaults to the customer's balance minus
+any payments in transit.
+
+B<payby>: the payment method; defaults to cust_main.payby
+
+B<realtime>: runs this as a realtime payment instead of adding it to a 
+batch.  Deprecated.
+
+B<invnum>: sets cust_pay_batch.invnum.
+
+B<address1>, B<address2>, B<city>, B<state>, B<zip>, B<country>: sets 
+the billing address for the payment; defaults to the customer's billing
+location.
+
+B<payinfo>, B<paydate>, B<payname>: sets the payment account, expiration
+date, and name; defaults to those fields in cust_main.
+
 =cut
 
 sub batch_card {
@@ -2540,10 +2559,10 @@ sub batch_card {
     'state'    => $options{state}    || $loc->state,
     'zip'      => $options{zip}      || $loc->zip,
     'country'  => $options{country}  || $loc->country,
-    'payby'    => $options{payby}    || $loc->payby,
-    'payinfo'  => $options{payinfo}  || $loc->payinfo,
-    'exp'      => $options{paydate}  || $loc->paydate,
-    'payname'  => $options{payname}  || $loc->payname,
+    'payby'    => $options{payby}    || $self->payby,
+    'payinfo'  => $options{payinfo}  || $self->payinfo,
+    'exp'      => $options{paydate}  || $self->paydate,
+    'payname'  => $options{payname}  || $self->payname,
     'amount'   => $amount,                         # consolidating
   } );
   
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index 2a2b9d0..c117386 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -130,6 +130,11 @@ The deposit account number.
 
 The teller number.
 
+=item pay_batch
+
+The number of the batch this payment came from (see L<FS::pay_batch>), 
+or null if it was processed through a realtime gateway or entered manually.
+
 =back
 
 =head1 METHODS
@@ -514,6 +519,7 @@ sub check {
     || $self->ut_alphan('depositor')
     || $self->ut_numbern('account')
     || $self->ut_numbern('teller')
+    || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
     || $self->payinfo_check()
   ;
   return $error if $error;
@@ -983,6 +989,21 @@ sub _upgrade_data {  #class method
   $class->_upgrade_otaker(%opts);
   $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
 
+  ###
+  # migrate batchnums from the misused 'paybatch' field to 'batchnum'
+  ###
+  my @cust_pay = qsearch( {
+      'table'     => 'cust_pay',
+      'addl_from' => ' JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS text) ',
+  } );
+  foreach my $cust_pay (@cust_pay) {
+    $cust_pay->set('batchnum' => $cust_pay->paybatch);
+    $cust_pay->set('paybatch' => '');
+    my $error = $cust_pay->replace;
+    warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n  $error"
+    if $error;
+  }
+
 }
 
 =back
diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm
index 5f21ff4..9f2e9dd 100644
--- a/FS/FS/cust_pay_batch.pm
+++ b/FS/FS/cust_pay_batch.pm
@@ -322,6 +322,7 @@ sub approve {
       'paid'      => $new->paid,
       '_date'     => $new->_date,
       'usernum'   => $new->usernum,
+      'batchnum'  => $new->batchnum,
     } );
   $error = $cust_pay->insert;
   if ( $error ) {
diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm
index 4f223e1..813d096 100644
--- a/FS/FS/pay_batch.pm
+++ b/FS/FS/pay_batch.pm
@@ -12,6 +12,8 @@ use Date::Parse qw(str2time);
 use Business::CreditCard qw(cardtype);
 use Scalar::Util 'blessed';
 use IO::Scalar;
+use FS::Misc qw(send_email); # for error notification
+use List::Util qw(sum);
 
 @ISA = qw(FS::Record);
 
@@ -49,10 +51,14 @@ from FS::Record.  The following fields are currently supported:
 
 =item status - O (Open), I (In-transit), or R (Resolved)
 
-=item download - 
+=item download - time when the batch was first downloaded
 
-=item upload - 
+=item upload - time when the batch was first uploaded
 
+=item title - unique batch identifier
+
+For incoming batches, the combination of 'title', 'payby', and 'agentnum'
+must be unique.
 
 =back
 
@@ -118,9 +124,22 @@ sub check {
     || $self->ut_enum('payby', [ 'CARD', 'CHEK' ])
     || $self->ut_enum('status', [ 'O', 'I', 'R' ])
     || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
+    || $self->ut_alphan('title')
   ;
   return $error if $error;
 
+  if ( $self->title ) {
+    my @existing = 
+      grep { !$self->batchnum or $_->batchnum != $self->batchnum } 
+      qsearch('pay_batch', {
+          payby     => $self->payby,
+          agentnum  => $self->agentnum,
+          title     => $self->title,
+      });
+    return "Batch already exists as batchnum ".$existing[0]->batchnum
+      if @existing;
+  }
+
   $self->SUPER::check;
 }
 
@@ -225,11 +244,6 @@ sub import_results {
   my $job = $param->{'job'};
   $job->update_statustext(0) if $job;
 
-  my $gateway = $param->{'gateway'};
-  if ( $gateway ) {
-    return $self->import_from_gateway($gateway, 'file' => $fh, 'job' => $job);
-  }
-
   my $format = $param->{'format'};
   my $info = $import_info{$format}
     or die "unknown format $format";
@@ -444,9 +458,6 @@ sub process_import_results {
   my $param = thaw(decode_base64(shift));
   $param->{'job'} = $job;
   warn Dumper($param) if $DEBUG;
-  my $batchnum = delete $param->{'batchnum'} or die "no batchnum specified\n";
-  my $batch = FS::pay_batch->by_key($batchnum) or die "batchnum '$batchnum' not found\n";
-
   my $gatewaynum = delete $param->{'gatewaynum'};
   if ( $gatewaynum ) {
     $param->{'gateway'} = FS::payment_gateway->by_key($gatewaynum)
@@ -461,12 +472,20 @@ sub process_import_results {
         '<',
         "$dir/$file" )
       or die "unable to open '$file'.\n";
-  my $error = $batch->import_results($param);
+  
+  my $error;
+  if ( $param->{gateway} ) {
+    $error = FS::pay_batch->import_from_gateway(%$param);
+  } else {
+    my $batchnum = delete $param->{'batchnum'} or die "no batchnum specified\n";
+    my $batch = FS::pay_batch->by_key($batchnum) or die "batchnum '$batchnum' not found\n";
+    $error = $batch->import_results($param);
+  }
   unlink $file;
   die $error if $error;
 }
 
-=item import_from_gateway GATEWAY [ OPTIONS ]
+=item import_from_gateway [ OPTIONS ]
 
 Import results from a L<FS::payment_gateway>, using Business::BatchPayment,
 and apply them.  GATEWAY must use the Business::BatchPayment namespace.
@@ -477,15 +496,16 @@ or declined payment can have its status changed by a later import.
 
 OPTIONS may include:
 
-- file: a file name or handle to use as a data source.
+- gateway: the L<FS::payment_gateway>, required
+- filehandle: a file name or handle to use as a data source.
 - job: an L<FS::queue> object to update with progress messages.
 
 =cut
 
 sub import_from_gateway {
   my $class = shift;
-  my $gateway = shift;
   my %opt = @_;
+  my $gateway = $opt{'gateway'};
   my $conf = FS::Conf->new;
 
   # unavoidable duplication with import_batch, for now
@@ -508,121 +528,250 @@ sub import_from_gateway {
     unless eval { $gateway->isa('FS::payment_gateway') };
 
   my %proc_opt = (
-    'input' => $opt{'file'}, # will do nothing if it's empty
+    'input' => $opt{'filehandle'}, # will do nothing if it's empty
     # any other constructor options go here
   );
 
+  my @item_errors;
+  my $mail_on_error = $conf->config('batch-errors_to');
+  if ( $mail_on_error ) {
+    # construct error trap
+    $proc_opt{'on_parse_error'} = sub {
+      my ($self, $line, $error) = @_;
+      push @item_errors, "  '$line'\n$error";
+    };
+  }
+
   my $processor = $gateway->batch_processor(%proc_opt);
 
   my @batches = $processor->receive;
-  my $error;
+
   my $num = 0;
 
+  my $total_items = sum( map{$_->count} @batches);
+
   # whether to allow items to change status
   my $reconsider = $conf->exists('batch-reconsider');
 
   # mutex all affected batches
   my %pay_batch_for_update;
 
+  my %bop2payby = (CC => 'CARD', ECHECK => 'CHEK');
+
   BATCH: foreach my $batch (@batches) {
+
+    my %incoming_batch = (
+      'CARD' => {},
+      'CHEK' => {},
+    );
+
     ITEM: foreach my $item ($batch->elements) {
-      # cust_pay_batch.paybatchnum should be in the 'tid' attribute
-      my $paybatchnum = $item->tid;
-      my $cust_pay_batch = FS::cust_pay_batch->by_key($paybatchnum);
-      if (!$cust_pay_batch) {
-        # XXX for one-way batch protocol this needs to create new payments
-        $error = "unknown paybatchnum $paybatchnum";
-        last ITEM;
-      }
 
-      my $batchnum = $cust_pay_batch->batchnum;
-      if ( $batch->batch_id and $batch->batch_id != $batchnum ) {
-        warn "batch ID ".$batch->batch_id.
-              " does not match batchnum ".$cust_pay_batch->batchnum."\n";
-      }
+      my $cust_pay_batch; # the new batch entry (with status)
+      my $pay_batch; # the freeside batch it belongs to
+      my $payby; # CARD or CHEK
+      my $error;
 
-      # lock the batch and check its status
-      my $pay_batch = FS::pay_batch->by_key($batchnum);
-      $pay_batch_for_update{$batchnum} ||= $pay_batch->select_for_update;
-      if ( $pay_batch->status ne 'I' and !$reconsider ) {
-        $error = "batch $batchnum no longer in transit";
-        last ITEM;
-      }
+      # follow realtime gateway practice here
+      # though eventually this stuff should go into separate fields...
+      my $paybatch = $gateway->gatewaynum .  '-' .  $gateway->gateway_module .
+        ':' . $item->authorization .  ':' . $item->order_number;
+
+      if ( $batch->incoming ) {
+        # This is a one-way batch.
+        # Locate the customer, find an open batch correct for them,
+        # create a payment.  Don't bother creating a cust_pay_batch
+        # entry.
+        my $cust_main;
+        if ( defined($item->customer_id) 
+             and $item->customer_id =~ /^\d+$/ 
+             and $item->customer_id > 0 ) {
+
+          $cust_main = FS::cust_main->by_key($item->customer_id)
+                       || qsearchs('cust_main', 
+                         { 'agent_custid' => $item->customer_id }
+                       );
+          if ( !$cust_main ) {
+            push @item_errors, "Unknown customer_id ".$item->customer_id;
+            next ITEM;
+          }
+        }
+        else {
+          push @item_errors, "Illegal customer_id '".$item->customer_id."'";
+          next ITEM;
+        }
+        # it may also make sense to allow selecting the customer by 
+        # invoice_number, but no modules currently work that way
+
+        $payby = $bop2payby{ $item->payment_type };
+        my $agentnum = '';
+        $agentnum = $cust_main->agentnum if $conf->exists('batch-spoolagent');
+
+        # create a batch if necessary
+        $pay_batch = $incoming_batch{$payby}->{$agentnum} ||= 
+          FS::pay_batch->new({
+              status    => 'R', # pre-resolve it
+              payby     => $payby,
+              agentnum  => $agentnum,
+              upload    => time,
+              title     => $batch->batch_id,
+          });
+        if ( !$pay_batch->batchnum ) {
+          $error = $pay_batch->insert;
+          die $error if $error; # can't do anything if this fails
+        }
+
+        if ( !$item->approved ) {
+          $error ||= "payment rejected - ".$item->error_message;
+        }
+        if ( !defined($item->amount) or $item->amount <= 0 ) {
+          $error ||= "no amount in item $num";
+        }
+
+        my $payinfo;
+        if ( $item->check_number ) {
+          $payby = 'BILL'; # right?
+          $payinfo = $item->check_number;
+        } elsif ( $item->assigned_token ) {
+          $payinfo = $item->assigned_token;
+        }
+        # create the payment
+        my $cust_pay = FS::cust_pay->new(
+          {
+            custnum     => $cust_main->custnum,
+            _date       => $item->payment_date->epoch,
+            paid        => sprintf('%.2f',$item->amount),
+            payby       => $payby,
+            invnum      => $item->invoice_number,
+            batchnum    => $pay_batch->batchnum,
+            paybatch    => $paybatch,
+            payinfo     => $payinfo,
+          }
+        );
+        $error ||= $cust_pay->insert;
+        eval { $cust_main->apply_payments };
+        $error ||= $@;
 
-      if ( $cust_pay_batch->status ) {
-        my $new_status = $item->approved ? 'approved' : 'declined';
-        if ( lc( $cust_pay_batch->status ) eq $new_status ) {
-          # already imported with this status, so don't touch
+        if ( $error ) {
+          push @item_errors, 'Payment for customer '.$item->customer_id."\n$error";
+        }
+
+      } else {
+        # This is a request/reply batch.
+        # Locate the request (the 'tid' attribute is the paybatchnum).
+        my $paybatchnum = $item->tid;
+        $cust_pay_batch = FS::cust_pay_batch->by_key($paybatchnum);
+        if (!$cust_pay_batch) {
+          push @item_errors, "paybatchnum $paybatchnum not found";
           next ITEM;
         }
-        elsif ( !$reconsider ) {
-          # then we're not allowed to change its status, so bail out
-          $error = "paybatchnum ".$item->tid.
+        $payby = $cust_pay_batch->payby;
+
+        my $batchnum = $cust_pay_batch->batchnum;
+        if ( $batch->batch_id and $batch->batch_id != $batchnum ) {
+          warn "batch ID ".$batch->batch_id.
+                " does not match batchnum ".$cust_pay_batch->batchnum."\n";
+        }
+
+        # lock the batch and check its status
+        $pay_batch = FS::pay_batch->by_key($batchnum);
+        $pay_batch_for_update{$batchnum} ||= $pay_batch->select_for_update;
+        if ( $pay_batch->status ne 'I' and !$reconsider ) {
+          $error = "batch $batchnum no longer in transit";
+        }
+
+        if ( $cust_pay_batch->status ) {
+          my $new_status = $item->approved ? 'approved' : 'declined';
+          if ( lc( $cust_pay_batch->status ) eq $new_status ) {
+            # already imported with this status, so don't touch
+            next ITEM;
+          }
+          elsif ( !$reconsider ) {
+            # then we're not allowed to change its status, so bail out
+            $error = "paybatchnum ".$item->tid.
             " already resolved with status '". $cust_pay_batch->status . "'";
-          last ITEM;
+          }
         }
-      }
 
-      # create a new cust_pay_batch with whatever information we got back
-      my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
-      my $new_payinfo;
-      # update payinfo, if needed
-      if ( $item->assigned_token ) {
-        $new_payinfo = $item->assigned_token;
-      } elsif ( $cust_pay_batch->payby eq 'CARD' ) {
-        $new_payinfo = $item->card_number if $item->card_number;
-      } else { #$cust_pay_batch->payby eq 'CHEK'
-        $new_payinfo = $item->account_number . '@' . $item->routing_code
-          if $item->account_number;
-      }
-      $new_cust_pay_batch->payinfo($new_payinfo) if $new_payinfo;
+        if ( $error ) {        
+          push @item_errors, "Payment for customer ".$cust_pay_batch->custnum."\n$error";
+          next ITEM;
+        }
 
-      # set "paid" pseudo-field (transfers to cust_pay) to the actual amount
-      # paid, if the batch says it's different from the amount requested
-      if ( defined $item->amount ) {
-        $new_cust_pay_batch->paid($item->amount);
-      } else {
-        $new_cust_pay_batch->paid($cust_pay_batch->amount);
-      }
+        my $new_payinfo;
+        # update payinfo, if needed
+        if ( $item->assigned_token ) {
+          $new_payinfo = $item->assigned_token;
+        } elsif ( $payby eq 'CARD' ) {
+          $new_payinfo = $item->card_number if $item->card_number;
+        } else { #$payby eq 'CHEK'
+          $new_payinfo = $item->account_number . '@' . $item->routing_code
+            if $item->account_number;
+        }
+        $cust_pay_batch->set('payinfo', $new_payinfo) if $new_payinfo;
+
+        # set "paid" pseudo-field (transfers to cust_pay) to the actual amount
+        # paid, if the batch says it's different from the amount requested
+        if ( defined $item->amount ) {
+          $cust_pay_batch->set('paid', $item->amount);
+        } else {
+          $cust_pay_batch->set('paid', $cust_pay_batch->amount);
+        }
+
+        # set payment date to when it was processed
+        $cust_pay_batch->_date($item->payment_date->epoch)
+          if $item->payment_date;
+
+        # approval status
+        if ( $item->approved ) {
+          # follow Billing_Realtime format for paybatch
+          $error = $cust_pay_batch->approve($paybatch);
+          $total += $cust_pay_batch->paid;
+        }
+        else {
+          $error = $cust_pay_batch->decline($item->error_message);
+        }
+
+        if ( $error ) {        
+          push @item_errors, "Payment for customer ".$cust_pay_batch->custnum."\n$error";
+          next ITEM;
+        }
+      } # $batch->incoming
 
-      # set payment date to when it was processed
-      $new_cust_pay_batch->_date($item->payment_date->epoch)
-        if $item->payment_date;
-
-      # approval status
-      if ( $item->approved ) {
-        # follow Billing_Realtime format for paybatch
-        my $paybatch = $gateway->gatewaynum .
-          '-' .
-          $gateway->gateway_module .
-          ':' .
-          $item->authorization .
-          ':' .
-          $item->order_number;
-
-        $error = $new_cust_pay_batch->approve($paybatch);
-        $total += $new_cust_pay_batch->paid;
-      }
-      else {
-        $error = $new_cust_pay_batch->decline($item->error_message);
-      }
-      last ITEM if $error;
       $num++;
-      $job->update_statustext(int(100 * $num/( $batch->count + 1 ) ),
+      $job->update_statustext(int(100 * $num/( $total_items ) ),
         'Importing batch items')
-        if $job;
+      if $job;
+
     } #foreach $item
 
-    if ( $error ) {
+  } #foreach $batch (input batch, not pay_batch)
+
+  # Format an error message
+  if ( @item_errors ) {
+    my $error_text = join("\n\n", 
+      "Errors during batch import: ".scalar(@item_errors),
+      @item_errors
+    );
+    if ( $mail_on_error ) {
+      my $subject = "Batch import errors"; #?
+      my $body = "Import from gateway ".$gateway->label."\n".$error_text;
+      send_email(
+        to      => $mail_on_error,
+        from    => $conf->config('invoice_from'),
+        subject => $subject,
+        body    => $body,
+      );
+    } else {
+      # Bail out.
       $dbh->rollback if $oldAutoCommit;
-      return $error;
+      die $error_text;
     }
+  }
 
-  } #foreach $batch (input batch, not pay_batch)
-
-  # Auto-resolve
+  # Auto-resolve (with brute-force error handling)
   foreach my $pay_batch (values %pay_batch_for_update) {
-    $error = $pay_batch->try_to_resolve;
+    my $error = $pay_batch->try_to_resolve;
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -637,7 +786,7 @@ sub import_from_gateway {
 =item try_to_resolve
 
 Resolve this batch if possible.  A batch can be resolved if all of its
-entries have a status.  If the system options 'batch-auto_resolve_days'
+entries have status.  If the system options 'batch-auto_resolve_days'
 and 'batch-auto_resolve_status' are set, and the batch's download date is
 at least (batch-auto_resolve_days) before the current time, then it can
 be auto-resolved; entries with no status will be approved or declined 
diff --git a/FS/FS/payment_gateway.pm b/FS/FS/payment_gateway.pm
index fac7384..4a7585e 100644
--- a/FS/FS/payment_gateway.pm
+++ b/FS/FS/payment_gateway.pm
@@ -219,7 +219,7 @@ Returns a semi-friendly label for the gateway.
 sub label {
   my $self = shift;
   $self->gatewaynum . ': ' . 
-  $self->gateway_username . '@' . 
+  ($self->gateway_username ? $self->gateway_username . '@' : '') . 
   $self->gateway_module
 }
 
diff --git a/httemplate/edit/payment_gateway.html b/httemplate/edit/payment_gateway.html
index 2840df3..e5897b0 100644
--- a/httemplate/edit/payment_gateway.html
+++ b/httemplate/edit/payment_gateway.html
@@ -19,8 +19,7 @@
 
 
 <SCRIPT TYPE="text/javascript">
-% my $json = JSON->new->canonical;
-  var modulesForNamespace = <% $json->encode(\%modules_for_namespace) %>;
+  var modulesForNamespace = <% to_json(\%modules_for_namespace, {canonical=>1}) %>;
   function changeNamespace(what) {
     var ns = what.value;
     var select_module = document.getElementById('gateway_module');
@@ -68,7 +67,6 @@ my %modules =  (
   'OpenECHO'              => 'Business::OnlinePayment',
   'PayConnect'            => 'Business::OnlinePayment',
   'PayflowPro'            => 'Business::OnlinePayment',
-  'Paymentech'            => 'Business::BatchPayment',
   'PaymenTech'            => 'Business::OnlinePayment',
   'PaymentsGateway'       => 'Business::OnlinePayment',
   'PayPal'                => 'Business::OnlinePayment',
@@ -90,6 +88,9 @@ my %modules =  (
   'VirtualNet'            => 'Business::OnlinePayment',
   'WesternACH'            => 'Business::OnlinePayment',
   'WorldPay'              => 'Business::OnlinePayment',
+
+  'KeyBank'               => 'Business::BatchPayment',
+  'Paymentech'            => 'Business::BatchPayment',
 );
 
 my %modules_for_namespace;
diff --git a/httemplate/search/elements/cust_pay_batch_top.html b/httemplate/search/elements/cust_pay_batch_top.html
index 005b761..739e65b 100644
--- a/httemplate/search/elements/cust_pay_batch_top.html
+++ b/httemplate/search/elements/cust_pay_batch_top.html
@@ -103,7 +103,7 @@ Batch is <% $statustext{$status} %><BR>
 % }
 </%def>
 <%shared>
-my $show_gateways = FS::payment_gateway->count("gateway_namespace = 'Business::BatchPayment'");
+my $show_gateways = FS::payment_gateway->count("gateway_namespace = 'Business::BatchPayment' AND disabled IS NULL");
 </%shared>
 <%init>
 my %opt = @_;
diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html
index dc3cb2a..c604111 100755
--- a/httemplate/search/elements/cust_pay_or_refund.html
+++ b/httemplate/search/elements/cust_pay_or_refund.html
@@ -357,6 +357,15 @@ if ( $cgi->param('magic') ) {
 
     $orderby = "LOWER(company || ' ' || last || ' ' || first )";
 
+  } elsif ( $cgi->param('magic') eq 'batchnum' ) {
+
+    $cgi->param('batchnum') =~ /^(\d+)$/
+      or die "illegal batchnum: ".$cgi->param('batchnum');
+
+    push @search, "batchnum = $1";
+
+    $orderby = "LOWER(company || ' ' || last || ' ' || first )";
+
   } else {
     die "unknown search magic: ". $cgi->param('magic');
   }
diff --git a/httemplate/search/pay_batch.cgi b/httemplate/search/pay_batch.cgi
index 05415f3..aeaa012 100755
--- a/httemplate/search/pay_batch.cgi
+++ b/httemplate/search/pay_batch.cgi
@@ -14,12 +14,13 @@
 		                      'Type',
 		                      'First Download',
 				      'Last Upload',
-				      'Items',
-                                      'Unresolved',
-				      'Amount',
+                                      '', # requests
+                                      '', # req amt
+                                      '', # payments
+                                      '', # pay amt
 				      'Status',
                                     ],
-		 'align'         => 'rcllrrc',
+		 'align'         => 'rcllrrrrc',
 		 'fields'        => [ 'batchnum',
 		                      sub { 
 				        FS::payby->shortname(shift->payby);
@@ -47,33 +48,44 @@
 					}
 				      },
 				      sub {
-                                        FS::cust_pay_batch->count(
-                                          'batchnum = '.$_[0]->batchnum
-                                        )
+                                        my $c = FS::cust_pay_batch->count('batchnum = '.$_[0]->batchnum);
+                                        $c ? "$c requested" : ''
                                       },
                                       sub {
-                                        FS::cust_pay_batch->count(
-                                          'status is null and batchnum = '.
-                                            $_[0]->batchnum
-                                        )
-                                      },
-				      sub {
                                         my $st = "SELECT SUM(amount) from cust_pay_batch WHERE batchnum=" . shift->batchnum;
                                         my $sth = dbh->prepare($st)
-				          or die dbh->errstr. "doing $st";
+                                          or die dbh->errstr. "doing $st";
                                         $sth->execute
-				          or die "Error executing \"$st\": ". $sth->errstr;
-                                        $sth->fetchrow_arrayref->[0];
-				      },
+                                          or die "Error executing \"$st\": ". $sth->errstr;
+                                        my $total = $sth->fetchrow_arrayref->[0];
+                                        $total ? $money_char.sprintf('%.2f',$total) : '';
+                                      },
+                                      sub {
+                                        my $c = FS::cust_pay->count('batchnum = '.$_[0]->batchnum);
+                                        $c ? "$c paid" : ''
+                                      },
+                                      sub {
+                                        my $st = "SELECT SUM(paid) from cust_pay WHERE batchnum=" . shift->batchnum;
+                                        my $sth = dbh->prepare($st)
+                                          or die dbh->errstr. "doing $st";
+                                        $sth->execute
+                                          or die "Error executing \"$st\": ". $sth->errstr;
+                                        my $total = $sth->fetchrow_arrayref->[0];
+                                        $total ? $money_char.sprintf('%.2f',$total) : '';
+                                      },
                                       sub {
 				        $statusmap{shift->status};
 				      },
 				    ],
 		 'links'         => [
-		                      $link,
+		                      '',
 				      '',
-				      sub { shift->status eq 'O' ? $link : '' },
-				      sub { shift->status eq 'I' ? $link : '' },
+                                      sub { shift->status eq 'O' ? $cpb_link : '' },
+                                      sub { shift->status eq 'I' ? $cpb_link : '' },
+                                      $cpb_link,
+                                      $cpb_link,
+                                      $pay_link,
+                                      $pay_link,
 				    ],
 		 'size'         => [
 		                      '',
@@ -88,9 +100,42 @@
 				      sub { shift->status eq 'I' ? "b" : '' },
 				    ],
                  'html_init'     => $html_init,
+                 'html_foot'     => include('.upload_incoming'),
       )
-
 %>
+<%def .upload_incoming>
+% if ( FS::payment_gateway->count("gateway_namespace = 'Business::BatchPayment' AND disabled IS NULL") > 0 ) { 
+<& /elements/form-file_upload.html,
+    name      => 'FileUpload',
+    action    => $p.'misc/upload-batch.cgi',
+    num_files => 1,
+    fields    => [ 'gatewaynum' ],
+    message   => 'Incoming batch uploaded.',
+&>
+<BR>
+<BR>
+Upload incoming batch from gateway 
+<& /elements/select-table.html,
+    table       => 'payment_gateway',
+    field       => 'gatewaynum',
+    name_col    => 'label',
+    value_col   => 'gatewaynum',
+    order_by    => 'ORDER BY gatewaynum',
+    empty_label => ' ',
+    hashref     =>
+      { 'gateway_namespace' => 'Business::BatchPayment',
+        'disabled' => '' },
+&>
+<BR>
+<& '/elements/file-upload.html',
+    field     => 'file',
+    label     => 'Filename',
+    no_table  => 1,
+&>
+<INPUT TYPE="submit" VALUE="Upload">
+</FORM>
+% }
+</%def>
 <%init>
 
 die "access denied"
@@ -134,11 +179,14 @@ push @where,
 
 my $extra_sql = scalar(@where) ? 'WHERE ' . join(' AND ', @where) : ''; 
 
-my $link = [ "${p}search/cust_pay_batch.cgi?dcln=1;batchnum=", 'batchnum' ];
+my $cpb_link = [ "${p}search/cust_pay_batch.cgi?dcln=1;batchnum=", 'batchnum' ];
+my $pay_link = [ "${p}search/cust_pay.html?magic=batchnum;batchnum=", 'batchnum' ];
 
 my $resolved = $cgi->param('resolved') || 0;
 $cgi->param('resolved' => !$resolved);
 my $html_init = '<A HREF="' . $cgi->self_url . '"><I>'.
     ($resolved ? 'Hide' : 'Show') . ' resolved batches</I></A><BR>';
 
+my $money_char = FS::Conf->new->config('money_char') || '$';
+
 </%init>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Conf.pm                                      |    7 +
 FS/FS/Schema.pm                                    |    4 +-
 FS/FS/cust_main.pm                                 |   27 ++-
 FS/FS/cust_pay.pm                                  |   21 ++
 FS/FS/cust_pay_batch.pm                            |    1 +
 FS/FS/pay_batch.pm                                 |  341 ++++++++++++++------
 FS/FS/payment_gateway.pm                           |    2 +-
 httemplate/edit/payment_gateway.html               |    7 +-
 httemplate/search/elements/cust_pay_batch_top.html |    2 +-
 httemplate/search/elements/cust_pay_or_refund.html |    9 +
 httemplate/search/pay_batch.cgi                    |   92 ++++--
 11 files changed, 385 insertions(+), 128 deletions(-)




More information about the freeside-commits mailing list