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';
15 use FS::Misc qw(send_email); # for error notification
16 use List::Util qw(sum);
18 @ISA = qw(FS::Record);
22 FS::pay_batch - Object methods for pay_batch records
28 $record = new FS::pay_batch \%hash;
29 $record = new FS::pay_batch { 'column' => 'value' };
31 $error = $record->insert;
33 $error = $new_record->replace($old_record);
35 $error = $record->delete;
37 $error = $record->check;
41 An FS::pay_batch object represents an payment batch. FS::pay_batch inherits
42 from FS::Record. The following fields are currently supported:
46 =item batchnum - primary key
48 =item agentnum - optional agent number for agent batches
50 =item payby - CARD or CHEK
52 =item status - O (Open), I (In-transit), or R (Resolved)
54 =item download - time when the batch was first downloaded
56 =item upload - time when the batch was first uploaded
58 =item title - unique batch identifier
60 For incoming batches, the combination of 'title', 'payby', and 'agentnum'
71 Creates a new batch. To add the batch to the database, see L<"insert">.
73 Note that this stores the hash reference, not a distinct copy of the hash it
74 points to. You can ask the object for a copy with the I<hash> method.
78 # the new method can be inherited from FS::Record, if a table method is defined
80 sub table { 'pay_batch'; }
84 Adds this record to the database. If there is an error, returns the error,
85 otherwise returns false.
89 # the insert method can be inherited from FS::Record
93 Delete this record from the database.
97 # the delete method can be inherited from FS::Record
99 =item replace OLD_RECORD
101 Replaces the OLD_RECORD with this one in the database. If there is an error,
102 returns the error, otherwise returns false.
106 # the replace method can be inherited from FS::Record
110 Checks all fields to make sure this is a valid batch. If there is
111 an error, returns the error, otherwise returns false. Called by the insert
116 # the check method should currently be supplied - FS::Record contains some
117 # data checking routines
123 $self->ut_numbern('batchnum')
124 || $self->ut_enum('payby', [ 'CARD', 'CHEK' ])
125 || $self->ut_enum('status', [ 'O', 'I', 'R' ])
126 || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
127 || $self->ut_alphan('title')
129 return $error if $error;
131 if ( $self->title ) {
133 grep { !$self->batchnum or $_->batchnum != $self->batchnum }
134 qsearch('pay_batch', {
135 payby => $self->payby,
136 agentnum => $self->agentnum,
137 title => $self->title,
139 return "Batch already exists as batchnum ".$existing[0]->batchnum
148 Returns the L<FS::agent> object for this batch.
153 qsearchs('agent', { 'agentnum' => $_[0]->agentnum });
158 Returns all L<FS::cust_pay_batch> objects for this batch.
163 qsearch('cust_pay_batch', { 'batchnum' => $_[0]->batchnum });
180 $self->status(shift);
181 $self->download(time)
182 if $self->status eq 'I' && ! $self->download;
184 if $self->status eq 'R' && ! $self->upload;
188 # further false laziness
190 %import_info = %export_info = ();
191 foreach my $INC (@INC) {
192 warn "globbing $INC/FS/pay_batch/*.pm\n" if $DEBUG;
193 foreach my $file ( glob("$INC/FS/pay_batch/*.pm")) {
194 warn "attempting to load batch format from $file\n" if $DEBUG;
195 $file =~ /\/(\w+)\.pm$/;
198 my ($import, $export, $name) =
199 eval "use FS::pay_batch::$mod;
200 ( \\%FS::pay_batch::$mod\::import_info,
201 \\%FS::pay_batch::$mod\::export_info,
202 \$FS::pay_batch::$mod\::name)";
203 $name ||= $mod; # in case it's not defined
205 # in FS::cdr this is a die, not a warn. That's probably a bug.
206 warn "error using FS::pay_batch::$mod (skipping): $@\n";
209 if(!keys(%$import)) {
210 warn "no \%import_info found in FS::pay_batch::$mod (skipping)\n";
213 $import_info{$name} = $import;
215 if(!keys(%$export)) {
216 warn "no \%export_info found in FS::pay_batch::$mod (skipping)\n";
219 $export_info{$name} = $export;
224 =item import_results OPTION => VALUE, ...
226 Import batch results.
230 I<filehandle> - open filehandle of results file.
232 I<format> - an L<FS::pay_batch> module
234 I<gateway> - an L<FS::payment_gateway> object for a batch gateway. This
235 takes precedence over I<format>.
242 my $param = ref($_[0]) ? shift : { @_ };
243 my $fh = $param->{'filehandle'};
244 my $job = $param->{'job'};
245 $job->update_statustext(0) if $job;
247 my $format = $param->{'format'};
248 my $info = $import_info{$format}
249 or die "unknown format $format";
251 my $conf = new FS::Conf;
253 my $filetype = $info->{'filetype'}; # CSV, fixed, variable
254 my @fields = @{ $info->{'fields'}};
255 my $formatre = $info->{'formatre'}; # for fixed
256 my $parse = $info->{'parse'}; # for variable
258 my $begin_condition = $info->{'begin_condition'};
259 my $end_condition = $info->{'end_condition'};
260 my $end_hook = $info->{'end_hook'};
261 my $skip_condition = $info->{'skip_condition'};
262 my $hook = $info->{'hook'};
263 my $approved_condition = $info->{'approved'};
264 my $declined_condition = $info->{'declined'};
265 my $close_condition = $info->{'close_condition'};
267 my $csv = new Text::CSV_XS;
269 local $SIG{HUP} = 'IGNORE';
270 local $SIG{INT} = 'IGNORE';
271 local $SIG{QUIT} = 'IGNORE';
272 local $SIG{TERM} = 'IGNORE';
273 local $SIG{TSTP} = 'IGNORE';
274 local $SIG{PIPE} = 'IGNORE';
276 my $oldAutoCommit = $FS::UID::AutoCommit;
277 local $FS::UID::AutoCommit = 0;
280 my $reself = $self->select_for_update;
282 if ( $reself->status ne 'I'
283 and !$conf->exists('batch-manual_approval') ) {
284 $dbh->rollback if $oldAutoCommit;
285 return "batchnum ". $self->batchnum. "no longer in transit";
291 if ($filetype eq 'XML') {
292 eval "use XML::Simple";
294 my @xmlkeys = @{ $info->{'xmlkeys'} }; # for XML
295 my $xmlrow = $info->{'xmlrow'}; # also for XML
297 # Do everything differently.
298 my $data = XML::Simple::XMLin($fh, KeepRoot => 1);
300 # $xmlrow = [ RootKey, FirstLevelKey, SecondLevelKey... ]
301 $rows = $rows->{$_} foreach( @$xmlrow );
302 if(!defined($rows)) {
303 $dbh->rollback if $oldAutoCommit;
304 return "can't find rows in XML file";
306 $rows = [ $rows ] if ref($rows) ne 'ARRAY';
307 foreach my $row (@$rows) {
308 push @all_values, [ @{$row}{@xmlkeys}, $row ];
312 while ( defined($line=<$fh>) ) {
314 next if $line =~ /^\s*$/; #skip blank lines
316 if ($filetype eq "CSV") {
317 $csv->parse($line) or do {
318 $dbh->rollback if $oldAutoCommit;
319 return "can't parse: ". $csv->error_input();
321 push @all_values, [ $csv->fields(), $line ];
322 }elsif ($filetype eq 'fixed'){
323 my @values = ( $line =~ /$formatre/ );
325 $dbh->rollback if $oldAutoCommit;
326 return "can't parse: ". $line;
329 push @all_values, \@values;
331 elsif ($filetype eq 'variable') {
332 my @values = ( eval { $parse->($self, $line) } );
334 $dbh->rollback if $oldAutoCommit;
338 push @all_values, \@values;
341 $dbh->rollback if $oldAutoCommit;
342 return "Unknown file type $filetype";
348 foreach (@all_values) {
351 $job->update_statustext(int(100 * $num/scalar(@all_values)));
356 my $line = pop @values;
357 foreach my $field ( @fields ) {
358 my $value = shift @values;
360 $hash{$field} = $value;
363 if ( defined($begin_condition) ) {
364 if ( &{$begin_condition}(\%hash, $line) ) {
365 undef $begin_condition;
372 if ( defined($end_condition) and &{$end_condition}(\%hash, $line) ) {
374 $error = &{$end_hook}(\%hash, $total, $line) if defined($end_hook);
376 $dbh->rollback if $oldAutoCommit;
382 if ( defined($skip_condition) and &{$skip_condition}(\%hash, $line) ) {
387 qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
388 unless ( $cust_pay_batch ) {
389 return "unknown paybatchnum $hash{'paybatchnum'}\n";
391 my $custnum = $cust_pay_batch->custnum,
392 my $payby = $cust_pay_batch->payby,
394 &{$hook}(\%hash, $cust_pay_batch->hashref);
396 my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
399 if ( &{$approved_condition}(\%hash) ) {
401 foreach ('paid', '_date', 'payinfo') {
402 $new_cust_pay_batch->$_($hash{$_}) if $hash{$_};
404 $error = $new_cust_pay_batch->approve(%hash);
405 $total += $hash{'paid'};
407 } elsif ( &{$declined_condition}(\%hash) ) {
409 $error = $new_cust_pay_batch->decline($hash{'error_message'});;
414 $dbh->rollback if $oldAutoCommit;
418 # purge CVV when the batch is processed
419 if ( $payby =~ /^(CARD|DCRD)$/ ) {
420 my $payinfo = $hash{'payinfo'} || $cust_pay_batch->payinfo;
421 if ( ! grep { $_ eq cardtype($payinfo) }
422 $conf->config('cvv-save') ) {
423 $new_cust_pay_batch->cust_main->remove_cvv;
428 } # foreach (@all_values)
431 if ( defined($close_condition) ) {
432 # Allow the module to decide whether to close the batch.
433 # $close_condition can also die() to abort the whole import.
434 $close = eval { $close_condition->($self) };
441 my $error = $self->set_status('R');
443 $dbh->rollback if $oldAutoCommit;
448 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
456 sub process_import_results {
458 my $param = thaw(decode_base64(shift));
459 $param->{'job'} = $job;
460 warn Dumper($param) if $DEBUG;
461 my $gatewaynum = delete $param->{'gatewaynum'};
463 $param->{'gateway'} = FS::payment_gateway->by_key($gatewaynum)
464 or die "gatewaynum '$gatewaynum' not found\n";
465 delete $param->{'format'}; # to avoid confusion
468 my $file = $param->{'uploaded_files'} or die "no files provided\n";
469 $file =~ s/^(\w+):([\.\w]+)$/$2/;
470 my $dir = '%%%FREESIDE_CACHE%%%/cache.' . $FS::UID::datasrc;
471 open( $param->{'filehandle'},
474 or die "unable to open '$file'.\n";
477 if ( $param->{gateway} ) {
478 $error = FS::pay_batch->import_from_gateway(%$param);
480 my $batchnum = delete $param->{'batchnum'} or die "no batchnum specified\n";
481 my $batch = FS::pay_batch->by_key($batchnum) or die "batchnum '$batchnum' not found\n";
482 $error = $batch->import_results($param);
485 die $error if $error;
488 =item import_from_gateway [ OPTIONS ]
490 Import results from a L<FS::payment_gateway>, using Business::BatchPayment,
491 and apply them. GATEWAY must use the Business::BatchPayment namespace.
493 This is a class method, since results can be applied to any batch.
494 The 'batch-reconsider' option determines whether an already-approved
495 or declined payment can have its status changed by a later import.
499 - gateway: the L<FS::payment_gateway>, required
500 - filehandle: a file name or handle to use as a data source.
501 - job: an L<FS::queue> object to update with progress messages.
505 sub import_from_gateway {
508 my $gateway = $opt{'gateway'};
509 my $conf = FS::Conf->new;
511 # unavoidable duplication with import_batch, for now
512 local $SIG{HUP} = 'IGNORE';
513 local $SIG{INT} = 'IGNORE';
514 local $SIG{QUIT} = 'IGNORE';
515 local $SIG{TERM} = 'IGNORE';
516 local $SIG{TSTP} = 'IGNORE';
517 local $SIG{PIPE} = 'IGNORE';
519 my $oldAutoCommit = $FS::UID::AutoCommit;
520 local $FS::UID::AutoCommit = 0;
523 my $job = delete($opt{'job'});
524 $job->update_statustext(0) if $job;
527 return "import_from_gateway requires a payment_gateway"
528 unless eval { $gateway->isa('FS::payment_gateway') };
531 'input' => $opt{'filehandle'}, # will do nothing if it's empty
532 # any other constructor options go here
536 my $mail_on_error = $conf->config('batch-errors_to');
537 if ( $mail_on_error ) {
538 # construct error trap
539 $proc_opt{'on_parse_error'} = sub {
540 my ($self, $line, $error) = @_;
541 push @item_errors, " '$line'\n$error";
545 my $processor = $gateway->batch_processor(%proc_opt);
547 my @batches = $processor->receive;
551 my $total_items = sum( map{$_->count} @batches);
553 # whether to allow items to change status
554 my $reconsider = $conf->exists('batch-reconsider');
556 # mutex all affected batches
557 my %pay_batch_for_update;
559 my %bop2payby = (CC => 'CARD', ECHECK => 'CHEK');
561 BATCH: foreach my $batch (@batches) {
563 my %incoming_batch = (
568 ITEM: foreach my $item ($batch->elements) {
570 my $cust_pay_batch; # the new batch entry (with status)
571 my $pay_batch; # the freeside batch it belongs to
572 my $payby; # CARD or CHEK
575 my $paybatch = $gateway->gatewaynum . '-' . $gateway->gateway_module .
576 ':' . $item->authorization . ':' . $item->order_number;
578 if ( $batch->incoming ) {
579 # This is a one-way batch.
580 # Locate the customer, find an open batch correct for them,
581 # create a payment. Don't bother creating a cust_pay_batch
584 if ( defined($item->customer_id)
585 and $item->customer_id =~ /^\d+$/
586 and $item->customer_id > 0 ) {
588 $cust_main = FS::cust_main->by_key($item->customer_id)
589 || qsearchs('cust_main',
590 { 'agent_custid' => $item->customer_id }
593 push @item_errors, "Unknown customer_id ".$item->customer_id;
598 push @item_errors, "Illegal customer_id '".$item->customer_id."'";
601 # it may also make sense to allow selecting the customer by
602 # invoice_number, but no modules currently work that way
604 $payby = $bop2payby{ $item->payment_type };
606 $agentnum = $cust_main->agentnum if $conf->exists('batch-spoolagent');
608 # create a batch if necessary
609 $pay_batch = $incoming_batch{$payby}->{$agentnum} ||=
611 status => 'R', # pre-resolve it
613 agentnum => $agentnum,
615 title => $batch->batch_id,
617 if ( !$pay_batch->batchnum ) {
618 $error = $pay_batch->insert;
619 die $error if $error; # can't do anything if this fails
622 if ( !$item->approved ) {
623 $error ||= "payment rejected - ".$item->error_message;
625 if ( !defined($item->amount) or $item->amount <= 0 ) {
626 $error ||= "no amount in item $num";
630 if ( $item->check_number ) {
631 $payby = 'BILL'; # right?
632 $payinfo = $item->check_number;
633 } elsif ( $item->assigned_token ) {
634 $payinfo = $item->assigned_token;
637 my $cust_pay = FS::cust_pay->new(
639 custnum => $cust_main->custnum,
640 _date => $item->payment_date->epoch,
641 paid => sprintf('%.2f',$item->amount),
643 invnum => $item->invoice_number,
644 batchnum => $pay_batch->batchnum,
646 gatewaynum => $gateway->gatewaynum,
647 processor => $gateway->gateway_module,
648 auth => $item->authorization,
649 order_number => $item->order_number,
652 $error ||= $cust_pay->insert;
653 eval { $cust_main->apply_payments };
657 push @item_errors, 'Payment for customer '.$item->customer_id."\n$error";
661 # This is a request/reply batch.
662 # Locate the request (the 'tid' attribute is the paybatchnum).
663 my $paybatchnum = $item->tid;
664 $cust_pay_batch = FS::cust_pay_batch->by_key($paybatchnum);
665 if (!$cust_pay_batch) {
666 push @item_errors, "paybatchnum $paybatchnum not found";
669 $payby = $cust_pay_batch->payby;
671 my $batchnum = $cust_pay_batch->batchnum;
672 if ( $batch->batch_id and $batch->batch_id != $batchnum ) {
673 warn "batch ID ".$batch->batch_id.
674 " does not match batchnum ".$cust_pay_batch->batchnum."\n";
677 # lock the batch and check its status
678 $pay_batch = FS::pay_batch->by_key($batchnum);
679 $pay_batch_for_update{$batchnum} ||= $pay_batch->select_for_update;
680 if ( $pay_batch->status ne 'I' and !$reconsider ) {
681 $error = "batch $batchnum no longer in transit";
684 if ( $cust_pay_batch->status ) {
685 my $new_status = $item->approved ? 'approved' : 'declined';
686 if ( lc( $cust_pay_batch->status ) eq $new_status ) {
687 # already imported with this status, so don't touch
690 elsif ( !$reconsider ) {
691 # then we're not allowed to change its status, so bail out
692 $error = "paybatchnum ".$item->tid.
693 " already resolved with status '". $cust_pay_batch->status . "'";
698 push @item_errors, "Payment for customer ".$cust_pay_batch->custnum."\n$error";
703 # update payinfo, if needed
704 if ( $item->assigned_token ) {
705 $new_payinfo = $item->assigned_token;
706 } elsif ( $payby eq 'CARD' ) {
707 $new_payinfo = $item->card_number if $item->card_number;
708 } else { #$payby eq 'CHEK'
709 $new_payinfo = $item->account_number . '@' . $item->routing_code
710 if $item->account_number;
712 $cust_pay_batch->set('payinfo', $new_payinfo) if $new_payinfo;
714 # set "paid" pseudo-field (transfers to cust_pay) to the actual amount
715 # paid, if the batch says it's different from the amount requested
716 if ( defined $item->amount ) {
717 $cust_pay_batch->set('paid', $item->amount);
719 $cust_pay_batch->set('paid', $cust_pay_batch->amount);
722 # set payment date to when it was processed
723 $cust_pay_batch->_date($item->payment_date->epoch)
724 if $item->payment_date;
727 if ( $item->approved ) {
728 # follow Billing_Realtime format for paybatch
729 $error = $cust_pay_batch->approve(
730 'gatewaynum' => $gateway->gatewaynum,
731 'processor' => $gateway->gateway_module,
732 'auth' => $item->authorization,
733 'order_number' => $item->order_number,
735 $total += $cust_pay_batch->paid;
738 $error = $cust_pay_batch->decline($item->error_message,
739 $item->failure_status);
743 push @item_errors, "Payment for customer ".$cust_pay_batch->custnum."\n$error";
749 $job->update_statustext(int(100 * $num/( $total_items ) ),
750 'Importing batch items')
755 } #foreach $batch (input batch, not pay_batch)
757 # Format an error message
758 if ( @item_errors ) {
759 my $error_text = join("\n\n",
760 "Errors during batch import: ".scalar(@item_errors),
763 if ( $mail_on_error ) {
764 my $subject = "Batch import errors"; #?
765 my $body = "Import from gateway ".$gateway->label."\n".$error_text;
767 to => $mail_on_error,
768 from => $conf->config('invoice_from'),
774 $dbh->rollback if $oldAutoCommit;
779 # Auto-resolve (with brute-force error handling)
780 foreach my $pay_batch (values %pay_batch_for_update) {
781 my $error = $pay_batch->try_to_resolve;
784 $dbh->rollback if $oldAutoCommit;
789 $dbh->commit if $oldAutoCommit;
795 Resolve this batch if possible. A batch can be resolved if all of its
796 entries have status. If the system options 'batch-auto_resolve_days'
797 and 'batch-auto_resolve_status' are set, and the batch's download date is
798 at least (batch-auto_resolve_days) before the current time, then it can
799 be auto-resolved; entries with no status will be approved or declined
800 according to the batch-auto_resolve_status setting.
806 my $conf = FS::Conf->new;;
808 return if $self->status ne 'I';
810 my @unresolved = qsearch('cust_pay_batch',
812 batchnum => $self->batchnum,
817 if ( @unresolved and $conf->exists('batch-auto_resolve_days') ) {
818 my $days = $conf->config('batch-auto_resolve_days'); # can be zero
819 # either 'approve' or 'decline'
820 my $action = $conf->config('batch-auto_resolve_status') || '';
824 time > ($self->download + 86400 * $days)
828 foreach my $cpb (@unresolved) {
829 if ( $action eq 'approve' ) {
830 # approve it for the full amount
831 $cpb->set('paid', $cpb->amount) unless ($cpb->paid || 0) > 0;
832 $error = $cpb->approve($self->batchnum);
834 elsif ( $action eq 'decline' ) {
835 $error = $cpb->decline('No response from processor');
837 return $error if $error;
839 } elsif ( @unresolved ) {
840 # auto resolve is not enabled, and we're not ready to resolve
844 $self->set_status('R');
847 =item prepare_for_export
849 Prepare the batch to be exported. This will:
850 - Set the status to "in transit".
851 - If batch-increment_expiration is set and this is a credit card batch,
852 increment expiration dates that are in the past.
853 - If this is the first download for this batch, adjust payment amounts to
854 not be greater than the customer's current balance. If the customer's
855 balance is zero, the entry will be removed.
857 Use this within a transaction.
861 sub prepare_for_export {
863 my $conf = FS::Conf->new;
864 my $curuser = $FS::CurrentUser::CurrentUser;
867 my $status = $self->status;
868 if ($status eq 'O') {
870 my $error = $self->set_status('I');
871 return "error updating pay_batch status: $error\n" if $error;
872 } elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) {
874 } elsif ($status eq 'R' &&
875 $curuser->access_right('Redownload resolved batches')) {
878 die "No pending batch.\n";
881 my @cust_pay_batch = sort { $a->paybatchnum <=> $b->paybatchnum }
882 $self->cust_pay_batch;
884 # handle batch-increment_expiration option
885 if ( $self->payby eq 'CARD' ) {
886 my ($cmon, $cyear) = (localtime(time))[4,5];
887 foreach (@cust_pay_batch) {
888 my $etime = str2time($_->exp) or next;
889 my ($day, $mon, $year) = (localtime($etime))[3,4,5];
890 if( $conf->exists('batch-increment_expiration') ) {
891 $year++ while( $year < $cyear or ($year == $cyear and $mon <= $cmon) );
892 $_->exp( sprintf('%4u-%02u-%02u', $year + 1900, $mon+1, $day) );
894 my $error = $_->replace;
895 return $error if $error;
899 if ($first_download) { #remove or reduce entries if customer's balance changed
901 foreach my $cust_pay_batch (@cust_pay_batch) {
903 my $balance = $cust_pay_batch->cust_main->balance;
904 if ($balance <= 0) { # then don't charge this customer
905 my $error = $cust_pay_batch->delete;
906 return $error if $error;
907 } elsif ($balance < $cust_pay_batch->amount) {
908 # reduce the charge to the remaining balance
909 $cust_pay_batch->amount($balance);
910 my $error = $cust_pay_batch->replace;
911 return $error if $error;
913 # else $balance >= $cust_pay_batch->amount
915 } #if $first_download
920 =item export_batch [ format => FORMAT | gateway => GATEWAY ]
922 Export batch for processing. FORMAT is the name of an L<FS::pay_batch>
923 module, in which case the configuration options are in 'batchconfig-FORMAT'.
925 Alternatively, GATEWAY can be an L<FS::payment_gateway> object set to a
926 L<Business::BatchPayment> module.
934 my $conf = new FS::Conf;
937 my $gateway = $opt{'gateway'};
939 # welcome to the future
940 my $fh = IO::Scalar->new(\$batch);
941 $self->export_to_gateway($gateway, 'file' => $fh);
945 my $format = $opt{'format'} || $conf->config('batch-default_format')
946 or die "No batch format configured\n";
948 my $info = $export_info{$format} or die "Format not found: '$format'\n";
950 &{$info->{'init'}}($conf, $self->agentnum) if exists($info->{'init'});
952 my $oldAutoCommit = $FS::UID::AutoCommit;
953 local $FS::UID::AutoCommit = 0;
956 my $error = $self->prepare_for_export;
958 die $error if $error;
962 my @cust_pay_batch = $self->cust_pay_batch;
964 my $delim = exists($info->{'delimiter'}) ? $info->{'delimiter'} : "\n";
966 my $h = $info->{'header'};
967 if (ref($h) eq 'CODE') {
968 $batch .= &$h($self, \@cust_pay_batch). $delim;
970 $batch .= $h. $delim;
973 foreach my $cust_pay_batch (@cust_pay_batch) {
975 $batchtotal += $cust_pay_batch->amount;
977 &{$info->{'row'}}($cust_pay_batch, $self, $batchcount, $batchtotal).
981 my $f = $info->{'footer'};
982 if (ref($f) eq 'CODE') {
983 $batch .= &$f($self, $batchcount, $batchtotal). $delim;
985 $batch .= $f. $delim;
988 if ($info->{'autopost'}) {
989 my $error = &{$info->{'autopost'}}($self, $batch);
991 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
996 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1000 =item export_to_gateway GATEWAY OPTIONS
1002 Given L<FS::payment_gateway> GATEWAY, export the items in this batch to
1003 that gateway via Business::BatchPayment. OPTIONS may include:
1005 - file: override the default transport and write to this file (name or handle)
1009 sub export_to_gateway {
1011 my ($self, $gateway, %opt) = @_;
1013 my $oldAutoCommit = $FS::UID::AutoCommit;
1014 local $FS::UID::AutoCommit = 0;
1017 my $error = $self->prepare_for_export;
1018 die $error if $error;
1021 'output' => $opt{'file'}, # will do nothing if it's empty
1022 # any other constructor options go here
1024 my $processor = $gateway->batch_processor(%proc_opt);
1026 my @items = map { $_->request_item } $self->cust_pay_batch;
1027 my $batch = Business::BatchPayment->create(Batch =>
1028 batch_id => $self->batchnum,
1031 $processor->submit($batch);
1033 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1037 sub manual_approve {
1041 my $usernum = $opt{'usernum'} || die "manual approval requires a usernum";
1042 my $conf = FS::Conf->new;
1043 return 'manual batch approval disabled'
1044 if ( ! $conf->exists('batch-manual_approval') );
1045 return 'batch already resolved' if $self->status eq 'R';
1046 return 'batch not yet submitted' if $self->status eq 'O';
1048 local $SIG{HUP} = 'IGNORE';
1049 local $SIG{INT} = 'IGNORE';
1050 local $SIG{QUIT} = 'IGNORE';
1051 local $SIG{TERM} = 'IGNORE';
1052 local $SIG{TSTP} = 'IGNORE';
1053 local $SIG{PIPE} = 'IGNORE';
1055 my $oldAutoCommit = $FS::UID::AutoCommit;
1056 local $FS::UID::AutoCommit = 0;
1060 foreach my $cust_pay_batch (
1061 qsearch('cust_pay_batch', { batchnum => $self->batchnum,
1064 my $new_cust_pay_batch = new FS::cust_pay_batch {
1065 $cust_pay_batch->hash,
1066 'paid' => $cust_pay_batch->amount,
1068 'usernum' => $usernum,
1070 my $error = $new_cust_pay_batch->approve();
1071 # there are no approval options here (authorization, order_number, etc.)
1072 # because the transaction wasn't really approved
1075 return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error";
1079 $self->set_status('R');
1085 # Set up configuration for gateways that have a Business::BatchPayment
1088 eval "use Class::MOP;";
1090 warn "Moose/Class::MOP not available.\n$@\nSkipping pay_batch upgrade.\n";
1093 my $conf = FS::Conf->new;
1094 for my $format (keys %export_info) {
1095 my $mod = "FS::pay_batch::$format";
1096 if ( $mod->can('_upgrade_gateway')
1097 and $conf->exists("batchconfig-$format") ) {
1100 my ($module, %gw_options) = $mod->_upgrade_gateway;
1101 my $gateway = FS::payment_gateway->new({
1102 gateway_namespace => 'Business::BatchPayment',
1103 gateway_module => $module,
1105 my $error = $gateway->insert(%gw_options);
1107 warn "Failed to migrate '$format' to a Business::BatchPayment::$module gateway:\n$error\n";
1111 # test whether it loads
1112 my $processor = eval { $gateway->batch_processor };
1113 if ( !$processor ) {
1114 warn "Couldn't load Business::BatchPayment module for '$format'.\n";
1115 # if not, remove it so it doesn't hang around and break things
1119 # remove the batchconfig-*
1120 warn "Created Business::BatchPayment gateway '".$gateway->label.
1121 "' for '$format' batch processing.\n";
1122 $conf->delete("batchconfig-$format");
1124 # and if appropriate, make it the system default
1125 for my $payby (qw(CARD CHEK)) {
1126 if ( ($conf->config("batch-fixed_format-$payby") || '') eq $format ) {
1127 warn "Setting as default for $payby.\n";
1128 $conf->set("batch-gateway-$payby", $gateway->gatewaynum);
1129 $conf->delete("batch-fixed_format-$payby");
1133 } #if can('_upgrade_gateway') and batchconfig-$format
1143 status is somewhat redundant now that download and upload exist
1147 L<FS::Record>, schema.html from the base documentation.