4 use vars qw( @ISA $DEBUG %import_info %export_info $conf );
7 use XML::Simple qw(XMLin XMLout);
8 use FS::Record qw( dbh qsearch qsearchs );
12 @ISA = qw(FS::Record);
16 FS::pay_batch - Object methods for pay_batch records
22 $record = new FS::pay_batch \%hash;
23 $record = new FS::pay_batch { 'column' => 'value' };
25 $error = $record->insert;
27 $error = $new_record->replace($old_record);
29 $error = $record->delete;
31 $error = $record->check;
35 An FS::pay_batch object represents an payment batch. FS::pay_batch inherits
36 from FS::Record. The following fields are currently supported:
40 =item batchnum - primary key
42 =item payby - CARD or CHEK
44 =item status - O (Open), I (In-transit), or R (Resolved)
59 Creates a new batch. To add the batch to the database, see L<"insert">.
61 Note that this stores the hash reference, not a distinct copy of the hash it
62 points to. You can ask the object for a copy with the I<hash> method.
66 # the new method can be inherited from FS::Record, if a table method is defined
68 sub table { 'pay_batch'; }
72 Adds this record to the database. If there is an error, returns the error,
73 otherwise returns false.
77 # the insert method can be inherited from FS::Record
81 Delete this record from the database.
85 # the delete method can be inherited from FS::Record
87 =item replace OLD_RECORD
89 Replaces the OLD_RECORD with this one in the database. If there is an error,
90 returns the error, otherwise returns false.
94 # the replace method can be inherited from FS::Record
98 Checks all fields to make sure this is a valid batch. If there is
99 an error, returns the error, otherwise returns false. Called by the insert
104 # the check method should currently be supplied - FS::Record contains some
105 # data checking routines
111 $self->ut_numbern('batchnum')
112 || $self->ut_enum('payby', [ 'CARD', 'CHEK' ])
113 || $self->ut_enum('status', [ 'O', 'I', 'R' ])
115 return $error if $error;
134 $self->status(shift);
135 $self->download(time)
136 if $self->status eq 'I' && ! $self->download;
138 if $self->status eq 'R' && ! $self->upload;
142 # further false laziness
144 %import_info = %export_info = ();
145 foreach my $INC (@INC) {
146 warn "globbing $INC/FS/pay_batch/*.pm\n" if $DEBUG;
147 foreach my $file ( glob("$INC/FS/pay_batch/*.pm")) {
148 warn "attempting to load batch format from $file\n" if $DEBUG;
149 $file =~ /\/(\w+)\.pm$/;
152 my ($import, $export, $name) =
153 eval "use FS::pay_batch::$mod;
154 ( \\%FS::pay_batch::$mod\::import_info,
155 \\%FS::pay_batch::$mod\::export_info,
156 \$FS::pay_batch::$mod\::name)";
157 $name ||= $mod; # in case it's not defined
159 # in FS::cdr this is a die, not a warn. That's probably a bug.
160 warn "error using FS::pay_batch::$mod (skipping): $@\n";
163 if(!keys(%$import)) {
164 warn "no \%import_info found in FS::pay_batch::$mod (skipping)\n";
167 $import_info{$name} = $import;
169 if(!keys(%$export)) {
170 warn "no \%export_info found in FS::pay_batch::$mod (skipping)\n";
173 $export_info{$name} = $export;
178 =item import_results OPTION => VALUE, ...
180 Import batch results.
184 I<filehandle> - open filehandle of results file.
186 I<format> - "csv-td_canada_trust-merchant_pc_batch", "csv-chase_canada-E-xactBatch", "ach-spiritone", or "PAP"
193 my $param = ref($_[0]) ? shift : { @_ };
194 my $fh = $param->{'filehandle'};
195 my $format = $param->{'format'};
196 my $info = $import_info{$format}
197 or die "unknown format $format";
199 my $filetype = $info->{'filetype'}; # CSV or fixed
200 my @fields = @{ $info->{'fields'} };
201 my $formatre = $info->{'formatre'}; # for fixed
203 my $begin_condition = $info->{'begin_condition'};
204 my $end_condition = $info->{'end_condition'};
205 my $end_hook = $info->{'end_hook'};
206 my $hook = $info->{'hook'};
207 my $approved_condition = $info->{'approved'};
208 my $declined_condition = $info->{'declined'};
210 my $csv = new Text::CSV_XS;
212 local $SIG{HUP} = 'IGNORE';
213 local $SIG{INT} = 'IGNORE';
214 local $SIG{QUIT} = 'IGNORE';
215 local $SIG{TERM} = 'IGNORE';
216 local $SIG{TSTP} = 'IGNORE';
217 local $SIG{PIPE} = 'IGNORE';
219 my $oldAutoCommit = $FS::UID::AutoCommit;
220 local $FS::UID::AutoCommit = 0;
223 my $reself = $self->select_for_update;
225 unless ( $reself->status eq 'I' ) {
226 $dbh->rollback if $oldAutoCommit;
227 return "batchnum ". $self->batchnum. "no longer in transit";
230 my $error = $self->set_status('R');
232 $dbh->rollback if $oldAutoCommit;
239 # Order of operations has been changed here.
240 # We now slurp everything into @all_values, then
241 # process one line at a time.
243 if ($filetype eq 'XML') {
244 my @xmlkeys = @{ $info->{'xmlkeys'} }; # for XML
245 my $xmlrow = $info->{'xmlrow'}; # also for XML
247 # Do everything differently.
248 my $data = XMLin($fh, KeepRoot => 1);
250 # $xmlrow = [ RootKey, FirstLevelKey, SecondLevelKey... ]
251 $rows = $rows->{$_} foreach( @$xmlrow );
252 if(!defined($rows)) {
253 $dbh->rollback if $oldAutoCommit;
254 return "can't find rows in XML file";
256 $rows = [ $rows ] if ref($rows) ne 'ARRAY';
257 foreach my $row (@$rows) {
258 push @all_values, [ @{$row}{@xmlkeys} ];
262 while ( defined($line=<$fh>) ) {
264 next if $line =~ /^\s*$/; #skip blank lines
266 if ($filetype eq "CSV") {
267 $csv->parse($line) or do {
268 $dbh->rollback if $oldAutoCommit;
269 return "can't parse: ". $csv->error_input();
271 push @all_values, [ $csv->fields() ];
272 }elsif ($filetype eq 'fixed'){
273 my @values = $line =~ /$formatre/;
275 $dbh->rollback if $oldAutoCommit;
276 return "can't parse: ". $line;
278 push @all_values, \@values;
280 $dbh->rollback if $oldAutoCommit;
281 return "Unknown file type $filetype";
286 foreach (@all_values) {
290 foreach my $field ( @fields ) {
291 my $value = shift @values;
293 $hash{$field} = $value;
296 if ( defined($end_condition) and &{$end_condition}(\%hash) ) {
298 $error = &{$end_hook}(\%hash, $total) if defined($end_hook);
300 $dbh->rollback if $oldAutoCommit;
307 qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
308 unless ( $cust_pay_batch ) {
309 return "unknown paybatchnum $hash{'paybatchnum'}\n";
311 my $custnum = $cust_pay_batch->custnum,
312 my $payby = $cust_pay_batch->payby,
314 my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
316 &{$hook}(\%hash, $cust_pay_batch->hashref);
318 if ( &{$approved_condition}(\%hash) ) {
320 $new_cust_pay_batch->status('Approved');
322 } elsif ( &{$declined_condition}(\%hash) ) {
324 $new_cust_pay_batch->status('Declined');
328 my $error = $new_cust_pay_batch->replace($cust_pay_batch);
330 $dbh->rollback if $oldAutoCommit;
331 return "error updating status of paybatchnum $hash{'paybatchnum'}: $error\n";
334 if ( $new_cust_pay_batch->status =~ /Approved/i ) {
336 my $cust_pay = new FS::cust_pay ( {
337 'custnum' => $custnum,
339 'paybatch' => $self->batchnum,
340 map { $_ => $hash{$_} } (qw( paid _date payinfo )),
342 $error = $cust_pay->insert;
344 $dbh->rollback if $oldAutoCommit;
345 return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n";
347 $total += $hash{'paid'};
349 $cust_pay->cust_main->apply_payments;
351 } elsif ( $new_cust_pay_batch->status =~ /Declined/i ) {
353 #false laziness w/cust_main::collect
355 my $due_cust_event = $new_cust_pay_batch->cust_main->due_cust_event(
356 #'check_freq' => '1d', #?
357 'eventtable' => 'cust_pay_batch',
358 'objects' => [ $new_cust_pay_batch ],
360 unless( ref($due_cust_event) ) {
361 $dbh->rollback if $oldAutoCommit;
362 return $due_cust_event;
365 foreach my $cust_event ( @$due_cust_event ) {
369 #re-eval event conditions (a previous event could have changed things)
370 next unless $cust_event->test_conditions;
372 if ( my $error = $cust_event->do_event() ) {
373 # gah, even with transactions.
374 #$dbh->commit if $oldAutoCommit; #well.
375 $dbh->rollback if $oldAutoCommit;
385 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
391 # Formerly httemplate/misc/download-batch.cgi
393 my $conf = new FS::Conf;
394 my $format = shift || $conf->config('batch-default_format')
395 or die "No batch format configured\n";
396 my $info = $export_info{$format} or die "Format not found: '$format'\n";
397 &{$info->{'init'}}($conf) if exists($info->{'init'});
399 my $oldAutoCommit = $FS::UID::AutoCommit;
400 local $FS::UID::AutoCommit = 0;
406 if($self->status eq 'O') {
409 elsif($self->status eq 'I' and
410 $FS::CurrentUser::CurrentUser->access_right('Reprocess batches')) {
414 die "No pending batch.\n"
417 $error = $self->set_status('I');
418 die "error updating pay_batch status: $error\n" if $error;
424 my @cust_pay_batch = sort { $a->paybatchnum <=> $b->paybatchnum }
425 qsearch('cust_pay_batch', { batchnum => $self->batchnum } );
427 my $h = $info->{'header'};
428 if(ref($h) eq 'CODE') {
429 $batch .= &$h($self, \@cust_pay_batch) . "\n";
434 foreach my $cust_pay_batch (@cust_pay_batch) {
435 if($first_download) {
436 my $balance = $cust_pay_batch->cust_main->balance;
438 if($balance <= 0) { # then don't charge this customer
439 $error = $cust_pay_batch->delete;
440 undef $cust_pay_batch;
442 elsif($balance < $cust_pay_batch->amount) { # then reduce the charge to the remaining balance
443 $cust_pay_batch->amount($balance);
444 $error = $cust_pay_batch->replace;
446 # else $balance >= $cust_pay_batch->amount
448 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
452 if($cust_pay_batch) { # that is, it wasn't deleted
454 $batchtotal += $cust_pay_batch->amount;
455 $batch .= &{$info->{'row'}}($cust_pay_batch, $self) . "\n";
458 my $f = $info->{'footer'};
459 if(ref($f) eq 'CODE') {
460 $batch .= &$f($self, $batchcount, $batchtotal) . "\n";
466 if ($info->{'autopost'}) {
467 $error = &{$info->{'autopost'}}($self, $batch);
469 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
474 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
482 status is somewhat redundant now that download and upload exist
486 L<FS::Record>, schema.html from the base documentation.