7 use FS::Record qw( dbh qsearch qsearchs );
10 @ISA = qw(FS::Record);
14 FS::pay_batch - Object methods for pay_batch records
20 $record = new FS::pay_batch \%hash;
21 $record = new FS::pay_batch { 'column' => 'value' };
23 $error = $record->insert;
25 $error = $new_record->replace($old_record);
27 $error = $record->delete;
29 $error = $record->check;
33 An FS::pay_batch object represents an payment batch. FS::pay_batch inherits
34 from FS::Record. The following fields are currently supported:
38 =item batchnum - primary key
40 =item payby - CARD or CHEK
42 =item status - O (Open), I (In-transit), or R (Resolved)
57 Creates a new batch. To add the batch to the database, see L<"insert">.
59 Note that this stores the hash reference, not a distinct copy of the hash it
60 points to. You can ask the object for a copy with the I<hash> method.
64 # the new method can be inherited from FS::Record, if a table method is defined
66 sub table { 'pay_batch'; }
70 Adds this record to the database. If there is an error, returns the error,
71 otherwise returns false.
75 # the insert method can be inherited from FS::Record
79 Delete this record from the database.
83 # the delete method can be inherited from FS::Record
85 =item replace OLD_RECORD
87 Replaces the OLD_RECORD with this one in the database. If there is an error,
88 returns the error, otherwise returns false.
92 # the replace method can be inherited from FS::Record
96 Checks all fields to make sure this is a valid batch. If there is
97 an error, returns the error, otherwise returns false. Called by the insert
102 # the check method should currently be supplied - FS::Record contains some
103 # data checking routines
109 $self->ut_numbern('batchnum')
110 || $self->ut_enum('payby', [ 'CARD', 'CHEK' ])
111 || $self->ut_enum('status', [ 'O', 'I', 'R' ])
113 return $error if $error;
132 $self->status(shift);
133 $self->download(time)
134 if $self->status eq 'I' && ! $self->download;
136 if $self->status eq 'R' && ! $self->upload;
140 =item import_results OPTION => VALUE, ...
142 Import batch results.
146 I<filehandle> - open filehandle of results file.
148 I<format> - "csv-td_canada_trust-merchant_pc_batch", "csv-chase_canada-E-xactBatch", "ach-spiritone", or "PAP"
155 my $param = ref($_[0]) ? shift : { @_ };
156 my $fh = $param->{'filehandle'};
157 my $format = $param->{'format'};
159 my $filetype; # CSV, Fixed80, Fixed264
161 my $formatre; # for Fixed.+
167 my $approved_condition;
168 my $declined_condition;
170 if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) {
175 'paybatchnum', # Reference#: Invoice number of the transaction
176 'paid', # Amount: Amount of the transaction. Dollars and cents
177 # with no decimal entered.
178 '', # Card Type: 0 - MCrd, 1 - Visa, 2 - AMEX, 3 - Discover,
179 # 4 - Insignia, 5 - Diners/EnRoute, 6 - JCB
180 '_date', # Transaction Date: Date the Transaction was processed
181 'time', # Transaction Time: Time the transaction was processed
182 'payinfo', # Card Number: Card number for the transaction
183 '', # Expiry Date: Expiry date of the card
184 '', # Auth#: Authorization number entered for force post
186 'type', # Transaction Type: 0 - purchase, 40 - refund,
188 'result', # Processing Result: 3 - Approval,
189 # 4 - Declined/Amount over limit,
190 # 5 - Invalid/Expired/stolen card,
192 '', # Terminal ID: Terminal ID used to process the transaction
195 $end_condition = sub {
197 $hash->{'type'} eq '0BC';
201 my( $hash, $total) = @_;
202 $total = sprintf("%.2f", $total);
203 my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 100 );
204 return "Our total $total does not match bank total $batch_total!"
205 if $total != $batch_total;
211 $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
212 $hash->{'_date'} = timelocal( substr($hash->{'time'}, 4, 2),
213 substr($hash->{'time'}, 2, 2),
214 substr($hash->{'time'}, 0, 2),
215 substr($hash->{'_date'}, 6, 2),
216 substr($hash->{'_date'}, 4, 2)-1,
217 substr($hash->{'_date'}, 0, 4)-1900, );
220 $approved_condition = sub {
222 $hash->{'type'} eq '0' && $hash->{'result'} == 3;
225 $declined_condition = sub {
227 $hash->{'type'} eq '0' && ( $hash->{'result'} == 4
228 || $hash->{'result'} == 5 );
232 }elsif ( $format eq 'csv-chase_canada-E-xactBatch' ) {
237 '', # Internal(bank) id of the transaction
238 '', # Transaction Type: 00 - purchase, 01 - preauth,
239 # 02 - completion, 03 - forcepost,
240 # 04 - refund, 05 - auth,
241 # 06 - purchase corr, 07 - refund corr,
242 # 08 - void 09 - void return
243 '', # gateway used to process this transaction
244 'paid', # Amount: Amount of the transaction. Dollars and cents
245 # with decimal entered.
246 'auth', # Auth#: Authorization number (if approved)
247 'payinfo', # Card Number: Card number for the transaction
248 '', # Expiry Date: Expiry date of the card
249 '', # Cardholder Name
250 'bankcode', # Bank response code (3 alphanumeric)
251 'bankmess', # Bank response message
252 'etgcode', # ETG response code (2 alphanumeric)
253 'etgmess', # ETG response message
254 '', # Returned customer number for the transaction
255 'paybatchnum', # Reference#: paybatch number of the transaction
256 '', # Reference#: Invoice number of the transaction
257 'result', # Processing Result: Approved of Declined
260 $end_condition = sub {
267 $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'}); #hmmmm
268 $hash->{'_date'} = time; # got a better one?
269 $hash->{'payinfo'} = $cpb->{'payinfo'}
270 if( substr($hash->{'payinfo'}, -4) eq substr($cpb->{'payinfo'}, -4) );
273 $approved_condition = sub {
275 $hash->{'etgcode'} eq '00' && $hash->{'result'} eq "Approved";
278 $declined_condition = sub {
280 $hash->{'etgcode'} ne '00' # internal processing error
281 || ( $hash->{'result'} eq "Declined" );
285 }elsif ( $format eq 'PAP' ) {
287 $filetype = "Fixed264";
290 'recordtype', # We are interested in the 'D' or debit records
291 'batchnum', # Record#: batch number we used when sending the file
292 'datacenter', # Where in the bowels of the bank the data was processed
293 'paid', # Amount: Amount of the transaction. Dollars and cents
294 # with no decimal entered.
295 '_date', # Transaction Date: Date the Transaction was processed
296 'bank', # Routing information
297 'payinfo', # Account number for the transaction
298 'paybatchnum', # Reference#: Invoice number of the transaction
301 $formatre = '^(.).{19}(.{4})(.{3})(.{10})(.{6})(.{9})(.{12}).{110}(.{19}).{71}$';
303 $end_condition = sub {
305 $hash->{'recordtype'} eq 'W';
309 my( $hash, $total) = @_;
310 $total = sprintf("%.2f", $total);
311 my $batch_total = $hash->{'datacenter'}.$hash->{'paid'}.
312 substr($hash->{'_date'},0,1); # YUCK!
313 $batch_total = sprintf("%.2f", $batch_total / 100 );
314 return "Our total $total does not match bank total $batch_total!"
315 if $total != $batch_total;
321 $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
322 my $tmpdate = timelocal( 0,0,1,1,0,substr($hash->{'_date'}, 0, 3)+2000);
323 $tmpdate += 86400*(substr($hash->{'_date'}, 3, 3)-1) ;
324 $hash->{'_date'} = $tmpdate;
325 $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'bank'};
328 $approved_condition = sub {
332 $declined_condition = sub {
336 }elsif ( $format eq 'ach-spiritone' ) {
342 'paybatchnum', # ID: Number of the transaction
343 'aba', # ABA Number for the transaction
344 'payinfo', # Bank Account Number for the transaction
345 '', # Transaction Type: 27 - debit
346 'paid', # Amount: Amount of the transaction. Dollars and cents
347 # with decimal entered.
348 '', # Default Transaction Type
349 '', # Default Amount: Dollars and cents with decimal entered.
352 $end_condition = sub {
358 $hash->{'_date'} = time; # got a better one?
359 $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'aba'};
362 $approved_condition = sub {
366 $declined_condition = sub {
372 return "Unknown format $format";
375 my $csv = new Text::CSV_XS;
377 local $SIG{HUP} = 'IGNORE';
378 local $SIG{INT} = 'IGNORE';
379 local $SIG{QUIT} = 'IGNORE';
380 local $SIG{TERM} = 'IGNORE';
381 local $SIG{TSTP} = 'IGNORE';
382 local $SIG{PIPE} = 'IGNORE';
384 my $oldAutoCommit = $FS::UID::AutoCommit;
385 local $FS::UID::AutoCommit = 0;
388 my $reself = $self->select_for_update;
390 unless ( $reself->status eq 'I' ) {
391 $dbh->rollback if $oldAutoCommit;
392 return "batchnum ". $self->batchnum. "no longer in transit";
395 my $error = $self->set_status('R');
397 $dbh->rollback if $oldAutoCommit;
403 while ( defined($line=<$fh>) ) {
405 next if $line =~ /^\s*$/; #skip blank lines
407 if ($filetype eq "CSV") {
408 $csv->parse($line) or do {
409 $dbh->rollback if $oldAutoCommit;
410 return "can't parse: ". $csv->error_input();
412 @values = $csv->fields();
413 }elsif ($filetype eq "Fixed80" || $filetype eq "Fixed264"){
414 @values = $line =~ /$formatre/;
416 $dbh->rollback if $oldAutoCommit;
417 return "can't parse: ". $line;
420 $dbh->rollback if $oldAutoCommit;
421 return "Unknown file type $filetype";
425 foreach my $field ( @fields ) {
426 my $value = shift @values;
428 $hash{$field} = $value;
431 if ( &{$end_condition}(\%hash) ) {
432 my $error = &{$end_hook}(\%hash, $total);
434 $dbh->rollback if $oldAutoCommit;
441 qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
442 unless ( $cust_pay_batch ) {
443 return "unknown paybatchnum $hash{'paybatchnum'}\n";
445 my $custnum = $cust_pay_batch->custnum,
446 my $payby = $cust_pay_batch->payby,
448 my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
450 &{$hook}(\%hash, $cust_pay_batch->hashref);
452 if ( &{$approved_condition}(\%hash) ) {
454 $new_cust_pay_batch->status('Approved');
456 } elsif ( &{$declined_condition}(\%hash) ) {
458 $new_cust_pay_batch->status('Declined');
462 my $error = $new_cust_pay_batch->replace($cust_pay_batch);
464 $dbh->rollback if $oldAutoCommit;
465 return "error updating status of paybatchnum $hash{'paybatchnum'}: $error\n";
468 if ( $new_cust_pay_batch->status =~ /Approved/i ) {
470 my $cust_pay = new FS::cust_pay ( {
471 'custnum' => $custnum,
473 'paybatch' => $self->batchnum,
474 map { $_ => $hash{$_} } (qw( paid _date payinfo )),
476 $error = $cust_pay->insert;
478 $dbh->rollback if $oldAutoCommit;
479 return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n";
481 $total += $hash{'paid'};
483 $cust_pay->cust_main->apply_payments;
485 } elsif ( $new_cust_pay_batch->status =~ /Declined/i ) {
487 #false laziness w/cust_main::collect
489 my $due_cust_event = $new_cust_pay_batch->cust_main->due_cust_event(
490 #'check_freq' => '1d', #?
491 'eventtable' => 'cust_pay_batch',
492 'objects' => [ $new_cust_pay_batch ],
494 unless( ref($due_cust_event) ) {
495 $dbh->rollback if $oldAutoCommit;
496 return $due_cust_event;
499 foreach my $cust_event ( @$due_cust_event ) {
503 #re-eval event conditions (a previous event could have changed things)
504 next unless $cust_event->test_conditions;
506 if ( my $error = $cust_event->do_event() ) {
507 # gah, even with transactions.
508 #$dbh->commit if $oldAutoCommit; #well.
509 $dbh->rollback if $oldAutoCommit;
520 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
529 status is somewhat redundant now that download and upload exist
533 L<FS::Record>, schema.html from the base documentation.