package FS::cust_pay_batch;
+use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
use strict;
-use vars qw( @ISA $DEBUG );
-use Carp qw( confess );
+use vars qw( $DEBUG );
+use Carp qw( carp confess );
use Business::CreditCard 0.28;
use FS::Record qw(dbh qsearch qsearchs);
-use FS::payinfo_Mixin;
-use FS::cust_main;
-use FS::cust_bill;
-
-@ISA = qw( FS::payinfo_Mixin FS::Record );
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
# 3 is even more information including possibly sensitive data
$DEBUG = 0;
+#@encrypted_fields = ('payinfo');
+sub nohistory_fields { ('payinfo'); }
+
=head1 NAME
FS::cust_pay_batch - Object methods for batch cards
=item batchnum - indentifies group in batch
-=item payby - CARD/CHEK/LECB/BILL/COMP
+=item payby - CARD/CHEK
=item payinfo
=item payname - name on card
+=item paytype - account type ((personal|business) (checking|savings))
+
=item first - name
=item last - name
=item country
-=item status
+=item status - 'Approved' or 'Declined'
+
+=item error_message - the error returned by the gateway if any
+
+=item failure_status - the normalized L<Business::BatchPayment> failure
+status, if any
=back
sub check {
my $self = shift;
+ my $conf = new FS::Conf;
+
my $error =
$self->ut_numbern('paybatchnum')
|| $self->ut_numbern('trancode') #deprecated
|| $self->ut_number('custnum')
|| $self->ut_text('address1')
|| $self->ut_textn('address2')
- || $self->ut_text('city')
+ || ($conf->exists('cust_main-no_city_in_address')
+ ? $self->ut_textn('city')
+ : $self->ut_text('city'))
|| $self->ut_textn('state')
;
$error = $self->payinfo_check();
return $error if $error;
+ if ( $self->payby eq 'CHEK' ) {
+ # because '' is on the list of paytypes:
+ my $paytype = $self->paytype or return "Bank account type required";
+ if (grep { $_ eq $paytype} FS::cust_payby->paytypes) {
+ #ok
+ } else {
+ return "Bank account type '$paytype' is not allowed"
+ }
+ } else {
+ $self->set('paytype', '');
+ }
+
if ( $self->exp eq '' ) {
return "Expiration date required"
- unless $self->payby =~ /^(CHEK|DCHK|LECB|WEST)$/;
+ unless $self->payby =~ /^(CHEK|DCHK|WEST)$/;
$self->exp('');
} else {
if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
Returns the customer (see L<FS::cust_main>) for this batched credit card
payment.
-=cut
-
-sub cust_main {
- my $self = shift;
- qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
-}
-
=item expmmyy
Returns the credit card expiration date in MMYY format. If this is a
=cut
-sub pay_batch {
- my $self = shift;
- FS::pay_batch->by_key($self->batchnum);
-}
-
#you know what, screw this in the new world of events. we should be able to
#get the event defs to retry (remove once.pm condition, add every.pm) without
#mucking about with statuses of previous cust_event records. right?
confess "deprecated method cust_pay_batch->retriable called; try removing ".
"the once condition and adding an every condition?";
- my $self = shift;
-
- local $SIG{HUP} = 'IGNORE'; #Hmm
- 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 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
- or return "event $self->eventnum references nonexistant invoice $self->invnum";
-
- warn "cust_pay_batch->retriable working with self of " . $self->paybatchnum . " and invnum of " . $self->invnum;
- my @cust_bill_event =
- sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
- grep {
- $_->part_bill_event->eventcode =~ /\$cust_bill->batch_card/
- && $_->status eq 'done'
- && ! $_->statustext
- }
- $cust_bill->cust_bill_event;
- # complain loudly if scalar(@cust_bill_event) > 1 ?
- my $error = $cust_bill_event[0]->retriable;
- if ($error ) {
- # gah, even with transactions.
- $dbh->commit if $oldAutoCommit; #well.
- return "error marking invoice event retriable: $error";
- }
- '';
}
-=item approve PAYBATCH
+=item approve OPTIONS
Approve this payment. This will replace the existing record with the
same paybatchnum, set its status to 'Approved', and generate a payment
record (L<FS::cust_pay>). This should only be called from the batch
import process.
+OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number".
+
=cut
sub approve {
# to break up the Big Wall of Code that is import_results
my $new = shift;
- my $paybatch = shift;
+ my %opt = @_;
my $paybatchnum = $new->paybatchnum;
my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
- or return "paybatchnum $paybatchnum not found";
+ or return "cannot approve, paybatchnum $paybatchnum not found";
# leave these restrictions in place until TD EFT is converted over
# to B::BP
- return "paybatchnum $paybatchnum already resolved ('".$old->status."')"
+ return "cannot approve paybatchnum $paybatchnum, already resolved ('".$old->status."')"
if $old->status;
$new->status('Approved');
my $error = $new->replace($old);
if ( $error ) {
- return "error updating status of paybatchnum $paybatchnum: $error\n";
+ return "error approving paybatchnum $paybatchnum: $error\n";
}
my $cust_pay = new FS::cust_pay ( {
'custnum' => $new->custnum,
'payby' => $new->payby,
- 'paybatch' => $paybatch,
'payinfo' => $new->payinfo || $old->payinfo,
'paid' => $new->paid,
'_date' => $new->_date,
'usernum' => $new->usernum,
'batchnum' => $new->batchnum,
+ 'gatewaynum' => $opt{'gatewaynum'},
+ 'processor' => $opt{'processor'},
+ 'auth' => $opt{'auth'},
+ 'order_number' => $opt{'order_number'}
} );
+
$error = $cust_pay->insert;
if ( $error ) {
return "error inserting payment for paybatchnum $paybatchnum: $error\n";
return;
}
-=item decline [ REASON ]
+=item decline [ REASON [ STATUS ] ]
Decline this payment. This will replace the existing record with the
same paybatchnum, set its status to 'Declined', and run collection events
as appropriate. This should only be called from the batch import process.
REASON is a string description of the decline reason, defaulting to
-'Returned payment'.
+'Returned payment', and will go into the "error_message" field.
+
+STATUS is a normalized failure status defined by L<Business::BatchPayment>,
+and will go into the "failure_status" field.
=cut
sub decline {
my $new = shift;
my $reason = shift || 'Returned payment';
+ my $failure_status = shift || '';
#my $conf = new FS::Conf;
my $paybatchnum = $new->paybatchnum;
my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
- or return "paybatchnum $paybatchnum not found";
+ or return "cannot decline, paybatchnum $paybatchnum not found";
if ( $old->status ) {
# Handle the case where payments are rejected after the batch has been
# approved. FS::pay_batch::import_results won't allow results to be
if ( lc($old->status) eq 'approved' ) {
# Void the payment
my $cust_pay = qsearchs('cust_pay', {
+ custnum => $new->custnum,
+ batchnum => $new->batchnum
+ });
+ # these should all be migrated over, but if it's not found, look for
+ # batchnum in the 'paybatch' field also
+ $cust_pay ||= qsearchs('cust_pay', {
custnum => $new->custnum,
paybatch => $new->batchnum
});
}
else {
# normal case: refuse to do anything
- return "paybatchnum $paybatchnum already resolved ('".$old->status."')";
+ return "cannot decline paybatchnum $paybatchnum, already resolved ('".$old->status."')";
}
} # !$old->status
$new->status('Declined');
+ $new->error_message($reason);
+ $new->failure_status($failure_status);
my $error = $new->replace($old);
if ( $error ) {
- return "error updating status of paybatchnum $paybatchnum: $error\n";
+ return "error declining paybatchnum $paybatchnum: $error\n";
}
my $due_cust_event = $new->cust_main->due_cust_event(
'eventtable' => 'cust_pay_batch',
$self->payinfo =~ /(\d+)@(\d+)/; # or else what?
$payment{account_number} = $1;
$payment{routing_code} = $2;
- $payment{account_type} = $cust_main->paytype;
+ $payment{account_type} = $self->paytype;
# XXX what if this isn't their regular payment method?
} else {
die "unsupported BatchPayment method: ".$pay_batch->payby;
}
+ my $recurring;
+ if ( $cust_main->status =~ /^active|suspended|ordered$/ ) {
+ if ( $self->payinfo_used ) {
+ $recurring = 'S'; # subsequent
+ } else {
+ $recurring = 'F'; # first use
+ }
+ } else {
+ $recurring = 'N'; # non-recurring
+ }
+
Business::BatchPayment->create(Item =>
# required
action => 'payment',
( map { $_ => $location->$_ } qw(address2 city state country zip) ),
invoice_number => $self->invnum,
+ recurring_billing => $recurring,
%payment,
);
}
+=item process_unbatch_and_delete
+
+L</unbatch_and_delete> run as a queued job, accepts I<$job> and I<$param>.
+
+=cut
+
+sub process_unbatch_and_delete {
+ my ($job, $param) = @_;
+ my $self = qsearchs('cust_pay_batch',{ 'paybatchnum' => scalar($param->{'paybatchnum'}) })
+ or die 'Could not find paybatchnum ' . $param->{'paybatchnum'};
+ my $error = $self->unbatch_and_delete;
+ die $error if $error;
+ return '';
+}
+
+=item unbatch_and_delete
+
+May only be called on a record with an empty status and an associated
+L<pay_batch> with a status of 'O' (not yet in transit.) Deletes all associated
+records from L<cust_bill_pay_batch> and then deletes this record.
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub unbatch_and_delete {
+ my $self = shift;
+
+ return 'Cannot unbatch a cust_pay_batch with status ' . $self->status
+ if $self->status;
+
+ my $pay_batch = qsearchs('pay_batch',{ 'batchnum' => $self->batchnum })
+ or return 'Cannot find associated pay_batch record';
+
+ return 'Cannot unbatch from a pay_batch with status ' . $pay_batch->status
+ if $pay_batch->status ne '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;
+
+ # have not generated actual payments yet, so should be safe to delete
+ foreach my $cust_bill_pay_batch (
+ qsearch('cust_bill_pay_batch',{ 'paybatchnum' => $self->paybatchnum })
+ ) {
+ my $error = $cust_bill_pay_batch->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ my $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item cust_bill
+
+Returns the invoice linked to this batched payment. Deprecated, will be
+removed.
+
+=cut
+
+sub cust_bill {
+ carp "FS::cust_pay_batch->cust_bill is deprecated";
+ my $self = shift;
+ $self->invnum ? qsearchs('cust_bill', { invnum => $self->invnum }) : '';
+}
+
=back
=head1 BUGS