X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_pay_batch.pm;h=2931fe79d7e3c179bf24f72cc4d3f56e82cf9614;hp=9f2e9ddfc3c63258ba2d7866e9045e9ade81c770;hb=3adb46fccf9f631e188ea5383bd147b340477639;hpb=0ad946f751d3a953c8a41eea1d30ad362ba38ace diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index 9f2e9ddfc..2931fe79d 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -1,21 +1,20 @@ 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 @@ -50,7 +49,7 @@ following fields are currently supported: =item batchnum - indentifies group in batch -=item payby - CARD/CHEK/LECB/BILL/COMP +=item payby - CARD/CHEK =item payinfo @@ -64,6 +63,8 @@ following fields are currently supported: =item payname - name on card +=item paytype - account type ((personal|business) (checking|savings)) + =item first - name =item last - name @@ -80,7 +81,12 @@ following fields are currently supported: =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 failure +status, if any =back @@ -125,6 +131,8 @@ and replace methods. sub check { my $self = shift; + my $conf = new FS::Conf; + my $error = $self->ut_numbern('paybatchnum') || $self->ut_numbern('trancode') #deprecated @@ -133,7 +141,9 @@ sub check { || $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') ; @@ -148,9 +158,21 @@ sub check { $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})$/ ) { @@ -197,13 +219,6 @@ sub check { Returns the customer (see L) 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 @@ -224,15 +239,10 @@ sub expmmyy { =item pay_batch -Returns the payment batch this payment belongs to (L). =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? @@ -254,76 +264,54 @@ sub retriable { 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). 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"; } + + return if $new->paycode eq "C"; + my $cust_pay = new FS::cust_pay ( { 'custnum' => $new->custnum, 'payby' => $new->payby, - 'paybatch' => $paybatch, 'payinfo' => $new->payinfo || $old->payinfo, + 'paymask' => $new->mask_payinfo, 'paid' => $new->paid, '_date' => $new->_date, 'usernum' => $new->usernum, 'batchnum' => $new->batchnum, + 'invnum' => $old->invnum, + '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"; @@ -332,25 +320,29 @@ sub approve { 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, +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 @@ -361,6 +353,12 @@ sub decline { # 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 }); if ( !$cust_pay ) { @@ -371,13 +369,15 @@ sub decline { } 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', @@ -427,12 +427,23 @@ sub request_item { $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', @@ -448,10 +459,93 @@ sub request_item { ( map { $_ => $location->$_ } qw(address2 city state country zip) ), invoice_number => $self->invnum, + recurring_billing => $recurring, %payment, ); } +=item process_unbatch_and_delete + +L 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 with a status of 'O' (not yet in transit.) Deletes all associated +records from L 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