X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=c1a8aafdee9dbde3456de3d202d0f47c20865baf;hb=7b125e587a4d1ee0aca692e23ea7897f671855ae;hp=fc16f14a6b69ed395d1e62df31aa497241e89958;hpb=59dedfd6c5b60665ecc7ea9e6e07ea45e5fdfcdd;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index fc16f14a6..c1a8aafde 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2,7 +2,8 @@ package FS::cust_main; require 5.006; use strict; -use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf +use base qw( FS::otaker_Mixin FS::payinfo_Mixin FS::Record ); +use vars qw( @EXPORT_OK $DEBUG $me $conf @encrypted_fields $import $ignore_expired_card $skip_fuzzyfiles @fuzzyfields @@ -25,7 +26,7 @@ use String::Approx qw(amatch); use Business::CreditCard 0.28; use Locale::Country; use FS::UID qw( getotaker dbh driver_name ); -use FS::Record qw( qsearchs qsearch dbdef ); +use FS::Record qw( qsearchs qsearch dbdef regexp_sql ); use FS::Misc qw( generate_email send_email generate_ps do_print ); use FS::Msgcat qw(gettext); use FS::payby; @@ -66,11 +67,8 @@ use FS::type_pkgs; use FS::payment_gateway; use FS::agent_payment_gateway; use FS::banned_pay; -use FS::payinfo_Mixin; use FS::TicketSystem; -@ISA = qw( FS::payinfo_Mixin FS::Record ); - @EXPORT_OK = qw( smart_search ); $realtime_bop_decline_quiet = 0; @@ -304,9 +302,9 @@ IP address from which payment information was received Tax exempt, empty or `Y' -=item otaker +=item usernum -Order taker (assigned automatically, see L) +Order taker (see L) =item comments @@ -1912,6 +1910,25 @@ sub has_ship_address { scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields ); } +=item location_hash + +Returns a list of key/value pairs, with the following keys: address1, adddress2, +city, county, state, zip, country. The shipping address is used if present. + +=cut + +#geocode? dependent on tax-ship_address config, not available in cust_location +#mostly. not yet then. + +sub location_hash { + my $self = shift; + my $prefix = $self->has_ship_address ? 'ship_' : ''; + + map { $_ => $self->get($prefix.$_) } + qw( address1 address2 city county state zip country geocode ); + #fields that cust_location has +} + =item all_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ] Returns all packages (see L) for this customer. @@ -2475,33 +2492,33 @@ sub bill_and_collect { $error = $self->cancel_expired_pkgs( $options{actual_time} ); if ( $error ) { $error = "Error expiring custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } } $error = $self->suspend_adjourned_pkgs( $options{actual_time} ); if ( $error ) { $error = "Error adjourning custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } } $error = $self->bill( %options ); if ( $error ) { $error = "Error billing custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } } $error = $self->apply_payments_and_credits; if ( $error ) { $error = "Error applying custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } } unless ( $conf->exists('cancelled_cust-noevents') @@ -2510,9 +2527,9 @@ sub bill_and_collect { $error = $self->collect( %options ); if ( $error ) { $error = "Error collecting custnum ". $self->custnum. ": $error"; - if ( $options{'fatal'} eq 'return' ) { return $error; } - elsif ( $options{'fatal'} ) { die $error; } - else { warn $error; } + if ($options{fatal} && $options{fatal} eq 'return') { return $error; } + elsif ($options{fatal} ) { die $error; } + else { warn $error; } } } @@ -2677,15 +2694,21 @@ sub bill { return $error; } - my @cust_bill_pkg = (); + #keep auto-charge and non-auto-charge line items separate + my @passes = ( '', 'no_auto' ); + + my %cust_bill_pkg = map { $_ => [] } @passes; ### # find the packages which are due for billing, find out how much they are # & generate invoice database. ### - my( $total_setup, $total_recur, $postal_charge ) = ( 0, 0, 0 ); - my %taxlisthash; + my %total_setup = map { my $z = 0; $_ => \$z; } @passes; + my %total_recur = map { my $z = 0; $_ => \$z; } @passes; + + my %taxlisthash = map { $_ => {} } @passes; + my @precommit_hooks = (); $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks? @@ -2708,14 +2731,16 @@ sub bill { $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill ); + my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : ''; + my $error = $self->_make_lines( 'part_pkg' => $part_pkg, 'cust_pkg' => $cust_pkg, 'precommit_hooks' => \@precommit_hooks, - 'line_items' => \@cust_bill_pkg, - 'setup' => \$total_setup, - 'recur' => \$total_recur, - 'tax_matrix' => \%taxlisthash, + 'line_items' => $cust_bill_pkg{$pass}, + 'setup' => $total_setup{$pass}, + 'recur' => $total_recur{$pass}, + 'tax_matrix' => $taxlisthash{$pass}, 'time' => $time, 'real_pkgpart' => $real_pkgpart, 'options' => \%options, @@ -2729,130 +2754,138 @@ sub bill { } #foreach my $cust_pkg - unless ( @cust_bill_pkg ) { #don't create an invoice w/o line items - #but do commit any package date cycling that happened - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - return ''; - } + #if the customer isn't on an automatic payby, everything can go on a single + #invoice anyway? + #if ( $cust_main->payby !~ /^(CARD|CHEK)$/ ) { + #merge everything into one list + #} - if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) || - !$conf->exists('postal_invoice-recurring_only') - ) - { + foreach my $pass (@passes) { # keys %cust_bill_pkg ) { - my $postal_pkg = $self->charge_postal_fee(); - if ( $postal_pkg && !ref( $postal_pkg ) ) { + my @cust_bill_pkg = @{ $cust_bill_pkg{$pass} }; - $dbh->rollback if $oldAutoCommit; - return "can't charge postal invoice fee for customer ". - $self->custnum. ": $postal_pkg"; - - } elsif ( $postal_pkg ) { - - my $real_pkgpart = $postal_pkg->pkgpart; - foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) { - my %postal_options = %options; - delete $postal_options{cancel}; - my $error = - $self->_make_lines( 'part_pkg' => $part_pkg, - 'cust_pkg' => $postal_pkg, - 'precommit_hooks' => \@precommit_hooks, - 'line_items' => \@cust_bill_pkg, - 'setup' => \$total_setup, - 'recur' => \$total_recur, - 'tax_matrix' => \%taxlisthash, - 'time' => $time, - 'real_pkgpart' => $real_pkgpart, - 'options' => \%postal_options, - ); - if ($error) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } + next unless @cust_bill_pkg; #don't create an invoice w/o line items - } + if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) || + !$conf->exists('postal_invoice-recurring_only') + ) + { - } + my $postal_pkg = $self->charge_postal_fee(); + if ( $postal_pkg && !ref( $postal_pkg ) ) { - my $listref_or_error = - $self->calculate_taxes( \@cust_bill_pkg, \%taxlisthash, $invoice_time); + $dbh->rollback if $oldAutoCommit; + return "can't charge postal invoice fee for customer ". + $self->custnum. ": $postal_pkg"; + + } elsif ( $postal_pkg ) { + + my $real_pkgpart = $postal_pkg->pkgpart; + foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) { + my %postal_options = %options; + delete $postal_options{cancel}; + my $error = + $self->_make_lines( 'part_pkg' => $part_pkg, + 'cust_pkg' => $postal_pkg, + 'precommit_hooks' => \@precommit_hooks, + 'line_items' => \@cust_bill_pkg, + 'setup' => $total_setup{$pass}, + 'recur' => $total_recur{$pass}, + 'tax_matrix' => $taxlisthash{$pass}, + 'time' => $time, + 'real_pkgpart' => $real_pkgpart, + 'options' => \%postal_options, + ); + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } - unless ( ref( $listref_or_error ) ) { - $dbh->rollback if $oldAutoCommit; - return $listref_or_error; - } + } - foreach my $taxline ( @$listref_or_error ) { - $total_setup = sprintf('%.2f', $total_setup+$taxline->setup ); - push @cust_bill_pkg, $taxline; - } + } - #add tax adjustments - warn "adding tax adjustments...\n" if $DEBUG > 2; - foreach my $cust_tax_adjustment ( - qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum, - 'billpkgnum' => '', - } - ) - ) { + my $listref_or_error = + $self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time); - my $tax = sprintf('%.2f', $cust_tax_adjustment->amount ); - - my $itemdesc = $cust_tax_adjustment->taxname; - $itemdesc = '' if $itemdesc eq 'Tax'; - - push @cust_bill_pkg, new FS::cust_bill_pkg { - 'pkgnum' => 0, - 'setup' => $tax, - 'recur' => 0, - 'sdate' => '', - 'edate' => '', - 'itemdesc' => $itemdesc, - 'itemcomment' => $cust_tax_adjustment->comment, - 'cust_tax_adjustment' => $cust_tax_adjustment, - #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location, - }; + unless ( ref( $listref_or_error ) ) { + $dbh->rollback if $oldAutoCommit; + return $listref_or_error; + } - } + foreach my $taxline ( @$listref_or_error ) { + ${ $total_setup{$pass} } = + sprintf('%.2f', ${ $total_setup{$pass} } + $taxline->setup ); + push @cust_bill_pkg, $taxline; + } - my $charged = sprintf('%.2f', $total_setup + $total_recur ); + #add tax adjustments + warn "adding tax adjustments...\n" if $DEBUG > 2; + foreach my $cust_tax_adjustment ( + qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum, + 'billpkgnum' => '', + } + ) + ) { - my @cust_bill = $self->cust_bill; - my $balance = $self->balance; - my $previous_balance = scalar(@cust_bill) - ? ( $cust_bill[$#cust_bill]->billing_balance || 0 ) - : 0; + my $tax = sprintf('%.2f', $cust_tax_adjustment->amount ); + + my $itemdesc = $cust_tax_adjustment->taxname; + $itemdesc = '' if $itemdesc eq 'Tax'; + + push @cust_bill_pkg, new FS::cust_bill_pkg { + 'pkgnum' => 0, + 'setup' => $tax, + 'recur' => 0, + 'sdate' => '', + 'edate' => '', + 'itemdesc' => $itemdesc, + 'itemcomment' => $cust_tax_adjustment->comment, + 'cust_tax_adjustment' => $cust_tax_adjustment, + #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location, + }; - $previous_balance += $cust_bill[$#cust_bill]->charged - if scalar(@cust_bill); - #my $balance_adjustments = - # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged); + } - #create the new invoice - my $cust_bill = new FS::cust_bill ( { - 'custnum' => $self->custnum, - '_date' => ( $invoice_time ), - 'charged' => $charged, - 'billing_balance' => $balance, - 'previous_balance' => $previous_balance, - 'invoice_terms' => $options{'invoice_terms'}, - } ); - $error = $cust_bill->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't create invoice for customer #". $self->custnum. ": $error"; - } + my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } ); - foreach my $cust_bill_pkg ( @cust_bill_pkg ) { - $cust_bill_pkg->invnum($cust_bill->invnum); - my $error = $cust_bill_pkg->insert; + my @cust_bill = $self->cust_bill; + my $balance = $self->balance; + my $previous_balance = scalar(@cust_bill) + ? ( $cust_bill[$#cust_bill]->billing_balance || 0 ) + : 0; + + $previous_balance += $cust_bill[$#cust_bill]->charged + if scalar(@cust_bill); + #my $balance_adjustments = + # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged); + + #create the new invoice + my $cust_bill = new FS::cust_bill ( { + 'custnum' => $self->custnum, + '_date' => ( $invoice_time ), + 'charged' => $charged, + 'billing_balance' => $balance, + 'previous_balance' => $previous_balance, + 'invoice_terms' => $options{'invoice_terms'}, + } ); + $error = $cust_bill->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "can't create invoice line item: $error"; + return "can't create invoice for customer #". $self->custnum. ": $error"; + } + + foreach my $cust_bill_pkg ( @cust_bill_pkg ) { + $cust_bill_pkg->invnum($cust_bill->invnum); + my $error = $cust_bill_pkg->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't create invoice line item: $error"; + } } - } - + + } #foreach my $pass ( keys %cust_bill_pkg ) foreach my $hook ( @precommit_hooks ) { eval { @@ -3065,7 +3098,7 @@ sub _make_lines { my $old_cust_pkg = new FS::cust_pkg \%hash; my @details = (); - + my @discounts = (); my $lineitems = 0; $cust_pkg->pkgpart($part_pkg->pkgpart); @@ -3150,6 +3183,7 @@ sub _make_lines { ); my %param = ( 'precommit_hooks' => $precommit_hooks, 'increment_next_bill' => $increment_next_bill, + 'discounts' => \@discounts, ); my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur'; @@ -3229,6 +3263,7 @@ sub _make_lines { 'unitrecur' => $unitrecur, 'quantity' => $cust_pkg->quantity, 'details' => \@details, + 'discounts' => \@discounts, 'hidden' => $part_pkg->hidden, }; @@ -3926,7 +3961,7 @@ sub due_cust_event { warn " invalid conditions not eliminated with condition_sql:\n". join('', map " $_: ".$unsat{$_}."\n", keys %unsat ) - if $DEBUG; # > 1; + if keys %unsat && $DEBUG; # > 1; ## # insert @@ -3940,1121 +3975,116 @@ sub due_cust_event { $dbh->rollback if $oldAutoCommit; return $error; } - - } - } - - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - - ## - # return - ## - - warn " returning events: ". Dumper(@cust_event). "\n" - if $DEBUG > 2; - - \@cust_event; - -} - -=item retry_realtime - -Schedules realtime / batch credit card / electronic check / LEC billing -events for for retry. Useful if card information has changed or manual -retry is desired. The 'collect' method must be called to actually retry -the transaction. - -Implementation details: For either this customer, or for each of this -customer's open invoices, changes the status of the first "done" (with -statustext error) realtime processing event to "failed". - -=cut - -sub retry_realtime { - my $self = shift; - - 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; - - #a little false laziness w/due_cust_event (not too bad, really) - - my $join = FS::part_event_condition->join_conditions_sql; - 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; - } - - 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 ); - $content{'card_number'} = $cust_pay->payinfo - if $cust_pay->payby eq 'CARD' - && $void->can('info') && $void->info('CC_void_requires_card'); - $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; + $dbh->commit or die $dbh->errstr if $oldAutoCommit; - #massage data - my $address = $self->address1; - $address .= ", ". $self->address2 if $self->address2; + ## + # return + ## - 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"; - } + warn " returning events: ". Dumper(@cust_event). "\n" + if $DEBUG > 2; - 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; - } + \@cust_event; - 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); +=item retry_realtime - my $payinfo = ''; - if ( $method eq 'CC' ) { +Schedules realtime / batch credit card / electronic check / LEC billing +events for for retry. Useful if card information has changed or manual +retry is desired. The 'collect' method must be called to actually retry +the transaction. - 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"; - } +Implementation details: For either this customer, or for each of this +customer's open invoices, changes the status of the first "done" (with +statustext error) realtime processing event to "failed". - } elsif ( $method eq 'ECHECK' ) { +=cut - 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; - } +sub retry_realtime { + my $self = shift; - #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 - ); - warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content ) - if $DEBUG > 1; - $refund->submit(); + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; - return "$processor error: ". $refund->error_message - unless $refund->is_success(); + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; - my %method2payby = ( - 'CC' => 'CARD', - 'ECHECK' => 'CHEK', - 'LEC' => 'LECB', - ); + #a little false laziness w/due_cust_event (not too bad, really) - my $paybatch = "$processor:". $refund->authorization; - $paybatch .= ':'. $refund->order_number - if $refund->can('order_number') && $refund->order_number; + my $join = FS::part_event_condition->join_conditions_sql; + 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) + . ') '; - 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; - } + #here is the agent virtualization + my $agent_virt = " ( part_event.agentnum IS NULL + OR part_event.agentnum = ". $self->agentnum. ' )'; - 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; - } - } + #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 + ); - ''; #no error + 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" + }); -# does the configuration indicate the new bop routines are required? + my %seen_invnum = (); + foreach my $cust_event (@cust_event) { -sub _new_bop_required { - my $self = shift; + #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 $botpp = 'Business::OnlineThirdPartyPayment'; + my $error = $cust_event->retry; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error scheduling event for retry: $error"; + } - 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' => '' } ) - ) - ) - ; + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; + } - + + +=cut + =item realtime_collect [ OPTION => VALUE ... ] Runs a realtime credit card, ACH (electronic check) or phone bill transaction @@ -5116,7 +4146,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 @@ -5126,7 +4156,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, @@ -5138,7 +4168,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. @@ -5156,6 +4188,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) = @_; @@ -5257,7 +4316,7 @@ my %bop_method2payby = ( 'LEC' => 'LECB', ); -sub _new_realtime_bop { +sub realtime_bop { my $self = shift; my %options = (); @@ -6060,7 +5119,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 @@ -6098,7 +5157,7 @@ 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 = (); @@ -6220,9 +5279,19 @@ sub _new_realtime_refund_bop { ) { warn " attempting void\n" if $DEBUG > 1; my $void = new Business::OnlinePayment( $processor, @bop_options ); - $content{'card_number'} = $cust_pay->payinfo - if $cust_pay->payby eq 'CARD' - && $void->can('info') && $void->info('CC_void_requires_card'); + 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 ) { @@ -7390,7 +6459,7 @@ sub referral_cust_main_ncancelled { Like referral_cust_main, except returns a flat list of all unsuspended (and uncancelled) packages for each customer. The number of items in this list may -be useful for comission calculations (perhaps after a Cpkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ). +be useful for commission calculations (perhaps after a Cpkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ). =cut @@ -7452,8 +6521,10 @@ sub credit { $cust_credit->set('reason', $reason) } - $cust_credit->addlinfo( delete $options{'addlinfo'} ) - if exists($options{'addlinfo'}); + for (qw( addlinfo eventnum )) { + $cust_credit->$_( delete $options{$_} ) + if exists($options{$_}); + } $cust_credit->insert(%options); @@ -7506,12 +6577,14 @@ sub charge { my ( $pkg, $comment, $additional ); my ( $setuptax, $taxclass ); #internal taxes my ( $taxproduct, $override ); #vendor (CCH) taxes + my $no_auto = ''; my $cust_pkg_ref = ''; my ( $bill_now, $invoice_terms ) = ( 0, '' ); if ( ref( $_[0] ) ) { $amount = $_[0]->{amount}; $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1; $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : ''; + $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : ''; $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge'; $comment = exists($_[0]->{comment}) ? $_[0]->{comment} : '$'. sprintf("%.2f",$amount); @@ -7589,6 +6662,7 @@ sub charge { 'pkgpart' => $pkgpart, 'quantity' => $quantity, 'start_date' => $start_date, + 'no_auto' => $no_auto, } ); $error = $cust_pkg->insert; @@ -8318,6 +7392,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 @@ -8325,10 +7405,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'} || ''; @@ -8462,6 +7544,15 @@ sub search { } ## + # do the same for user + ## + + if ( $params->{'usernum'} =~ /^(\d+)$/ and $1 ) { + push @where, + "cust_main.usernum = $1"; + } + + ## # parse status ## @@ -8496,13 +7587,23 @@ sub search { next unless exists($params->{$field}); - my($beginning, $ending) = @{$params->{$field}}; + my($beginning, $ending, $hour) = @{$params->{$field}}; push @where, "cust_main.$field IS NOT NULL", "cust_main.$field >= $beginning", "cust_main.$field <= $ending"; + # XXX: do this for mysql and/or pull it out of here + if(defined $hour) { + if ($dbh->{Driver}->{Name} eq 'Pg') { + push @where, "extract(hour from to_timestamp(cust_main.$field)) = $hour"; + } + else { + warn "search by time of day not supported on ".$dbh->{Driver}->{Name}." databases"; + } + } + $orderby ||= "ORDER BY cust_main.$field"; } @@ -9648,14 +8749,7 @@ sub _agent_plandata { my $agentnum = $self->agentnum; - my $regexp = ''; - if ( driver_name =~ /^Pg/i ) { - $regexp = '~'; - } elsif ( driver_name =~ /^mysql/i ) { - $regexp = 'REGEXP'; - } else { - die "don't know how to use regular expressions in ". driver_name. " databases"; - } + my $regexp = regexp_sql(); my $part_event_option = qsearchs({ @@ -9721,6 +8815,9 @@ 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; + $class->_upgrade_otaker(%opts); + } =back