7 use FS::Record qw( dbh qsearch qsearchs );
9 use FS::part_bill_event qw(due_events);
11 @ISA = qw(FS::Record);
15 FS::pay_batch - Object methods for pay_batch records
21 $record = new FS::pay_batch \%hash;
22 $record = new FS::pay_batch { 'column' => 'value' };
24 $error = $record->insert;
26 $error = $new_record->replace($old_record);
28 $error = $record->delete;
30 $error = $record->check;
34 An FS::pay_batch object represents an example. FS::pay_batch inherits from
35 FS::Record. The following fields are currently supported:
39 =item batchnum - primary key
41 =item payby - CARD or CHEK
43 =item status - O (Open), I (In-transit), or R (Resolved)
58 Creates a new example. To add the example to the database, see L<"insert">.
60 Note that this stores the hash reference, not a distinct copy of the hash it
61 points to. You can ask the object for a copy with the I<hash> method.
65 # the new method can be inherited from FS::Record, if a table method is defined
67 sub table { 'pay_batch'; }
71 Adds this record to the database. If there is an error, returns the error,
72 otherwise returns false.
76 # the insert method can be inherited from FS::Record
80 Delete this record from the database.
84 # the delete method can be inherited from FS::Record
86 =item replace OLD_RECORD
88 Replaces the OLD_RECORD with this one in the database. If there is an error,
89 returns the error, otherwise returns false.
93 # the replace method can be inherited from FS::Record
97 Checks all fields to make sure this is a valid example. If there is
98 an error, returns the error, otherwise returns false. Called by the insert
103 # the check method should currently be supplied - FS::Record contains some
104 # data checking routines
110 $self->ut_numbern('batchnum')
111 || $self->ut_enum('payby', [ 'CARD', 'CHEK' ])
112 || $self->ut_enum('status', [ 'O', 'I', 'R' ])
114 return $error if $error;
133 $self->status(shift);
134 $self->download(time)
135 if $self->status eq 'I' && ! $self->download;
137 if $self->status eq 'R' && ! $self->upload;
141 =item import_results OPTION => VALUE, ...
143 Import batch results.
147 I<filehandle> - open filehandle of results file.
149 I<format> - "csv-td_canada_trust-merchant_pc_batch", "csv-chase_canada-E-xactBatch", "ach-spiritone", or "PAP"
156 my $param = ref($_[0]) ? shift : { @_ };
157 my $fh = $param->{'filehandle'};
158 my $format = $param->{'format'};
160 my $filetype; # CSV, Fixed80, Fixed264
162 my $formatre; # for Fixed.+
169 my $approved_condition;
170 my $declined_condition;
172 if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) {
177 'paybatchnum', # Reference#: Invoice number of the transaction
178 'paid', # Amount: Amount of the transaction. Dollars and cents
179 # with no decimal entered.
180 '', # Card Type: 0 - MCrd, 1 - Visa, 2 - AMEX, 3 - Discover,
181 # 4 - Insignia, 5 - Diners/EnRoute, 6 - JCB
182 '_date', # Transaction Date: Date the Transaction was processed
183 'time', # Transaction Time: Time the transaction was processed
184 'payinfo', # Card Number: Card number for the transaction
185 '', # Expiry Date: Expiry date of the card
186 '', # Auth#: Authorization number entered for force post
188 'type', # Transaction Type: 0 - purchase, 40 - refund,
190 'result', # Processing Result: 3 - Approval,
191 # 4 - Declined/Amount over limit,
192 # 5 - Invalid/Expired/stolen card,
194 '', # Terminal ID: Terminal ID used to process the transaction
197 $end_condition = sub {
199 $hash->{'type'} eq '0BC';
203 my( $hash, $total) = @_;
204 $total = sprintf("%.2f", $total);
205 my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 100 );
206 return "Our total $total does not match bank total $batch_total!"
207 if $total != $batch_total;
213 $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
214 $hash->{'_date'} = timelocal( substr($hash->{'time'}, 4, 2),
215 substr($hash->{'time'}, 2, 2),
216 substr($hash->{'time'}, 0, 2),
217 substr($hash->{'_date'}, 6, 2),
218 substr($hash->{'_date'}, 4, 2)-1,
219 substr($hash->{'_date'}, 0, 4)-1900, );
222 $approved_condition = sub {
224 $hash->{'type'} eq '0' && $hash->{'result'} == 3;
227 $declined_condition = sub {
229 $hash->{'type'} eq '0' && ( $hash->{'result'} == 4
230 || $hash->{'result'} == 5 );
234 }elsif ( $format eq 'csv-chase_canada-E-xactBatch' ) {
239 '', # Internal(bank) id of the transaction
240 '', # Transaction Type: 00 - purchase, 01 - preauth,
241 # 02 - completion, 03 - forcepost,
242 # 04 - refund, 05 - auth,
243 # 06 - purchase corr, 07 - refund corr,
244 # 08 - void 09 - void return
245 '', # gateway used to process this transaction
246 'paid', # Amount: Amount of the transaction. Dollars and cents
247 # with decimal entered.
248 'auth', # Auth#: Authorization number (if approved)
249 'payinfo', # Card Number: Card number for the transaction
250 '', # Expiry Date: Expiry date of the card
251 '', # Cardholder Name
252 'bankcode', # Bank response code (3 alphanumeric)
253 'bankmess', # Bank response message
254 'etgcode', # ETG response code (2 alphanumeric)
255 'etgmess', # ETG response message
256 '', # Returned customer number for the transaction
257 'paybatchnum', # Reference#: paybatch number of the transaction
258 '', # Reference#: Invoice number of the transaction
259 'result', # Processing Result: Approved of Declined
262 $end_condition = sub {
269 $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'}); #hmmmm
270 $hash->{'_date'} = time; # got a better one?
271 $hash->{'payinfo'} = $cpb->{'payinfo'}
272 if( substr($hash->{'payinfo'}, -4) eq substr($cpb->{'payinfo'}, -4) );
275 $approved_condition = sub {
277 $hash->{'etgcode'} eq '00' && $hash->{'result'} eq "Approved";
280 $declined_condition = sub {
282 $hash->{'etgcode'} ne '00' # internal processing error
283 || ( $hash->{'result'} eq "Declined" );
287 }elsif ( $format eq 'PAP' ) {
289 $filetype = "Fixed264";
292 'recordtype', # We are interested in the 'D' or debit records
293 'batchnum', # Record#: batch number we used when sending the file
294 'datacenter', # Where in the bowels of the bank the data was processed
295 'paid', # Amount: Amount of the transaction. Dollars and cents
296 # with no decimal entered.
297 '_date', # Transaction Date: Date the Transaction was processed
298 'bank', # Routing information
299 'payinfo', # Account number for the transaction
300 'paybatchnum', # Reference#: Invoice number of the transaction
303 $formatre = '^(.).{19}(.{4})(.{3})(.{10})(.{6})(.{9})(.{12}).{110}(.{19}).{71}$';
305 $end_condition = sub {
307 $hash->{'recordtype'} eq 'W';
311 my( $hash, $total) = @_;
312 $total = sprintf("%.2f", $total);
313 my $batch_total = $hash->{'datacenter'}.$hash->{'paid'}.
314 substr($hash->{'_date'},0,1); # YUCK!
315 $batch_total = sprintf("%.2f", $batch_total / 100 );
316 return "Our total $total does not match bank total $batch_total!"
317 if $total != $batch_total;
323 $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
324 my $tmpdate = timelocal( 0,0,1,1,0,substr($hash->{'_date'}, 0, 3)+2000);
325 $tmpdate += 86400*(substr($hash->{'_date'}, 3, 3)-1) ;
326 $hash->{'_date'} = $tmpdate;
327 $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'bank'};
330 $approved_condition = sub {
334 $declined_condition = sub {
338 }elsif ( $format eq 'ach-spiritone' ) {
344 'custnum' , # ID: Customer number of the transaction
345 'aba', # ABA Number for the transaction
346 'payinfo', # Bank Account Number for the transaction
347 '', # Transaction Type: 27 - debit
348 'paid', # Amount: Amount of the transaction. Dollars and cents
349 # with decimal entered.
350 '', # Default Transaction Type
351 '', # Default Amount: Dollars and cents with decimal entered.
354 $end_condition = sub {
360 my @cust_pay_batch = # this is dodgy, it works due to autoposting
361 qsearch('cust_pay_batch', { 'custnum' => $hash->{'custnum'}+0,
364 if ( scalar(@cust_pay_batch) == 1 ) {
365 $hash->{'paybatchnum'} = $cust_pay_batch[0]->paybatchnum;
367 return "can't find batch payment for customer number " .$hash->{custnum};
374 $hash->{'_date'} = time; # got a better one?
375 $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'aba'};
378 $approved_condition = sub {
382 $declined_condition = sub {
388 return "Unknown format $format";
391 my $csv = new Text::CSV_XS;
393 local $SIG{HUP} = 'IGNORE';
394 local $SIG{INT} = 'IGNORE';
395 local $SIG{QUIT} = 'IGNORE';
396 local $SIG{TERM} = 'IGNORE';
397 local $SIG{TSTP} = 'IGNORE';
398 local $SIG{PIPE} = 'IGNORE';
400 my $oldAutoCommit = $FS::UID::AutoCommit;
401 local $FS::UID::AutoCommit = 0;
404 my $reself = $self->select_for_update;
406 unless ( $reself->status eq 'I' ) {
407 $dbh->rollback if $oldAutoCommit;
408 return "batchnum ". $self->batchnum. "no longer in transit";
411 my $error = $self->set_status('R');
413 $dbh->rollback if $oldAutoCommit;
419 while ( defined($line=<$fh>) ) {
421 next if $line =~ /^\s*$/; #skip blank lines
423 if ($filetype eq "CSV") {
424 $csv->parse($line) or do {
425 $dbh->rollback if $oldAutoCommit;
426 return "can't parse: ". $csv->error_input();
428 @values = $csv->fields();
429 }elsif ($filetype eq "Fixed80" || $filetype eq "Fixed264"){
430 @values = $line =~ /$formatre/;
432 $dbh->rollback if $oldAutoCommit;
433 return "can't parse: ". $line;
436 $dbh->rollback if $oldAutoCommit;
437 return "Unknown file type $filetype";
441 foreach my $field ( @fields ) {
442 my $value = shift @values;
444 $hash{$field} = $value;
447 if ( defined($pre_hook) ) {
448 my $error = &{$pre_hook}(\%hash);
450 $dbh->rollback if $oldAutoCommit;
455 if ( &{$end_condition}(\%hash) ) {
456 my $error = &{$end_hook}(\%hash, $total);
458 $dbh->rollback if $oldAutoCommit;
465 qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
466 unless ( $cust_pay_batch ) {
467 return "unknown paybatchnum $hash{'paybatchnum'}\n";
469 my $custnum = $cust_pay_batch->custnum,
470 my $payby = $cust_pay_batch->payby,
472 my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
474 &{$hook}(\%hash, $cust_pay_batch->hashref);
476 if ( &{$approved_condition}(\%hash) ) {
478 $new_cust_pay_batch->status('Approved');
480 my $cust_pay = new FS::cust_pay ( {
481 'custnum' => $custnum,
483 'paybatch' => $self->batchnum,
484 map { $_ => $hash{$_} } (qw( paid _date payinfo )),
486 $error = $cust_pay->insert;
488 $dbh->rollback if $oldAutoCommit;
489 return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n";
491 $total += $hash{'paid'};
493 $cust_pay->cust_main->apply_payments;
495 } elsif ( &{$declined_condition}(\%hash) ) {
497 $new_cust_pay_batch->status('Declined');
499 foreach my $part_bill_event ( due_events ( $new_cust_pay_batch,
504 # don't run subsequent events if balance<=0
505 last if $cust_pay_batch->cust_main->balance <= 0;
507 if (my $error = $part_bill_event->do_event($new_cust_pay_batch)) {
508 # gah, even with transactions.
509 $dbh->commit if $oldAutoCommit; #well.
517 my $error = $new_cust_pay_batch->replace($cust_pay_batch);
519 $dbh->rollback if $oldAutoCommit;
520 return "error updating status of paybatchnum $hash{'paybatchnum'}: $error\n";
525 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
534 status is somewhat redundant now that download and upload exist
538 L<FS::Record>, schema.html from the base documentation.