+ 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 $@;
+ }
+ }
+ if ( $close ) {
+ my $error = $self->set_status('R');
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+use MIME::Base64;
+use Storable 'thaw';
+use Data::Dumper;
+sub process_import_results {
+ my $job = shift;
+ my $param = thaw(decode_base64(shift));
+ $param->{'job'} = $job;
+ warn Dumper($param) if $DEBUG;
+ my $batchnum = delete $param->{'batchnum'} or die "no batchnum specified\n";
+ my $batch = FS::pay_batch->by_key($batchnum) or die "batchnum '$batchnum' not found\n";
+
+ my $gatewaynum = delete $param->{'gatewaynum'};
+ if ( $gatewaynum ) {
+ $param->{'gateway'} = FS::payment_gateway->by_key($gatewaynum)
+ or die "gatewaynum '$gatewaynum' not found\n";
+ delete $param->{'format'}; # to avoid confusion
+ }
+
+ my $file = $param->{'uploaded_files'} or die "no files provided\n";
+ $file =~ s/^(\w+):([\.\w]+)$/$2/;
+ my $dir = '%%%FREESIDE_CACHE%%%/cache.' . $FS::UID::datasrc;
+ open( $param->{'filehandle'},
+ '<',
+ "$dir/$file" )
+ or die "unable to open '$file'.\n";
+ my $error = $batch->import_results($param);
+ unlink $file;
+ die $error if $error;
+}
+
+=item import_from_gateway GATEWAY [ OPTIONS ]
+
+Import results from a L<FS::payment_gateway>, using Business::BatchPayment,
+and apply them. GATEWAY must use the Business::BatchPayment namespace.
+
+This is a class method, since results can be applied to any batch.
+The 'batch-reconsider' option determines whether an already-approved
+or declined payment can have its status changed by a later import.
+
+OPTIONS may include:
+
+- file: a file name or handle to use as a data source.
+- job: an L<FS::queue> object to update with progress messages.
+
+=cut
+
+sub import_from_gateway {
+ my $class = shift;
+ my $gateway = shift;
+ my %opt = @_;
+ my $conf = FS::Conf->new;
+
+ # unavoidable duplication with import_batch, for now
+ 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 $job = delete($opt{'job'});
+ $job->update_statustext(0) if $job;
+
+ my $total = 0;
+ return "import_from_gateway requires a payment_gateway"
+ unless eval { $gateway->isa('FS::payment_gateway') };
+
+ my %proc_opt = (
+ 'input' => $opt{'file'}, # will do nothing if it's empty
+ # any other constructor options go here
+ );
+
+ my $processor = $gateway->batch_processor(%proc_opt);
+
+ my @batches = $processor->receive;
+ my $error;
+ my $num = 0;
+
+ # whether to allow items to change status
+ my $reconsider = $conf->exists('batch-reconsider');
+
+ # mutex all affected batches
+ my %pay_batch_for_update;
+
+ BATCH: foreach my $batch (@batches) {
+ ITEM: foreach my $item ($batch->elements) {
+ # cust_pay_batch.paybatchnum should be in the 'tid' attribute
+ my $paybatchnum = $item->tid;
+ my $cust_pay_batch = FS::cust_pay_batch->by_key($paybatchnum);
+ if (!$cust_pay_batch) {
+ # XXX for one-way batch protocol this needs to create new payments
+ $error = "unknown paybatchnum $paybatchnum";
+ last ITEM;
+ }
+
+ my $batchnum = $cust_pay_batch->batchnum;
+ if ( $batch->batch_id and $batch->batch_id != $batchnum ) {
+ warn "batch ID ".$batch->batch_id.
+ " does not match batchnum ".$cust_pay_batch->batchnum."\n";
+ }
+
+ # lock the batch and check its status
+ my $pay_batch = FS::pay_batch->by_key($batchnum);
+ $pay_batch_for_update{$batchnum} ||= $pay_batch->select_for_update;
+ if ( $pay_batch->status ne 'I' and !$reconsider ) {
+ $error = "batch $batchnum no longer in transit";
+ last ITEM;