use strict;
use vars qw( @ISA $DEBUG );
use FS::Record qw(dbh qsearch qsearchs);
-use Business::CreditCard;
+use FS::payinfo_Mixin;
+use Business::CreditCard 0.28;
-@ISA = qw( FS::Record );
+@ISA = qw( FS::Record FS::payinfo_Mixin );
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
$error = $record->check;
+ $error = $record->retriable;
+
=head1 DESCRIPTION
An FS::cust_pay_batch object represents a credit card transaction ready to be
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_numbern('trancode') #deprecated
|| $self->ut_money('amount')
|| $self->ut_number('invnum')
|| $self->ut_number('custnum')
$self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
$self->first($1);
- $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREP|CASH|WEST|MCRD)$/
- or return "Illegal payby";
- $self->payby($1);
-
- # FIXME
- # there is no point in false laziness here
- # we will effectively set "check_payinfo to 0"
- # we can change that when we finish the refactor
-
- #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";
+ $error = $self->payinfo_check();
+ return $error if $error;
if ( $self->exp eq '' ) {
return "Expiration date required"
$self->payname($1);
}
- #$self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/
- # or return "Illegal zip: ". $self->zip;
- #$self->zip($1);
+ #we have lots of old zips in there... don't hork up batch results cause of em
+ $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
+ or return "Illegal zip: ". $self->zip;
+ $self->zip($1);
$self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
$self->country($1);
- $error = $self->ut_zip('zip', $self->country);
- return $error if $error;
+ #$error = $self->ut_zip('zip', $self->country);
+ #return $error if $error;
#check invnum, custnum, ?
qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
}
-=back
+=item retriable
-=head1 SUBROUTINES
+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.
-=over 4
-
-=item import_results
+Implementation details: For the named customer's invoice, changes the
+statustext of the 'done' (without statustext) event to 'retriable.'
=cut
-sub import_results {
- use Time::Local;
- use FS::cust_pay;
- eval "use Text::CSV_XS;";
- die $@ if $@;
-#
- my $param = shift;
- my $fh = $param->{'filehandle'};
- 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;
- my $approved_condition;
- my $declined_condition;
-
- 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
- # with no decimal entered.
- '', # Card Type: 0 - MCrd, 1 - Visa, 2 - AMEX, 3 - Discover,
- # 4 - Insignia, 5 - Diners/EnRoute, 6 - JCB
- '_date', # Transaction Date: Date the Transaction was processed
- 'time', # Transaction Time: Time the transaction was processed
- 'payinfo', # Card Number: Card number for the transaction
- '', # Expiry Date: Expiry date of the card
- '', # Auth#: Authorization number entered for force post
- # transaction
- 'type', # Transaction Type: 0 - purchase, 40 - refund,
- # 20 - force post
- 'result', # Processing Result: 3 - Approval,
- # 4 - Declined/Amount over limit,
- # 5 - Invalid/Expired/stolen card,
- # 6 - Comm Error
- '', # Terminal ID: Terminal ID used to process the transaction
- );
-
- $end_condition = sub {
- my $hash = shift;
- $hash->{'type'} eq '0BC';
- };
-
- $end_hook = sub {
- my( $hash, $total) = @_;
- $total = sprintf("%.2f", $total);
- my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 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 );
- $hash->{'_date'} = timelocal( substr($hash->{'time'}, 4, 2),
- substr($hash->{'time'}, 2, 2),
- substr($hash->{'time'}, 0, 2),
- substr($hash->{'_date'}, 6, 2),
- substr($hash->{'_date'}, 4, 2)-1,
- substr($hash->{'_date'}, 0, 4)-1900, );
- };
-
- $approved_condition = sub {
- my $hash = shift;
- $hash->{'type'} eq '0' && $hash->{'result'} == 3;
- };
-
- $declined_condition = sub {
- my $hash = shift;
- $hash->{'type'} eq '0' && ( $hash->{'result'} == 4
- || $hash->{'result'} == 5 );
- };
-
-
- }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";
- }
-
- my $csv = new Text::CSV_XS;
+sub retriable {
+ my $self = shift;
- local $SIG{HUP} = 'IGNORE';
+ local $SIG{HUP} = 'IGNORE'; #Hmm
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
local $SIG{TERM} = 'IGNORE';
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
-
- 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 "Unknown file type $filetype";
- }
-
- my %hash;
- foreach my $field ( @fields ) {
- my $value = shift @values;
- next unless $field;
- $hash{$field} = $value;
- }
-
- if ( &{$end_condition}(\%hash) ) {
- my $error = &{$end_hook}(\%hash, $total);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- last;
- }
-
- my $cust_pay_batch =
- 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 $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' => $payby,
- 'paybatch' => $paybatch,
- map { $_ => $hash{$_} } (qw( paid _date payinfo )),
- } );
- $error = $cust_pay->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n";
- }
- $total += $hash{'paid'};
-
- $cust_pay->cust_main->apply_payments;
-
- } elsif ( &{$declined_condition}(\%hash) ) {
-
- $new_cust_pay_batch->status('Declined');
-
- #this should be configurable... if anybody else ever uses batches
- # $cust_pay_batch->cust_main->suspend;
-
- foreach my $part_bill_event (
- sort { $a->seconds <=> $b->seconds
- || $a->weight <=> $b->weight
- || $a->eventpart <=> $b->eventpart }
- grep { ! qsearch( 'cust_bill_event', {
- 'invnum' => $cust_pay_batch->invnum,
- 'eventpart' => $_->eventpart,
- 'status' => 'done',
- } )
- }
- qsearch( {
- 'table' => 'part_bill_event',
- 'hashref' => { 'payby' => 'DCLN',
- 'disabled' => '', },
- } )
- ) {
-
- # don't run subsequent events if balance<=0
- last if $cust_pay_batch->cust_main->balance <= 0;
-
- warn " calling invoice event (". $part_bill_event->eventcode. ")\n"
- if $DEBUG > 1;
- my $cust_main = $cust_pay_batch->cust_main; #for callback
-
- my $error;
- {
- local $SIG{__DIE__}; # don't want Mason __DIE__ handler active
- $error = eval $part_bill_event->eventcode;
- }
-
- my $status = '';
- my $statustext = '';
- if ( $@ ) {
- $status = 'failed';
- $statustext = $@;
- } elsif ( $error ) {
- $status = 'done';
- $statustext = $error;
- } else {
- $status = 'done'
- }
-
- #add cust_bill_event
- my $cust_bill_event = new FS::cust_bill_event {
- 'invnum' => $cust_pay_batch->invnum,
- 'eventpart' => $part_bill_event->eventpart,
- '_date' => time,
- 'status' => $status,
- 'statustext' => $statustext,
- };
- $error = $cust_bill_event->insert;
- if ( $error ) {
- # gah, even with transactions.
- $dbh->commit if $oldAutoCommit; #well.
- my $e = 'WARNING: Event run but database not updated - '.
- 'error inserting cust_bill_event, invnum #'. $cust_pay_batch->invnum.
- ', eventpart '. $part_bill_event->eventpart.
- ": $error";
- warn $e;
- return $e;
- }
-
- }
-
- }
-
- 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";
- }
-
+ 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";
}
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
-
}
=back