X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=9c16294905824723eb50c3b1e62b514c349df97e;hb=fd93bd0bf90836be82c5271bb36e46cca83735f4;hp=c0b9bc51c99d9c30a2c505cb979d68140d22e9de;hpb=072a39e8f246c4964f28d70265355d23def05bd9;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index c0b9bc51c..9c1629490 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -62,6 +62,7 @@ use FS::queue; use FS::part_pkg; use FS::part_event; use FS::part_event_condition; +use FS::part_export; #use FS::cust_event; use FS::type_pkgs; use FS::payment_gateway; @@ -86,7 +87,7 @@ $skip_fuzzyfiles = 0; @fuzzyfields = ( 'first', 'last', 'company', 'address1' ); @encrypted_fields = ('payinfo', 'paycvv'); -sub nohistory_fields { ('paycvv'); } +sub nohistory_fields { ('payinfo', 'paycvv'); } @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings'); @@ -546,6 +547,45 @@ sub insert { } } + # cust_main exports! + warn " exporting\n" if $DEBUG > 1; + + my $export_args = $options{'export_args'} || []; + + my @part_export = + map qsearch( 'part_export', {exportnum=>$_} ), + $conf->config('cust_main-exports'); #, $agentnum + + foreach my $part_export ( @part_export ) { + my $error = $part_export->export_insert($self, @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "exporting to ". $part_export->exporttype. + " (transaction rolled back): $error"; + } + } + + #foreach my $depend_jobnum ( @$depend_jobnums ) { + # warn "[$me] inserting dependancies on supplied job $depend_jobnum\n" + # if $DEBUG; + # foreach my $jobnum ( @jobnums ) { + # my $queue = qsearchs('queue', { 'jobnum' => $jobnum } ); + # warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n" + # if $DEBUG; + # my $error = $queue->depend_insert($depend_jobnum); + # if ( $error ) { + # $dbh->rollback if $oldAutoCommit; + # return "error queuing job dependancy: $error"; + # } + # } + # } + # + #} + # + #if ( exists $options{'jobnums'} ) { + # push @{ $options{'jobnums'} }, @jobnums; + #} + warn " insert complete; committing transaction\n" if $DEBUG > 1; @@ -1340,6 +1380,23 @@ sub delete { return $error; } + # cust_main exports! + + #my $export_args = $options{'export_args'} || []; + + my @part_export = + map qsearch( 'part_export', {exportnum=>$_} ), + $conf->config('cust_main-exports'); #, $agentnum + + foreach my $part_export ( @part_export ) { + my $error = $part_export->export_delete( $self ); #, @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "exporting to ". $part_export->exporttype. + " (transaction rolled back): $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -1453,8 +1510,15 @@ sub replace { } - if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ && - grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) { + if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ + && ( ( $self->get('payinfo') ne $old->get('payinfo') + && $self->get('payinfo') !~ /^99\d{14}$/ + ) + || grep { $self->get($_) ne $old->get($_) } qw(paydate payname) + ) + ) + { + # card/check/lec info has changed, want to retry realtime_ invoice events my $error = $self->retry_realtime; if ( $error ) { @@ -1471,6 +1535,23 @@ sub replace { } } + # cust_main exports! + + my $export_args = $options{'export_args'} || []; + + my @part_export = + map qsearch( 'part_export', {exportnum=>$_} ), + $conf->config('cust_main-exports'); #, $agentnum + + foreach my $part_export ( @part_export ) { + my $error = $part_export->export_replace( $self, $old, @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "exporting to ". $part_export->exporttype. + " (transaction rolled back): $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -1708,12 +1789,7 @@ sub check { # If it is encrypted and the private key is not availaible then we can't # check the credit card. - - my $check_payinfo = 1; - - if ($self->is_encrypted($self->payinfo)) { - $check_payinfo = 0; - } + my $check_payinfo = ! $self->is_encrypted($self->payinfo); if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) { @@ -1727,7 +1803,8 @@ sub check { or return gettext('invalid_card'); # . ": ". $self->payinfo; return gettext('unknown_card_type') - if cardtype($self->payinfo) eq "Unknown"; + if $self->payinfo !~ /^99\d{14}$/ #token + && cardtype($self->payinfo) eq "Unknown"; my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref); if ( $ban ) { @@ -4025,1083 +4102,66 @@ sub retry_realtime { my $order = FS::part_event_condition->order_conditions_sql; my $mine = '( ' - . join ( ' OR ' , map { - "( part_event.eventtable = " . dbh->quote($_) - . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ; - } FS::part_event->eventtables) - . ') '; - - #here is the agent virtualization - my $agent_virt = " ( part_event.agentnum IS NULL - OR part_event.agentnum = ". $self->agentnum. ' )'; - - #XXX this shouldn't be hardcoded, actions should declare it... - my @realtime_events = qw( - cust_bill_realtime_card - cust_bill_realtime_check - cust_bill_realtime_lec - cust_bill_batch - ); - - my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'", - @realtime_events - ). - ' ) '; - - my @cust_event = qsearchs({ - 'table' => 'cust_event', - 'select' => 'cust_event.*', - 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join", - 'hashref' => { 'status' => 'done' }, - 'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ". - " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1" - }); - - my %seen_invnum = (); - foreach my $cust_event (@cust_event) { - - #max one for the customer, one for each open invoice - my $cust_X = $cust_event->cust_X; - next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill' - ? $cust_X->invnum - : 0 - }++ - or $cust_event->part_event->eventtable eq 'cust_bill' - && ! $cust_X->owed; - - my $error = $cust_event->retry; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error scheduling event for retry: $error"; - } - - } - - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - ''; - -} - -# some horrid false laziness here to avoid refactor fallout -# eventually realtime realtime_bop and realtime_refund_bop should go -# away and be replaced by _new_realtime_bop and _new_realtime_refund_bop - -=item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ] - -Runs a realtime credit card, ACH (electronic check) or phone bill transaction -via a Business::OnlinePayment realtime gateway. See -L for supported gateways. - -Available methods are: I, I and I - -Available options are: I, I, I, I, I - -The additional options I, I, I, I, I, -I, I and I are also available. Any of these options, -if set, will override the value from the customer record. - -I is a free-text field passed to the gateway. It defaults to -the value defined by the business-onlinepayment-description configuration -option, or "Internet services" if that is unset. - -If an I is specified, this payment (if successful) is applied to the -specified invoice. If you don't specify an I you might want to -call the B method or set the I option. - -I can be set to true to apply a resulting payment. - -I can be set true to surpress email decline notices. - -I can be set to a scalar reference. It will be filled in with the -resulting paynum, if any. - -I is a unique identifier for this payment. - -(moved from cust_bill) (probably should get realtime_{card,ach,lec} here too) - -=cut - -sub realtime_bop { - my $self = shift; - - return $self->_new_realtime_bop(@_) - if $self->_new_bop_required(); - - my($method, $amount); - my %options = (); - if (ref($_[0]) eq 'HASH') { - %options = %{$_[0]}; - $method = $options{method}; - $amount = $options{amount}; - } else { - ( $method, $amount ) = ( shift, shift ); - %options = @_; - } - if ( $DEBUG ) { - warn "$me realtime_bop: $method $amount\n"; - warn " $_ => $options{$_}\n" foreach keys %options; - } - - return "Amount must be greater than 0" unless $amount > 0; - - unless ( $options{'description'} ) { - if ( $conf->exists('business-onlinepayment-description') ) { - my $dtempl = $conf->config('business-onlinepayment-description'); - - my $agent = $self->agent->agent; - #$pkgs... not here - $options{'description'} = eval qq("$dtempl"); - } else { - $options{'description'} = 'Internet services'; - } - } - - return $self->fake_bop($method, $amount, %options) if $options{'fake'}; - - eval "use Business::OnlinePayment"; - die $@ if $@; - - my $payinfo = exists($options{'payinfo'}) - ? $options{'payinfo'} - : $self->payinfo; - - my %method2payby = ( - 'CC' => 'CARD', - 'ECHECK' => 'CHEK', - 'LEC' => 'LECB', - ); - - ### - # check for banned credit card/ACH - ### - - my $ban = qsearchs('banned_pay', { - 'payby' => $method2payby{$method}, - 'payinfo' => md5_base64($payinfo), - } ); - return "Banned credit card" if $ban; - - ### - # set taxclass and trans_is_recur based on invnum if there is one - ### - - my $taxclass = ''; - my $trans_is_recur = 0; - if ( $options{'invnum'} ) { - - my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } ); - die "invnum ". $options{'invnum'}. " not found" unless $cust_bill; - - my @part_pkg = - map { $_->part_pkg } - grep { $_ } - map { $_->cust_pkg } - $cust_bill->cust_bill_pkg; - - my @taxclasses = map $_->taxclass, @part_pkg; - $taxclass = $taxclasses[0] - unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are - #different taxclasses - $trans_is_recur = 1 - if grep { $_->freq ne '0' } @part_pkg; - - } - - ### - # select a gateway - ### - - #look for an agent gateway override first - my $cardtype; - if ( $method eq 'CC' ) { - $cardtype = cardtype($payinfo); - } elsif ( $method eq 'ECHECK' ) { - $cardtype = 'ACH'; - } else { - $cardtype = $method; - } - - my $override = - qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, - cardtype => $cardtype, - taxclass => $taxclass, } ) - || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, - cardtype => '', - taxclass => $taxclass, } ) - || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, - cardtype => $cardtype, - taxclass => '', } ) - || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, - cardtype => '', - taxclass => '', } ); - - my $payment_gateway = ''; - my( $processor, $login, $password, $action, @bop_options ); - if ( $override ) { #use a payment gateway override - - $payment_gateway = $override->payment_gateway; - - $processor = $payment_gateway->gateway_module; - $login = $payment_gateway->gateway_username; - $password = $payment_gateway->gateway_password; - $action = $payment_gateway->gateway_action; - @bop_options = $payment_gateway->options; - - } else { #use the standard settings from the config - - ( $processor, $login, $password, $action, @bop_options ) = - $self->default_payment_gateway($method); - - } - - ### - # massage data - ### - - my $address = exists($options{'address1'}) - ? $options{'address1'} - : $self->address1; - my $address2 = exists($options{'address2'}) - ? $options{'address2'} - : $self->address2; - $address .= ", ". $address2 if length($address2); - - my $o_payname = exists($options{'payname'}) - ? $options{'payname'} - : $self->payname; - my($payname, $payfirst, $paylast); - if ( $o_payname && $method ne 'ECHECK' ) { - ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ - or return "Illegal payname $payname"; - ($payfirst, $paylast) = ($1, $2); - } else { - $payfirst = $self->getfield('first'); - $paylast = $self->getfield('last'); - $payname = "$payfirst $paylast"; - } - - my @invoicing_list = $self->invoicing_list_emailonly; - if ( $conf->exists('emailinvoiceautoalways') - || $conf->exists('emailinvoiceauto') && ! @invoicing_list - || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { - push @invoicing_list, $self->all_emails; - } - - my $email = ($conf->exists('business-onlinepayment-email-override')) - ? $conf->config('business-onlinepayment-email-override') - : $invoicing_list[0]; - - my %content = (); - - my $payip = exists($options{'payip'}) - ? $options{'payip'} - : $self->payip; - $content{customer_ip} = $payip - if length($payip); - - $content{invoice_number} = $options{'invnum'} - if exists($options{'invnum'}) && length($options{'invnum'}); - - $content{email_customer} = - ( $conf->exists('business-onlinepayment-email_customer') - || $conf->exists('business-onlinepayment-email-override') ); - - my $paydate = ''; - if ( $method eq 'CC' ) { - - $content{card_number} = $payinfo; - $paydate = exists($options{'paydate'}) - ? $options{'paydate'} - : $self->paydate; - $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; - $content{expiration} = "$2/$1"; - - my $paycvv = exists($options{'paycvv'}) - ? $options{'paycvv'} - : $self->paycvv; - $content{cvv2} = $paycvv - if length($paycvv); - - my $paystart_month = exists($options{'paystart_month'}) - ? $options{'paystart_month'} - : $self->paystart_month; - - my $paystart_year = exists($options{'paystart_year'}) - ? $options{'paystart_year'} - : $self->paystart_year; - - $content{card_start} = "$paystart_month/$paystart_year" - if $paystart_month && $paystart_year; - - my $payissue = exists($options{'payissue'}) - ? $options{'payissue'} - : $self->payissue; - $content{issue_number} = $payissue if $payissue; - - if ( $self->_bop_recurring_billing( 'payinfo' => $payinfo, - 'trans_is_recur' => $trans_is_recur, - ) - ) - { - $content{recurring_billing} = 'YES'; - $content{acct_code} = 'rebill' - if $conf->exists('credit_card-recurring_billing_acct_code'); - } - - } elsif ( $method eq 'ECHECK' ) { - ( $content{account_number}, $content{routing_code} ) = - split('@', $payinfo); - $content{bank_name} = $o_payname; - $content{bank_state} = exists($options{'paystate'}) - ? $options{'paystate'} - : $self->getfield('paystate'); - $content{account_type} = exists($options{'paytype'}) - ? uc($options{'paytype'}) || 'CHECKING' - : uc($self->getfield('paytype')) || 'CHECKING'; - $content{account_name} = $payname; - $content{customer_org} = $self->company ? 'B' : 'I'; - $content{state_id} = exists($options{'stateid'}) - ? $options{'stateid'} - : $self->getfield('stateid'); - $content{state_id_state} = exists($options{'stateid_state'}) - ? $options{'stateid_state'} - : $self->getfield('stateid_state'); - $content{customer_ssn} = exists($options{'ss'}) - ? $options{'ss'} - : $self->ss; - } elsif ( $method eq 'LEC' ) { - $content{phone} = $payinfo; - } - - ### - # run transaction(s) - ### - - my $balance = exists( $options{'balance'} ) - ? $options{'balance'} - : $self->balance; - - $self->select_for_update; #mutex ... just until we get our pending record in - - #the checks here are intended to catch concurrent payments - #double-form-submission prevention is taken care of in cust_pay_pending::check - - #check the balance - return "The customer's balance has changed; $method transaction aborted." - if $self->balance < $balance; - #&& $self->balance < $amount; #might as well anyway? - - #also check and make sure there aren't *other* pending payments for this cust - - my @pending = qsearch('cust_pay_pending', { - 'custnum' => $self->custnum, - 'status' => { op=>'!=', value=>'done' } - }); - return "A payment is already being processed for this customer (". - join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ). - "); $method transaction aborted." - if scalar(@pending); - - #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out - - my $cust_pay_pending = new FS::cust_pay_pending { - 'custnum' => $self->custnum, - #'invnum' => $options{'invnum'}, - 'paid' => $amount, - '_date' => '', - 'payby' => $method2payby{$method}, - 'payinfo' => $payinfo, - 'paydate' => $paydate, - 'recurring_billing' => $content{recurring_billing}, - 'pkgnum' => $options{'pkgnum'}, - 'status' => 'new', - 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ), - }; - $cust_pay_pending->payunique( $options{payunique} ) - if defined($options{payunique}) && length($options{payunique}); - my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted - return $cpp_new_err if $cpp_new_err; - - my( $action1, $action2 ) = split(/\s*\,\s*/, $action ); - - my $transaction = new Business::OnlinePayment( $processor, @bop_options ); - $transaction->content( - 'type' => $method, - 'login' => $login, - 'password' => $password, - 'action' => $action1, - 'description' => $options{'description'}, - 'amount' => $amount, - #'invoice_number' => $options{'invnum'}, - 'customer_id' => $self->custnum, - 'last_name' => $paylast, - 'first_name' => $payfirst, - 'name' => $payname, - 'address' => $address, - 'city' => ( exists($options{'city'}) - ? $options{'city'} - : $self->city ), - 'state' => ( exists($options{'state'}) - ? $options{'state'} - : $self->state ), - 'zip' => ( exists($options{'zip'}) - ? $options{'zip'} - : $self->zip ), - 'country' => ( exists($options{'country'}) - ? $options{'country'} - : $self->country ), - 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/ - 'email' => $email, - 'phone' => $self->daytime || $self->night, - %content, #after - ); - - $cust_pay_pending->status('pending'); - my $cpp_pending_err = $cust_pay_pending->replace; - return $cpp_pending_err if $cpp_pending_err; - - #config? - my $BOP_TESTING = 0; - my $BOP_TESTING_SUCCESS = 1; - - unless ( $BOP_TESTING ) { - $transaction->submit(); - } else { - if ( $BOP_TESTING_SUCCESS ) { - $transaction->is_success(1); - $transaction->authorization('fake auth'); - } else { - $transaction->is_success(0); - $transaction->error_message('fake failure'); - } - } - - if ( $transaction->is_success() && $action2 ) { - - $cust_pay_pending->status('authorized'); - my $cpp_authorized_err = $cust_pay_pending->replace; - return $cpp_authorized_err if $cpp_authorized_err; - - my $auth = $transaction->authorization; - my $ordernum = $transaction->can('order_number') - ? $transaction->order_number - : ''; - - my $capture = - new Business::OnlinePayment( $processor, @bop_options ); - - my %capture = ( - %content, - type => $method, - action => $action2, - login => $login, - password => $password, - order_number => $ordernum, - amount => $amount, - authorization => $auth, - description => $options{'description'}, - ); - - foreach my $field (qw( authorization_source_code returned_ACI - transaction_identifier validation_code - transaction_sequence_num local_transaction_date - local_transaction_time AVS_result_code )) { - $capture{$field} = $transaction->$field() if $transaction->can($field); - } - - $capture->content( %capture ); - - $capture->submit(); - - unless ( $capture->is_success ) { - my $e = "Authorization successful but capture failed, custnum #". - $self->custnum. ': '. $capture->result_code. - ": ". $capture->error_message; - warn $e; - return $e; - } - - } - - $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined'); - my $cpp_captured_err = $cust_pay_pending->replace; - return $cpp_captured_err if $cpp_captured_err; - - ### - # remove paycvv after initial transaction - ### - - #false laziness w/misc/process/payment.cgi - check both to make sure working - # correctly - if ( defined $self->dbdef_table->column('paycvv') - && length($self->paycvv) - && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save') - ) { - my $error = $self->remove_cvv; - if ( $error ) { - warn "WARNING: error removing cvv: $error\n"; - } - } - - ### - # result handling - ### - - if ( $transaction->is_success() ) { - - my $paybatch = ''; - if ( $payment_gateway ) { # agent override - $paybatch = $payment_gateway->gatewaynum. '-'; - } - - $paybatch .= "$processor:". $transaction->authorization; - - $paybatch .= ':'. $transaction->order_number - if $transaction->can('order_number') - && length($transaction->order_number); - - my $cust_pay = new FS::cust_pay ( { - 'custnum' => $self->custnum, - 'invnum' => $options{'invnum'}, - 'paid' => $amount, - '_date' => '', - 'payby' => $method2payby{$method}, - 'payinfo' => $payinfo, - 'paybatch' => $paybatch, - 'paydate' => $paydate, - 'pkgnum' => $options{'pkgnum'}, - } ); - #doesn't hurt to know, even though the dup check is in cust_pay_pending now - $cust_pay->payunique( $options{payunique} ) - if defined($options{payunique}) && length($options{payunique}); - - my $oldAutoCommit = $FS::UID::AutoCommit; - local $FS::UID::AutoCommit = 0; - my $dbh = dbh; - - #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction - - my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () ); - - if ( $error ) { - $cust_pay->invnum(''); #try again with no specific invnum - my $error2 = $cust_pay->insert( $options{'manual'} ? - ( 'manual' => 1 ) : () - ); - if ( $error2 ) { - # gah. but at least we have a record of the state we had to abort in - # from cust_pay_pending now. - my $e = "WARNING: $method captured but payment not recorded - ". - "error inserting payment ($processor): $error2". - " (previously tried insert with invnum #$options{'invnum'}" . - ": $error ) - pending payment saved as paypendingnum ". - $cust_pay_pending->paypendingnum. "\n"; - warn $e; - return $e; - } - } - - if ( $options{'paynum_ref'} ) { - ${ $options{'paynum_ref'} } = $cust_pay->paynum; - } - - $cust_pay_pending->status('done'); - $cust_pay_pending->statustext('captured'); - $cust_pay_pending->paynum($cust_pay->paynum); - my $cpp_done_err = $cust_pay_pending->replace; - - if ( $cpp_done_err ) { - - $dbh->rollback or die $dbh->errstr if $oldAutoCommit; - my $e = "WARNING: $method captured but payment not recorded - ". - "error updating status for paypendingnum ". - $cust_pay_pending->paypendingnum. ": $cpp_done_err \n"; - warn $e; - return $e; - - } else { - - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - - if ( $options{'apply'} ) { - my $apply_error = $self->apply_payments_and_credits; - if ( $apply_error ) { - warn "WARNING: error applying payment: $apply_error\n"; - #but we still should return no error cause the payment otherwise went - #through... - } - } - - return ''; #no error - - } - - } else { - - my $perror = "$processor error: ". $transaction->error_message; - - unless ( $transaction->error_message ) { - - my $t_response; - if ( $transaction->can('response_page') ) { - $t_response = { - 'page' => ( $transaction->can('response_page') - ? $transaction->response_page - : '' - ), - 'code' => ( $transaction->can('response_code') - ? $transaction->response_code - : '' - ), - 'headers' => ( $transaction->can('response_headers') - ? $transaction->response_headers - : '' - ), - }; - } else { - $t_response .= - "No additional debugging information available for $processor"; - } - - $perror .= "No error_message returned from $processor -- ". - ( ref($t_response) ? Dumper($t_response) : $t_response ); - - } - - if ( !$options{'quiet'} && !$realtime_bop_decline_quiet - && $conf->exists('emaildecline') - && grep { $_ ne 'POST' } $self->invoicing_list - && ! grep { $transaction->error_message =~ /$_/ } - $conf->config('emaildecline-exclude') - ) { - my @templ = $conf->config('declinetemplate'); - my $template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", @templ ], - ) or return "($perror) can't create template: $Text::Template::ERROR"; - $template->compile() - or return "($perror) can't compile template: $Text::Template::ERROR"; - - my $templ_hash = { - 'company_name' => - scalar( $conf->config('company_name', $self->agentnum ) ), - 'company_address' => - join("\n", $conf->config('company_address', $self->agentnum ) ), - 'error' => $transaction->error_message, - }; - - my $error = send_email( - 'from' => $conf->config('invoice_from', $self->agentnum ), - 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ], - 'subject' => 'Your payment could not be processed', - 'body' => [ $template->fill_in(HASH => $templ_hash) ], - ); - - $perror .= " (also received error sending decline notification: $error)" - if $error; - - } - - $cust_pay_pending->status('done'); - $cust_pay_pending->statustext("declined: $perror"); - my $cpp_done_err = $cust_pay_pending->replace; - if ( $cpp_done_err ) { - my $e = "WARNING: $method declined but pending payment not resolved - ". - "error updating status for paypendingnum ". - $cust_pay_pending->paypendingnum. ": $cpp_done_err \n"; - warn $e; - $perror = "$e ($perror)"; - } - - return $perror; - } - -} - -sub _bop_recurring_billing { - my( $self, %opt ) = @_; - - my $method = scalar($conf->config('credit_card-recurring_billing_flag')); - - if ( defined($method) && $method eq 'transaction_is_recur' ) { - - return 1 if $opt{'trans_is_recur'}; - - } else { - - my %hash = ( 'custnum' => $self->custnum, - 'payby' => 'CARD', - ); - - return 1 - if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } ) - || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD', - $opt{'payinfo'} ) - } ); - - } - - return 0; - -} - - -=item realtime_refund_bop METHOD [ OPTION => VALUE ... ] - -Refunds a realtime credit card, ACH (electronic check) or phone bill transaction -via a Business::OnlinePayment realtime gateway. See -L for supported gateways. - -Available methods are: I, I and I - -Available options are: I, I, I, I - -Most gateways require a reference to an original payment transaction to refund, -so you probably need to specify a I. - -I defaults to the original amount of the payment if not specified. - -I specifies a reason for the refund. - -I specifies the expiration date for a credit card overriding the -value from the customer record or the payment record. Specified as yyyy-mm-dd - -Implementation note: If I is unspecified or equal to the amount of the -orignal payment, first an attempt is made to "void" the transaction via -the gateway (to cancel a not-yet settled transaction) and then if that fails, -the normal attempt is made to "refund" ("credit") the transaction via the -gateway is attempted. - -#The additional options I, I, I, I, I, -#I, I and I are also available. Any of these options, -#if set, will override the value from the customer record. - -#If an I is specified, this payment (if successful) is applied to the -#specified invoice. If you don't specify an I you might want to -#call the B method. - -=cut - -#some false laziness w/realtime_bop, not enough to make it worth merging -#but some useful small subs should be pulled out -sub realtime_refund_bop { - my $self = shift; - - return $self->_new_realtime_refund_bop(@_) - if $self->_new_bop_required(); - - my( $method, %options ) = @_; - if ( $DEBUG ) { - warn "$me realtime_refund_bop: $method refund\n"; - warn " $_ => $options{$_}\n" foreach keys %options; - } - - eval "use Business::OnlinePayment"; - die $@ if $@; - - ### - # look up the original payment and optionally a gateway for that payment - ### - - my $cust_pay = ''; - my $amount = $options{'amount'}; - - my( $processor, $login, $password, @bop_options ) ; - my( $auth, $order_number ) = ( '', '', '' ); - - if ( $options{'paynum'} ) { - - warn " paynum: $options{paynum}\n" if $DEBUG > 1; - $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } ) - or return "Unknown paynum $options{'paynum'}"; - $amount ||= $cust_pay->paid; - - $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/ - or return "Can't parse paybatch for paynum $options{'paynum'}: ". - $cust_pay->paybatch; - my $gatewaynum = ''; - ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 ); - - if ( $gatewaynum ) { #gateway for the payment to be refunded - - my $payment_gateway = - qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } ); - die "payment gateway $gatewaynum not found" - unless $payment_gateway; - - $processor = $payment_gateway->gateway_module; - $login = $payment_gateway->gateway_username; - $password = $payment_gateway->gateway_password; - @bop_options = $payment_gateway->options; - - } else { #try the default gateway - - my( $conf_processor, $unused_action ); - ( $conf_processor, $login, $password, $unused_action, @bop_options ) = - $self->default_payment_gateway($method); - - return "processor of payment $options{'paynum'} $processor does not". - " match default processor $conf_processor" - unless $processor eq $conf_processor; - - } - - - } else { # didn't specify a paynum, so look for agent gateway overrides - # like a normal transaction - - my $cardtype; - if ( $method eq 'CC' ) { - $cardtype = cardtype($self->payinfo); - } elsif ( $method eq 'ECHECK' ) { - $cardtype = 'ACH'; - } else { - $cardtype = $method; - } - my $override = - qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, - cardtype => $cardtype, - taxclass => '', } ) - || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, - cardtype => '', - taxclass => '', } ); - - if ( $override ) { #use a payment gateway override - - my $payment_gateway = $override->payment_gateway; - - $processor = $payment_gateway->gateway_module; - $login = $payment_gateway->gateway_username; - $password = $payment_gateway->gateway_password; - #$action = $payment_gateway->gateway_action; - @bop_options = $payment_gateway->options; - - } else { #use the standard settings from the config - - my $unused_action; - ( $processor, $login, $password, $unused_action, @bop_options ) = - $self->default_payment_gateway($method); - - } - - } - return "neither amount nor paynum specified" unless $amount; - - my %content = ( - 'type' => $method, - 'login' => $login, - 'password' => $password, - 'order_number' => $order_number, - 'amount' => $amount, - 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/ - ); - $content{authorization} = $auth - if length($auth); #echeck/ACH transactions have an order # but no auth - #(at least with authorize.net) - - my $disable_void_after; - if ($conf->exists('disable_void_after') - && $conf->config('disable_void_after') =~ /^(\d+)$/) { - $disable_void_after = $1; - } - - #first try void if applicable - if ( $cust_pay && $cust_pay->paid == $amount - && ( - ( not defined($disable_void_after) ) - || ( time < ($cust_pay->_date + $disable_void_after ) ) - ) - ) { - warn " attempting void\n" if $DEBUG > 1; - my $void = new Business::OnlinePayment( $processor, @bop_options ); - if ( $void->can('info') ) { - if ( $cust_pay->payby eq 'CARD' - && $void->info('CC_void_requires_card') ) - { - $content{'card_number'} = $cust_pay->payinfo - } elsif ( $cust_pay->payby eq 'CHEK' - && $void->info('ECHECK_void_requires_account') ) - { - ( $content{'account_number'}, $content{'routing_code'} ) = - split('@', $cust_pay->payinfo); - $content{'name'} = $self->get('first'). ' '. $self->get('last'); - } - } - $void->content( 'action' => 'void', %content ); - $void->submit(); - if ( $void->is_success ) { - my $error = $cust_pay->void($options{'reason'}); - if ( $error ) { - # gah, even with transactions. - my $e = 'WARNING: Card/ACH voided but database not updated - '. - "error voiding payment: $error"; - warn $e; - return $e; - } - warn " void successful\n" if $DEBUG > 1; - return ''; - } - } - - warn " void unsuccessful, trying refund\n" - if $DEBUG > 1; - - #massage data - my $address = $self->address1; - $address .= ", ". $self->address2 if $self->address2; - - my($payname, $payfirst, $paylast); - if ( $self->payname && $method ne 'ECHECK' ) { - $payname = $self->payname; - $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ - or return "Illegal payname $payname"; - ($payfirst, $paylast) = ($1, $2); - } else { - $payfirst = $self->getfield('first'); - $paylast = $self->getfield('last'); - $payname = "$payfirst $paylast"; - } - - my @invoicing_list = $self->invoicing_list_emailonly; - if ( $conf->exists('emailinvoiceautoalways') - || $conf->exists('emailinvoiceauto') && ! @invoicing_list - || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { - push @invoicing_list, $self->all_emails; - } - - my $email = ($conf->exists('business-onlinepayment-email-override')) - ? $conf->config('business-onlinepayment-email-override') - : $invoicing_list[0]; - - my $payip = exists($options{'payip'}) - ? $options{'payip'} - : $self->payip; - $content{customer_ip} = $payip - if length($payip); - - my $payinfo = ''; - if ( $method eq 'CC' ) { - - if ( $cust_pay ) { - $content{card_number} = $payinfo = $cust_pay->payinfo; - (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate) - =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ && - ($content{expiration} = "$2/$1"); # where available - } else { - $content{card_number} = $payinfo = $self->payinfo; - (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate) - =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; - $content{expiration} = "$2/$1"; - } - - } elsif ( $method eq 'ECHECK' ) { - - if ( $cust_pay ) { - $payinfo = $cust_pay->payinfo; - } else { - $payinfo = $self->payinfo; - } - ( $content{account_number}, $content{routing_code} )= split('@', $payinfo ); - $content{bank_name} = $self->payname; - $content{account_type} = 'CHECKING'; - $content{account_name} = $payname; - $content{customer_org} = $self->company ? 'B' : 'I'; - $content{customer_ssn} = $self->ss; - } elsif ( $method eq 'LEC' ) { - $content{phone} = $payinfo = $self->payinfo; - } + . join ( ' OR ' , map { + "( part_event.eventtable = " . dbh->quote($_) + . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ; + } FS::part_event->eventtables) + . ') '; - #then try refund - my $refund = new Business::OnlinePayment( $processor, @bop_options ); - my %sub_content = $refund->content( - 'action' => 'credit', - 'customer_id' => $self->custnum, - 'last_name' => $paylast, - 'first_name' => $payfirst, - 'name' => $payname, - 'address' => $address, - 'city' => $self->city, - 'state' => $self->state, - 'zip' => $self->zip, - 'country' => $self->country, - 'email' => $email, - 'phone' => $self->daytime || $self->night, - %content, #after + #here is the agent virtualization + my $agent_virt = " ( part_event.agentnum IS NULL + OR part_event.agentnum = ". $self->agentnum. ' )'; + + #XXX this shouldn't be hardcoded, actions should declare it... + my @realtime_events = qw( + cust_bill_realtime_card + cust_bill_realtime_check + cust_bill_realtime_lec + cust_bill_batch ); - warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content ) - if $DEBUG > 1; - $refund->submit(); - return "$processor error: ". $refund->error_message - unless $refund->is_success(); + my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'", + @realtime_events + ). + ' ) '; - my %method2payby = ( - 'CC' => 'CARD', - 'ECHECK' => 'CHEK', - 'LEC' => 'LECB', - ); + my @cust_event = qsearchs({ + 'table' => 'cust_event', + 'select' => 'cust_event.*', + 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join", + 'hashref' => { 'status' => 'done' }, + 'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ". + " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1" + }); - my $paybatch = "$processor:". $refund->authorization; - $paybatch .= ':'. $refund->order_number - if $refund->can('order_number') && $refund->order_number; + my %seen_invnum = (); + foreach my $cust_event (@cust_event) { - while ( $cust_pay && $cust_pay->unapplied < $amount ) { - my @cust_bill_pay = $cust_pay->cust_bill_pay; - last unless @cust_bill_pay; - my $cust_bill_pay = pop @cust_bill_pay; - my $error = $cust_bill_pay->delete; - last if $error; - } + #max one for the customer, one for each open invoice + my $cust_X = $cust_event->cust_X; + next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill' + ? $cust_X->invnum + : 0 + }++ + or $cust_event->part_event->eventtable eq 'cust_bill' + && ! $cust_X->owed; - my $cust_refund = new FS::cust_refund ( { - 'custnum' => $self->custnum, - 'paynum' => $options{'paynum'}, - 'refund' => $amount, - '_date' => '', - 'payby' => $method2payby{$method}, - 'payinfo' => $payinfo, - 'paybatch' => $paybatch, - 'reason' => $options{'reason'} || 'card or ACH refund', - } ); - my $error = $cust_refund->insert; - if ( $error ) { - $cust_refund->paynum(''); #try again with no specific paynum - my $error2 = $cust_refund->insert; - if ( $error2 ) { - # gah, even with transactions. - my $e = 'WARNING: Card/ACH refunded but database not updated - '. - "error inserting refund ($processor): $error2". - " (previously tried insert with paynum #$options{'paynum'}" . - ": $error )"; - warn $e; - return $e; + my $error = $cust_event->retry; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error scheduling event for retry: $error"; } + } - ''; #no error + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; } -# does the configuration indicate the new bop routines are required? - -sub _new_bop_required { - my $self = shift; - - my $botpp = 'Business::OnlineThirdPartyPayment'; - return 1 - if ( ( $conf->exists('business-onlinepayment-namespace') - && $conf->config('business-onlinepayment-namespace') eq $botpp - ) - or scalar( grep { $_->gateway_namespace eq $botpp } - qsearch( 'payment_gateway', { 'disabled' => '' } ) - ) - ) - ; +=cut - ''; -} - =item realtime_collect [ OPTION => VALUE ... ] Runs a realtime credit card, ACH (electronic check) or phone bill transaction @@ -5163,7 +4223,7 @@ sub realtime_collect { } -=item _realtime_bop { [ ARG => VALUE ... ] } +=item realtime_bop { [ ARG => VALUE ... ] } Runs a realtime credit card, ACH (electronic check) or phone bill transaction via a Business::OnlinePayment realtime gateway. See @@ -5173,7 +4233,7 @@ Required arguments in the hashref are I, and I Available methods are: I, I and I -Available optional arguments are: I, I, I, I, I, I +Available optional arguments are: I, I, I, I, I, I, I The additional options I, I, I, I, I, I, I and I are also available. Any of these options, @@ -5185,7 +4245,9 @@ option, or "Internet services" if that is unset. If an I is specified, this payment (if successful) is applied to the specified invoice. If you don't specify an I you might want to -call the B method. +call the B method or set the I option. + +I can be set to true to apply a resulting payment. I can be set true to surpress email decline notices. @@ -5203,6 +4265,33 @@ I allows payment capture to unlock export jobs =cut # some helper routines +sub _bop_recurring_billing { + my( $self, %opt ) = @_; + + my $method = scalar($conf->config('credit_card-recurring_billing_flag')); + + if ( defined($method) && $method eq 'transaction_is_recur' ) { + + return 1 if $opt{'trans_is_recur'}; + + } else { + + my %hash = ( 'custnum' => $self->custnum, + 'payby' => 'CARD', + ); + + return 1 + if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } ) + || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD', + $opt{'payinfo'} ) + } ); + + } + + return 0; + +} + sub _payment_gateway { my ($self, $options) = @_; @@ -5227,6 +4316,7 @@ sub _bop_options { $options->{payment_gateway}->gatewaynum ? $options->{payment_gateway}->options : @{ $options->{payment_gateway}->get('options') }; + } sub _bop_defaults { @@ -5253,14 +4343,6 @@ sub _bop_content { my ($self, $options) = @_; my %content = (); - $content{address} = exists($options->{'address1'}) - ? $options->{'address1'} - : $self->address1; - my $address2 = exists($options->{'address2'}) - ? $options->{'address2'} - : $self->address2; - $content{address} .= ", ". $address2 if length($address2); - my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip; $content{customer_ip} = $payip if length($payip); @@ -5271,14 +4353,30 @@ sub _bop_content { ( $conf->exists('business-onlinepayment-email_customer') || $conf->exists('business-onlinepayment-email-override') ); - $content{payfirst} = $self->getfield('first'); - $content{paylast} = $self->getfield('last'); + my ($payname, $payfirst, $paylast); + if ( $options->{payname} && $options->{method} ne 'ECHECK' ) { + ($payname = $options->{payname}) =~ + /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ + or return "Illegal payname $payname"; + ($payfirst, $paylast) = ($1, $2); + } else { + $payfirst = $self->getfield('first'); + $paylast = $self->getfield('last'); + $payname = "$payfirst $paylast"; + } - $content{account_name} = "$content{payfirst} $content{paylast}" - if $options->{method} eq 'ECHECK'; + $content{last_name} = $paylast; + $content{first_name} = $payfirst; - $content{name} = $options->{payname}; - $content{name} = $content{account_name} if exists($content{account_name}); + $content{name} = $payname; + + $content{address} = exists($options->{'address1'}) + ? $options->{'address1'} + : $self->address1; + my $address2 = exists($options->{'address2'}) + ? $options->{'address2'} + : $self->address2; + $content{address} .= ", ". $address2 if length($address2); $content{city} = exists($options->{city}) ? $options->{city} @@ -5292,10 +4390,11 @@ sub _bop_content { $content{country} = exists($options->{country}) ? $options->{country} : $self->country; + $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/ $content{phone} = $self->daytime || $self->night; - (%content); + \%content; } my %bop_method2payby = ( @@ -5304,7 +4403,7 @@ my %bop_method2payby = ( 'LEC' => 'LECB', ); -sub _new_realtime_bop { +sub realtime_bop { my $self = shift; my %options = (); @@ -5371,13 +4470,8 @@ sub _new_realtime_bop { # massage data ### - my (%bop_content) = $self->_bop_content(\%options); - - if ( $options{method} ne 'ECHECK' ) { - $options{payname} =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ - or return "Illegal payname $options{payname}"; - ($bop_content{payfirst}, $bop_content{paylast}) = ($1, $2); - } + my $bop_content = $self->_bop_content(\%options); + return $bop_content unless ref($bop_content); my @invoicing_list = $self->invoicing_list_emailonly; if ( $conf->exists('emailinvoiceautoalways') @@ -5443,6 +4537,9 @@ sub _new_realtime_bop { $content{account_type} = exists($options{'paytype'}) ? uc($options{'paytype'}) || 'CHECKING' : uc($self->getfield('paytype')) || 'CHECKING'; + $content{account_name} = $self->getfield('first'). ' '. + $self->getfield('last'); + $content{customer_org} = $self->company ? 'B' : 'I'; $content{state_id} = exists($options{'stateid'}) ? $options{'stateid'} @@ -5527,7 +4624,7 @@ sub _new_realtime_bop { 'amount' => $options{amount}, #'invoice_number' => $options{'invnum'}, 'customer_id' => $self->custnum, - %bop_content, + %$bop_content, 'reference' => $cust_pay_pending->paypendingnum, #for now 'email' => $email, %content, #after @@ -5542,6 +4639,8 @@ sub _new_realtime_bop { my $BOP_TESTING_SUCCESS = 1; unless ( $BOP_TESTING ) { + $transaction->test_transaction(1) + if $conf->exists('business-onlinepayment-test_transaction'); $transaction->submit(); } else { if ( $BOP_TESTING_SUCCESS ) { @@ -5594,6 +4693,8 @@ sub _new_realtime_bop { $capture->content( %capture ); + $capture->test_transaction(1) + if $conf->exists('business-onlinepayment-test_transaction'); $capture->submit(); unless ( $capture->is_success ) { @@ -5623,6 +4724,25 @@ sub _new_realtime_bop { } ### + # Tokenize + ### + + + if ( $transaction->can('card_token') && $transaction->card_token ) { + + $self->card_token($transaction->card_token); + + if ( $options{'payinfo'} eq $self->payinfo ) { + $self->payinfo($transaction->card_token); + my $error = $self->replace; + if ( $error ) { + warn "WARNING: error storing token: $error, but proceeding anyway\n"; + } + } + + } + + ### # result handling ### @@ -5745,7 +4865,7 @@ sub _realtime_bop_result { 'paid' => $cust_pay_pending->paid, '_date' => '', 'payby' => $cust_pay_pending->payby, - #'payinfo' => $payinfo, + 'payinfo' => $options{'payinfo'}, 'paybatch' => $paybatch, 'paydate' => $cust_pay_pending->paydate, 'pkgnum' => $cust_pay_pending->pkgnum, @@ -6107,7 +5227,7 @@ sub remove_cvv { ''; } -=item _new_realtime_refund_bop METHOD [ OPTION => VALUE ... ] +=item realtime_refund_bop METHOD [ OPTION => VALUE ... ] Refunds a realtime credit card, ACH (electronic check) or phone bill transaction via a Business::OnlinePayment realtime gateway. See @@ -6145,11 +5265,11 @@ gateway is attempted. #some false laziness w/realtime_bop, not enough to make it worth merging #but some useful small subs should be pulled out -sub _new_realtime_refund_bop { +sub realtime_refund_bop { my $self = shift; my %options = (); - if (ref($_[0]) ne 'HASH') { + if (ref($_[0]) eq 'HASH') { %options = %{$_[0]}; } else { my $method = shift; @@ -6281,6 +5401,8 @@ sub _new_realtime_refund_bop { } } $void->content( 'action' => 'void', %content ); + $void->test_transaction(1) + if $conf->exists('business-onlinepayment-test_transaction'); $void->submit(); if ( $void->is_success ) { my $error = $cust_pay->void($options{'reason'}); @@ -6383,6 +5505,8 @@ sub _new_realtime_refund_bop { ); warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content ) if $DEBUG > 1; + $refund->test_transaction(1) + if $conf->exists('business-onlinepayment-test_transaction'); $refund->submit(); return "$processor error: ". $refund->error_message @@ -6821,29 +5945,17 @@ sub total_owed_date { my $self = shift; my $time = shift; -# my $custnum = $self->custnum; -# -# my $owed_sql = FS::cust_bill->owed_sql; -# -# my $sql = " -# SELECT SUM($owed_sql) FROM cust_bill -# WHERE custnum = $custnum -# AND _date <= $time -# "; -# -# my $sth = dbh->prepare($sql) or die dbh->errstr; -# $sth->execute() or die $sth->errstr; -# -# return sprintf( '%.2f', $sth->fetchrow_arrayref->[0] ); + my $custnum = $self->custnum; - my $total_bill = 0; - foreach my $cust_bill ( - grep { $_->_date <= $time } - qsearch('cust_bill', { 'custnum' => $self->custnum, } ) - ) { - $total_bill += $cust_bill->owed; - } - sprintf( "%.2f", $total_bill ); + my $owed_sql = FS::cust_bill->owed_sql; + + my $sql = " + SELECT SUM($owed_sql) FROM cust_bill + WHERE custnum = $custnum + AND _date <= $time + "; + + sprintf( "%.2f", $self->scalar_sql($sql) ); } @@ -6913,9 +6025,18 @@ sub total_credited { sub total_unapplied_credits { my $self = shift; - my $total_credit = 0; - $total_credit += $_->credited foreach $self->cust_credit; - sprintf( "%.2f", $total_credit ); + + my $custnum = $self->custnum; + + my $unapplied_sql = FS::cust_credit->unapplied_sql; + + my $sql = " + SELECT SUM($unapplied_sql) FROM cust_credit + WHERE custnum = $custnum + "; + + sprintf( "%.2f", $self->scalar_sql($sql) ); + } =item total_unapplied_credits_pkgnum PKGNUM @@ -6942,9 +6063,18 @@ See L. sub total_unapplied_payments { my $self = shift; - my $total_unapplied = 0; - $total_unapplied += $_->unapplied foreach $self->cust_pay; - sprintf( "%.2f", $total_unapplied ); + + my $custnum = $self->custnum; + + my $unapplied_sql = FS::cust_pay->unapplied_sql; + + my $sql = " + SELECT SUM($unapplied_sql) FROM cust_pay + WHERE custnum = $custnum + "; + + sprintf( "%.2f", $self->scalar_sql($sql) ); + } =item total_unapplied_payments_pkgnum PKGNUM @@ -6972,9 +6102,17 @@ customer. See L. sub total_unapplied_refunds { my $self = shift; - my $total_unapplied = 0; - $total_unapplied += $_->unapplied foreach $self->cust_refund; - sprintf( "%.2f", $total_unapplied ); + my $custnum = $self->custnum; + + my $unapplied_sql = FS::cust_refund->unapplied_sql; + + my $sql = " + SELECT SUM($unapplied_sql) FROM cust_refund + WHERE custnum = $custnum + "; + + sprintf( "%.2f", $self->scalar_sql($sql) ); + } =item balance @@ -6986,12 +6124,7 @@ total_unapplied_credits minus total_unapplied_payments). sub balance { my $self = shift; - sprintf( "%.2f", - $self->total_owed - + $self->total_unapplied_refunds - - $self->total_unapplied_credits - - $self->total_unapplied_payments - ); + $self->balance_date_range; } =item balance_date TIME @@ -7006,19 +6139,13 @@ functions. sub balance_date { my $self = shift; - my $time = shift; - sprintf( "%.2f", - $self->total_owed_date($time) - + $self->total_unapplied_refunds - - $self->total_unapplied_credits - - $self->total_unapplied_payments - ); + $self->balance_date_range(shift); } -=item balance_date_range START_TIME [ END_TIME [ OPTION => VALUE ... ] ] +=item balance_date_range [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ] -Returns the balance for this customer, only considering invoices with date -earlier than START_TIME, and optionally not later than END_TIME +Returns the balance for this customer, optionally considering invoices with +date earlier than START_TIME, and not later than END_TIME (total_owed_date minus total_unapplied_credits minus total_unapplied_payments). Times are specified as SQL fragments or numeric @@ -8082,6 +7209,8 @@ Returns a status string for this customer, currently: =item prospect - No packages have ever been ordered +=item ordered - Recurring packages all are new (not yet billed). + =item active - One or more recurring packages is active =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled) @@ -8098,7 +7227,8 @@ sub status { shift->cust_status(@_); } sub cust_status { my $self = shift; - for my $status (qw( prospect active inactive suspended cancelled )) { + # prospect ordered active inactive suspended cancelled + for my $status ( FS::cust_main->statuses() ) { my $method = $status.'_sql'; my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g; my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr; @@ -8133,6 +7263,7 @@ use vars qw(%statuscolor); tie %statuscolor, 'Tie::IxHash', 'prospect' => '7e0079', #'000000', #black? naw, purple 'active' => '00CC00', #green + 'ordered' => '009999', #teal? cyan? 'inactive' => '0000CC', #blue 'suspended' => 'FF9900', #yellow 'cancelled' => 'FF0000', #red @@ -8242,9 +7373,20 @@ sub select_count_pkgs_sql { $select_count_pkgs; } -sub prospect_sql { " - 0 = ( $select_count_pkgs ) -"; } +sub prospect_sql { + " 0 = ( $select_count_pkgs ) "; +} + +=item ordered_sql + +Returns an SQL expression identifying ordered cust_main records (customers with +recurring packages not yet setup). + +=cut + +sub ordered_sql { + " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->ordered_sql. " ) "; +} =item active_sql @@ -8253,10 +7395,9 @@ active recurring packages). =cut -sub active_sql { " - 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " - ) -"; } +sub active_sql { + " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) "; +} =item inactive_sql @@ -8345,10 +7486,10 @@ sub balance_sql { " WHERE cust_refund.custnum = cust_main.custnum ) "; } -=item balance_date_sql START_TIME [ END_TIME [ OPTION => VALUE ... ] ] +=item balance_date_sql [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ] -Returns an SQL fragment to retreive the balance for this customer, only -considering invoices with date earlier than START_TIME, and optionally not +Returns an SQL fragment to retreive the balance for this customer, optionally +considering invoices with date earlier than START_TIME, and not later than END_TIME (total_owed_date minus total_unapplied_credits minus total_unapplied_payments). @@ -8380,6 +7521,12 @@ WHERE clause hashref (elements "AND"ed together) (typically used with the total (unused. obsolete?) JOIN clause (typically used with the total option) +=item cutoff + +An absolute cutoff time. Payments, credits, and refunds I after this +time will be ignored. Note that START_TIME and END_TIME only limit the date +range for invoices and I payments, credits, and refunds. + =back =cut @@ -8387,10 +7534,12 @@ JOIN clause (typically used with the total option) sub balance_date_sql { my( $class, $start, $end, %opt ) = @_; - my $owed = FS::cust_bill->owed_sql; - my $unapp_refund = FS::cust_refund->unapplied_sql; - my $unapp_credit = FS::cust_credit->unapplied_sql; - my $unapp_pay = FS::cust_pay->unapplied_sql; + my $cutoff = $opt{'cutoff'}; + + my $owed = FS::cust_bill->owed_sql($cutoff); + my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff); + my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff); + my $unapp_pay = FS::cust_pay->unapplied_sql($cutoff); my $j = $opt{'join'} || ''; @@ -8423,9 +7572,11 @@ Available options are: =cut sub unapplied_payments_date_sql { - my( $class, $start, $end, ) = @_; + my( $class, $start, $end, %opt ) = @_; + + my $cutoff = $opt{'cutoff'}; - my $unapp_pay = FS::cust_pay->unapplied_sql; + my $unapp_pay = FS::cust_pay->unapplied_sql($cutoff); my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end, 'unapplied_date'=>1 ); @@ -8536,7 +7687,7 @@ sub search { # parse status ## - #prospect active inactive suspended cancelled + #prospect ordered active inactive suspended cancelled if ( grep { $params->{'status'} eq $_ } FS::cust_main->statuses() ) { my $method = $params->{'status'}. '_sql'; #push @where, $class->$method(); @@ -9778,14 +8929,23 @@ sub _agent_plandata { } +=item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ] + +Subroutine (not a method), designed to be called from the queue. + +Takes a list of options and values. + +Pulls up the customer record via the custnum option and calls bill_and_collect. + +=cut + sub queued_bill { - ## actual sub, not a method, designed to be called from the queue. - ## sets up the customer, and calls the bill_and_collect my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_; + my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } ); - $cust_main->bill_and_collect( - %args, - ); + warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid + + $cust_main->bill_and_collect( %args ); } sub _upgrade_data { #class method @@ -9795,7 +8955,7 @@ sub _upgrade_data { #class method my $sth = dbh->prepare($sql) or die dbh->errstr; $sth->execute or die $sth->errstr; - local($ignore_expired_card) = 1 + local($ignore_expired_card) = 1; $class->_upgrade_otaker(%opts); }