manual batch approval and TD EFT fixes, RT#10545
authormark <mark>
Fri, 24 Dec 2010 00:40:02 +0000 (00:40 +0000)
committermark <mark>
Fri, 24 Dec 2010 00:40:02 +0000 (00:40 +0000)
FS/FS/Conf.pm
FS/FS/cust_pay_batch.pm
FS/FS/pay_batch.pm
FS/FS/pay_batch/td_eft1464.pm
httemplate/misc/download-batch.cgi
httemplate/misc/process/pay_batch-approve.cgi [new file with mode: 0644]
httemplate/search/cust_pay_batch.cgi

index 5fb5a27..132ee47 100644 (file)
@@ -2995,13 +2995,13 @@ and customer address. Include units.',
     'type'        => 'textarea',
   },
 
     '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',
   {
     'key'         => 'payment_history-years',
     'section'     => 'UI',
index 9ef1e1c..9fa1459 100644 (file)
@@ -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
 =back
 
 =head1 BUGS
index d142411..5cd40cd 100644 (file)
@@ -356,20 +356,21 @@ sub import_results {
 
     &{$hook}(\%hash, $cust_pay_batch->hashref);
 
 
     &{$hook}(\%hash, $cust_pay_batch->hashref);
 
+    my $error = '';
     if ( &{$approved_condition}(\%hash) ) {
 
     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) ) {
 
 
     } 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;
     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
     }
 
     # 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;
       }
           $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.
 
   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 ( $@ ) {
     # $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;
 }
 
   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
 =back
 
 =head1 BUGS
index c3294d1..7f58ab5 100644 (file)
@@ -76,13 +76,13 @@ $name = 'td_eft1464';
     my @cust_pay_batch = @{(shift)};
     my $time = $pay_batch->download || time;
     my $now = sprintf("%03u%03u", 
     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(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'} = 
       (localtime($duedate))[7]+1);
 
     $opt{'fcn'} = 
@@ -94,7 +94,8 @@ $name = 'td_eft1464';
       $opt{'fcn'},
       $now,
       $opt{'datacenter'},
       $opt{'fcn'},
       $now,
       $opt{'datacenter'},
-      ' ' x 1429 #filler
+      ' ' x 1429, #filler
+      "\r"
     );
   },
   row => sub {
     );
   },
   row => sub {
@@ -116,7 +117,7 @@ $name = 'td_eft1464';
       sprintf('%09u', $aba),
       sprintf('%-12s', $account),
       ' ' x 22,
       sprintf('%09u', $aba),
       sprintf('%-12s', $account),
       ' ' x 22,
-      ' ' x 3,
+      '0' x 3,
       $opt{'shortname'},
       sprintf('%-30s', 
         join(' ',
       $opt{'shortname'},
       sprintf('%-30s', 
         join(' ',
@@ -127,11 +128,12 @@ $name = 'td_eft1464';
       sprintf('%-19s', $cust_pay_batch->paybatchnum), # originator reference num
       $opt{'retbranch'},
       $opt{'retacct'}, 
       sprintf('%-19s', $cust_pay_batch->paybatchnum), # originator reference num
       $opt{'retbranch'},
       $opt{'retacct'}, 
+      ' ' x 15,
       ' ' x 22,
       ' ' x 2,
       '0' x 11,
     );
       ' ' x 22,
       ' ' x 2,
       '0' x 11,
     );
-    return $control . $payment . (' ' x 720);
+    return sprintf('%-1464s',$control . $payment) . "\r";
   },
   footer => sub {
     my ($pay_batch, $batchcount, $batchtotal) = @_;
   },
   footer => sub {
     my ($pay_batch, $batchcount, $batchtotal) = @_;
@@ -145,6 +147,7 @@ $name = 'td_eft1464';
       '0' x 14, # total of credit txns
       '0' x 8, # total of credit txns
       ' ' x 1396,
       '0' x 14, # total of credit txns
       '0' x 8, # total of credit txns
       ' ' x 1396,
+      "\r"
     )
   },
 );
     )
   },
 );
index 01bf5d2..23deba7 100644 (file)
@@ -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...
 
 #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 (file)
index 0000000..f857e23
--- /dev/null
@@ -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>
index df635ee..cb101d4 100755 (executable)
@@ -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="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>';
                     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) {
 }
 
 if ($pay_batch) {