package FS::cust_pay_batch;
use strict;
-use vars qw( @ISA );
-use FS::Record qw(dbh qsearchs);
-use Business::CreditCard;
+use vars qw( @ISA $DEBUG );
+use FS::Record qw(dbh qsearch qsearchs);
+use FS::part_bill_event qw(due_events);
+use Business::CreditCard 0.28;
@ISA = qw( FS::Record );
+# 1 is mostly method/subroutine entry and options
+# 2 traces progress of some operations
+# 3 is even more information including possibly sensitive data
+$DEBUG = 0;
+
=head1 NAME
FS::cust_pay_batch - Object methods for batch cards
$error = $record->check;
+ $error = $record->retriable;
+
=head1 DESCRIPTION
An FS::cust_pay_batch object represents a credit card transaction ready to be
=item paybatchnum - primary key (automatically assigned)
-=item cardnum
+=item batchnum - indentifies group in batch
+
+=item payby - CARD/CHEK/LECB/BILL/COMP
+
+=item payinfo
=item exp - card expiration
=item country
+=item status
+
=back
=head1 METHODS
=item replace OLD_RECORD
-#inactive
-#
-#Replaces the OLD_RECORD with this one in the database. If there is an error,
-#returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
- return "Can't (yet?) replace batched transactions!";
-}
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
=item check
Checks all fields to make sure this is a valid transaction. If there is
an error, returns the error, otherwise returns false. Called by the insert
-and repalce methods.
+and replace methods.
=cut
my $error =
$self->ut_numbern('paybatchnum')
|| $self->ut_numbern('trancode') #depriciated
- || $self->ut_number('cardnum')
|| $self->ut_money('amount')
|| $self->ut_number('invnum')
|| $self->ut_number('custnum')
$self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
$self->first($1);
- my $cardnum = $self->cardnum;
- $cardnum =~ s/\D//g;
- $cardnum =~ /^(\d{13,16})$/
- or return "Illegal credit card number";
- $cardnum = $1;
- $self->cardnum($cardnum);
- validate($cardnum) or return "Illegal credit card number";
- return "Unknown card type" if cardtype($cardnum) eq "Unknown";
+ $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREP|CASH|WEST|MCRD)$/
+ or return "Illegal payby";
+ $self->payby($1);
+
+ $error = FS::payby::payinfo_check($self->payby, \$self->payinfo);
+ return $error if $error;
if ( $self->exp eq '' ) {
- return "Expriation date required"; #unless
+ return "Expiration date required"
+ unless $self->payby =~ /^(CHEK|DCHK|LECB|WEST)$/;
$self->exp('');
} else {
if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
}
+=item retriable
+
+Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
+credit card payment as retriable. Useful if the corresponding financial
+institution account was declined for temporary reasons and/or a manual
+retry is desired.
+
+Implementation details: For the named customer's invoice, changes the
+statustext of the 'done' (without statustext) event to 'retriable.'
+
+=cut
+
+sub retriable {
+ my $self = shift;
+
+ local $SIG{HUP} = 'IGNORE'; #Hmm
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
+ or return "event $self->eventnum references nonexistant invoice $self->invnum";
+
+ warn "cust_pay_batch->retriable working with self of " . $self->paybatchnum . " and invnum of " . $self->invnum;
+ my @cust_bill_event =
+ sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
+ grep {
+ $_->part_bill_event->eventcode =~ /\$cust_bill->batch_card/
+ && $_->status eq 'done'
+ && ! $_->statustext
+ }
+ $cust_bill->cust_bill_event;
+ # complain loudly if scalar(@cust_bill_event) > 1 ?
+ my $error = $cust_bill_event[0]->retriable;
+ if ($error ) {
+ # gah, even with transactions.
+ $dbh->commit if $oldAutoCommit; #well.
+ return "error marking invoice event retriable: $error";
+ }
+ '';
+}
+
=back
=head1 SUBROUTINES
my $format = $param->{'format'};
my $paybatch = $param->{'paybatch'};
+ my $filetype; # CSV, Fixed80, Fixed264
my @fields;
+ my $formatre; # for Fixed.+
+ my @values;
+ my $begin_condition;
my $end_condition;
my $end_hook;
my $hook;
if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) {
+ $filetype = "CSV";
+
@fields = (
'paybatchnum', # Reference#: Invoice number of the transaction
'paid', # Amount: Amount of the transaction. Dollars and cents
};
+ }elsif ( $format eq 'PAP' ) {
+
+ $filetype = "Fixed264";
+
+ @fields = (
+ 'recordtype', # We are interested in the 'D' or debit records
+ 'batchnum', # Record#: batch number we used when sending the file
+ 'datacenter', # Where in the bowels of the bank the data was processed
+ 'paid', # Amount: Amount of the transaction. Dollars and cents
+ # with no decimal entered.
+ '_date', # Transaction Date: Date the Transaction was processed
+ 'bank', # Routing information
+ 'payinfo', # Account number for the transaction
+ 'paybatchnum', # Reference#: Invoice number of the transaction
+ );
+
+ $formatre = '^(.).{19}(.{4})(.{3})(.{10})(.{6})(.{9})(.{12}).{110}(.{19}).{71}$';
+
+ $end_condition = sub {
+ my $hash = shift;
+ $hash->{'recordtype'} eq 'W';
+ };
+
+ $end_hook = sub {
+ my( $hash, $total) = @_;
+ $total = sprintf("%.2f", $total);
+ my $batch_total = $hash->{'datacenter'}.$hash->{'paid'}.
+ substr($hash->{'_date'},0,1); # YUCK!
+ $batch_total = sprintf("%.2f", $batch_total / 100 );
+ return "Our total $total does not match bank total $batch_total!"
+ if $total != $batch_total;
+ '';
+ };
+
+ $hook = sub {
+ my $hash = shift;
+ $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
+ my $tmpdate = timelocal( 0,0,1,1,0,substr($hash->{'_date'}, 0, 3)+2000);
+ $tmpdate += 86400*(substr($hash->{'_date'}, 3, 3)-1) ;
+ $hash->{'_date'} = $tmpdate;
+ $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'bank'};
+ };
+
+ $approved_condition = sub {
+ 1;
+ };
+
+ $declined_condition = sub {
+ 0;
+ };
+
+
} else {
return "Unknown format $format";
}
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ my $pay_batch = qsearchs('pay_batch',{'batchnum'=> $paybatch});
+ unless ($pay_batch && $pay_batch->status eq 'I') {
+ $dbh->rollback if $oldAutoCommit;
+ return "batch $paybatch is not in transit";
+ };
+
+ my $newbatch = new FS::pay_batch { $pay_batch->hash };
+ $newbatch->status('R'); # Resolved
+ $newbatch->upload(time);
+ my $error = $newbatch->replace($pay_batch);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error
+ }
+
my $total = 0;
my $line;
while ( defined($line=<$fh>) ) {
next if $line =~ /^\s*$/; #skip blank lines
- $csv->parse($line) or do {
+ if ($filetype eq "CSV") {
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+ @values = $csv->fields();
+ }elsif ($filetype eq "Fixed80" || $filetype eq "Fixed264"){
+ @values = $line =~ /$formatre/;
+ unless (@values) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $line;
+ };
+ }else{
$dbh->rollback if $oldAutoCommit;
- return "can't parse: ". $csv->error_input();
- };
+ return "Unknown file type $filetype";
+ }
- my @values = $csv->fields();
my %hash;
foreach my $field ( @fields ) {
my $value = shift @values;
}
my $cust_pay_batch =
- qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'} } );
+ qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
unless ( $cust_pay_batch ) {
$dbh->rollback if $oldAutoCommit;
return "unknown paybatchnum $hash{'paybatchnum'}\n";
}
my $custnum = $cust_pay_batch->custnum,
+ my $payby = $cust_pay_batch->payby,
- my $error = $cust_pay_batch->delete;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "error removing paybatchnum $hash{'paybatchnum'}: $error\n";
- }
+ my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
&{$hook}(\%hash);
if ( &{$approved_condition}(\%hash) ) {
+ $new_cust_pay_batch->status('Approved');
+
my $cust_pay = new FS::cust_pay ( {
'custnum' => $custnum,
- 'payby' => 'CARD',
+ 'payby' => $payby,
'paybatch' => $paybatch,
map { $_ => $hash{$_} } (qw( paid _date payinfo )),
} );
} elsif ( &{$declined_condition}(\%hash) ) {
- #this should be configurable... if anybody else ever uses batches
- $cust_pay_batch->cust_main->suspend;
+ $new_cust_pay_batch->status('Declined');
+
+ foreach my $part_bill_event ( due_events ( $new_cust_pay_batch,
+ 'DCLN',
+ '',
+ '') ) {
+
+ # don't run subsequent events if balance<=0
+ last if $cust_pay_batch->cust_main->balance <= 0;
+
+ if (my $error = $part_bill_event->do_event($new_cust_pay_batch)) {
+ # gah, even with transactions.
+ $dbh->commit if $oldAutoCommit; #well.
+ return $error;
+ }
+
+ }
+
+ }
+ my $error = $new_cust_pay_batch->replace($cust_pay_batch);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error updating status of paybatchnum $hash{'paybatchnum'}: $error\n";
}
}