summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2012-07-13 10:56:10 -0700
committerMark Wells <mark@freeside.biz>2012-07-13 10:56:10 -0700
commit0ad946f751d3a953c8a41eea1d30ad362ba38ace (patch)
tree6780a5e8eaee29e65750cd19c090555062b84c22
parent26004f55ce70242d07fc8de51e24439e783e9e49 (diff)
one-way check batches, #17623
-rw-r--r--FS/FS/Conf.pm7
-rw-r--r--FS/FS/Schema.pm4
-rw-r--r--FS/FS/cust_main.pm27
-rw-r--r--FS/FS/cust_pay.pm21
-rw-r--r--FS/FS/cust_pay_batch.pm1
-rw-r--r--FS/FS/pay_batch.pm341
-rw-r--r--FS/FS/payment_gateway.pm2
-rw-r--r--httemplate/edit/payment_gateway.html7
-rw-r--r--httemplate/search/elements/cust_pay_batch_top.html2
-rwxr-xr-xhttemplate/search/elements/cust_pay_or_refund.html9
-rwxr-xr-xhttemplate/search/pay_batch.cgi92
11 files changed, 385 insertions, 128 deletions
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>