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;
+use Date::Parse qw(str2time);
+use Business::CreditCard qw(cardtype);
@ISA = qw(FS::Record);
my $info = $import_info{$format}
or die "unknown format $format";
- my $filetype = $info->{'filetype'}; # CSV or fixed
- my @fields = @{ $info->{'fields'} };
+ my $job = $param->{'job'};
+ $job->update_statustext(0) if $job;
+
+ my $conf = new FS::Conf;
+
+ my $filetype = $info->{'filetype'}; # CSV, fixed, variable
+ my @fields = @{ $info->{'fields'}};
my $formatre = $info->{'formatre'}; # for fixed
+ my $parse = $info->{'parse'}; # for variable
my @all_values;
my $begin_condition = $info->{'begin_condition'};
my $end_condition = $info->{'end_condition'};
my $end_hook = $info->{'end_hook'};
+ my $skip_condition = $info->{'skip_condition'};
my $hook = $info->{'hook'};
my $approved_condition = $info->{'approved'};
my $declined_condition = $info->{'declined'};
+ my $close_condition = $info->{'close_condition'};
my $csv = new Text::CSV_XS;
# process one line at a time.
if ($filetype eq 'XML') {
+ eval "use XML::Simple";
+ die $@ if $@;
my @xmlkeys = @{ $info->{'xmlkeys'} }; # for XML
my $xmlrow = $info->{'xmlrow'}; # also for XML
# Do everything differently.
- my $data = XMLin($fh, KeepRoot => 1);
+ my $data = XML::Simple::XMLin($fh, KeepRoot => 1);
my $rows = $data;
# $xmlrow = [ RootKey, FirstLevelKey, SecondLevelKey... ]
$rows = $rows->{$_} foreach( @$xmlrow );
}
$rows = [ $rows ] if ref($rows) ne 'ARRAY';
foreach my $row (@$rows) {
- push @all_values, [ @{$row}{@xmlkeys} ];
+ push @all_values, [ @{$row}{@xmlkeys}, $row ];
}
}
else {
$dbh->rollback if $oldAutoCommit;
return "can't parse: ". $csv->error_input();
};
- push @all_values, [ $csv->fields() ];
+ push @all_values, [ $csv->fields(), $line ];
}elsif ($filetype eq 'fixed'){
- my @values = $line =~ /$formatre/;
+ my @values = ( $line =~ /$formatre/ );
unless (@values) {
$dbh->rollback if $oldAutoCommit;
return "can't parse: ". $line;
};
+ push @values, $line;
+ push @all_values, \@values;
+ }
+ elsif ($filetype eq 'variable') {
+ my @values = ( eval { $parse->($self, $line) } );
+ if( $@ ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $@;
+ };
+ push @values, $line;
push @all_values, \@values;
- }else{
+ }
+ else {
$dbh->rollback if $oldAutoCommit;
return "Unknown file type $filetype";
}
}
}
+ my $num = 0;
foreach (@all_values) {
+ if($job) {
+ $num++;
+ $job->update_statustext(int(100 * $num/scalar(@all_values)));
+ }
my @values = @$_;
my %hash;
+ my $line = pop @values;
foreach my $field ( @fields ) {
my $value = shift @values;
next unless $field;
$hash{$field} = $value;
}
- if ( defined($end_condition) and &{$end_condition}(\%hash) ) {
+ if ( defined($begin_condition) ) {
+ if ( &{$begin_condition}(\%hash, $line) ) {
+ undef $begin_condition;
+ }
+ else {
+ next;
+ }
+ }
+
+ if ( defined($end_condition) and &{$end_condition}(\%hash, $line) ) {
my $error;
- $error = &{$end_hook}(\%hash, $total) if defined($end_hook);
+ $error = &{$end_hook}(\%hash, $total, $line) if defined($end_hook);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
last;
}
+ if ( defined($skip_condition) and &{$skip_condition}(\%hash, $line) ) {
+ next;
+ }
+
my $cust_pay_batch =
qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } );
unless ( $cust_pay_batch ) {
return "error updating status of paybatchnum $hash{'paybatchnum'}: $error\n";
}
+ # purge CVV when the batch is processed
+ if ( $payby =~ /^(CARD|DCRD)$/ ) {
+ my $payinfo = $hash{'payinfo'} || $cust_pay_batch->payinfo;
+ if ( ! grep { $_ eq cardtype($payinfo) }
+ $conf->config('cvv-save') ) {
+ $new_cust_pay_batch->cust_main->remove_cvv;
+ }
+ }
+
if ( $new_cust_pay_batch->status =~ /Approved/i ) {
my $cust_pay = new FS::cust_pay ( {
'custnum' => $custnum,
'payby' => $payby,
- 'paybatch' => $self->batchnum,
- map { $_ => $hash{$_} } (qw( paid _date payinfo )),
+ 'paybatch' => $hash{'paybatch'} || $self->batchnum,
+ 'payinfo' => ( $hash{'payinfo'} || $cust_pay_batch->payinfo ),
+ map { $_ => $hash{$_} } (qw( paid _date )),
} );
$error = $cust_pay->insert;
if ( $error ) {
}
}
-
+
+ if ( defined($close_condition) ) {
+ # Allow the module to decide whether to close the batch.
+ # This is used for TD EFT, which requires two imports before
+ # closing.
+ # $close_condition can also die() to abort the whole import.
+ my $close = eval { $close_condition->($self) };
+ if ( $@ ) {
+ $dbh->rollback;
+ die $@;
+ }
+ $self->set_status('I') if !$close;
+ }
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
-sub export_batch {
+use MIME::Base64;
+use Storable 'thaw';
+use Data::Dumper;
+sub process_import_results {
+ my $job = shift;
+ my $param = thaw(decode_base64(shift));
+ $param->{'job'} = $job;
+ warn Dumper($param) if $DEBUG;
+ my $batchnum = delete $param->{'batchnum'} or die "no batchnum specified\n";
+ my $batch = FS::pay_batch->by_key($batchnum) or die "batchnum '$batchnum' not found\n";
+
+ my $file = $param->{'uploaded_files'} or die "no files provided\n";
+ $file =~ s/^(\w+):([\.\w]+)$/$2/;
+ my $dir = '%%%FREESIDE_CACHE%%%/cache.' . $FS::UID::datasrc;
+ open( $param->{'filehandle'},
+ '<',
+ "$dir/$file" )
+ or die "unable to open '$file'.\n";
+ my $error = $batch->import_results($param);
+ unlink $file;
+ die $error if $error;
+}
+
# Formerly httemplate/misc/download-batch.cgi
+sub export_batch {
my $self = shift;
my $conf = new FS::Conf;
my $format = shift || $conf->config('batch-default_format')
my $info = $export_info{$format} or die "Format not found: '$format'\n";
&{$info->{'init'}}($conf) if exists($info->{'init'});
+ my $curuser = $FS::CurrentUser::CurrentUser;
+
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $error;
-
my $first_download;
- if($self->status eq 'O') {
+ my $status = $self->status;
+ if ($status eq 'O') {
$first_download = 1;
- }
- elsif($self->status eq 'I' and
- $FS::CurrentUser::CurrentUser->access_right('Reprocess batches')) {
+ my $error = $self->set_status('I');
+ die "error updating pay_batch status: $error\n" if $error;
+ } elsif ($status eq 'I' && $curuser->access_right('Reprocess batches')) {
$first_download = 0;
+ } else {
+ die "No pending batch.\n";
}
- 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 @cust_pay_batch = sort { $a->paybatchnum <=> $b->paybatchnum }
qsearch('cust_pay_batch', { batchnum => $self->batchnum } );
-
+
+ # handle batch-increment_expiration option
+ if ( $self->payby eq 'CARD' ) {
+ my ($cmon, $cyear) = (localtime(time))[4,5];
+ foreach (@cust_pay_batch) {
+ my $etime = str2time($_->exp) or next;
+ my ($day, $mon, $year) = (localtime($etime))[3,4,5];
+ if( $conf->exists('batch-increment_expiration') ) {
+ $year++ while( $year < $cyear or ($year == $cyear and $mon <= $cmon) );
+ $_->exp( sprintf('%4u-%02u-%02u', $year + 1900, $mon+1, $day) );
+ }
+ $_->setfield('expmmyy', sprintf('%02u%02u', $mon+1, $year % 100));
+ }
+ }
my $h = $info->{'header'};
if(ref($h) eq 'CODE') {
$batch .= &$h($self, \@cust_pay_batch) . "\n";
$batch .= $h . "\n";
}
foreach my $cust_pay_batch (@cust_pay_batch) {
- if($first_download) {
+
+ 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
+ if ($balance <= 0) { # then don't charge this customer
+ 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) {
+ # reduce the charge to the remaining balance
$cust_pay_batch->amount($balance);
- $error = $cust_pay_batch->replace;
+ my $error = $cust_pay_batch->replace;
+ if ( $error ) {
+ $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ die $error;
+ }
}
# 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";
}
+
+ $batchcount++;
+ $batchtotal += $cust_pay_batch->amount;
+ $batch .= &{$info->{'row'}}($cust_pay_batch, $self, $batchcount, $batchtotal) . "\n";
+
}
+
my $f = $info->{'footer'};
if(ref($f) eq 'CODE') {
$batch .= &$f($self, $batchcount, $batchtotal) . "\n";
}
if ($info->{'autopost'}) {
- $error = &{$info->{'autopost'}}($self, $batch);
+ my $error = &{$info->{'autopost'}}($self, $batch);
if($error) {
$dbh->rollback or die $dbh->errstr if $oldAutoCommit;
die $error;