+ 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;
+ }
+
+ if ( $cust_pay_batch->status ) {
+ my $new_status = $item->approved ? 'approved' : 'declined';
+ if ( lc( $cust_pay_batch->status ) eq $new_status ) {
+ # already imported with this status, so don't touch
+ next ITEM;
+ }
+ elsif ( !$reconsider ) {
+ # then we're not allowed to change its status, so bail out
+ $error = "paybatchnum ".$item->tid.
+ " already resolved with status '". $cust_pay_batch->status . "'";
+ last ITEM;
+ }
+ }
+
+ # create a new cust_pay_batch with whatever information we got back
+ my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
+ my $new_payinfo;
+ # update payinfo, if needed
+ if ( $item->assigned_token ) {
+ $new_payinfo = $item->assigned_token;
+ } elsif ( $cust_pay_batch->payby eq 'CARD' ) {
+ $new_payinfo = $item->card_number if $item->card_number;
+ } else { #$cust_pay_batch->payby eq 'CHEK'
+ $new_payinfo = $item->account_number . '@' . $item->routing_code
+ if $item->account_number;
+ }
+ $new_cust_pay_batch->payinfo($new_payinfo) if $new_payinfo;
+
+ # set "paid" pseudo-field (transfers to cust_pay) to the actual amount
+ # paid, if the batch says it's different from the amount requested
+ if ( defined $item->amount ) {
+ $new_cust_pay_batch->paid($item->amount);
+ } else {
+ $new_cust_pay_batch->paid($cust_pay_batch->amount);
+ }
+
+ # set payment date to when it was processed
+ $new_cust_pay_batch->_date($item->payment_date->epoch)
+ if $item->payment_date;
+
+ # approval status
+ if ( $item->approved ) {
+ # follow Billing_Realtime format for paybatch
+ my $paybatch = $gateway->gatewaynum .
+ '-' .
+ $gateway->gateway_module .
+ ':' .
+ $item->authorization .
+ ':' .
+ $item->order_number;
+
+ $error = $new_cust_pay_batch->approve($paybatch);
+ $total += $new_cust_pay_batch->paid;
+ }
+ else {
+ $error = $new_cust_pay_batch->decline($item->error_message);
+ }
+ last ITEM if $error;
+ $num++;
+ $job->update_statustext(int(100 * $num/( $batch->count + 1 ) ),
+ 'Importing batch items')
+ if $job;
+ } #foreach $item
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ } #foreach $batch (input batch, not pay_batch)
+
+ # Auto-resolve
+ foreach my $pay_batch (values %pay_batch_for_update) {
+ $error = $pay_batch->try_to_resolve;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $dbh->commit if $oldAutoCommit;
+ return;
+}
+
+=item try_to_resolve
+
+Resolve this batch if possible. A batch can be resolved if all of its
+entries have a status. If the system options 'batch-auto_resolve_days'
+and 'batch-auto_resolve_status' are set, and the batch's download date is
+at least (batch-auto_resolve_days) before the current time, then it can
+be auto-resolved; entries with no status will be approved or declined
+according to the batch-auto_resolve_status setting.
+
+=cut
+
+sub try_to_resolve {
+ my $self = shift;
+ my $conf = FS::Conf->new;;
+
+ return if $self->status ne 'I';
+
+ my @unresolved = qsearch('cust_pay_batch',
+ {
+ batchnum => $self->batchnum,
+ status => ''
+ }
+ );
+
+ if ( @unresolved ) {
+ my $days = $conf->config('batch-auto_resolve_days') || '';
+ # either 'approve' or 'decline'
+ my $action = $conf->config('batch-auto_resolve_status') || '';
+ return unless
+ length($days) and
+ length($action) and
+ time > ($self->download + 86400 * $days)
+ ;
+
+ my $error;
+ foreach my $cpb (@unresolved) {
+ if ( $action eq 'approve' ) {
+ # approve it for the full amount
+ $cpb->set('paid', $cpb->amount) unless ($cpb->paid || 0) > 0;
+ $error = $cpb->approve($self->batchnum);
+ }
+ elsif ( $action eq 'decline' ) {
+ $error = $cpb->decline('No response from processor');
+ }
+ return $error if $error;
+ }
+ }
+
+ $self->set_status('R');
+}
+
+=item prepare_for_export
+
+Prepare the batch to be exported. This will:
+- Set the status to "in transit".
+- If batch-increment_expiration is set and this is a credit card batch,
+ increment expiration dates that are in the past.
+- If this is the first download for this batch, adjust payment amounts to
+ not be greater than the customer's current balance. If the customer's
+ balance is zero, the entry will be removed.
+
+Use this within a transaction.
+
+=cut
+
+sub prepare_for_export {
+ my $self = shift;
+ my $conf = FS::Conf->new;
+ my $curuser = $FS::CurrentUser::CurrentUser;