4 use vars qw( @ISA $DEBUG %import_info %export_info $conf );
7 use FS::Record qw( dbh qsearch qsearchs );
11 use Date::Parse qw(str2time);
12 use Business::CreditCard qw(cardtype);
13 use Scalar::Util 'blessed';
16 @ISA = qw(FS::Record);
20 FS::pay_batch - Object methods for pay_batch records
26 $record = new FS::pay_batch \%hash;
27 $record = new FS::pay_batch { 'column' => 'value' };
29 $error = $record->insert;
31 $error = $new_record->replace($old_record);
33 $error = $record->delete;
35 $error = $record->check;
39 An FS::pay_batch object represents an payment batch. FS::pay_batch inherits
40 from FS::Record. The following fields are currently supported:
44 =item batchnum - primary key
46 =item agentnum - optional agent number for agent batches
48 =item payby - CARD or CHEK
50 =item status - O (Open), I (In-transit), or R (Resolved)
65 Creates a new batch. To add the batch to the database, see L<"insert">.
67 Note that this stores the hash reference, not a distinct copy of the hash it
68 points to. You can ask the object for a copy with the I<hash> method.
72 # the new method can be inherited from FS::Record, if a table method is defined
74 sub table { 'pay_batch'; }
78 Adds this record to the database. If there is an error, returns the error,
79 otherwise returns false.
83 # the insert method can be inherited from FS::Record
87 Delete this record from the database.
91 # the delete method can be inherited from FS::Record
93 =item replace OLD_RECORD
95 Replaces the OLD_RECORD with this one in the database. If there is an error,
96 returns the error, otherwise returns false.
100 # the replace method can be inherited from FS::Record
104 Checks all fields to make sure this is a valid batch. If there is
105 an error, returns the error, otherwise returns false. Called by the insert
110 # the check method should currently be supplied - FS::Record contains some
111 # data checking routines
117 $self->ut_numbern('batchnum')
118 || $self->ut_enum('payby', [ 'CARD', 'CHEK' ])
119 || $self->ut_enum('status', [ 'O', 'I', 'R' ])
120 || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
122 return $error if $error;
129 Returns the L<FS::agent> object for this batch.
134 qsearchs('agent', { 'agentnum' => $_[0]->agentnum });
139 Returns all L<FS::cust_pay_batch> objects for this batch.
144 qsearch('cust_pay_batch', { 'batchnum' => $_[0]->batchnum });
161 $self->status(shift);
162 $self->download(time)
163 if $self->status eq 'I' && ! $self->download;
165 if $self->status eq 'R' && ! $self->upload;
169 # further false laziness
171 %import_info = %export_info = ();
172 foreach my $INC (@INC) {
173 warn "globbing $INC/FS/pay_batch/*.pm\n" if $DEBUG;
174 foreach my $file ( glob("$INC/FS/pay_batch/*.pm")) {
175 warn "attempting to load batch format from $file\n" if $DEBUG;
176 $file =~ /\/(\w+)\.pm$/;
179 my ($import, $export, $name) =
180 eval "use FS::pay_batch::$mod;
181 ( \\%FS::pay_batch::$mod\::import_info,
182 \\%FS::pay_batch::$mod\::export_info,
183 \$FS::pay_batch::$mod\::name)";
184 $name ||= $mod; # in case it's not defined
186 # in FS::cdr this is a die, not a warn. That's probably a bug.
187 warn "error using FS::pay_batch::$mod (skipping): $@\n";
190 if(!keys(%$import)) {
191 warn "no \%import_info found in FS::pay_batch::$mod (skipping)\n";
194 $import_info{$name} = $import;
196 if(!keys(%$export)) {
197 warn "no \%export_info found in FS::pay_batch::$mod (skipping)\n";
200 $export_info{$name} = $export;
205 =item import_results OPTION => VALUE, ...
207 Import batch results.
211 I<filehandle> - open filehandle of results file.
213 I<format> - an L<FS::pay_batch> module
215 I<gateway> - an L<FS::payment_gateway> object for a batch gateway. This
216 takes precedence over I<format>.
223 my $param = ref($_[0]) ? shift : { @_ };
224 my $fh = $param->{'filehandle'};
225 my $job = $param->{'job'};
226 $job->update_statustext(0) if $job;
228 my $gateway = $param->{'gateway'};
230 return $self->import_from_gateway($gateway, 'file' => $fh, 'job' => $job);
233 my $format = $param->{'format'};
234 my $info = $import_info{$format}
235 or die "unknown format $format";
237 my $conf = new FS::Conf;
239 my $filetype = $info->{'filetype'}; # CSV, fixed, variable
240 my @fields = @{ $info->{'fields'}};
241 my $formatre = $info->{'formatre'}; # for fixed
242 my $parse = $info->{'parse'}; # for variable
244 my $begin_condition = $info->{'begin_condition'};
245 my $end_condition = $info->{'end_condition'};
246 my $end_hook = $info->{'end_hook'};
247 my $skip_condition = $info->{'skip_condition'};
248 my $hook = $info->{'hook'};
249 my $approved_condition = $info->{'approved'};
250 my $declined_condition = $info->{'declined'};
251 my $close_condition = $info->{'close_condition'};
253 my $csv = new Text::CSV_XS;
255 local $SIG{HUP} = 'IGNORE';
256 local $SIG{INT} = 'IGNORE';
257 local $SIG{QUIT} = 'IGNORE';
258 local $SIG{TERM} = 'IGNORE';
259 local $SIG{TSTP} = 'IGNORE';
260 local $SIG{PIPE} = 'IGNORE';
262 my $oldAutoCommit = $FS::UID::AutoCommit;
263 local $FS::UID::AutoCommit = 0;
266 my $reself = $self->select_for_update;
268 if ( $reself->status ne 'I'
269 and !$conf->exists('batch-manual_approval') ) {
270 $dbh->rollback if $oldAutoCommit;
271 return "batchnum ". $self->batchnum. "no longer in transit";
277 if ($filetype eq 'XML') {
278 eval "use XML::Simple";
280 my @xmlkeys = @{ $info->{'xmlkeys'} }; # for XML
281 my $xmlrow = $info->{'xmlrow'}; # also for XML
283 # Do everything differently.
284 my $data = XML::Simple::XMLin($fh, KeepRoot => 1);
286 # $xmlrow = [ RootKey, FirstLevelKey, SecondLevelKey... ]
287 $rows = $rows->{$_} foreach( @$xmlrow );
288 if(!defined($rows)) {
289 $dbh->rollback if $oldAutoCommit;
290 return "can't find rows in XML file";
292 $rows = [ $rows ] if ref($rows) ne 'ARRAY';
293 foreach my $row (@$rows) {
294 push @all_values, [ @{$row}{@xmlkeys}, $row ];
298 while ( defined($line=<$fh>) ) {
300 next if $line =~ /^\s*$/; #skip blank lines
302 if ($filetype eq "CSV") {
303 $csv->parse($line) or do {
304 $dbh->rollback if $oldAutoCommit;
305 return "can't parse: ". $csv->error_input();
307 push @all_values, [ $csv->fields(), $line ];
308 }elsif ($filetype eq 'fixed'){
309 my @values = ( $line =~ /$formatre/ );
311 $dbh->rollback if $oldAutoCommit;
312 return "can't parse: ". $line;
315 push @all_values, \@values;
317 elsif ($filetype eq 'variable') {
318 my @values = ( eval { $parse->($self, $line) } );
320 $dbh->rollback if $oldAutoCommit;
324 push @all_values, \@values;
327 $dbh->rollback if $oldAutoCommit;
328 return "Unknown file type $filetype";
334 foreach (@all_values) {
337 $job->update_statustext(int(100 * $num/scalar(@all_values)));
342 my $line = pop @values;
343 foreach my $field ( @fields ) {
344 my $value = shift @values;
346 $hash{$field} = $value;
349 if ( defined($begin_condition) ) {
350 if ( &{$begin_condition}(\%hash, $line) ) {
351 undef $begin_condition;
358 if ( defined($end_condition) and &{$end_condition}(\%hash, $line) ) {
360 $error = &{$end_hook}(\%hash, $total, $line) if defined($end_hook);
362 $dbh->rollback if $oldAutoCommit;
368 if ( defined($skip_condition) and &{$skip_condition}(\%hash, $line) ) {
373 qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
374 unless ( $cust_pay_batch ) {
375 return "unknown paybatchnum $hash{'paybatchnum'}\n";
377 my $custnum = $cust_pay_batch->custnum,
378 my $payby = $cust_pay_batch->payby,
380 &{$hook}(\%hash, $cust_pay_batch->hashref);
382 my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
385 if ( &{$approved_condition}(\%hash) ) {
387 foreach ('paid', '_date', 'payinfo') {
388 $new_cust_pay_batch->$_($hash{$_}) if $hash{$_};
390 $error = $new_cust_pay_batch->approve($hash{'paybatch'} || $self->batchnum);
391 $total += $hash{'paid'};
393 } elsif ( &{$declined_condition}(\%hash) ) {
395 $error = $new_cust_pay_batch->decline;
400 $dbh->rollback if $oldAutoCommit;
404 # purge CVV when the batch is processed
405 if ( $payby =~ /^(CARD|DCRD)$/ ) {
406 my $payinfo = $hash{'payinfo'} || $cust_pay_batch->payinfo;
407 if ( ! grep { $_ eq cardtype($payinfo) }
408 $conf->config('cvv-save') ) {
409 $new_cust_pay_batch->cust_main->remove_cvv;
414 } # foreach (@all_values)
417 if ( defined($close_condition) ) {
418 # Allow the module to decide whether to close the batch.
419 # $close_condition can also die() to abort the whole import.
420 $close = eval { $close_condition->($self) };
427 my $error = $self->set_status('R');
429 $dbh->rollback if $oldAutoCommit;
434 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
442 sub process_import_results {
444 my $param = thaw(decode_base64(shift));
445 $param->{'job'} = $job;
446 warn Dumper($param) if $DEBUG;
447 my $batchnum = delete $param->{'batchnum'} or die "no batchnum specified\n";
448 my $batch = FS::pay_batch->by_key($batchnum) or die "batchnum '$batchnum' not found\n";
450 my $gatewaynum = delete $param->{'gatewaynum'};
452 $param->{'gateway'} = FS::payment_gateway->by_key($gatewaynum)
453 or die "gatewaynum '$gatewaynum' not found\n";
454 delete $param->{'format'}; # to avoid confusion
457 my $file = $param->{'uploaded_files'} or die "no files provided\n";
458 $file =~ s/^(\w+):([\.\w]+)$/$2/;
459 my $dir = '%%%FREESIDE_CACHE%%%/cache.' . $FS::UID::datasrc;
460 open( $param->{'filehandle'},
463 or die "unable to open '$file'.\n";
464 my $error = $batch->import_results($param);
466 die $error if $error;
469 =item import_from_gateway GATEWAY [ OPTIONS ]
471 Import results from a L<FS::payment_gateway>, using Business::BatchPayment,
472 and apply them. GATEWAY must use the Business::BatchPayment namespace.
474 This is a class method, since results can be applied to any batch.
475 The 'batch-reconsider' option determines whether an already-approved
476 or declined payment can have its status changed by a later import.
480 - file: a file name or handle to use as a data source.
481 - job: an L<FS::queue> object to update with progress messages.
485 sub import_from_gateway {
489 my $conf = FS::Conf->new;
491 # unavoidable duplication with import_batch, for now
492 local $SIG{HUP} = 'IGNORE';
493 local $SIG{INT} = 'IGNORE';
494 local $SIG{QUIT} = 'IGNORE';
495 local $SIG{TERM} = 'IGNORE';
496 local $SIG{TSTP} = 'IGNORE';
497 local $SIG{PIPE} = 'IGNORE';
499 my $oldAutoCommit = $FS::UID::AutoCommit;
500 local $FS::UID::AutoCommit = 0;
503 my $job = delete($opt{'job'});
504 $job->update_statustext(0) if $job;
507 return "import_from_gateway requires a payment_gateway"
508 unless eval { $gateway->isa('FS::payment_gateway') };
511 'input' => $opt{'file'}, # will do nothing if it's empty
512 # any other constructor options go here
515 my $processor = $gateway->batch_processor(%proc_opt);
517 my @batches = $processor->receive;
521 # whether to allow items to change status
522 my $reconsider = $conf->exists('batch-reconsider');
524 # mutex all affected batches
525 my %pay_batch_for_update;
527 BATCH: foreach my $batch (@batches) {
528 ITEM: foreach my $item ($batch->elements) {
529 # cust_pay_batch.paybatchnum should be in the 'tid' attribute
530 my $paybatchnum = $item->tid;
531 my $cust_pay_batch = FS::cust_pay_batch->by_key($paybatchnum);
532 if (!$cust_pay_batch) {
533 # XXX for one-way batch protocol this needs to create new payments
534 $error = "unknown paybatchnum $paybatchnum";
538 my $batchnum = $cust_pay_batch->batchnum;
539 if ( $batch->batch_id and $batch->batch_id != $batchnum ) {
540 warn "batch ID ".$batch->batch_id.
541 " does not match batchnum ".$cust_pay_batch->batchnum."\n";
544 # lock the batch and check its status
545 my $pay_batch = FS::pay_batch->by_key($batchnum);
546 $pay_batch_for_update{$batchnum} ||= $pay_batch->select_for_update;
547 if ( $pay_batch->status ne 'I' and !$reconsider ) {
548 $error = "batch $batchnum no longer in transit";
552 if ( $cust_pay_batch->status ) {
553 my $new_status = $item->approved ? 'approved' : 'declined';
554 if ( lc( $cust_pay_batch->status ) eq $new_status ) {
555 # already imported with this status, so don't touch
558 elsif ( !$reconsider ) {
559 # then we're not allowed to change its status, so bail out
560 $error = "paybatchnum ".$item->tid.
561 " already resolved with status '". $cust_pay_batch->status . "'";
566 # create a new cust_pay_batch with whatever information we got back
567 my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
569 # update payinfo, if needed
570 if ( $item->assigned_token ) {
571 $new_payinfo = $item->assigned_token;
572 } elsif ( $cust_pay_batch->payby eq 'CARD' ) {
573 $new_payinfo = $item->card_number if $item->card_number;
574 } else { #$cust_pay_batch->payby eq 'CHEK'
575 $new_payinfo = $item->account_number . '@' . $item->routing_code
576 if $item->account_number;
578 $new_cust_pay_batch->payinfo($new_payinfo) if $new_payinfo;
580 # set "paid" pseudo-field (transfers to cust_pay) to the actual amount
581 # paid, if the batch says it's different from the amount requested
582 if ( defined $item->amount ) {
583 $new_cust_pay_batch->paid($item->amount);
585 $new_cust_pay_batch->paid($cust_pay_batch->amount);
588 # set payment date to when it was processed
589 $new_cust_pay_batch->_date($item->payment_date->epoch)
590 if $item->payment_date;
593 if ( $item->approved ) {
594 # follow Billing_Realtime format for paybatch
595 my $paybatch = $gateway->gatewaynum .
597 $gateway->gateway_module .
599 $item->authorization .
603 $error = $new_cust_pay_batch->approve($paybatch);
604 $total += $new_cust_pay_batch->paid;
607 $error = $new_cust_pay_batch->decline($item->error_message);
611 $job->update_statustext(int(100 * $num/( $batch->count + 1 ) ),
612 'Importing batch items')
617 $dbh->rollback if $oldAutoCommit;
621 } #foreach $batch (input batch, not pay_batch)
624 foreach my $pay_batch (values %pay_batch_for_update) {
625 $error = $pay_batch->try_to_resolve;
628 $dbh->rollback if $oldAutoCommit;
633 $dbh->commit if $oldAutoCommit;
639 Resolve this batch if possible. A batch can be resolved if all of its
640 entries have a status. If the system options 'batch-auto_resolve_days'
641 and 'batch-auto_resolve_status' are set, and the batch's download date is
642 at least (batch-auto_resolve_days) before the current time, then it can
643 be auto-resolved; entries with no status will be approved or declined
644 according to the batch-auto_resolve_status setting.
650 my $conf = FS::Conf->new;;
652 return if $self->status ne 'I';
654 my @unresolved = qsearch('cust_pay_batch',
656 batchnum => $self->batchnum,
662 my $days = $conf->config('batch-auto_resolve_days') || '';
663 # either 'approve' or 'decline'
664 my $action = $conf->config('batch-auto_resolve_status') || '';
668 time > ($self->download + 86400 * $days)
672 foreach my $cpb (@unresolved) {
673 if ( $action eq 'approve' ) {
674 # approve it for the full amount
675 $cpb->set('paid', $cpb->amount) unless ($cpb->paid || 0) > 0;
676 $error = $cpb->approve($self->batchnum);
678 elsif ( $action eq 'decline' ) {
679 $error = $cpb->decline('No response from processor');
681 return $error if $error;
685 $self->set_status('R');
688 =item prepare_for_export
690 Prepare the batch to be exported. This will:
691 - Set the status to "in transit".
692 - If batch-increment_expiration is set and this is a credit card batch,
693 increment expiration dates that are in the past.
694 - If this is the first download for this batch, adjust payment amounts to
695 not be greater than the customer's current balance. If the customer's
696 balance is zero, the entry will be removed.
698 Use this within a transaction.
702 sub prepare_for_export {
704 my $conf = FS::Conf->new;
705 my $curuser = $FS::CurrentUser::CurrentUser;
708 my $status = $self->status;
709 if ($status eq 'O') {
711 my $error = $self->set_status('I');
712 return "error updating pay_batch status: $error\n" if $error;
713 } elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) {
716 die "No pending batch.\n";
719 my @cust_pay_batch = sort { $a->paybatchnum <=> $b->paybatchnum }
720 $self->cust_pay_batch;
722 # handle batch-increment_expiration option
723 if ( $self->payby eq 'CARD' ) {
724 my ($cmon, $cyear) = (localtime(time))[4,5];
725 foreach (@cust_pay_batch) {
726 my $etime = str2time($_->exp) or next;
727 my ($day, $mon, $year) = (localtime($etime))[3,4,5];
728 if( $conf->exists('batch-increment_expiration') ) {
729 $year++ while( $year < $cyear or ($year == $cyear and $mon <= $cmon) );
730 $_->exp( sprintf('%4u-%02u-%02u', $year + 1900, $mon+1, $day) );
732 my $error = $_->replace;
733 return $error if $error;
737 if ($first_download) { #remove or reduce entries if customer's balance changed
739 foreach my $cust_pay_batch (@cust_pay_batch) {
741 my $balance = $cust_pay_batch->cust_main->balance;
742 if ($balance <= 0) { # then don't charge this customer
743 my $error = $cust_pay_batch->delete;
744 return $error if $error;
745 } elsif ($balance < $cust_pay_batch->amount) {
746 # reduce the charge to the remaining balance
747 $cust_pay_batch->amount($balance);
748 my $error = $cust_pay_batch->replace;
749 return $error if $error;
751 # else $balance >= $cust_pay_batch->amount
753 } #if $first_download
758 =item export_batch [ format => FORMAT | gateway => GATEWAY ]
760 Export batch for processing. FORMAT is the name of an L<FS::pay_batch>
761 module, in which case the configuration options are in 'batchconfig-FORMAT'.
763 Alternatively, GATEWAY can be an L<FS::payment_gateway> object set to a
764 L<Business::BatchPayment> module.
772 my $conf = new FS::Conf;
775 my $gateway = $opt{'gateway'};
777 # welcome to the future
778 my $fh = IO::Scalar->new(\$batch);
779 $self->export_to_gateway($gateway, 'file' => $fh);
783 my $format = $opt{'format'} || $conf->config('batch-default_format')
784 or die "No batch format configured\n";
786 my $info = $export_info{$format} or die "Format not found: '$format'\n";
788 &{$info->{'init'}}($conf) if exists($info->{'init'});
790 my $oldAutoCommit = $FS::UID::AutoCommit;
791 local $FS::UID::AutoCommit = 0;
794 my $error = $self->prepare_for_export;
796 die $error if $error;
800 my @cust_pay_batch = $self->cust_pay_batch;
802 my $delim = exists($info->{'delimiter'}) ? $info->{'delimiter'} : "\n";
804 my $h = $info->{'header'};
805 if (ref($h) eq 'CODE') {
806 $batch .= &$h($self, \@cust_pay_batch). $delim;
808 $batch .= $h. $delim;
811 foreach my $cust_pay_batch (@cust_pay_batch) {
813 $batchtotal += $cust_pay_batch->amount;
815 &{$info->{'row'}}($cust_pay_batch, $self, $batchcount, $batchtotal).
819 my $f = $info->{'footer'};
820 if (ref($f) eq 'CODE') {
821 $batch .= &$f($self, $batchcount, $batchtotal). $delim;
823 $batch .= $f. $delim;
826 if ($info->{'autopost'}) {
827 my $error = &{$info->{'autopost'}}($self, $batch);
829 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
834 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
838 =item export_to_gateway GATEWAY OPTIONS
840 Given L<FS::payment_gateway> GATEWAY, export the items in this batch to
841 that gateway via Business::BatchPayment. OPTIONS may include:
843 - file: override the default transport and write to this file (name or handle)
847 sub export_to_gateway {
849 my ($self, $gateway, %opt) = @_;
851 my $oldAutoCommit = $FS::UID::AutoCommit;
852 local $FS::UID::AutoCommit = 0;
855 my $error = $self->prepare_for_export;
856 die $error if $error;
859 'output' => $opt{'file'}, # will do nothing if it's empty
860 # any other constructor options go here
862 my $processor = $gateway->batch_processor(%proc_opt);
864 my @items = map { $_->request_item } $self->cust_pay_batch;
865 my $batch = Business::BatchPayment->create(Batch =>
866 batch_id => $self->batchnum,
869 $processor->submit($batch);
871 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
879 my $paybatch = $opt{'paybatch'} || $self->batchnum;
880 my $usernum = $opt{'usernum'} || die "manual approval requires a usernum";
881 my $conf = FS::Conf->new;
882 return 'manual batch approval disabled'
883 if ( ! $conf->exists('batch-manual_approval') );
884 return 'batch already resolved' if $self->status eq 'R';
885 return 'batch not yet submitted' if $self->status eq 'O';
887 local $SIG{HUP} = 'IGNORE';
888 local $SIG{INT} = 'IGNORE';
889 local $SIG{QUIT} = 'IGNORE';
890 local $SIG{TERM} = 'IGNORE';
891 local $SIG{TSTP} = 'IGNORE';
892 local $SIG{PIPE} = 'IGNORE';
894 my $oldAutoCommit = $FS::UID::AutoCommit;
895 local $FS::UID::AutoCommit = 0;
899 foreach my $cust_pay_batch (
900 qsearch('cust_pay_batch', { batchnum => $self->batchnum,
903 my $new_cust_pay_batch = new FS::cust_pay_batch {
904 $cust_pay_batch->hash,
905 'paid' => $cust_pay_batch->amount,
907 'usernum' => $usernum,
909 my $error = $new_cust_pay_batch->approve($paybatch);
912 return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error";
916 $self->set_status('R');
922 # Set up configuration for gateways that have a Business::BatchPayment
925 eval "use Class::MOP;";
927 warn "Moose/Class::MOP not available.\n$@\nSkipping pay_batch upgrade.\n";
930 my $conf = FS::Conf->new;
931 for my $format (keys %export_info) {
932 my $mod = "FS::pay_batch::$format";
933 if ( $mod->can('_upgrade_gateway')
934 and length( $conf->config("batchconfig-$format") ) ) {
937 my ($module, %gw_options) = $mod->_upgrade_gateway;
938 my $gateway = FS::payment_gateway->new({
939 gateway_namespace => 'Business::BatchPayment',
940 gateway_module => $module,
942 my $error = $gateway->insert(%gw_options);
944 warn "Failed to migrate '$format' to a Business::BatchPayment::$module gateway:\n$error\n";
948 # test whether it loads
949 my $processor = eval { $gateway->batch_processor };
951 warn "Couldn't load Business::BatchPayment module for '$format'.\n";
952 # if not, remove it so it doesn't hang around and break things
956 # remove the batchconfig-*
957 warn "Created Business::BatchPayment gateway '".$gateway->label.
958 "' for '$format' batch processing.\n";
959 $conf->delete("batchconfig-$format");
961 # and if appropriate, make it the system default
962 for my $payby (qw(CARD CHEK)) {
963 if ( $conf->config("batch-fixed_format-$payby") eq $format ) {
964 warn "Setting as default for $payby.\n";
965 $conf->set("batch-gateway-$payby", $gateway->gatewaynum);
966 $conf->delete("batch-fixed_format-$payby");
970 } #if can('_upgrade_gateway') and batchconfig-$format
980 status is somewhat redundant now that download and upload exist
984 L<FS::Record>, schema.html from the base documentation.