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);
742 push @item_errors, "Payment for customer ".$cust_pay_batch->custnum."\n$error";
748 $job->update_statustext(int(100 * $num/( $total_items ) ),
749 'Importing batch items')
754 } #foreach $batch (input batch, not pay_batch)
756 # Format an error message
757 if ( @item_errors ) {
758 my $error_text = join("\n\n",
759 "Errors during batch import: ".scalar(@item_errors),
762 if ( $mail_on_error ) {
763 my $subject = "Batch import errors"; #?
764 my $body = "Import from gateway ".$gateway->label."\n".$error_text;
766 to => $mail_on_error,
767 from => $conf->invoice_from_full(),
773 $dbh->rollback if $oldAutoCommit;
778 # Auto-resolve (with brute-force error handling)
779 foreach my $pay_batch (values %pay_batch_for_update) {
780 my $error = $pay_batch->try_to_resolve;
783 $dbh->rollback if $oldAutoCommit;
788 $dbh->commit if $oldAutoCommit;
794 Resolve this batch if possible. A batch can be resolved if all of its
795 entries have status. If the system options 'batch-auto_resolve_days'
796 and 'batch-auto_resolve_status' are set, and the batch's download date is
797 at least (batch-auto_resolve_days) before the current time, then it can
798 be auto-resolved; entries with no status will be approved or declined
799 according to the batch-auto_resolve_status setting.
805 my $conf = FS::Conf->new;;
807 return if $self->status ne 'I';
809 my @unresolved = qsearch('cust_pay_batch',
811 batchnum => $self->batchnum,
816 if ( @unresolved and $conf->exists('batch-auto_resolve_days') ) {
817 my $days = $conf->config('batch-auto_resolve_days'); # can be zero
818 # either 'approve' or 'decline'
819 my $action = $conf->config('batch-auto_resolve_status') || '';
823 time > ($self->download + 86400 * $days)
827 foreach my $cpb (@unresolved) {
828 if ( $action eq 'approve' ) {
829 # approve it for the full amount
830 $cpb->set('paid', $cpb->amount) unless ($cpb->paid || 0) > 0;
831 $error = $cpb->approve($self->batchnum);
833 elsif ( $action eq 'decline' ) {
834 $error = $cpb->decline('No response from processor');
836 return $error if $error;
838 } elsif ( @unresolved ) {
839 # auto resolve is not enabled, and we're not ready to resolve
843 $self->set_status('R');
846 =item prepare_for_export
848 Prepare the batch to be exported. This will:
849 - Set the status to "in transit".
850 - If batch-increment_expiration is set and this is a credit card batch,
851 increment expiration dates that are in the past.
852 - If this is the first download for this batch, adjust payment amounts to
853 not be greater than the customer's current balance. If the customer's
854 balance is zero, the entry will be removed.
856 Use this within a transaction.
860 sub prepare_for_export {
862 my $conf = FS::Conf->new;
863 my $curuser = $FS::CurrentUser::CurrentUser;
866 my $status = $self->status;
867 if ($status eq 'O') {
869 my $error = $self->set_status('I');
870 return "error updating pay_batch status: $error\n" if $error;
871 } elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) {
873 } elsif ($status eq 'R' &&
874 $curuser->access_right('Redownload resolved batches')) {
877 die "No pending batch.\n";
880 my @cust_pay_batch = sort { $a->paybatchnum <=> $b->paybatchnum }
881 $self->cust_pay_batch;
883 # handle batch-increment_expiration option
884 if ( $self->payby eq 'CARD' ) {
885 my ($cmon, $cyear) = (localtime(time))[4,5];
886 foreach (@cust_pay_batch) {
887 my $etime = str2time($_->exp) or next;
888 my ($day, $mon, $year) = (localtime($etime))[3,4,5];
889 if( $conf->exists('batch-increment_expiration') ) {
890 $year++ while( $year < $cyear or ($year == $cyear and $mon <= $cmon) );
891 $_->exp( sprintf('%4u-%02u-%02u', $year + 1900, $mon+1, $day) );
893 my $error = $_->replace;
894 return $error if $error;
898 if ($first_download) { #remove or reduce entries if customer's balance changed
900 foreach my $cust_pay_batch (@cust_pay_batch) {
902 my $balance = $cust_pay_batch->cust_main->balance;
903 if ($balance <= 0) { # then don't charge this customer
904 my $error = $cust_pay_batch->delete;
905 return $error if $error;
906 } elsif ($balance < $cust_pay_batch->amount) {
907 # reduce the charge to the remaining balance
908 $cust_pay_batch->amount($balance);
909 my $error = $cust_pay_batch->replace;
910 return $error if $error;
912 # else $balance >= $cust_pay_batch->amount
914 } #if $first_download
919 =item export_batch [ format => FORMAT | gateway => GATEWAY ]
921 Export batch for processing. FORMAT is the name of an L<FS::pay_batch>
922 module, in which case the configuration options are in 'batchconfig-FORMAT'.
924 Alternatively, GATEWAY can be an L<FS::payment_gateway> object set to a
925 L<Business::BatchPayment> module.
933 my $conf = new FS::Conf;
936 my $gateway = $opt{'gateway'};
938 # welcome to the future
939 my $fh = IO::Scalar->new(\$batch);
940 $self->export_to_gateway($gateway, 'file' => $fh);
944 my $format = $opt{'format'} || $conf->config('batch-default_format')
945 or die "No batch format configured\n";
947 my $info = $export_info{$format} or die "Format not found: '$format'\n";
949 &{$info->{'init'}}($conf, $self->agentnum) if exists($info->{'init'});
951 my $oldAutoCommit = $FS::UID::AutoCommit;
952 local $FS::UID::AutoCommit = 0;
955 my $error = $self->prepare_for_export;
957 die $error if $error;
961 my @cust_pay_batch = $self->cust_pay_batch;
963 my $delim = exists($info->{'delimiter'}) ? $info->{'delimiter'} : "\n";
965 my $h = $info->{'header'};
966 if (ref($h) eq 'CODE') {
967 $batch .= &$h($self, \@cust_pay_batch). $delim;
969 $batch .= $h. $delim;
972 foreach my $cust_pay_batch (@cust_pay_batch) {
974 $batchtotal += $cust_pay_batch->amount;
976 &{$info->{'row'}}($cust_pay_batch, $self, $batchcount, $batchtotal).
980 my $f = $info->{'footer'};
981 if (ref($f) eq 'CODE') {
982 $batch .= &$f($self, $batchcount, $batchtotal). $delim;
984 $batch .= $f. $delim;
987 if ($info->{'autopost'}) {
988 my $error = &{$info->{'autopost'}}($self, $batch);
990 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
995 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
999 =item export_to_gateway GATEWAY OPTIONS
1001 Given L<FS::payment_gateway> GATEWAY, export the items in this batch to
1002 that gateway via Business::BatchPayment. OPTIONS may include:
1004 - file: override the default transport and write to this file (name or handle)
1008 sub export_to_gateway {
1010 my ($self, $gateway, %opt) = @_;
1012 my $oldAutoCommit = $FS::UID::AutoCommit;
1013 local $FS::UID::AutoCommit = 0;
1016 my $error = $self->prepare_for_export;
1017 die $error if $error;
1020 'output' => $opt{'file'}, # will do nothing if it's empty
1021 # any other constructor options go here
1023 my $processor = $gateway->batch_processor(%proc_opt);
1025 my @items = map { $_->request_item } $self->cust_pay_batch;
1026 my $batch = Business::BatchPayment->create(Batch =>
1027 batch_id => $self->batchnum,
1030 $processor->submit($batch);
1032 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1036 sub manual_approve {
1040 my $usernum = $opt{'usernum'} || die "manual approval requires a usernum";
1041 my $conf = FS::Conf->new;
1042 return 'manual batch approval disabled'
1043 if ( ! $conf->exists('batch-manual_approval') );
1044 return 'batch already resolved' if $self->status eq 'R';
1045 return 'batch not yet submitted' if $self->status eq 'O';
1047 local $SIG{HUP} = 'IGNORE';
1048 local $SIG{INT} = 'IGNORE';
1049 local $SIG{QUIT} = 'IGNORE';
1050 local $SIG{TERM} = 'IGNORE';
1051 local $SIG{TSTP} = 'IGNORE';
1052 local $SIG{PIPE} = 'IGNORE';
1054 my $oldAutoCommit = $FS::UID::AutoCommit;
1055 local $FS::UID::AutoCommit = 0;
1059 foreach my $cust_pay_batch (
1060 qsearch('cust_pay_batch', { batchnum => $self->batchnum,
1063 my $new_cust_pay_batch = new FS::cust_pay_batch {
1064 $cust_pay_batch->hash,
1065 'paid' => $cust_pay_batch->amount,
1067 'usernum' => $usernum,
1069 my $error = $new_cust_pay_batch->approve();
1070 # there are no approval options here (authorization, order_number, etc.)
1071 # because the transaction wasn't really approved
1074 return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error";
1078 $self->set_status('R');
1084 # Set up configuration for gateways that have a Business::BatchPayment
1087 eval "use Class::MOP;";
1089 warn "Moose/Class::MOP not available.\n$@\nSkipping pay_batch upgrade.\n";
1092 my $conf = FS::Conf->new;
1093 for my $format (keys %export_info) {
1094 my $mod = "FS::pay_batch::$format";
1095 if ( $mod->can('_upgrade_gateway')
1096 and $conf->exists("batchconfig-$format") ) {
1099 my ($module, %gw_options) = $mod->_upgrade_gateway;
1100 my $gateway = FS::payment_gateway->new({
1101 gateway_namespace => 'Business::BatchPayment',
1102 gateway_module => $module,
1104 my $error = $gateway->insert(%gw_options);
1106 warn "Failed to migrate '$format' to a Business::BatchPayment::$module gateway:\n$error\n";
1110 # test whether it loads
1111 my $processor = eval { $gateway->batch_processor };
1112 if ( !$processor ) {
1113 warn "Couldn't load Business::BatchPayment module for '$format'.\n";
1114 # if not, remove it so it doesn't hang around and break things
1118 # remove the batchconfig-*
1119 warn "Created Business::BatchPayment gateway '".$gateway->label.
1120 "' for '$format' batch processing.\n";
1121 $conf->delete("batchconfig-$format");
1123 # and if appropriate, make it the system default
1124 for my $payby (qw(CARD CHEK)) {
1125 if ( ($conf->config("batch-fixed_format-$payby") || '') eq $format ) {
1126 warn "Setting as default for $payby.\n";
1127 $conf->set("batch-gateway-$payby", $gateway->gatewaynum);
1128 $conf->delete("batch-fixed_format-$payby");
1132 } #if can('_upgrade_gateway') and batchconfig-$format
1142 status is somewhat redundant now that download and upload exist
1146 L<FS::Record>, schema.html from the base documentation.