diff options
author | mark <mark> | 2009-09-25 02:30:21 +0000 |
---|---|---|
committer | mark <mark> | 2009-09-25 02:30:21 +0000 |
commit | 25b0525eb1f0d018b893a7bdc96b92a8f446020f (patch) | |
tree | 757ee624a31470f4be42619ca8b50d392ab7de35 /FS | |
parent | 03bd5bb743b3b39e4e00fd169543532598d3b5e5 (diff) |
Batch payment refactoring
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/Conf.pm | 7 | ||||
-rw-r--r-- | FS/FS/pay_batch.pm | 429 | ||||
-rw-r--r-- | FS/FS/pay_batch/BoM.pm | 73 | ||||
-rw-r--r-- | FS/FS/pay_batch/PAP.pm | 103 | ||||
-rw-r--r-- | FS/FS/pay_batch/ach_spiritone.pm | 65 | ||||
-rw-r--r-- | FS/FS/pay_batch/chase_canada.pm | 104 | ||||
-rw-r--r-- | FS/FS/pay_batch/paymentech.pm | 112 | ||||
-rw-r--r-- | FS/FS/pay_batch/td_canada_trust.pm | 104 |
8 files changed, 759 insertions, 238 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; + |