summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Conf.pm7
-rw-r--r--FS/FS/pay_batch.pm429
-rw-r--r--FS/FS/pay_batch/BoM.pm73
-rw-r--r--FS/FS/pay_batch/PAP.pm103
-rw-r--r--FS/FS/pay_batch/ach_spiritone.pm65
-rw-r--r--FS/FS/pay_batch/chase_canada.pm104
-rw-r--r--FS/FS/pay_batch/paymentech.pm112
-rw-r--r--FS/FS/pay_batch/td_canada_trust.pm104
-rw-r--r--httemplate/misc/download-batch.cgi200
-rwxr-xr-xhttemplate/search/cust_pay_batch.cgi2
10 files changed, 766 insertions, 433 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 09545720c..bd1c004f4 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2386,6 +2386,13 @@ worry that config_items is freeside-specific and icky.
},
{
+ 'key' => 'batchconfig-paymentech',
+ 'section' => 'billing',
+ 'description' => 'Configuration for Chase Paymentech batching, four lines: 1. BIN, 2. Terminal ID, 3. Merchant ID, 4. Username',
+ 'type' => 'textarea',
+ },
+
+ {
'key' => 'payment_history-years',
'section' => 'UI',
'description' => 'Number of years of payment history to show by default. Currently defaults to 2.',
diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm
index 5448b031e..ffa6e200e 100644
--- a/FS/FS/pay_batch.pm
+++ b/FS/FS/pay_batch.pm
@@ -1,11 +1,13 @@
package FS::pay_batch;
use strict;
-use vars qw( @ISA );
+use vars qw( @ISA $DEBUG %import_info %export_info $conf );
use Time::Local;
use Text::CSV_XS;
+use XML::Simple qw(XMLin XMLout);
use FS::Record qw( dbh qsearch qsearchs );
use FS::cust_pay;
+use FS::Conf;
@ISA = qw(FS::Record);
@@ -137,6 +139,42 @@ sub set_status {
$self->replace();
}
+# further false laziness
+
+%import_info = %export_info = ();
+foreach my $INC (@INC) {
+ warn "globbing $INC/FS/pay_batch/*.pm\n" if $DEBUG;
+ foreach my $file ( glob("$INC/FS/pay_batch/*.pm")) {
+ warn "attempting to load batch format from $file\n" if $DEBUG;
+ $file =~ /\/(\w+)\.pm$/;
+ next if !$1;
+ my $mod = $1;
+ my ($import, $export, $name) =
+ eval "use FS::pay_batch::$mod;
+ ( \\%FS::pay_batch::$mod\::import_info,
+ \\%FS::pay_batch::$mod\::export_info,
+ \$FS::pay_batch::$mod\::name)";
+ $name ||= $mod; # in case it's not defined
+ if( $@) {
+ # in FS::cdr this is a die, not a warn. That's probably a bug.
+ warn "error using FS::pay_batch::$mod (skipping): $@\n";
+ next;
+ }
+ if(!keys(%$import)) {
+ warn "no \%import_info found in FS::pay_batch::$mod (skipping)\n";
+ }
+ else {
+ $import_info{$name} = $import;
+ }
+ if(!keys(%$export)) {
+ warn "no \%export_info found in FS::pay_batch::$mod (skipping)\n";
+ }
+ else {
+ $export_info{$name} = $export;
+ }
+ }
+}
+
=item import_results OPTION => VALUE, ...
Import batch results.
@@ -155,222 +193,19 @@ sub import_results {
my $param = ref($_[0]) ? shift : { @_ };
my $fh = $param->{'filehandle'};
my $format = $param->{'format'};
-
- 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 'csv-chase_canada-E-xactBatch' ) {
-
- $filetype = "CSV";
-
- @fields = (
- '', # Internal(bank) id of the transaction
- '', # Transaction Type: 00 - purchase, 01 - preauth,
- # 02 - completion, 03 - forcepost,
- # 04 - refund, 05 - auth,
- # 06 - purchase corr, 07 - refund corr,
- # 08 - void 09 - void return
- '', # gateway used to process this transaction
- 'paid', # Amount: Amount of the transaction. Dollars and cents
- # with decimal entered.
- 'auth', # Auth#: Authorization number (if approved)
- 'payinfo', # Card Number: Card number for the transaction
- '', # Expiry Date: Expiry date of the card
- '', # Cardholder Name
- 'bankcode', # Bank response code (3 alphanumeric)
- 'bankmess', # Bank response message
- 'etgcode', # ETG response code (2 alphanumeric)
- 'etgmess', # ETG response message
- '', # Returned customer number for the transaction
- 'paybatchnum', # Reference#: paybatch number of the transaction
- '', # Reference#: Invoice number of the transaction
- 'result', # Processing Result: Approved of Declined
- );
-
- $end_condition = sub {
- '';
- };
-
- $hook = sub {
- my $hash = shift;
- my $cpb = shift;
- $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'}); #hmmmm
- $hash->{'_date'} = time; # got a better one?
- $hash->{'payinfo'} = $cpb->{'payinfo'}
- if( substr($hash->{'payinfo'}, -4) eq substr($cpb->{'payinfo'}, -4) );
- };
-
- $approved_condition = sub {
- my $hash = shift;
- $hash->{'etgcode'} eq '00' && $hash->{'result'} eq "Approved";
- };
-
- $declined_condition = sub {
- my $hash = shift;
- $hash->{'etgcode'} ne '00' # internal processing error
- || ( $hash->{'result'} eq "Declined" );
- };
-
-
- }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;
- };
-
- }elsif ( $format eq 'ach-spiritone' ) {
-
- $filetype = "CSV";
-
- @fields = (
- '', # Name
- 'paybatchnum', # ID: Number of the transaction
- 'aba', # ABA Number for the transaction
- 'payinfo', # Bank Account Number for the transaction
- '', # Transaction Type: 27 - debit
- 'paid', # Amount: Amount of the transaction. Dollars and cents
- # with decimal entered.
- '', # Default Transaction Type
- '', # Default Amount: Dollars and cents with decimal entered.
- );
-
- $end_condition = sub {
- '';
- };
-
- $hook = sub {
- my $hash = shift;
- $hash->{'_date'} = time; # got a better one?
- $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'aba'};
- };
-
- $approved_condition = sub {
- 1;
- };
-
- $declined_condition = sub {
- 0;
- };
-
-
- } else {
- return "Unknown format $format";
- }
+ my $info = $import_info{$format}
+ or die "unknown format $format";
+
+ my $filetype = $info->{'filetype'}; # CSV or fixed
+ my @fields = @{ $info->{'fields'} };
+ my $formatre = $info->{'formatre'}; # for fixed
+ my @all_values;
+ my $begin_condition = $info->{'begin_condition'};
+ my $end_condition = $info->{'end_condition'};
+ my $end_hook = $info->{'end_hook'};
+ my $hook = $info->{'hook'};
+ my $approved_condition = $info->{'approved'};
+ my $declined_condition = $info->{'declined'};
my $csv = new Text::CSV_XS;
@@ -390,36 +225,66 @@ sub import_results {
unless ( $reself->status eq 'I' ) {
$dbh->rollback if $oldAutoCommit;
return "batchnum ". $self->batchnum. "no longer in transit";
- };
+ }
my $error = $self->set_status('R');
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return $error
+ return $error;
}
my $total = 0;
my $line;
- while ( defined($line=<$fh>) ) {
- next if $line =~ /^\s*$/; #skip blank lines
+ # Order of operations has been changed here.
+ # We now slurp everything into @all_values, then
+ # process one line at a time.
- 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{
+ if ($filetype eq 'XML') {
+ my @xmlkeys = @{ $info->{'xmlkeys'} }; # for XML
+ my $xmlrow = $info->{'xmlrow'}; # also for XML
+
+ # Do everything differently.
+ my $data = XMLin($fh, KeepRoot => 1);
+ my $rows = $data;
+ # $xmlrow = [ RootKey, FirstLevelKey, SecondLevelKey... ]
+ $rows = $rows->{$_} foreach( @$xmlrow );
+ if(!defined($rows)) {
$dbh->rollback if $oldAutoCommit;
- return "Unknown file type $filetype";
+ return "can't find rows in XML file";
+ }
+ $rows = [ $rows ] if ref($rows) ne 'ARRAY';
+ foreach my $row (@$rows) {
+ push @all_values, [ @{$row}{@xmlkeys} ];
}
+ }
+ else {
+ 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();
+ };
+ push @all_values, [ $csv->fields() ];
+ }elsif ($filetype eq 'fixed'){
+ my @values = $line =~ /$formatre/;
+ unless (@values) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $line;
+ };
+ push @all_values, \@values;
+ }else{
+ $dbh->rollback if $oldAutoCommit;
+ return "Unknown file type $filetype";
+ }
+ }
+ }
+
+ foreach (@all_values) {
+ my @values = @$_;
my %hash;
foreach my $field ( @fields ) {
@@ -428,8 +293,9 @@ sub import_results {
$hash{$field} = $value;
}
- if ( &{$end_condition}(\%hash) ) {
- my $error = &{$end_hook}(\%hash, $total);
+ if ( defined($end_condition) and &{$end_condition}(\%hash) ) {
+ my $error;
+ $error = &{$end_hook}(\%hash, $total) if defined($end_hook);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
@@ -514,7 +380,6 @@ sub import_results {
}
-
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -522,6 +387,94 @@ sub import_results {
}
+sub export_batch {
+# Formerly httemplate/misc/download-batch.cgi
+ my $self = shift;
+ my $conf = new FS::Conf;
+ my $format = shift || $conf->config('batch-default_format')
+ or die "No batch format configured\n";
+ my $info = $export_info{$format} or die "Format not found: '$format'\n";
+ &{$info->{'init'}}($conf) if exists($info->{'init'});
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $error;
+
+ my $first_download;
+ if($self->status eq 'O') {
+ $first_download = 1;
+ }
+ elsif($self->status eq 'I' and
+ $FS::CurrentUser::CurrentUser->access_right('Reprocess batches')) {
+ $first_download = 0;
+ }
+ else {
+ die "No pending batch.\n"
+ }
+
+ $error = $self->set_status('I');
+ die "error updating pay_batch status: $error\n" if $error;
+
+ my $batch = '';
+ my $batchtotal = 0;
+ my $batchcount = 0;
+
+ my @cust_pay_batch = sort { $a->paybatchnum <=> $b->paybatchnum }
+ qsearch('cust_pay_batch', { batchnum => $self->batchnum } );
+
+ my $h = $info->{'header'};
+ if(ref($h) eq 'CODE') {
+ $batch .= &$h($self, \@cust_pay_batch) . "\n";
+ }
+ else {
+ $batch .= $h . "\n";
+ }
+ foreach my $cust_pay_batch (@cust_pay_batch) {
+ if($first_download) {
+ my $balance = $cust_pay_batch->cust_main->balance;
+ $error = '';
+ if($balance <= 0) { # then don't charge this customer
+ $error = $cust_pay_batch->delete;
+ undef $cust_pay_batch;
+ }
+ elsif($balance < $cust_pay_batch->amount) { # then reduce the charge to the remaining balance
+ $cust_pay_batch->amount($balance);
+ $error = $cust_pay_batch->replace;
+ }
+ # else $balance >= $cust_pay_batch->amount
+ if($error) {
+ $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ die $error;
+ }
+ }
+ if($cust_pay_batch) { # that is, it wasn't deleted
+ $batchcount++;
+ $batchtotal += $cust_pay_batch->amount;
+ $batch .= &{$info->{'row'}}($cust_pay_batch, $self) . "\n";
+ }
+ }
+ my $f = $info->{'footer'};
+ if(ref($f) eq 'CODE') {
+ $batch .= &$f($self, $batchcount, $batchtotal) . "\n";
+ }
+ else {
+ $batch .= $f . "\n";
+ }
+
+ if ($info->{'autopost'}) {
+ $error = &{$info->{'autopost'}}($self, $batch);
+ if($error) {
+ $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ die $error;
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ return $batch;
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/pay_batch/BoM.pm b/FS/FS/pay_batch/BoM.pm
new file mode 100644
index 000000000..7bfc22a64
--- /dev/null
+++ b/FS/FS/pay_batch/BoM.pm
@@ -0,0 +1,73 @@
+package FS::pay_batch::BoM;
+
+use strict;
+use vars qw(@ISA %import_info %export_info $name);
+use Time::Local 'timelocal';
+use FS::Conf;
+
+my $conf;
+my ($origid, $datacenter, $typecode, $shortname, $longname, $mybank, $myacct);
+
+$name = 'BoM';
+
+%import_info = (
+ 'filetype' => 'CSV',
+ 'fields' => [],
+ 'hook' => sub { die "Can't import BoM" },
+ 'approved' => sub { 1 },
+ 'declined' => sub { 0 },
+);
+
+%export_info = (
+ init => sub {
+ $conf = shift;
+ ($origid,
+ $datacenter,
+ $typecode,
+ $shortname,
+ $longname,
+ $mybank,
+ $myacct) = $conf->config("batchconfig-BoM");
+ },
+ header => sub {
+ my $pay_batch = shift;
+ sprintf( "A%10s%04u%06u%05u%54s\n",
+ $origid,
+ $pay_batch->batchnum,
+ jdate($pay_batch->download),
+ $datacenter,
+ "") .
+ sprintf( "XD%03u%06u%-15s%-30s%09u%-12s \n",
+ $typecode,
+ jdate($pay_batch->download),
+ $shortname,
+ $longname,
+ $mybank,
+ $myacct);
+ },
+ row => sub {
+ my ($cust_pay_batch, $pay_batch) = @_;
+ my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
+ sprintf( "D%010.0f%09u%-12s%-29s%-19s\n",
+ $cust_pay_batch->amount * 100,
+ $aba,
+ $account,
+ $cust_pay_batch->payname,
+ $cust_pay_batch->paybatchnum
+ );
+ },
+ footer => sub {
+ my ($pay_batch, $batchcount, $batchtotal) = @_;
+ sprintf( "YD%08u%014.0f%56s\n", $batchcount, $batchtotal*100, "").
+ sprintf( "Z%014u%04u%014u%05u%41s\n",
+ $batchtotal*100, $batchcount, "0", "0", "");
+ },
+);
+
+sub jdate {
+ my (@date) = localtime(shift);
+ sprintf("%03d%03d", $date[5] % 100, $date[7] + 1);
+}
+
+1;
+
diff --git a/FS/FS/pay_batch/PAP.pm b/FS/FS/pay_batch/PAP.pm
new file mode 100644
index 000000000..432ef07ed
--- /dev/null
+++ b/FS/FS/pay_batch/PAP.pm
@@ -0,0 +1,103 @@
+package FS::pay_batch::PAP;
+
+use strict;
+use vars qw(@ISA %import_info %export_info $name);
+use Time::Local 'timelocal';
+use FS::Conf;
+
+my $conf;
+my ($origid, $datacenter, $typecode, $shortname, $longname, $mybank, $myacct);
+
+$name = 'PAP';
+
+%import_info = (
+ 'filetype' => 'fixed',
+ 'formatre' => '^(.).{19}(.{4})(.{3})(.{10})(.{6})(.{9})(.{12}).{110}(.{19}).{71}$',
+ 'fields' => [
+ 'recordtype',
+ 'batchnum',
+ 'datacenter',
+ 'paid',
+ '_date',
+ 'bank',
+ 'payinfo',
+ 'paybatchnum',
+ ],
+ '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' => sub { 1 },
+ 'declined' => sub { 0 },
+# Why does pay_batch.pm have approved_condition and declined_condition?
+# It doesn't even try to handle the case of neither condition being met.
+ '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;
+ '';
+ },
+ 'end_condition' => sub {
+ my $hash = shift;
+ $hash->{recordtype} eq 'W';
+ },
+);
+
+%export_info = (
+ init => sub {
+ $conf = shift;
+ ($origid,
+ $datacenter,
+ $typecode,
+ $shortname,
+ $longname,
+ $mybank,
+ $myacct) = $conf->config("batchconfig-PAP");
+ },
+ header => sub {
+ my $pay_batch = shift;
+ sprintf( "H%10sD%3s%06u%-15s%09u%-12s%04u%19s\n",
+ $origid,
+ $typecode,
+ cdate($pay_batch->download),
+ $shortname,
+ $mybank,
+ $myacct,
+ $pay_batch->batchnum,
+ "" )
+ },
+ row => sub {
+ my ($cust_pay_batch, $pay_batch) = @_;
+ my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
+ sprintf( "D%-23s%06u%-19s%09u%-12s%010.0f\n",
+ $cust_pay_batch->payname,
+ cdate($pay_batch->download),
+ $cust_pay_batch->paybatchnum,
+ $aba,
+ $account,
+ $cust_pay_batch->amount*100 );
+ },
+ footer => sub {
+ my ($pay_batch, $batchcount, $batchtotal) = @_;
+ sprintf( "T%08u%014.0f%57s\n",
+ $batchcount,
+ $batchtotal*100,
+ "" );
+ },
+);
+
+sub cdate {
+ my (@date) = localtime(shift);
+ sprintf("%02d%02d%02d", $date[3], $date[4] + 1, $date[5] % 100);
+}
+
+1;
+
diff --git a/FS/FS/pay_batch/ach_spiritone.pm b/FS/FS/pay_batch/ach_spiritone.pm
new file mode 100644
index 000000000..bd3bb14c3
--- /dev/null
+++ b/FS/FS/pay_batch/ach_spiritone.pm
@@ -0,0 +1,65 @@
+package FS::pay_batch::ach_spiritone;
+
+use strict;
+use vars qw(@ISA %import_info %export_info $name);
+use Time::Local 'timelocal';
+use FS::Conf;
+use File::Temp;
+
+my $conf;
+my ($origid, $datacenter, $typecode, $shortname, $longname, $mybank, $myacct);
+
+$name = 'ach-spiritone'; # note spelling
+
+%import_info = (
+ 'filetype' => 'CSV',
+ 'fields' => [
+ '', #name
+ 'paybatchnum',
+ 'aba',
+ 'payinfo',
+ '', #transaction type
+ 'paid',
+ '', #default transaction type
+ '', #default amount
+ ],
+ 'hook' => sub {
+ my $hash = shift;
+ $hash->{'_date'} = time;
+ $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'aba'};
+ },
+ 'approved' => sub { 1 },
+ 'declined' => sub { 0 },
+);
+
+%export_info = (
+# This is the simplest case.
+ row => sub {
+ my ($cust_pay_batch, $pay_batch) = @_;
+ my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
+ my $payname = $cust_pay_batch->first . ' ' . $cust_pay_batch->last;
+ $payname =~ tr/",/ /;
+ qq!"$payname","!.$cust_pay_batch->paybatchnum.
+ qq!","$aba","$account","27","!.$cust_pay_batch->amount.
+ qq!","27","0.00"!; #"
+ },
+ autopost => sub {
+ my ($pay_batch, $batch) = @_;
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+ my $fh = new File::Temp(
+ TEMPLATE => 'paybatch.'. $pay_batch->batchnum .'.XXXXXXXX',
+ DIR => $dir,
+ ) or return "can't open temp file: $!\n";
+
+ print $fh $batch;
+ seek $fh, 0, 0;
+
+ my $error = $pay_batch->import_results( 'filehandle' => $fh,
+ 'format' => $name,
+ );
+ return $error if $error;
+ },
+);
+
+1;
+
diff --git a/FS/FS/pay_batch/chase_canada.pm b/FS/FS/pay_batch/chase_canada.pm
new file mode 100644
index 000000000..909e4ae18
--- /dev/null
+++ b/FS/FS/pay_batch/chase_canada.pm
@@ -0,0 +1,104 @@
+package FS::pay_batch::chase_canada;
+
+use strict;
+use vars qw(@ISA %import_info %export_info $name);
+use Time::Local 'timelocal';
+use FS::Conf;
+
+my $conf;
+my $origid;
+
+$name = 'csv-chase_canada-E-xactBatch';
+
+%import_info = (
+ 'filetype' => 'CSV',
+ 'fields' => [
+ '',
+ '',
+ '',
+ 'paid',
+ 'auth',
+ 'payinfo',
+ '',
+ '',
+ 'bankcode',
+ 'bankmess',
+ 'etgcode',
+ 'etgmess',
+ '',
+ 'paybatchnum',
+ '',
+ 'result',
+ ],
+ 'hook' => sub {
+ my $hash = shift;
+ my $cpb = shift;
+ $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} );
+ $hash->{'_date'} = time;
+ $hash->{'payinfo'} = $cpb->{'payinfo'}
+ if( substr($hash->{'payinfo'}, -4) eq substr($cpb->{'payinfo'}, -4) );
+ },
+ 'approved' => sub {
+ my $hash = shift;
+ $hash->{'etgcode'} eq '00' && $hash->{'result'} eq 'Approved';
+ },
+ 'declined' => sub {
+ my $hash = shift;
+ $hash->{'etgcode'} ne '00' || $hash->{'result'} eq 'Declined';
+ },
+);
+
+%export_info = (
+ init => sub {
+ $conf = shift;
+ ($origid) = $conf->config("batchconfig-$name");
+ },
+ header => sub {
+ my $pay_batch = shift;
+ sprintf( '$$E-xactBatchFileV1.0$$%s:%03u$$%s',
+ sdate($pay_batch->download),
+ $pay_batch->batchnum,
+ $origid );
+ },
+ row => sub {
+ my ($cust_pay_batch, $pay_batch) = @_;
+ my $payname = $cust_pay_batch->payname;
+ $payname =~ tr/",/ /;
+
+ join(',',
+ $cust_pay_batch->paybatchnum,
+ $cust_pay_batch->custnum,
+ $cust_pay_batch->invnum,
+ qq!"$payname"!,
+ '00',
+ $cust_pay_batch->payinfo,
+ $cust_pay_batch->amount,
+ expdate($cust_pay_batch->exp),
+ '',
+ ''
+ );
+ },
+ # no footer
+);
+
+sub sdate {
+ my (@date) = localtime(shift);
+ sprintf('%02d/%02d/%02d', $date[5] % 100, $date[4] + 1, $date[3]);
+}
+
+sub expdate {
+ my $exp = shift;
+ $exp =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ my ($mon, $y) = ($2, $1);
+ if($conf->exists('batch-increment_expiration')) {
+ my ($curmon, $curyear) = (localtime(time))[4,5];
+ $curmon++;
+ $curyear -= 100;
+ $y++ while $y < $curyear || ($y == $curyear && $mon < $curmon);
+ }
+ $mon = "0$mon" if $mon =~ /^\d$/;
+ $y = "0$y" if $y =~ /^\d$/;
+ return "$mon$y";
+}
+
+1;
diff --git a/FS/FS/pay_batch/paymentech.pm b/FS/FS/pay_batch/paymentech.pm
new file mode 100644
index 000000000..33a9fda73
--- /dev/null
+++ b/FS/FS/pay_batch/paymentech.pm
@@ -0,0 +1,112 @@
+package FS::pay_batch::paymentech;
+
+use strict;
+use vars qw(@ISA %import_info %export_info $name);
+use Time::Local;
+use Date::Format 'time2str';
+use Date::Parse 'str2time';
+use FS::Conf;
+use XML::Simple qw(XMLin XMLout);
+
+my $conf;
+my ($bin, $merchantID, $terminalID, $username);
+$name = 'paymentech';
+
+%import_info = (
+ filetype => 'XML',
+ xmlrow => [ qw(transResponse newOrderResp) ],
+ fields => [
+ 'paybatchnum',
+ '_date',
+ 'approvalStatus',
+ ],
+ xmlkeys => [
+ 'orderID',
+ 'respDateTime',
+ 'approvalStatus',
+ ],
+ 'hook' => sub {
+ my ($hash, $oldhash) = @_;
+ my ($mon, $day, $year, $hour, $min, $sec) =
+ $hash->{'_date'} =~ /^(..)(..)(....)(..)(..)(..)$/;
+ $hash->{'_date'} = timelocal($sec, $min, $hour, $day, $mon-1, $year);
+ $hash->{'paid'} = $oldhash->{'amount'};
+ },
+ 'approved' => sub { my $hash = shift;
+ $hash->{'approvalStatus'}
+ },
+ 'declined' => sub { my $hash = shift;
+ ! $hash->{'approvalStatus'}
+ },
+);
+
+my %paytype = (
+ 'personal checking' => 'C',
+ 'personal savings' => 'S',
+ 'business checking' => 'X',
+ 'business savings' => 'X',
+ );
+
+%export_info = (
+ init => sub {
+ my $conf = shift;
+ ($bin, $terminalID, $merchantID, $username) =
+ $conf->config('batchconfig-paymentech');
+ },
+# Here we do all the work in the header function.
+ header => sub {
+ my $pay_batch = shift;
+ my @cust_pay_batch = @{(shift)};
+ my $count = 0;
+ XMLout( {
+ transRequest => {
+ RequestCount => scalar(@cust_pay_batch),
+ batchFileID => {
+ userID => $username,
+ fileDateTime => time2str('%Y%m%d%H%M%s',time),
+ fileID => 'batch'.time2str('%Y%m%d',time),
+ },
+ newOrder => [ map { {
+ # $_ here refers to a cust_pay_batch record.
+ BatchRequestNo => $count++,
+ industryType => 'EC',
+ transType => 'AC',
+ bin => $bin,
+ merchantID => $merchantID,
+ terminalID => $terminalID,
+ ($_->payby eq 'CARD') ? (
+ # Credit card stuff
+ ccAccountNum => $_->payinfo,
+ ccExp => time2str('%y%m',str2time($_->exp)),
+ ) : (
+ # ECP (electronic check) stuff
+ ecpCheckRT => ($_->payinfo =~ /@(\d+)/),
+ ecpCheckDDA => ($_->payinfo =~ /(\d+)@/),
+ ecpBankAcctType => $paytype{lc($_->cust_main->paytype)},
+ ecpDelvMethod => 'B'
+ ),
+ avsZip => $_->zip,
+ avsAddress1 => $_->address1,
+ avsAddress2 => $_->address2,
+ avsCity => $_->city,
+ avsState => $_->state,
+ avsName => $_->first . ' ' . $_->last,
+ avsCountryCode => $_->country,
+ orderID => $_->paybatchnum,
+ amount => $_->amount * 100,
+ } } @cust_pay_batch
+ ],
+ endOfDay => {
+ BatchRequestNo => $count++,
+ bin => $bin,
+ merchantID => $merchantID,
+ terminalID => $terminalID
+ },
+ }
+ }, KeepRoot => 1, NoAttr => 1);
+ },
+ row => sub {},
+);
+
+1;
+
diff --git a/FS/FS/pay_batch/td_canada_trust.pm b/FS/FS/pay_batch/td_canada_trust.pm
new file mode 100644
index 000000000..43b92371e
--- /dev/null
+++ b/FS/FS/pay_batch/td_canada_trust.pm
@@ -0,0 +1,104 @@
+package FS::pay_batch::td_canada_trust;
+
+# Formerly known as csv-td_canada_trust-merchant_pc_batch,
+# which I'm sure we can all agree is both a terrible name
+# and an illegal Perl identifier.
+
+use strict;
+use vars qw(@ISA %import_info %export_info $name);
+use Time::Local 'timelocal';
+use FS::Conf;
+
+my $conf;
+my ($origid, $datacenter, $typecode, $shortname, $longname, $mybank, $myacct);
+
+$name = 'csv-td_canada_trust-merchant_pc_batch';
+
+%import_info = (
+ 'filetype' => 'CSV',
+ 'fields' => [
+ 'paybatchnum',
+ 'paid',
+ '', # card type
+ '_date',
+ 'time',
+ 'payinfo',
+ '', # expiry date
+ '', # auth number
+ 'type', # transaction type
+ 'result', # processing result
+ '', # terminal ID
+ ],
+ 'hook' => sub {
+ my $hash = shift;
+ my $date = $hash->{'_date'};
+ my $time = $hash->{'time'};
+ $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100);
+ $hash->{'_date'} = timelocal( substr($time, 4, 2),
+ substr($time, 2, 2),
+ substr($time, 0, 2),
+ substr($date, 6, 2),
+ substr($date, 4, 2)-1,
+ substr($date, 0, 4)-1900 );
+ },
+ 'approved' => sub {
+ my $hash = shift;
+ $hash->{'type'} eq '0' && $hash->{'result'} == 3
+ },
+ 'declined' => sub {
+ my $hash = shift;
+ $hash->{'type'} eq '0' && ( $hash->{'result'} == 4
+ || $hash->{'result'} == 5 )
+ },
+ '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;
+ },
+);
+
+%export_info = (
+ init => sub {
+ $conf = shift;
+ },
+ # no header
+ row => sub {
+ my ($cust_pay_batch, $pay_batch) = @_;
+
+ return join(',',
+ '',
+ '',
+ '',
+ '',
+ $cust_pay_batch->payinfo,
+ expdate($cust_pay_batch->exp),
+ $cust_pay_batch->amount,
+ $cust_pay_batch->paybatchnum
+ );
+ },
+# no footer
+);
+
+sub expdate {
+ my $exp = shift;
+ $exp =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ my ($mon, $y) = ($2, $1);
+ if($conf->exists('batch-increment_expiration')) {
+ my ($curmon, $curyear) = (localtime(time))[4,5];
+ $curmon++;
+ $curyear -= 100;
+ $y++ while $y < $curyear || ($y == $curyear && $mon < $curmon);
+ }
+ $mon = "0$mon" if $mon =~ /^\d$/;
+ $y = "0$y" if $y =~ /^\d$/;
+ return "$mon$y";
+}
+
+1;
+
diff --git a/httemplate/misc/download-batch.cgi b/httemplate/misc/download-batch.cgi
index 57905daf9..01bf5d25f 100644
--- a/httemplate/misc/download-batch.cgi
+++ b/httemplate/misc/download-batch.cgi
@@ -1,146 +1,9 @@
-%if ($format eq "BoM") {
-%
-% my($origid,$datacenter,$typecode,$shortname,$longname,$mybank,$myacct) =
-% $conf->config("batchconfig-$format");
-%
-<% sprintf( "A%10s%04u%06u%05u%54s\n",$origid,$pay_batch->batchnum,$jdate,$datacenter,"").
- sprintf( "XD%03u%06u%-15s%-30s%09u%-12s \n",$typecode,$jdate,$shortname,$longname,$mybank,$myacct )
- %>
-%
-%}elsif ($format eq "PAP"){
-%
-% my($origid,$datacenter,$typecode,$shortname,$longname,$mybank,$myacct) =
-% $conf->config("batchconfig-$format");
-%
-<% sprintf( "H%10sD%3s%06u%-15s%09u%-12s%04u%19s\n",$origid,$typecode,$cdate,$shortname,$mybank,$myacct,$pay_batch->batchnum,"") %>
-%
-%
-%}elsif ($format eq "csv-td_canada_trust-merchant_pc_batch"){
-%# 1;
-%}elsif ($format eq "csv-chase_canada-E-xactBatch"){
-%
-% my($origid) = $conf->config("batchconfig-$format");
-<% sprintf( '$$E-xactBatchFileV1.0$$%s:%03u$$%s',$sdate,$pay_batch->batchnum, $origid)
- %>
-%
-%}elsif ($format eq "ach-spiritone"){
-%# 1;
-%}else{
-% die "Unknown format for batch in batchconfig. \n";
-%}
-%
-%
-%for my $cust_pay_batch ( sort { $a->paybatchnum <=> $b->paybatchnum }
-% qsearch('cust_pay_batch',
-% {'batchnum'=>$pay_batch->batchnum} )
-%) {
-%
-% $cust_pay_batch->exp =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
-% my( $mon, $y ) = ( $2, $1 );
-% if ( $conf->exists('batch-increment_expiration') ) {
-% my( $curmon, $curyear ) = (localtime(time))[4,5];
-% $curmon++; $curyear-=100;
-% $y++ while $y < $curyear || ( $y == $curyear && $mon < $curmon );
-% }
-% $mon = "0$mon" if $mon =~ /^\d$/;
-% $y = "0$y" if $y =~ /^\d$/;
-% my $exp = "$mon$y";
-%
-% if ( $first_download ) {
-% my $balance = $cust_pay_batch->cust_main->balance;
-% if ( $balance <= 0 ) {
-% my $error = $cust_pay_batch->delete;
-% if ( $error ) {
-% $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
-% die $error;
-% }
-% next;
-% } elsif ( $balance < $cust_pay_batch->amount ) {
-% $cust_pay_batch->amount($balance);
-% my $error = $cust_pay_batch->replace;
-% if ( $error ) {
-% $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
-% die $error;
-% }
-% #} elsif ( $balance > $cust_pay_batch->amount ) {
-% }
-% }
-%
-% $batchcount++;
-% $batchtotal += $cust_pay_batch->amount;
-%
-% if ($format eq "BoM") {
-%
-% my( $account, $aba ) = split( '@', $cust_pay_batch->payinfo );
-%
-<% sprintf( "D%010.0f%09u%-12s%-29s%-19s\n",$cust_pay_batch->amount*100,$aba,$account,$cust_pay_batch->payname,$cust_pay_batch->paybatchnum) %>
-%
-%
-% } elsif ($format eq "PAP"){
-%
-% my( $account, $aba ) = split( '@', $cust_pay_batch->payinfo );
-%
-<% sprintf( "D%-23s%06u%-19s%09u%-12s%010.0f\n",$cust_pay_batch->payname,$cdate,$cust_pay_batch->paybatchnum,$aba,$account,$cust_pay_batch->amount*100) %>
-%
-%
-% } elsif ($format eq "csv-td_canada_trust-merchant_pc_batch") {
-%
-%
-,,,,<% $cust_pay_batch->payinfo %>,<% $exp %>,<% $cust_pay_batch->amount %>,<% $cust_pay_batch->paybatchnum %>
-%
-%
-% } elsif ($format eq "csv-chase_canada-E-xactBatch"){
-%
-% my $payname=$cust_pay_batch->payname; $payname =~ tr/",/ /; #payinfo too? :P
-<% $cust_pay_batch->paybatchnum %>,<% $cust_pay_batch->custnum %>,<% $cust_pay_batch->invnum %>,"<% $payname %>",00,<% $cust_pay_batch->payinfo %>,<% $cust_pay_batch->amount %>,<% $exp %>,,
-%
-%
-% }elsif ($format eq "ach-spiritone"){
-%
-% my( $account, $aba ) = split( '@', $cust_pay_batch->payinfo );
-% my $payname=$cust_pay_batch->first. " ". $cust_pay_batch->last;
-% $payname =~ tr/",/ /; #payinfo too?
-% my $batchline = qq!"$payname","!.$cust_pay_batch->paybatchnum.
-% qq!","$aba","$account","27","!.$cust_pay_batch->amount.
-% qq!","27","0.00"!;
-% push @batchlines, $batchline;
-<% $batchline %>
-%
-% } else {
-% die "I'm already dead, but you did not know that.\n";
-% }
-%
-%}
-%
-%if ($format eq "BoM") {
-%
-%
-<% sprintf( "YD%08u%014.0f%56s\n",$batchcount,$batchtotal*100,"" ).
- sprintf( "Z%014u%05u%014u%05u%41s\n",$batchtotal*100,$batchcount,"0","0","" ) %>
-%
-%
-%} elsif ($format eq "PAP"){
-%
-%
-<% sprintf( "T%08u%014.0f%57s\n",$batchcount,$batchtotal*100,"" ) %>
-%
-%
-%} elsif ($format eq "csv-td_canada_trust-merchant_pc_batch"){
-% #1;
-%} elsif ($format eq "csv-chase_canada-E-xactBatch"){
-% #1;
-%} elsif ($format eq "ach-spiritone"){
-% #1;
-%} else {
-% die "I'm already dead (again), but you did not know that.\n";
-%}
-%
-<%init>
+<% $pay_batch->export_batch($format) %>
-my $conf=new FS::Conf;
+<%init>
#http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
-http_header('Content-Type' => 'text/plain' );
+http_header('Content-Type' => 'text/plain' ); # not necessarily correct...
my $batchnum;
if ( $cgi->param('batchnum') =~ /^(\d+)$/ ) {
@@ -152,62 +15,9 @@ if ( $cgi->param('batchnum') =~ /^(\d+)$/ ) {
my $format;
if ( $cgi->param('format') =~ /^([\w\- ]+)$/ ) {
$format = $1;
-} else {
- $format = $conf->config('batch-default_format');
-}
-
-my $autopost;
-if ( $format eq 'ach-spiritone' ) {
- $autopost = 1;
-}else{
- $autopost = 0;
-}
-
-my $oldAutoCommit = $FS::UID::AutoCommit;
-local $FS::UID::AutoCommit = 0;
-my $dbh = dbh;
-
-my $pay_batch = qsearchs('pay_batch', {'batchnum'=>$batchnum, 'status'=>'O'} );
-my $first_download = 1;
-unless ($pay_batch) {
- $pay_batch = qsearchs('pay_batch', {'batchnum'=>$batchnum, 'status'=>'I'} )
- if $FS::CurrentUser::CurrentUser->access_right('Reprocess batches');
- $first_download = 0;
}
-die "No pending batch. \n" unless $pay_batch;
-my $error = $pay_batch->set_status('I');
-die "error updating batch status: $error\n" if $error;
+my $pay_batch = qsearchs('pay_batch', { batchnum => $batchnum } );
+die "Batch not found: '$batchnum'" if !$pay_batch;
-my $batchtotal=0;
-my $batchcount=0;
-
-my (@date)=localtime($pay_batch->download);
-my $jdate = sprintf("%03d", $date[5] % 100).sprintf("%03d", $date[7] + 1);
-my $cdate = sprintf("%02d", $date[3]).sprintf("%02d", $date[4] + 1).
- sprintf("%02d", $date[5] % 100);
-my $sdate = sprintf("%02d", $date[5] % 100).'/'.sprintf("%02d", $date[4] + 1).
- '/'.sprintf("%02d", $date[3]);
-
-my @batchlines = ();
</%init>
-<%cleanup>
-if ($autopost) {
- my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
- my $fh = new File::Temp(
- TEMPLATE => 'paybatch.'. $batchnum .'.XXXXXXXX',
- DIR => $dir,
- ) or die "can't open temp file: $!\n";
-
- print $fh map{ "$_\n" } @batchlines;
- seek $fh, 0, 0;
-
- $error = $pay_batch->import_results( 'filehandle' => $fh,
- 'format' => $format,
- );
- die $error if $error;
-}
-
-$dbh->commit or die $dbh->errstr if $oldAutoCommit;
-
-</%cleanup>
diff --git a/httemplate/search/cust_pay_batch.cgi b/httemplate/search/cust_pay_batch.cgi
index 2fceb93f8..2056876b6 100755
--- a/httemplate/search/cust_pay_batch.cgi
+++ b/httemplate/search/cust_pay_batch.cgi
@@ -147,6 +147,7 @@ if ( $pay_batch ) {
qq!<OPTION VALUE="PAP">80 byte file for TD Canada Trust PAP Batch</OPTION>!.
qq!<OPTION VALUE="BoM">Bank of Montreal ECA batch</OPTION>!.
qq!<OPTION VALUE="ach-spiritone">Spiritone ACH batch</OPTION>!.
+ qq!<OPTION VALUE="paymentech">Chase Paymentech</OPTION>!.
qq!</SELECT>!;
}
$html_init .= qq!<INPUT TYPE="hidden" NAME="batchnum" VALUE="$batchnum"><INPUT TYPE="submit" VALUE="Download"></FORM><BR>!;
@@ -171,6 +172,7 @@ if ( $pay_batch ) {
qq!<OPTION VALUE="PAP">264 byte results for TD Canada Trust PAP Batch</OPTION>!.
qq!<OPTION VALUE="BoM">Bank of Montreal ECA results</OPTION>!.
qq!<OPTION VALUE="ach-spiritone">Spiritone ACH batch</OPTION>!.
+ qq!<OPTION VALUE="paymentech">Chase Paymentech</OPTION>!.
qq!</SELECT><BR>!;
}
$html_init .= qq!<INPUT TYPE="hidden" NAME="batchnum" VALUE="$batchnum">!;