summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Conf.pm14
-rw-r--r--FS/FS/cust_pay_batch.pm76
-rw-r--r--FS/FS/pay_batch.pm108
-rw-r--r--FS/FS/pay_batch/td_eft1464.pm89
-rw-r--r--httemplate/misc/download-batch.cgi4
-rw-r--r--httemplate/misc/process/pay_batch-approve.cgi16
-rwxr-xr-xhttemplate/search/cust_pay_batch.cgi10
7 files changed, 167 insertions, 150 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 4624199bf..f86f437f1 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2995,13 +2995,13 @@ and customer address. Include units.',
'type' => 'textarea',
},
-# {
-# 'key' => 'batch-manual_approval',
-# 'section' => 'billing',
-# 'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status. This is very dangerous.',
-# 'type' => 'checkbox',
-# },
-#
+ {
+ 'key' => 'batch-manual_approval',
+ 'section' => 'billing',
+ 'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status. This is not advised, but is needed for payment processors that provide a report of rejected rather than approved payments.',
+ 'type' => 'checkbox',
+ },
+
{
'key' => 'payment_history-years',
'section' => 'UI',
diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm
index 9ef1e1cc1..9fa14598a 100644
--- a/FS/FS/cust_pay_batch.pm
+++ b/FS/FS/cust_pay_batch.pm
@@ -260,6 +260,82 @@ sub retriable {
'';
}
+=item approve PAYBATCH
+
+Approve this payment. This will replace the existing record with the
+same paybatchnum, set its status to 'Approved', and generate a payment
+record (L<FS::cust_pay>). This should only be called from the batch
+import process.
+
+=cut
+
+sub approve {
+ # to break up the Big Wall of Code that is import_results
+ my $new = shift;
+ my $paybatch = shift;
+ my $paybatchnum = $new->paybatchnum;
+ my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
+ or return "paybatchnum $paybatchnum not found";
+ return "paybatchnum $paybatchnum already resolved ('".$old->status."')"
+ if $old->status;
+ $new->status('Approved');
+ my $error = $new->replace($old);
+ if ( $error ) {
+ return "error updating status of paybatchnum $paybatchnum: $error\n";
+ }
+ my $cust_pay = new FS::cust_pay ( {
+ 'custnum' => $new->custnum,
+ 'payby' => $new->payby,
+ 'paybatch' => $paybatch,
+ 'payinfo' => $new->payinfo || $old->payinfo,
+ 'paid' => $new->paid,
+ '_date' => $new->_date,
+ } );
+ $error = $cust_pay->insert;
+ if ( $error ) {
+ return "error inserting payment for paybatchnum $paybatchnum: $error\n";
+ }
+ $cust_pay->cust_main->apply_payments;
+ return;
+}
+
+=item decline
+
+Decline this payment. This will replace the existing record with the
+same paybatchnum, set its status to 'Declined', and run collection events
+as appropriate. This should only be called from the batch import process.
+
+
+=cut
+sub decline {
+ my $new = shift;
+ my $paybatchnum = $new->paybatchnum;
+ my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
+ or return "paybatchnum $paybatchnum not found";
+ return "paybatchnum $paybatchnum already resolved ('".$old->status."')"
+ if $old->status;
+ $new->status('Declined');
+ my $error = $new->replace($old);
+ if ( $error ) {
+ return "error updating status of paybatchnum $paybatchnum: $error\n";
+ }
+ my $due_cust_event = $new->cust_main->due_cust_event(
+ 'eventtable' => 'cust_pay_batch',
+ 'objects' => [ $new ],
+ );
+ if ( !ref($due_cust_event) ) {
+ return $due_cust_event;
+ }
+ # XXX breaks transaction integrity
+ foreach my $cust_event (@$due_cust_event) {
+ next unless $cust_event->test_conditions;
+ if ( my $error = $cust_event->do_event() ) {
+ return $error;
+ }
+ }
+ return;
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm
index d142411e5..5cd40cda0 100644
--- a/FS/FS/pay_batch.pm
+++ b/FS/FS/pay_batch.pm
@@ -356,20 +356,21 @@ sub import_results {
&{$hook}(\%hash, $cust_pay_batch->hashref);
+ my $error = '';
if ( &{$approved_condition}(\%hash) ) {
- $new_cust_pay_batch->status('Approved');
+ $error = $new_cust_pay_batch->approve($hash{'paybatch'} || $self->batchnum);
+ $total += $hash{'paid'};
} elsif ( &{$declined_condition}(\%hash) ) {
- $new_cust_pay_batch->status('Declined');
+ $error = $new_cust_pay_batch->decline;
}
- my $error = $new_cust_pay_batch->replace($cust_pay_batch);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "error updating status of paybatchnum $hash{'paybatchnum'}: $error\n";
+ return $error;
}
# purge CVV when the batch is processed
@@ -379,64 +380,13 @@ sub import_results {
$conf->config('cvv-save') ) {
$new_cust_pay_batch->cust_main->remove_cvv;
}
- }
-
- if ( $new_cust_pay_batch->status =~ /Approved/i ) {
-
- my $cust_pay = new FS::cust_pay ( {
- 'custnum' => $custnum,
- 'payby' => $payby,
- 'paybatch' => $hash{'paybatch'} || $self->batchnum,
- 'payinfo' => ( $hash{'payinfo'} || $cust_pay_batch->payinfo ),
- map { $_ => $hash{$_} } (qw( paid _date )),
- } );
- $error = $cust_pay->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n";
- }
- $total += $hash{'paid'};
-
- $cust_pay->cust_main->apply_payments;
-
- } elsif ( $new_cust_pay_batch->status =~ /Declined/i ) {
-
- #false laziness w/cust_main::collect
-
- my $due_cust_event = $new_cust_pay_batch->cust_main->due_cust_event(
- #'check_freq' => '1d', #?
- 'eventtable' => 'cust_pay_batch',
- 'objects' => [ $new_cust_pay_batch ],
- );
- unless( ref($due_cust_event) ) {
- $dbh->rollback if $oldAutoCommit;
- return $due_cust_event;
- }
-
- foreach my $cust_event ( @$due_cust_event ) {
-
- #XXX lock event
-
- #re-eval event conditions (a previous event could have changed things)
- next unless $cust_event->test_conditions;
-
- if ( my $error = $cust_event->do_event() ) {
- # gah, even with transactions.
- #$dbh->commit if $oldAutoCommit; #well.
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
- }
}
- }
+ } # foreach (@all_values)
if ( defined($close_condition) ) {
# Allow the module to decide whether to close the batch.
- # This is used for TD EFT, which requires two imports before
- # closing.
# $close_condition can also die() to abort the whole import.
my $close = eval { $close_condition->($self) };
if ( $@ ) {
@@ -577,6 +527,52 @@ sub export_batch {
return $batch;
}
+sub manual_approve {
+ my $self = shift;
+ my $date = time;
+ my %opt = @_;
+ my $paybatch = $opt{'paybatch'} || $self->batchnum;
+ my $conf = FS::Conf->new;
+ return 'manual batch approval disabled'
+ if ( ! $conf->exists('batch-manual_approval') );
+ return 'batch already resolved' if $self->status eq 'R';
+ return 'batch not yet submitted' if $self->status eq 'O';
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $payments = 0;
+ foreach my $cust_pay_batch (
+ qsearch('cust_pay_batch', { batchnum => $self->batchnum,
+ status => '' })
+ ) {
+ my $new_cust_pay_batch = new FS::cust_pay_batch {
+ $cust_pay_batch->hash,
+ 'paid' => $cust_pay_batch->amount,
+ '_date' => $date,
+ };
+ my $error = $new_cust_pay_batch->approve($paybatch);
+ if ( $error ) {
+ $dbh->rollback;
+ return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error";
+ }
+ $payments++;
+ }
+ return 'no unresolved payments in batch' if $payments == 0;
+ $self->set_status('R');
+
+ $dbh->commit;
+ return;
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/pay_batch/td_eft1464.pm b/FS/FS/pay_batch/td_eft1464.pm
index 1fbf2ade2..1409364ed 100644
--- a/FS/FS/pay_batch/td_eft1464.pm
+++ b/FS/FS/pay_batch/td_eft1464.pm
@@ -49,84 +49,8 @@ my $i;
$name = 'td_eft1464';
# TD Bank EFT 1464 Byte format
-%import_info = (
- 'filetype' => 'variable',
- 'parse' => \&parse,
- 'fields' => [ qw(
- status
- paid
- paybatchnum
- ) ],
- 'hook' => sub {
- my $hash = shift;
- $hash->{'_date'} = time;
- $hash->{'paid'} = sprintf('%.2f', $hash->{'paid'});
- },
- 'approved' => sub {
- my $hash = shift;
- $hash->{'status'} eq 'A'
- },
- 'declined' => sub {
- my $hash = shift;
- $hash->{'status'} eq 'D';
- },
- 'begin_condition' => sub {
- my $hash = shift;
- $hash->{'status'} eq 'A' or $hash->{'status'} eq 'D';
- },
- 'end_condition' => sub {
- my $hash = shift;
- $hash->{'status'} eq 'END'
- },
- 'close_condition' => sub {
- my $batch = shift;
- my @cust_pay_batch = qsearch('cust_pay_batch',
- { batchnum => $batch->batchnum }
- );
- return ( (grep {! length($_->status) } @cust_pay_batch) == 0 );
- },
-);
-
-sub parse {
- my ($batch, $line) = @_;
- $batch->setfield('import_state','') if !$batch->import_state;
- return 'END' if $batch->import_state eq 'END';
- if( $batch->import_state eq '212' ) {
- # APX212 fields:
- # trace number, trans type, amount, due date, routing number,
- # account number, xref number, return routing number and account
- # The only ones we take are amount and xref number.
- if( $line =~ /CREDITS\s+DEBITS/ ) {
- $batch->setfield('import_state', 'END');
- return 'END';
- }
- $line =~ /^\d{22} D\d{3} (.{14}) \d{5} \d{4}-\d{5} .{12} (.{19}).*$/
- or die "can't parse: $line";
- # strip leading zeroes/spaces from paybatchnum at this point
- return ('A', $1, sprintf('%u',$2));
- }
- elsif( $batch->import_state eq '234' ) {
- # APX234 fields:
- # payor name, xref number, due date, routing number, account number,
- # amount, reason for return
- if( $line =~ /TOTAL NUMBER -/ ) {
- $batch->setfield('import_state', 'END');
- return 'END';
- }
- $line =~ /^.{22} (.{19}) \d\d\/\d\d\/\d\d \d{9} .{12} (.{14}).*$/
- or die "can't parse: $line";
- return ('D', $2, sprintf('%u',$1));
- }
- else {
- if ( $line =~ /ITEM TRACE NUMBER/ ) {
- $batch->setfield('import_state','212');
- }
- elsif ( $line =~ /REASON FOR RETURN/ ) {
- $batch->setfield('import_state','234');
- } # else leave it undefined
- return 'HEADER';
- }
-}
+%import_info = ( filetype => 'NONE' );
+# just to suppress warning; importing this format is a fatal error
%export_info = (
init => sub {
@@ -152,13 +76,13 @@ sub parse {
my @cust_pay_batch = @{(shift)};
my $time = $pay_batch->download || time;
my $now = sprintf("%03u%03u",
- (localtime(time))[5],#year since 1900
+ (localtime(time))[5] % 100,#year since 1900
(localtime(time))[7]+1);#day of year
# Request settlement the next day
my $duedate = time+86400;
$opt{'due'} = sprintf("%03u%03u",
- (localtime($duedate))[5],
+ (localtime($duedate))[5] % 100,
(localtime($duedate))[7]+1);
$opt{'fcn'} =
@@ -192,7 +116,7 @@ sub parse {
sprintf('%09u', $aba),
sprintf('%-12s', $account),
' ' x 22,
- ' ' x 3,
+ '0' x 3,
$opt{'shortname'},
sprintf('%-30s',
join(' ',
@@ -203,11 +127,12 @@ sub parse {
sprintf('%-19s', $cust_pay_batch->paybatchnum), # originator reference num
$opt{'retbranch'},
$opt{'retacct'},
+ ' ' x 15,
' ' x 22,
' ' x 2,
'0' x 11,
);
- return $control . $payment . (' ' x 720);
+ return sprintf('%-1464s',$control . $payment);
},
footer => sub {
my ($pay_batch, $batchcount, $batchtotal) = @_;
diff --git a/httemplate/misc/download-batch.cgi b/httemplate/misc/download-batch.cgi
index 01bf5d25f..23deba712 100644
--- a/httemplate/misc/download-batch.cgi
+++ b/httemplate/misc/download-batch.cgi
@@ -1,6 +1,4 @@
-<% $pay_batch->export_batch($format) %>
-
-<%init>
+<% $pay_batch->export_batch($format) %><%init>
#http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
http_header('Content-Type' => 'text/plain' ); # not necessarily correct...
diff --git a/httemplate/misc/process/pay_batch-approve.cgi b/httemplate/misc/process/pay_batch-approve.cgi
new file mode 100644
index 000000000..f857e2318
--- /dev/null
+++ b/httemplate/misc/process/pay_batch-approve.cgi
@@ -0,0 +1,16 @@
+% if ( $error ) {
+% $cgi->param('error', $error);
+% }
+<% $cgi->redirect(popurl(3)."search/cust_pay_batch.cgi?dcln=1;batchnum=$batchnum") %>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Process batches');
+
+my $batchnum = $cgi->param('batchnum');
+# make a record in the paybatch of who did this
+my $paybatch = 'manual-'.$FS::CurrentUser::CurrentUser->username.
+ '-' . time2str('%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
+my $pay_batch = qsearchs('pay_batch', { 'batchnum' => $batchnum })
+ or die "batchnum '$batchnum' not found";
+my $error = $pay_batch->manual_approve('paybatch' => $paybatch);
+</%init>
diff --git a/httemplate/search/cust_pay_batch.cgi b/httemplate/search/cust_pay_batch.cgi
index df635ee8d..cb101d4db 100755
--- a/httemplate/search/cust_pay_batch.cgi
+++ b/httemplate/search/cust_pay_batch.cgi
@@ -193,13 +193,19 @@ if ( $pay_batch ) {
qq!<OPTION VALUE="ach-spiritone">Spiritone ACH batch</OPTION>!.
qq!<OPTION VALUE="paymentech">Chase Paymentech XML</OPTION>!.
qq!<OPTION VALUE="RBC">Royal Bank of Canada PDS</OPTION>!.
- qq!<OPTION VALUE="td_eft1464">TD Commercial Banking EFT 1464 byte</OPTION>!.
+ qq!<OPTION VALUE="td_eftack264">TD EFT Acknowledgement</OPTION>!.
+ qq!<OPTION VALUE="td_eftret80">TD EFT Returned Items</OPTION>!.
qq!</SELECT><BR></TR>!;
}
$html_init .= qq!<INPUT TYPE="hidden" NAME="batchnum" VALUE="$batchnum">!;
$html_init .= '<TR> <INPUT TYPE="submit" VALUE="Upload"></FORM><BR> </TR>';
+ if ( $conf->exists('batch-manual_approval') and $pay_batch->status eq 'I') {
+ $html_init .= qq!<TR><INPUT TYPE="button" VALUE="Manually approve" onclick="
+if ( confirm('Approve all remaining payments in this batch?') )
+ window.location.href='${p}misc/process/pay_batch-approve.cgi?batchnum=$batchnum';"></TR>!
+ }
}
- $html_init .= '</TABLE>'
+ $html_init .= '</TABLE>';
}
if ($pay_batch) {