X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_pay_batch.pm;h=d28085cbd47e1824a9fb3d48efb28709c070e920;hb=622c72770c971ae44d37dfb59a0f25972051a25a;hp=1eeabb69a26be674ae4063989b508e15940fa2ef;hpb=c8cccb4a92adceb943c635fe62dad0d034462ce0;p=freeside.git diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index 1eeabb69a..d28085cbd 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -2,17 +2,23 @@ package FS::cust_pay_batch; use strict; use vars qw( @ISA $DEBUG ); -use FS::Record qw(dbh qsearch qsearchs); -use FS::part_bill_event qw(due_events); +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::cust_main_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 @@ -32,7 +38,7 @@ FS::cust_pay_batch - Object methods for batch cards $error = $record->check; - $error = $record->retriable; + #deprecated# $error = $record->retriable; =head1 DESCRIPTION @@ -77,7 +83,9 @@ 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 =back @@ -122,15 +130,19 @@ and replace methods. sub check { my $self = shift; + my $conf = new FS::Conf; + my $error = $self->ut_numbern('paybatchnum') - || $self->ut_numbern('trancode') #depriciated + || $self->ut_numbern('trancode') #deprecated || $self->ut_money('amount') || $self->ut_number('invnum') || $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') ; @@ -142,11 +154,7 @@ sub check { $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name"; $self->first($1); - $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREP|CASH|WEST|MCRD)$/ - or return "Illegal payby"; - $self->payby($1); - - $error = FS::payby::payinfo_check($self->payby, \$self->payinfo); + $error = $self->payinfo_check(); return $error if $error; if ( $self->exp eq '' ) { @@ -177,15 +185,16 @@ 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, ? @@ -204,19 +213,56 @@ sub cust_main { qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); } -=item retriable +=item expmmyy -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. +Returns the credit card expiration date in MMYY format. If this is a +CHEK payment, returns an empty string. -Implementation details: For the named customer's invoice, changes the -statustext of the 'done' (without statustext) event to 'retriable.' +=cut + +sub expmmyy { + my $self = shift; + if ( $self->payby eq 'CARD' ) { + $self->get('exp') =~ /^(\d{4})-(\d{2})-(\d{2})$/; + return sprintf('%02u%02u', $2, ($1 % 100)); + } + else { + return ''; + } +} + +=item pay_batch + +Returns the payment batch this payment belongs to (Lby_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? +# +#=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 + 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 @@ -252,288 +298,180 @@ sub retriable { ''; } -=back +=item approve OPTIONS -=head1 SUBROUTINES +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. -=over 4 - -=item import_results +OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number". =cut -sub import_results { - use Time::Local; - use FS::cust_pay; - eval "use Text::CSV_XS;"; - die $@ if $@; -# - my $param = shift; - my $fh = $param->{'filehandle'}; - my $format = $param->{'format'}; - my $paybatch = $param->{'paybatch'}; - - my $filetype; # CSV, Fixed80, Fixed264 - my @fields; - my $formatre; # for Fixed.+ - my @values; - my $begin_condition; - my $end_condition; - my $end_hook; - my $hook; - my $approved_condition; - my $declined_condition; - - if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) { - - $filetype = "CSV"; - - @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 - ); - - $end_condition = sub { - my $hash = shift; - $hash->{'type'} eq '0BC'; - }; - - $end_hook = sub { - my( $hash, $total) = @_; - $total = sprintf("%.2f", $total); - my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 100 ); - return "Our total $total does not match bank total $batch_total!" - if $total != $batch_total; - ''; - }; - - $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, ); - }; - - $approved_condition = sub { - my $hash = shift; - $hash->{'type'} eq '0' && $hash->{'result'} == 3; - }; - - $declined_condition = sub { - my $hash = shift; - $hash->{'type'} eq '0' && ( $hash->{'result'} == 4 - || $hash->{'result'} == 5 ); - }; - - - }elsif ( $format eq 'PAP' ) { - - $filetype = "Fixed264"; - - @fields = ( - 'recordtype', # We are interested in the 'D' or debit records - 'batchnum', # Record#: batch number we used when sending the file - 'datacenter', # Where in the bowels of the bank the data was processed - 'paid', # Amount: Amount of the transaction. Dollars and cents - # with no decimal entered. - '_date', # Transaction Date: Date the Transaction was processed - 'bank', # Routing information - 'payinfo', # Account number for the transaction - 'paybatchnum', # Reference#: Invoice number of the transaction - ); - - $formatre = '^(.).{19}(.{4})(.{3})(.{10})(.{6})(.{9})(.{12}).{110}(.{19}).{71}$'; - - $end_condition = sub { - my $hash = shift; - $hash->{'recordtype'} eq 'W'; - }; - - $end_hook = sub { - my( $hash, $total) = @_; - $total = sprintf("%.2f", $total); - my $batch_total = $hash->{'datacenter'}.$hash->{'paid'}. - substr($hash->{'_date'},0,1); # YUCK! - $batch_total = sprintf("%.2f", $batch_total / 100 ); - return "Our total $total does not match bank total $batch_total!" - if $total != $batch_total; - ''; - }; - - $hook = sub { - my $hash = shift; - $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 ); - my $tmpdate = timelocal( 0,0,1,1,0,substr($hash->{'_date'}, 0, 3)+2000); - $tmpdate += 86400*(substr($hash->{'_date'}, 3, 3)-1) ; - $hash->{'_date'} = $tmpdate; - $hash->{'payinfo'} = $hash->{'payinfo'} . '@' . $hash->{'bank'}; - }; - - $approved_condition = sub { - 1; - }; - - $declined_condition = sub { - 0; - }; - - - } else { - return "Unknown format $format"; +sub approve { + # to break up the Big Wall of Code that is import_results + my $new = shift; + my %opt = @_; + my $paybatchnum = $new->paybatchnum; + my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum }) + or return "cannot approve, paybatchnum $paybatchnum not found"; + # leave these restrictions in place until TD EFT is converted over + # to B::BP + return "cannot approve paybatchnum $paybatchnum, already resolved ('".$old->status."')" + if $old->status; + $new->status('Approved'); + my $error = $new->replace($old); + if ( $error ) { + return "error approving paybatchnum $paybatchnum: $error\n"; } - - my $csv = new Text::CSV_XS; - - 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 $pay_batch = qsearchs('pay_batch',{'batchnum'=> $paybatch}); - unless ($pay_batch && $pay_batch->status eq 'I') { - $dbh->rollback if $oldAutoCommit; - return "batch $paybatch is not in transit"; - }; - - my $newbatch = new FS::pay_batch { $pay_batch->hash }; - $newbatch->status('R'); # Resolved - $newbatch->upload(time); - my $error = $newbatch->replace($pay_batch); + my $cust_pay = new FS::cust_pay ( { + 'custnum' => $new->custnum, + 'payby' => $new->payby, + '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 ) { - $dbh->rollback if $oldAutoCommit; - return $error + return "error inserting payment for paybatchnum $paybatchnum: $error\n"; } + $cust_pay->cust_main->apply_payments; + return; +} - my $total = 0; - my $line; - while ( defined($line=<$fh>) ) { - - next if $line =~ /^\s*$/; #skip blank lines - - if ($filetype eq "CSV") { - $csv->parse($line) or do { - $dbh->rollback if $oldAutoCommit; - return "can't parse: ". $csv->error_input(); - }; - @values = $csv->fields(); - }elsif ($filetype eq "Fixed80" || $filetype eq "Fixed264"){ - @values = $line =~ /$formatre/; - unless (@values) { - $dbh->rollback if $oldAutoCommit; - return "can't parse: ". $line; - }; - }else{ - $dbh->rollback if $oldAutoCommit; - return "Unknown file type $filetype"; - } - - my %hash; - foreach my $field ( @fields ) { - my $value = shift @values; - next unless $field; - $hash{$field} = $value; - } - - if ( &{$end_condition}(\%hash) ) { - my $error = &{$end_hook}(\%hash, $total); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - last; - } - - my $cust_pay_batch = - qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'}+0 } ); - unless ( $cust_pay_batch ) { - $dbh->rollback if $oldAutoCommit; - return "unknown paybatchnum $hash{'paybatchnum'}\n"; - } - 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 }; +=item decline [ REASON ] - &{$hook}(\%hash); +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. - if ( &{$approved_condition}(\%hash) ) { +REASON is a string description of the decline reason, defaulting to +'Returned payment'. - $new_cust_pay_batch->status('Approved'); +=cut - my $cust_pay = new FS::cust_pay ( { - 'custnum' => $custnum, - 'payby' => $payby, - '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"; +sub decline { + my $new = shift; + my $reason = shift || 'Returned payment'; + #my $conf = new FS::Conf; + + my $paybatchnum = $new->paybatchnum; + my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum }) + 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 + # imported to a closed batch unless batch-manual_approval is enabled, + # so we don't check it here. +# if ( $conf->exists('batch-manual_approval') and + 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 + }); + if ( !$cust_pay ) { + # should never happen... + return "failed to revoke paybatchnum $paybatchnum, payment not found"; } - $total += $hash{'paid'}; - - $cust_pay->cust_main->apply_payments; - - } elsif ( &{$declined_condition}(\%hash) ) { - - $new_cust_pay_batch->status('Declined'); - - foreach my $part_bill_event ( due_events ( $new_cust_pay_batch, - 'DCLN', - '', - '') ) { + $cust_pay->void($reason); + } + else { + # normal case: refuse to do anything + return "cannot decline paybatchnum $paybatchnum, already resolved ('".$old->status."')"; + } + } # !$old->status + $new->status('Declined'); + $new->error_message($reason); + my $error = $new->replace($old); + if ( $error ) { + return "error declining paybatchnum $paybatchnum: $error\n"; + } + 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; +} - # don't run subsequent events if balance<=0 - last if $cust_pay_batch->cust_main->balance <= 0; +=item request_item [ OPTIONS ] - if (my $error = $part_bill_event->do_event($new_cust_pay_batch)) { - # gah, even with transactions. - $dbh->commit if $oldAutoCommit; #well. - return $error; - } +Returns a L object for this batch payment +entry. This can be submitted to a processor. - } +OPTIONS can be a list of key/values to append to the attributes. The most +useful case of this is "process_date" to set a processing date based on the +date the batch is being submitted. - } +=cut - 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"; - } +sub request_item { + local $@; + my $self = shift; + eval "use Business::BatchPayment;"; + die "couldn't load Business::BatchPayment: $@" if $@; + + my $cust_main = $self->cust_main; + my $location = $cust_main->bill_location; + my $pay_batch = $self->pay_batch; + + my %payment; + $payment{payment_type} = FS::payby->payby2bop( $pay_batch->payby ); + if ( $payment{payment_type} eq 'CC' ) { + $payment{card_number} = $self->payinfo, + $payment{expiration} = $self->expmmyy, + } elsif ( $payment{payment_type} eq 'ECHECK' ) { + $self->payinfo =~ /(\d+)@(\d+)/; # or else what? + $payment{account_number} = $1; + $payment{routing_code} = $2; + $payment{account_type} = $cust_main->paytype; + # XXX what if this isn't their regular payment method? + } else { + die "unsupported BatchPayment method: ".$pay_batch->payby; } - - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - ''; + Business::BatchPayment->create(Item => + # required + action => 'payment', + tid => $self->paybatchnum, + amount => $self->amount, + + # customer info + customer_id => $self->custnum, + first_name => $cust_main->first, + last_name => $cust_main->last, + company => $cust_main->company, + address => $location->address1, + ( map { $_ => $location->$_ } qw(address2 city state country zip) ), + + invoice_number => $self->invnum, + %payment, + ); } =back