use Time::Local;
use Text::CSV_XS;
use FS::Record qw( dbh qsearch qsearchs );
-use FS::cust_pay;
use FS::Conf;
+use FS::cust_pay;
+use FS::agent;
+use Date::Parse qw(str2time);
+use Business::CreditCard qw(cardtype);
@ISA = qw(FS::Record);
=item batchnum - primary key
+=item agentnum - optional agent number for agent batches
+
=item payby - CARD or CHEK
=item status - O (Open), I (In-transit), or R (Resolved)
$self->ut_numbern('batchnum')
|| $self->ut_enum('payby', [ 'CARD', 'CHEK' ])
|| $self->ut_enum('status', [ 'O', 'I', 'R' ])
+ || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
;
return $error if $error;
$self->SUPER::check;
}
+=item agent
+
+Returns the L<FS::agent> object for this template.
+
+=cut
+
+sub agent {
+ qsearchs('agent', { 'agentnum' => $_[0]->agentnum });
+}
+
=item rebalance
=cut
my $job = $param->{'job'};
$job->update_statustext(0) if $job;
- my $filetype = $info->{'filetype'}; # CSV or fixed
+ 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 $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;
my $reself = $self->select_for_update;
- unless ( $reself->status eq 'I' ) {
+ if ( $reself->status ne 'I'
+ and !$conf->exists('batch-manual_approval') ) {
$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;
- }
-
my $total = 0;
my $line;
};
push @values, $line;
push @all_values, \@values;
- }else{
+ }
+ elsif ($filetype eq 'variable') {
+ my @values = ( eval { $parse->($self, $line) } );
+ if( $@ ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $@;
+ };
+ push @values, $line;
+ push @all_values, \@values;
+ }
+ else {
$dbh->rollback if $oldAutoCommit;
return "Unknown file type $filetype";
}
my $custnum = $cust_pay_batch->custnum,
my $payby = $cust_pay_batch->payby,
- my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
-
&{$hook}(\%hash, $cust_pay_batch->hashref);
+ my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
+
+ my $error = '';
if ( &{$approved_condition}(\%hash) ) {
- $new_cust_pay_batch->status('Approved');
+ foreach ('paid', '_date', 'payinfo') {
+ $new_cust_pay_batch->$_($hash{$_}) if $hash{$_};
+ }
+ $error = $new_cust_pay_batch->approve($hash{'paybatch'});
+ $total += $hash{'paid'};
} elsif ( &{$declined_condition}(\%hash) ) {
- $new_cust_pay_batch->status('Declined');
+ $error = $new_cust_pay_batch->decline($hash{'error_message'});;
}
- my $error = $new_cust_pay_batch->replace($cust_pay_batch);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "error updating status of paybatchnum $hash{'paybatchnum'}: $error\n";
+ return $error;
}
- if ( $new_cust_pay_batch->status =~ /Approved/i ) {
-
- my $cust_pay = new FS::cust_pay ( {
- 'custnum' => $custnum,
- 'payby' => $payby,
- 'paybatch' => $self->batchnum,
- 'payinfo' => ( $hash{'payinfo'} || $cust_pay_batch->payinfo ),
- map { $_ => $hash{$_} } (qw( paid _date )),
- } );
- $error = $cust_pay->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "error adding payment 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;
}
- $total += $hash{'paid'};
-
- $cust_pay->cust_main->apply_payments;
-
- } elsif ( $new_cust_pay_batch->status =~ /Declined/i ) {
- #false laziness w/cust_main::collect
-
- my $due_cust_event = $new_cust_pay_batch->cust_main->due_cust_event(
- #'check_freq' => '1d', #?
- 'eventtable' => 'cust_pay_batch',
- 'objects' => [ $new_cust_pay_batch ],
- );
- unless( ref($due_cust_event) ) {
- $dbh->rollback if $oldAutoCommit;
- return $due_cust_event;
- }
-
- foreach my $cust_event ( @$due_cust_event ) {
-
- #XXX lock event
-
- #re-eval event conditions (a previous event could have changed things)
- next unless $cust_event->test_conditions;
-
- if ( my $error = $cust_event->do_event() ) {
- # gah, even with transactions.
- #$dbh->commit if $oldAutoCommit; #well.
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
+ }
- }
+ } # foreach (@all_values)
+ my $close = 1;
+ if ( defined($close_condition) ) {
+ # Allow the module to decide whether to close the batch.
+ # $close_condition can also die() to abort the whole import.
+ $close = eval { $close_condition->($self) };
+ if ( $@ ) {
+ $dbh->rollback;
+ die $@;
}
-
}
-
+ if ( $close ) {
+ my $error = $self->set_status('R');
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
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";
+
+ # 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));
+ }
}
- foreach my $cust_pay_batch (@cust_pay_batch) {
- if($first_download) {
+
+ if ($first_download) { #remove or reduce entries if customer's balance changed
+
+ my @new = ();
+ foreach my $cust_pay_batch (@cust_pay_batch) {
+
my $balance = $cust_pay_batch->cust_main->balance;
- my $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, $batchcount, $batchtotal) . "\n";
+
+ push @new, $cust_pay_batch;
}
+ @cust_pay_batch = @new;
+
}
- my $f = $info->{'footer'};
- if(ref($f) eq 'CODE') {
- $batch .= &$f($self, $batchcount, $batchtotal) . "\n";
+
+ my $delim = exists($info->{'delimiter'}) ? $info->{'delimiter'} : "\n";
+
+ my $h = $info->{'header'};
+ if (ref($h) eq 'CODE') {
+ $batch .= &$h($self, \@cust_pay_batch). $delim;
+ } else {
+ $batch .= $h. $delim;
}
- else {
- $batch .= $f . "\n";
+
+ foreach my $cust_pay_batch (@cust_pay_batch) {
+ $batchcount++;
+ $batchtotal += $cust_pay_batch->amount;
+ $batch .=
+ &{$info->{'row'}}($cust_pay_batch, $self, $batchcount, $batchtotal).
+ $delim;
+ }
+
+ my $f = $info->{'footer'};
+ if (ref($f) eq 'CODE') {
+ $batch .= &$f($self, $batchcount, $batchtotal). $delim;
+ } else {
+ $batch .= $f. $delim;
}
if ($info->{'autopost'}) {
return $batch;
}
+sub manual_approve {
+ my $self = shift;
+ my $date = time;
+ my %opt = @_;
+ my $usernum = $opt{'usernum'} || die "manual approval requires a usernum";
+ my $conf = FS::Conf->new;
+ return 'manual batch approval disabled'
+ if ( ! $conf->exists('batch-manual_approval') );
+ return 'batch already resolved' if $self->status eq 'R';
+ return 'batch not yet submitted' if $self->status eq 'O';
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $payments = 0;
+ foreach my $cust_pay_batch (
+ qsearch('cust_pay_batch', { batchnum => $self->batchnum,
+ status => '' })
+ ) {
+ my $new_cust_pay_batch = new FS::cust_pay_batch {
+ $cust_pay_batch->hash,
+ 'paid' => $cust_pay_batch->amount,
+ '_date' => $date,
+ 'usernum' => $usernum,
+ };
+ my $error = $new_cust_pay_batch->approve();
+ # there are no approval options here (authorization, order_number, etc.)
+ # because the transaction wasn't really approved
+ if ( $error ) {
+ $dbh->rollback;
+ return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error";
+ }
+ $payments++;
+ }
+ $self->set_status('R');
+ $dbh->commit;
+ return;
+}
+
=back
=head1 BUGS