From 25b0525eb1f0d018b893a7bdc96b92a8f446020f Mon Sep 17 00:00:00 2001 From: mark Date: Fri, 25 Sep 2009 02:30:21 +0000 Subject: [PATCH] Batch payment refactoring --- FS/FS/Conf.pm | 7 + FS/FS/pay_batch.pm | 429 ++++++++++++++++------------------- FS/FS/pay_batch/BoM.pm | 73 ++++++ FS/FS/pay_batch/PAP.pm | 103 +++++++++ FS/FS/pay_batch/ach_spiritone.pm | 65 ++++++ FS/FS/pay_batch/chase_canada.pm | 104 +++++++++ FS/FS/pay_batch/paymentech.pm | 112 +++++++++ FS/FS/pay_batch/td_canada_trust.pm | 104 +++++++++ httemplate/misc/download-batch.cgi | 200 +--------------- httemplate/search/cust_pay_batch.cgi | 2 + 10 files changed, 766 insertions(+), 433 deletions(-) create mode 100644 FS/FS/pay_batch/BoM.pm create mode 100644 FS/FS/pay_batch/PAP.pm create mode 100644 FS/FS/pay_batch/ach_spiritone.pm create mode 100644 FS/FS/pay_batch/chase_canada.pm create mode 100644 FS/FS/pay_batch/paymentech.pm create mode 100644 FS/FS/pay_batch/td_canada_trust.pm 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 = (); -<%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; - - 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!!. qq!!. qq!!. + qq!!. qq!!; } $html_init .= qq!
!; @@ -171,6 +172,7 @@ if ( $pay_batch ) { qq!!. qq!!. qq!!. + qq!!. qq!
!; } $html_init .= qq!!; -- 2.11.0