diff options
-rw-r--r-- | FS/FS/Conf.pm | 14 | ||||
-rw-r--r-- | FS/FS/cust_pay_batch.pm | 76 | ||||
-rw-r--r-- | FS/FS/pay_batch.pm | 108 | ||||
-rw-r--r-- | FS/FS/pay_batch/td_eft1464.pm | 89 | ||||
-rw-r--r-- | httemplate/misc/download-batch.cgi | 4 | ||||
-rw-r--r-- | httemplate/misc/process/pay_batch-approve.cgi | 16 | ||||
-rwxr-xr-x | httemplate/search/cust_pay_batch.cgi | 10 |
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) { |