automate RBC payment batch transfer, #35228
[freeside.git] / FS / FS / pay_batch.pm
index 813d096..df969a0 100644 (file)
@@ -1,21 +1,19 @@
 package FS::pay_batch;
+use base qw( FS::Record );
 
 use strict;
-use vars qw( @ISA $DEBUG %import_info %export_info $conf );
+use vars qw( $DEBUG %import_info %export_info $conf );
+use Scalar::Util qw(blessed);
+use IO::Scalar;
+use List::Util qw(sum);
 use Time::Local;
 use Text::CSV_XS;
-use FS::Record qw( dbh qsearch qsearchs );
-use FS::Conf;
-use FS::cust_pay;
-use FS::agent;
 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);
+use FS::Record qw( dbh qsearch qsearchs );
+use FS::Conf;
+use FS::cust_pay;
 
 =head1 NAME
 
@@ -147,22 +145,10 @@ sub check {
 
 Returns the L<FS::agent> object for this batch.
 
-=cut
-
-sub agent {
-  qsearchs('agent', { 'agentnum' => $_[0]->agentnum });
-}
-
 =item cust_pay_batch
 
 Returns all L<FS::cust_pay_batch> objects for this batch.
 
-=cut
-
-sub cust_pay_batch {
-  qsearch('cust_pay_batch', { 'batchnum' => $_[0]->batchnum });
-}
-
 =item rebalance
 
 =cut
@@ -201,7 +187,7 @@ foreach my $INC (@INC) {
              \\%FS::pay_batch::$mod\::export_info,
              \$FS::pay_batch::$mod\::name)";
     $name ||= $mod; # in case it's not defined
-    if$@) {
+    if ($@) {
       # in FS::cdr this is a die, not a warn.  That's probably a bug.
       warn "error using FS::pay_batch::$mod (skipping): $@\n";
       next;
@@ -223,7 +209,9 @@ foreach my $INC (@INC) {
 
 =item import_results OPTION => VALUE, ...
 
-Import batch results.
+Import batch results. Can be called as an instance method, if you want to 
+automatically adjust status on a specific batch, or a class method, if you 
+don't know which batch(es) the results apply to.
 
 Options are:
 
@@ -234,6 +222,36 @@ I<format> - an L<FS::pay_batch> module
 I<gateway> - an L<FS::payment_gateway> object for a batch gateway.  This 
 takes precedence over I<format>.
 
+Supported format keys (defined in the specified FS::pay_batch module) are:
+
+I<filetype> - required, can be CSV, fixed, variable, XML
+
+I<fields> - required list of field names for each row/line
+
+I<formatre> - regular expression for fixed filetype
+
+I<parse> - required for variable filetype
+
+I<xmlkeys> - required for XML filetype
+
+I<xmlrow> - required for XML filetype
+
+I<begin_condition> - sub, ignore all lines before this returns true
+
+I<end_condition> - sub, stop processing lines when this returns true
+
+I<end_hook> - sub, runs immediately after end_condition returns true
+
+I<skip_condition> - sub, skip lines when this returns true
+
+I<hook> - required, sub, runs before approved/declined conditions are checked
+
+I<approved> - required, sub, returns true when approved
+
+I<declined> - required, sub, returns true when declined
+
+I<close_condition> - sub, decide whether or not to close the batch
+
 =cut
 
 sub import_results {
@@ -264,6 +282,8 @@ sub import_results {
   my $declined_condition  = $info->{'declined'};
   my $close_condition     = $info->{'close_condition'};
 
+  my %target_batches; # batches that had at least one payment updated
+
   my $csv = new Text::CSV_XS;
 
   local $SIG{HUP} = 'IGNORE';
@@ -277,13 +297,17 @@ sub import_results {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $reself = $self->select_for_update;
+  if ( ref($self) ) {
+    # if called on a specific pay_batch, check the status of that batch
+    # before continuing
+    my $reself = $self->select_for_update;
 
-  if ( $reself->status ne 'I' 
-      and !$conf->exists('batch-manual_approval') ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "batchnum ". $self->batchnum. "no longer in transit";
-  }
+    if ( $reself->status ne 'I' 
+        and !$conf->exists('batch-manual_approval') ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "batchnum ". $self->batchnum. "no longer in transit";
+    }
+  } # otherwise we can't enforce this constraint. sorry.
 
   my $total = 0;
   my $line;
@@ -329,6 +353,7 @@ sub import_results {
         push @all_values, \@values;
       }
       elsif ($filetype eq 'variable') {
+        # no longer used
         my @values = ( eval { $parse->($self, $line) } );
         if( $@ ) {
           $dbh->rollback if $oldAutoCommit;
@@ -388,6 +413,9 @@ sub import_results {
     unless ( $cust_pay_batch ) {
       return "unknown paybatchnum $hash{'paybatchnum'}\n";
     }
+    # remember that we've touched this batch
+    $target_batches{ $cust_pay_batch->batchnum } = 1;
+
     my $custnum = $cust_pay_batch->custnum,
     my $payby = $cust_pay_batch->payby,
 
@@ -401,12 +429,12 @@ sub import_results {
       foreach ('paid', '_date', 'payinfo') {
         $new_cust_pay_batch->$_($hash{$_}) if $hash{$_};
       }
-      $error = $new_cust_pay_batch->approve($hash{'paybatch'} || $self->batchnum);
+      $error = $new_cust_pay_batch->approve(%hash);
       $total += $hash{'paid'};
 
     } elsif ( &{$declined_condition}(\%hash) ) {
 
-      $error = $new_cust_pay_batch->decline;
+      $error = $new_cust_pay_batch->decline($hash{'error_message'});;
 
     }
 
@@ -427,21 +455,25 @@ sub import_results {
 
   } # foreach (@all_values)
 
-  my $close = 1;
-  if ( defined($close_condition) ) {
-    # Allow the module to decide whether to close the batch.
-    # $close_condition can also die() to abort the whole import.
-    $close = eval { $close_condition->($self) };
-    if ( $@ ) {
-      $dbh->rollback;
-      die $@;
+  # decide whether to close batches that had payments posted
+  foreach my $batchnum (keys %target_batches) {
+    my $pay_batch = FS::pay_batch->by_key($batchnum);
+    my $close = 1;
+    if ( defined($close_condition) ) {
+      # Allow the module to decide whether to close the batch.
+      # $close_condition can also die() to abort the whole import.
+      $close = eval { $close_condition->($pay_batch) };
+      if ( $@ ) {
+        $dbh->rollback;
+        die $@;
+      }
     }
-  }
-  if ( $close ) {
-    my $error = $self->set_status('R');
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
+    if ( $close ) {
+      my $error = $pay_batch->set_status('R');
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
     }
   }
 
@@ -450,12 +482,10 @@ sub import_results {
 
 }
 
-use MIME::Base64;
-use Storable 'thaw';
 use Data::Dumper;
 sub process_import_results {
   my $job = shift;
-  my $param = thaw(decode_base64(shift));
+  my $param = shift;
   $param->{'job'} = $job;
   warn Dumper($param) if $DEBUG;
   my $gatewaynum = delete $param->{'gatewaynum'};
@@ -544,7 +574,14 @@ sub import_from_gateway {
 
   my $processor = $gateway->batch_processor(%proc_opt);
 
-  my @batches = $processor->receive;
+  my @processor_ids = map { $_->processor_id } 
+                        qsearch({
+                          'table' => 'pay_batch',
+                          'hashref' => { 'status' => 'I' },
+                          'extra_sql' => q( AND processor_id != '' AND processor_id IS NOT NULL)
+                        });
+
+  my @batches = $processor->receive(@processor_ids);
 
   my $num = 0;
 
@@ -572,8 +609,6 @@ sub import_from_gateway {
       my $payby; # CARD or CHEK
       my $error;
 
-      # 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;
 
@@ -644,8 +679,11 @@ sub import_from_gateway {
             payby       => $payby,
             invnum      => $item->invoice_number,
             batchnum    => $pay_batch->batchnum,
-            paybatch    => $paybatch,
             payinfo     => $payinfo,
+            gatewaynum  => $gateway->gatewaynum,
+            processor   => $gateway->gateway_module,
+            auth        => $item->authorization,
+            order_number => $item->order_number,
           }
         );
         $error ||= $cust_pay->insert;
@@ -725,11 +763,17 @@ sub import_from_gateway {
         # approval status
         if ( $item->approved ) {
           # follow Billing_Realtime format for paybatch
-          $error = $cust_pay_batch->approve($paybatch);
+          $error = $cust_pay_batch->approve(
+            'gatewaynum'    => $gateway->gatewaynum,
+            'processor'     => $gateway->gateway_module,
+            'auth'          => $item->authorization,
+            'order_number'  => $item->order_number,
+          );
           $total += $cust_pay_batch->paid;
         }
         else {
-          $error = $cust_pay_batch->decline($item->error_message);
+          $error = $cust_pay_batch->decline($item->error_message,
+                                            $item->failure_status);
         }
 
         if ( $error ) {        
@@ -758,7 +802,7 @@ sub import_from_gateway {
       my $body = "Import from gateway ".$gateway->label."\n".$error_text;
       send_email(
         to      => $mail_on_error,
-        from    => $conf->config('invoice_from'),
+        from    => $conf->invoice_from_full(),
         subject => $subject,
         body    => $body,
       );
@@ -807,8 +851,8 @@ sub try_to_resolve {
     }
   );
 
-  if ( @unresolved ) {
-    my $days = $conf->config('batch-auto_resolve_days') || '';
+  if ( @unresolved and $conf->exists('batch-auto_resolve_days') ) {
+    my $days = $conf->config('batch-auto_resolve_days'); # can be zero
     # either 'approve' or 'decline'
     my $action = $conf->config('batch-auto_resolve_status') || '';
     return unless 
@@ -829,6 +873,9 @@ sub try_to_resolve {
       }
       return $error if $error;
     }
+  } elsif ( @unresolved ) {
+    # auto resolve is not enabled, and we're not ready to resolve
+    return;
   }
 
   $self->set_status('R');
@@ -861,6 +908,9 @@ sub prepare_for_export {
     return "error updating pay_batch status: $error\n" if $error;
   } elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) {
     $first_download = 0;
+  } elsif ($status eq 'R' && 
+           $curuser->access_right('Redownload resolved batches')) {
+    $first_download = 0;
   } else {
     die "No pending batch.\n";
   }
@@ -934,7 +984,7 @@ sub export_batch {
 
   my $info = $export_info{$format} or die "Format not found: '$format'\n";
 
-  &{$info->{'init'}}($conf) if exists($info->{'init'});
+  &{$info->{'init'}}($conf, $self->agentnum) if exists($info->{'init'});
 
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
@@ -1017,6 +1067,11 @@ sub export_to_gateway {
   );
   $processor->submit($batch);
 
+  if ($batch->processor_id) {
+    $self->set('processor_id',$batch->processor_id);
+    $self->replace;
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 }
@@ -1025,7 +1080,6 @@ sub manual_approve {
   my $self = shift;
   my $date = time;
   my %opt = @_;
-  my $paybatch = $opt{'paybatch'} || $self->batchnum;
   my $usernum = $opt{'usernum'} || die "manual approval requires a usernum";
   my $conf = FS::Conf->new;
   return 'manual batch approval disabled' 
@@ -1055,7 +1109,9 @@ sub manual_approve {
       '_date'   => $date,
       'usernum' => $usernum,
     };
-    my $error = $new_cust_pay_batch->approve($paybatch);
+    my $error = $new_cust_pay_batch->approve();
+    # there are no approval options here (authorization, order_number, etc.)
+    # because the transaction wasn't really approved
     if ( $error ) {
       $dbh->rollback;
       return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error";
@@ -1080,7 +1136,7 @@ sub _upgrade_data {
   for my $format (keys %export_info) {
     my $mod = "FS::pay_batch::$format";
     if ( $mod->can('_upgrade_gateway') 
-        and length( $conf->config("batchconfig-$format") ) ) {
+        and $conf->exists("batchconfig-$format") ) {
 
       local $@;
       my ($module, %gw_options) = $mod->_upgrade_gateway;
@@ -1109,7 +1165,7 @@ sub _upgrade_data {
 
         # and if appropriate, make it the system default
         for my $payby (qw(CARD CHEK)) {
-          if ( $conf->config("batch-fixed_format-$payby") eq $format ) {
+          if ( ($conf->config("batch-fixed_format-$payby") || '') eq $format ) {
             warn "Setting as default for $payby.\n";
             $conf->set("batch-gateway-$payby", $gateway->gatewaynum);
             $conf->delete("batch-fixed_format-$payby");