X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_pay_batch.pm;h=171ec9fcfae5af7040599de357a6d585f6b80714;hp=97c168159632349ddfaaf34968fae36ecadadc4a;hb=b49c1bd5510a5f10b05bffacc6cc8b6a1b2153e8;hpb=c8f80bd5aaeb0f3844a7cece4bfe250d4f89f745 diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index 97c168159..171ec9fcf 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -1,11 +1,20 @@ package FS::cust_pay_batch; use strict; -use vars qw( @ISA ); -use FS::Record qw(dbh qsearchs); -use Business::CreditCard; +use vars qw( @ISA $DEBUG ); +use Carp qw( 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::Record ); +@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; =head1 NAME @@ -26,6 +35,8 @@ FS::cust_pay_batch - Object methods for batch cards $error = $record->check; + #deprecated# $error = $record->retriable; + =head1 DESCRIPTION An FS::cust_pay_batch object represents a credit card transaction ready to be @@ -37,7 +48,11 @@ following fields are currently supported: =item paybatchnum - primary key (automatically assigned) -=item cardnum +=item batchnum - indentifies group in batch + +=item payby - CARD/CHEK/LECB/BILL/COMP + +=item payinfo =item exp - card expiration @@ -65,6 +80,8 @@ following fields are currently supported: =item country +=item status + =back =head1 METHODS @@ -94,22 +111,14 @@ otherwise returns false. =item replace OLD_RECORD -#inactive -# -#Replaces the OLD_RECORD with this one in the database. If there is an error, -#returns the error, otherwise returns false. - -=cut - -sub replace { - return "Can't (yet?) replace batched transactions!"; -} +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. =item check Checks all fields to make sure this is a valid transaction. If there is an error, returns the error, otherwise returns false. Called by the insert -and repalce methods. +and replace methods. =cut @@ -118,8 +127,7 @@ sub check { my $error = $self->ut_numbern('paybatchnum') - || $self->ut_numbern('trancode') #depriciated - || $self->ut_number('cardnum') + || $self->ut_numbern('trancode') #deprecated || $self->ut_money('amount') || $self->ut_number('invnum') || $self->ut_number('custnum') @@ -137,17 +145,12 @@ sub check { $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name"; $self->first($1); - my $cardnum = $self->cardnum; - $cardnum =~ s/\D//g; - $cardnum =~ /^(\d{13,16})$/ - or return "Illegal credit card number"; - $cardnum = $1; - $self->cardnum($cardnum); - validate($cardnum) or return "Illegal credit card number"; - return "Unknown card type" if cardtype($cardnum) eq "Unknown"; + $error = $self->payinfo_check(); + return $error if $error; if ( $self->exp eq '' ) { - return "Expriation date required"; #unless + return "Expiration date required" + unless $self->payby =~ /^(CHEK|DCHK|LECB|WEST)$/; $self->exp(''); } else { if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) { @@ -173,92 +176,58 @@ sub check { $self->payname($1); } - #$self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/ - # or return "Illegal zip: ". $self->zip; - #$self->zip($1); + #we have lots of old zips in there... don't hork up batch results cause of em + $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/ + or return "Illegal zip: ". $self->zip; + $self->zip($1); $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country; $self->country($1); - $error = $self->ut_zip('zip', $self->country); - return $error if $error; + #$error = $self->ut_zip('zip', $self->country); + #return $error if $error; #check invnum, custnum, ? $self->SUPER::check; } -=back +=item cust_main -=head1 SUBROUTINES - -=over 4 - -=item import_results +Returns the customer (see L) for this batched credit card +payment. =cut -sub import_results { - use Time::Local; - use FS::cust_pay; - eval "use Text::CSV_XS;"; - die $@ if $@; +sub cust_main { + my $self = shift; + qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); +} + +#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? # - my $param = shift; - my $fh = $param->{'filehandle'}; - my $format = $param->{'format'}; - my $paybatch = $param->{'paybatch'}; - - my @fields; - my $condition; - my $hook; - - if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) { - - @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 - ); - - $condition = sub { - my $hash = shift; - $hash->{'result'} == 3 && $hash->{'type'} == 0; - }; - - $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, ); - }; +#=item retriable +# +#Marks the corresponding event (see L) for this batched +#credit card payment as retriable. Useful if the corresponding financial +#institution account was declined for temporary reasons and/or a manual +#retry is desired. +# +#Implementation details: For the named customer's invoice, changes the +#statustext of the 'done' (without statustext) event to 'retriable.' +# +#=cut - } else { - return "Unknown format $format"; - } +sub retriable { - my $csv = new Text::CSV_XS; + confess "deprecated method cust_pay_batch->retriable called; try removing ". + "the once condition and adding an every condition?"; - local $SIG{HUP} = 'IGNORE'; + my $self = shift; + + local $SIG{HUP} = 'IGNORE'; #Hmm local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; local $SIG{TERM} = 'IGNORE'; @@ -269,59 +238,124 @@ sub import_results { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $line; - while ( defined($line=<$fh>) ) { - - $csv->parse($line) or do { - $dbh->rollback if $oldAutoCommit; - return "can't parse: ". $csv->error_input(); - }; + 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"; + } + ''; +} - my @values = $csv->fields(); - my %hash; - foreach my $field ( @fields ) { - my $value = shift @values; - next unless $field; - $hash{$field} = $value; - } +=item approve PAYBATCH - my $cust_pay_batch = - qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'} } ); - unless ( $cust_pay_batch ) { - $dbh->rollback if $oldAutoCommit; - return "unknown paybatchnum $hash{'paybatchnum'}\n"; - } - my $custnum = $cust_pay_batch->custnum, +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. - my $error = $cust_pay_batch->delete; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error removing paybatchnum $hash{'paybatchnum'}: $error\n"; - } +=cut - next unless &{$condition}(\%hash); +sub approve { + # to break up the Big Wall of Code that is import_results + my $new = shift; + my $paybatch = shift; + my $paybatchnum = $new->paybatchnum; + my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum }) + or return "paybatchnum $paybatchnum not found"; + return "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"; + } + 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, + } ); + $error = $cust_pay->insert; + if ( $error ) { + return "error inserting payment for paybatchnum $paybatchnum: $error\n"; + } + $cust_pay->cust_main->apply_payments; + return; +} - &{$hook}(\%hash); +=item decline - my $cust_pay = new FS::cust_pay ( { - 'custnum' => $custnum, - 'payby' => 'CARD', - 'paybatch' => $paybatch, - map { $_ => $hash{$_} } (qw( paid _date payinfo )), - } ); - $error = $cust_pay->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n"; - } +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. - $cust_pay->cust_main->apply_payments; +=cut +sub decline { + my $new = shift; + my $conf = new FS::Conf; + + my $paybatchnum = $new->paybatchnum; + my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum }) + or return "paybatchnum $paybatchnum not found"; + if ( $old->status ) { + # Handle the case where payments are rejected after the batch has been + # approved. Only if manual approval is enabled. + if ( $conf->exists('batch-manual_approval') + and lc($old->status) eq 'approved' ) { + # Void the payment + my $cust_pay = qsearchs('cust_pay', { + custnum => $new->custnum, + paybatch => $new->batchnum + }); + if ( !$cust_pay ) { + # should never happen... + return "failed to revoke paybatchnum $paybatchnum, payment not found"; + } + $cust_pay->void('Returned payment'); + } + else { + # normal case: refuse to do anything + return "paybatchnum $paybatchnum already resolved ('".$old->status."')"; + } + } # !$old->status + $new->status('Declined'); + my $error = $new->replace($old); + if ( $error ) { + return "error updating status of paybatchnum $paybatchnum: $error\n"; } - - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - ''; - + my $due_cust_event = $new->cust_main->due_cust_event( + 'eventtable' => 'cust_pay_batch', + 'objects' => [ $new ], + ); + if ( !ref($due_cust_event) ) { + return $due_cust_event; + } + # XXX breaks transaction integrity + foreach my $cust_event (@$due_cust_event) { + next unless $cust_event->test_conditions; + if ( my $error = $cust_event->do_event() ) { + return $error; + } + } + return; } =back