communigate provisioning phase 2: Domain:Account Defaults:Settings: RulesAllowed...
[freeside.git] / FS / FS / pay_batch.pm
index c097534..6a27554 100644 (file)
@@ -1,12 +1,12 @@
 package FS::pay_batch;
 
 use strict;
 package FS::pay_batch;
 
 use strict;
-use vars qw( @ISA );
+use vars qw( @ISA $DEBUG %import_info %export_info $conf );
 use Time::Local;
 use Text::CSV_XS;
 use FS::Record qw( dbh qsearch qsearchs );
 use FS::cust_pay;
 use Time::Local;
 use Text::CSV_XS;
 use FS::Record qw( dbh qsearch qsearchs );
 use FS::cust_pay;
-use FS::part_bill_event qw(due_events);
+use FS::Conf;
 
 @ISA = qw(FS::Record);
 
 
 @ISA = qw(FS::Record);
 
@@ -31,8 +31,8 @@ FS::pay_batch - Object methods for pay_batch records
 
 =head1 DESCRIPTION
 
 
 =head1 DESCRIPTION
 
-An FS::pay_batch object represents an example.  FS::pay_batch inherits from
-FS::Record.  The following fields are currently supported:
+An FS::pay_batch object represents an payment batch.  FS::pay_batch inherits
+from FS::Record.  The following fields are currently supported:
 
 =over 4
 
 
 =over 4
 
@@ -55,7 +55,7 @@ FS::Record.  The following fields are currently supported:
 
 =item new HASHREF
 
 
 =item new HASHREF
 
-Creates a new example.  To add the example to the database, see L<"insert">.
+Creates a new batch.  To add the batch to the database, see L<"insert">.
 
 Note that this stores the hash reference, not a distinct copy of the hash it
 points to.  You can ask the object for a copy with the I<hash> method.
 
 Note that this stores the hash reference, not a distinct copy of the hash it
 points to.  You can ask the object for a copy with the I<hash> method.
@@ -94,7 +94,7 @@ returns the error, otherwise returns false.
 
 =item check
 
 
 =item check
 
-Checks all fields to make sure this is a valid example.  If there is
+Checks all fields to make sure this is a valid batch.  If there is
 an error, returns the error, otherwise returns false.  Called by the insert
 and replace methods.
 
 an error, returns the error, otherwise returns false.  Called by the insert
 and replace methods.
 
@@ -138,6 +138,42 @@ sub set_status {
   $self->replace();
 }
 
   $self->replace();
 }
 
+# further false laziness
+
+%import_info = %export_info = ();
+foreach my $INC (@INC) {
+  warn "globbing $INC/FS/pay_batch/*.pm\n" if $DEBUG;
+  foreach my $file ( glob("$INC/FS/pay_batch/*.pm")) {
+    warn "attempting to load batch format from $file\n" if $DEBUG;
+    $file =~ /\/(\w+)\.pm$/;
+    next if !$1;
+    my $mod = $1;
+    my ($import, $export, $name) = 
+      eval "use FS::pay_batch::$mod; 
+           ( \\%FS::pay_batch::$mod\::import_info,
+             \\%FS::pay_batch::$mod\::export_info,
+             \$FS::pay_batch::$mod\::name)";
+    $name ||= $mod; # in case it's not defined
+    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;
+    }
+    if(!keys(%$import)) {
+      warn "no \%import_info found in FS::pay_batch::$mod (skipping)\n";
+    }
+    else {
+      $import_info{$name} = $import;
+    }
+    if(!keys(%$export)) {
+      warn "no \%export_info found in FS::pay_batch::$mod (skipping)\n";
+    }
+    else {
+      $export_info{$name} = $export;
+    }
+  }
+}
+
 =item import_results OPTION => VALUE, ...
 
 Import batch results.
 =item import_results OPTION => VALUE, ...
 
 Import batch results.
@@ -156,237 +192,20 @@ sub import_results {
   my $param = ref($_[0]) ? shift : { @_ };
   my $fh = $param->{'filehandle'};
   my $format = $param->{'format'};
   my $param = ref($_[0]) ? shift : { @_ };
   my $fh = $param->{'filehandle'};
   my $format = $param->{'format'};
-
-  my $filetype;      # CSV, Fixed80, Fixed264
-  my @fields;
-  my $formatre;      # for Fixed.+
-  my @values;
-  my $begin_condition;
-  my $pre_hook;
-  my $end_condition;
-  my $end_hook;
-  my $hook;
-  my $approved_condition;
-  my $declined_condition;
-
-  if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) {
-
-    $filetype = "CSV";
-
-    @fields = (
-      'paybatchnum', # Reference#:  Invoice number of the transaction
-      'paid',        # Amount:  Amount of the transaction.  Dollars and cents
-                     #          with no decimal entered.
-      '',            # Card Type:  0 - MCrd, 1 - Visa, 2 - AMEX, 3 - Discover,
-                     #             4 - Insignia, 5 - Diners/EnRoute, 6 - JCB
-      '_date',       # Transaction Date:  Date the Transaction was processed
-      'time',        # Transaction Time:  Time the transaction was processed
-      'payinfo',     # Card Number:  Card number for the transaction
-      '',            # Expiry Date:  Expiry date of the card
-      '',            # Auth#:  Authorization number entered for force post
-                     #         transaction
-      'type',        # Transaction Type:  0 - purchase, 40 - refund,
-                     #                    20 - force post
-      'result',      # Processing Result: 3 - Approval,
-                     #                    4 - Declined/Amount over limit,
-                     #                    5 - Invalid/Expired/stolen card,
-                     #                    6 - Comm Error
-      '',            # Terminal ID: Terminal ID used to process the transaction
-    );
-
-    $end_condition = sub {
-      my $hash = shift;
-      $hash->{'type'} eq '0BC';
-    };
-
-    $end_hook = sub {
-      my( $hash, $total) = @_;
-      $total = sprintf("%.2f", $total);
-      my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 100 );
-      return "Our total $total does not match bank total $batch_total!"
-        if $total != $batch_total;
-      '';
-    };
-
-    $hook = sub {
-      my $hash = shift;
-      $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
-      $hash->{'_date'} = timelocal( substr($hash->{'time'},  4, 2),
-                                    substr($hash->{'time'},  2, 2),
-                                    substr($hash->{'time'},  0, 2),
-                                    substr($hash->{'_date'}, 6, 2),
-                                    substr($hash->{'_date'}, 4, 2)-1,
-                                    substr($hash->{'_date'}, 0, 4)-1900, );
-    };
-
-    $approved_condition = sub {
-      my $hash = shift;
-      $hash->{'type'} eq '0' && $hash->{'result'} == 3;
-    };
-
-    $declined_condition = sub {
-      my $hash = shift;
-      $hash->{'type'} eq '0' && (    $hash->{'result'} == 4
-                                  || $hash->{'result'} == 5 );
-    };
-
-
-  }elsif ( $format eq 'csv-chase_canada-E-xactBatch' ) {
-
-    $filetype = "CSV";
-
-    @fields = (
-      '',            # Internal(bank) id of the transaction
-      '',            # Transaction Type:  00 - purchase,      01 - preauth,
-                     #                    02 - completion,    03 - forcepost,
-                     #                    04 - refund,        05 - auth,
-                     #                    06 - purchase corr, 07 - refund corr,
-                     #                    08 - void           09 - void return
-      '',            # gateway used to process this transaction
-      'paid',        # Amount:  Amount of the transaction.  Dollars and cents
-                     #          with decimal entered.
-      'auth',        # Auth#:  Authorization number (if approved)
-      'payinfo',     # Card Number:  Card number for the transaction
-      '',            # Expiry Date:  Expiry date of the card
-      '',            # Cardholder Name
-      'bankcode',    # Bank response code (3 alphanumeric)
-      'bankmess',    # Bank response message
-      'etgcode',     # ETG response code (2 alphanumeric)
-      'etgmess',     # ETG response message
-      '',            # Returned customer number for the transaction
-      'paybatchnum', # Reference#:  paybatch number of the transaction
-      '',            # Reference#:  Invoice number of the transaction
-      'result',      # Processing Result: Approved of Declined
-    );
-
-    $end_condition = sub {
-      '';
-    };
-
-    $hook = sub {
-      my $hash = shift;
-      my $cpb = shift;
-      $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'}); #hmmmm
-      $hash->{'_date'} = time;  # got a better one?
-      $hash->{'payinfo'} = $cpb->{'payinfo'}
-        if( substr($hash->{'payinfo'}, -4) eq substr($cpb->{'payinfo'}, -4) );
-    };
-
-    $approved_condition = sub {
-      my $hash = shift;
-      $hash->{'etgcode'} eq '00' && $hash->{'result'} eq "Approved";
-    };
-
-    $declined_condition = sub {
-      my $hash = shift;
-      $hash->{'etgcode'} ne '00' # internal processing error
-        || ( $hash->{'result'} eq "Declined" );
-    };
-
-
-  }elsif ( $format eq 'PAP' ) {
-
-    $filetype = "Fixed264";
-
-    @fields = (
-      'recordtype',  # We are interested in the 'D' or debit records
-      'batchnum',    # Record#:  batch number we used when sending the file
-      'datacenter',  # Where in the bowels of the bank the data was processed
-      'paid',        # Amount:  Amount of the transaction.  Dollars and cents
-                     #          with no decimal entered.
-      '_date',       # Transaction Date:  Date the Transaction was processed
-      'bank',        # Routing information
-      'payinfo',     # Account number for the transaction
-      'paybatchnum', # Reference#:  Invoice number of the transaction
-    );
-
-    $formatre = '^(.).{19}(.{4})(.{3})(.{10})(.{6})(.{9})(.{12}).{110}(.{19}).{71}$'; 
-
-    $end_condition = sub {
-      my $hash = shift;
-      $hash->{'recordtype'} eq 'W';
-    };
-
-    $end_hook = sub {
-      my( $hash, $total) = @_;
-      $total = sprintf("%.2f", $total);
-      my $batch_total = $hash->{'datacenter'}.$hash->{'paid'}.
-                        substr($hash->{'_date'},0,1);          # YUCK!
-      $batch_total = sprintf("%.2f", $batch_total / 100 );
-      return "Our total $total does not match bank total $batch_total!"
-        if $total != $batch_total;
-      '';
-    };
-
-    $hook = sub {
-      my $hash = shift;
-      $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
-      my $tmpdate = timelocal( 0,0,1,1,0,substr($hash->{'_date'}, 0, 3)+2000); 
-      $tmpdate += 86400*(substr($hash->{'_date'}, 3, 3)-1) ;
-      $hash->{'_date'} = $tmpdate;
-      $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'bank'};
-    };
-
-    $approved_condition = sub {
-      1;
-    };
-
-    $declined_condition = sub {
-      0;
-    };
-
-  }elsif ( $format eq 'ach-spiritone' ) {
-
-    $filetype = "CSV";
-
-    @fields = (
-      '',            # Name
-      'custnum'    , # ID:  Customer number of the transaction
-      'aba',         # ABA Number for the transaction
-      'payinfo',     # Bank Account Number for the transaction
-      '',            # Transaction Type:  27 - debit
-      'paid',        # Amount:  Amount of the transaction.  Dollars and cents
-                     #          with decimal entered.
-      '',            # Default Transaction Type
-      '',            # Default Amount:  Dollars and cents with decimal entered.
-    );
-
-    $end_condition = sub {
-      '';
-    };
-
-    $pre_hook = sub {
-      my $hash = shift;
-      my @cust_pay_batch =    # this is dodgy, it works due to autoposting
-        qsearch('cust_pay_batch', { 'custnum' => $hash->{'custnum'}+0, 
-                                    'status'  => ''
-                                  } );
-      if ( scalar(@cust_pay_batch) == 1 ) {
-        $hash->{'paybatchnum'} = $cust_pay_batch[0]->paybatchnum;
-      }else{
-        return "can't find batch payment for customer number " .$hash->{custnum};
-      }
-      '';
-    };
-
-    $hook = sub {
-      my $hash = shift;
-      $hash->{'_date'} = time;  # got a better one?
-      $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'aba'};
-    };
-
-    $approved_condition = sub {
-      1;
-    };
-
-    $declined_condition = sub {
-      0;
-    };
-
-
-  } else {
-    return "Unknown format $format";
-  }
+  my $info = $import_info{$format}
+    or die "unknown format $format";
+
+  my $filetype            = $info->{'filetype'};      # CSV or fixed
+  my @fields              = @{ $info->{'fields'}};
+  my $formatre            = $info->{'formatre'};      # for fixed
+  my @all_values;
+  my $begin_condition     = $info->{'begin_condition'};
+  my $end_condition       = $info->{'end_condition'};
+  my $end_hook            = $info->{'end_hook'};
+  my $skip_condition      = $info->{'skip_condition'};
+  my $hook                = $info->{'hook'};
+  my $approved_condition  = $info->{'approved'};
+  my $declined_condition  = $info->{'declined'};
 
   my $csv = new Text::CSV_XS;
 
 
   my $csv = new Text::CSV_XS;
 
@@ -406,54 +225,90 @@ sub import_results {
   unless ( $reself->status eq 'I' ) {
     $dbh->rollback if $oldAutoCommit;
     return "batchnum ". $self->batchnum. "no longer in transit";
   unless ( $reself->status eq 'I' ) {
     $dbh->rollback if $oldAutoCommit;
     return "batchnum ". $self->batchnum. "no longer in transit";
-  };
+  }
 
   my $error = $self->set_status('R');
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
 
   my $error = $self->set_status('R');
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
-    return $error
+    return $error;
   }
 
   my $total = 0;
   my $line;
   }
 
   my $total = 0;
   my $line;
-  while ( defined($line=<$fh>) ) {
-
-    next if $line =~ /^\s*$/; #skip blank lines
 
 
-    if ($filetype eq "CSV") {
-      $csv->parse($line) or do {
-        $dbh->rollback if $oldAutoCommit;
-        return "can't parse: ". $csv->error_input();
-      };
-      @values = $csv->fields();
-    }elsif ($filetype eq "Fixed80" || $filetype eq "Fixed264"){
-      @values = $line =~ /$formatre/;
-      unless (@values) {
-        $dbh->rollback if $oldAutoCommit;
-        return "can't parse: ". $line;
-      };
-    }else{
+  # Order of operations has been changed here.
+  # We now slurp everything into @all_values, then 
+  # process one line at a time.
+
+  if ($filetype eq 'XML') {
+    eval "use XML::Simple";
+    die $@ if $@;
+    my @xmlkeys = @{ $info->{'xmlkeys'} };  # for XML
+    my $xmlrow  = $info->{'xmlrow'};        # also for XML
+
+    # Do everything differently.
+    my $data = XML::Simple::XMLin($fh, KeepRoot => 1);
+    my $rows = $data;
+    # $xmlrow = [ RootKey, FirstLevelKey, SecondLevelKey... ]
+    $rows = $rows->{$_} foreach( @$xmlrow );
+    if(!defined($rows)) {
       $dbh->rollback if $oldAutoCommit;
       $dbh->rollback if $oldAutoCommit;
-      return "Unknown file type $filetype";
+      return "can't find rows in XML file";
     }
     }
+    $rows = [ $rows ] if ref($rows) ne 'ARRAY';
+    foreach my $row (@$rows) {
+      push @all_values, [ @{$row}{@xmlkeys}, $row ];
+    }
+  }
+  else {
+    while ( defined($line=<$fh>) ) {
+
+      next if $line =~ /^\s*$/; #skip blank lines
+
+      if ($filetype eq "CSV") {
+        $csv->parse($line) or do {
+          $dbh->rollback if $oldAutoCommit;
+          return "can't parse: ". $csv->error_input();
+        };
+        push @all_values, [ $csv->fields(), $line ];
+      }elsif ($filetype eq 'fixed'){
+        my @values = ( $line =~ /$formatre/ );
+        unless (@values) {
+          $dbh->rollback if $oldAutoCommit;
+          return "can't parse: ". $line;
+        };
+        push @values, $line;
+        push @all_values, \@values;
+      }else{
+        $dbh->rollback if $oldAutoCommit;
+        return "Unknown file type $filetype";
+      }
+    }
+  }
+
+  foreach (@all_values) {
+    my @values = @$_;
 
     my %hash;
 
     my %hash;
+    my $line = pop @values;
     foreach my $field ( @fields ) {
       my $value = shift @values;
       next unless $field;
       $hash{$field} = $value;
     }
 
     foreach my $field ( @fields ) {
       my $value = shift @values;
       next unless $field;
       $hash{$field} = $value;
     }
 
-    if ( defined($pre_hook) ) {
-      my $error = &{$pre_hook}(\%hash);
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return $error;
+    if ( defined($begin_condition) ) {
+      if ( &{$begin_condition}(\%hash, $line) ) {
+        undef $begin_condition;
+      }
+      else {
+        next;
       }
     }
 
       }
     }
 
-    if ( &{$end_condition}(\%hash) ) {
-      my $error = &{$end_hook}(\%hash, $total);
+    if ( defined($end_condition) and &{$end_condition}(\%hash, $line) ) {
+      my $error;
+      $error = &{$end_hook}(\%hash, $total, $line) if defined($end_hook);
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return $error;
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return $error;
@@ -461,6 +316,10 @@ sub import_results {
       last;
     }
 
       last;
     }
 
+    if ( defined($skip_condition) and &{$skip_condition}(\%hash, $line) ) {
+      next;
+    }
+
     my $cust_pay_batch =
       qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
     unless ( $cust_pay_batch ) {
     my $cust_pay_batch =
       qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
     unless ( $cust_pay_batch ) {
@@ -477,6 +336,20 @@ sub import_results {
 
       $new_cust_pay_batch->status('Approved');
 
 
       $new_cust_pay_batch->status('Approved');
 
+    } elsif ( &{$declined_condition}(\%hash) ) {
+
+      $new_cust_pay_batch->status('Declined');
+
+    }
+
+    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";
+    }
+
+    if ( $new_cust_pay_batch->status =~ /Approved/i ) {
+
       my $cust_pay = new FS::cust_pay ( {
         'custnum'  => $custnum,
        'payby'    => $payby,
       my $cust_pay = new FS::cust_pay ( {
         'custnum'  => $custnum,
        'payby'    => $payby,
@@ -492,34 +365,38 @@ sub import_results {
   
       $cust_pay->cust_main->apply_payments;
 
   
       $cust_pay->cust_main->apply_payments;
 
-    } elsif ( &{$declined_condition}(\%hash) ) {
+    } elsif ( $new_cust_pay_batch->status =~ /Declined/i ) {
 
 
-      $new_cust_pay_batch->status('Declined');
+      #false laziness w/cust_main::collect
 
 
-      foreach my $part_bill_event ( due_events ( $new_cust_pay_batch,
-                                                 'DCLN',
-                                                '',
-                                                '') ) {
+      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;
+      }
 
 
-        # don't run subsequent events if balance<=0
-        last if $cust_pay_batch->cust_main->balance <= 0;
+      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 = $part_bill_event->do_event($new_cust_pay_batch)) {
+       if ( my $error = $cust_event->do_event() ) {
          # gah, even with transactions.
          # gah, even with transactions.
-         $dbh->commit if $oldAutoCommit; #well.
-         return $error;
+         #$dbh->commit if $oldAutoCommit; #well.
+         $dbh->rollback if $oldAutoCommit;
+          return $error;
        }
 
       }
 
     }
 
        }
 
       }
 
     }
 
-    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";
-    }
-
   }
   
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   }
   
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -527,6 +404,91 @@ sub import_results {
 
 }
 
 
 }
 
+# Formerly httemplate/misc/download-batch.cgi
+sub export_batch {
+  my $self = shift;
+  my $conf = new FS::Conf;
+  my $format = shift || $conf->config('batch-default_format')
+               or die "No batch format configured\n";
+  my $info = $export_info{$format} or die "Format not found: '$format'\n";
+  &{$info->{'init'}}($conf) if exists($info->{'init'});
+
+  my $curuser = $FS::CurrentUser::CurrentUser;
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;  
+
+  my $first_download;
+  my $status = $self->status;
+  if ($status eq 'O') {
+    $first_download = 1;
+    my $error = $self->set_status('I');
+    die "error updating pay_batch status: $error\n" if $error;
+  } elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) {
+    $first_download = 0;
+  } else {
+    die "No pending batch.\n";
+  }
+
+  my $batch = '';
+  my $batchtotal = 0;
+  my $batchcount = 0;
+
+  my @cust_pay_batch = sort { $a->paybatchnum <=> $b->paybatchnum }
+                      qsearch('cust_pay_batch', { batchnum => $self->batchnum } );
+
+  my $h = $info->{'header'};
+  if(ref($h) eq 'CODE') {
+    $batch .= &$h($self, \@cust_pay_batch) . "\n";
+  }
+  else {
+    $batch .= $h . "\n";
+  }
+  foreach my $cust_pay_batch (@cust_pay_batch) {
+    if($first_download) {
+      my $balance = $cust_pay_batch->cust_main->balance;
+      my $error = '';
+      if($balance <= 0) { # then don't charge this customer
+        $error = $cust_pay_batch->delete;
+        undef $cust_pay_batch;
+      }
+      elsif($balance < $cust_pay_batch->amount) { # then reduce the charge to the remaining balance
+        $cust_pay_batch->amount($balance);
+        $error = $cust_pay_batch->replace;
+      }
+      # else $balance >= $cust_pay_batch->amount
+      if($error) {
+        $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+        die $error;
+      }
+    }
+    if($cust_pay_batch) { # that is, it wasn't deleted
+      $batchcount++;
+      $batchtotal += $cust_pay_batch->amount;
+      $batch .= &{$info->{'row'}}($cust_pay_batch, $self, $batchcount, $batchtotal) . "\n";
+    }
+  }
+  my $f = $info->{'footer'};
+  if(ref($f) eq 'CODE') {
+    $batch .= &$f($self, $batchcount, $batchtotal) . "\n";
+  }
+  else {
+    $batch .= $f . "\n";
+  }
+
+  if ($info->{'autopost'}) {
+    my $error = &{$info->{'autopost'}}($self, $batch);
+    if($error) {
+      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+      die $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  return $batch;
+}
+
 =back
 
 =head1 BUGS
 =back
 
 =head1 BUGS