use strict;
use vars qw( $conf $DEBUG $me );
use Carp;
+use List::Util qw( min );
use FS::UID qw( dbh );
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs dbdef );
use FS::cust_bill;
use FS::cust_bill_pkg;
use FS::cust_bill_pkg_display;
use FS::tax_rate_location;
use FS::cust_bill_pkg_tax_location;
use FS::cust_bill_pkg_tax_rate_location;
+use FS::part_event;
+use FS::part_event_condition;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
=head1 SYNOPSIS
-=head1 DESCRIPTIONS
+=head1 DESCRIPTION
These methods are available on FS::cust_main objects.
If set true, re-charges setup fees.
+=item recurring_only
+
+If set true then only bill recurring charges, not setup, usage, one time
+charges, etc.
+
+=item freq_override
+
+If set, then override the normal frequency and look for a part_pkg_discount
+to take at that frequency.
+
=item time
Bills the customer as if it were that time. Specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion functions. For example:
fees since the last billing. Setup charges may be charged. Not all package
plans support this feature (they tend to charge 0).
+=item no_usage_reset
+
+Prevent the resetting of usage limits during this call.
+
+=item no_commit
+
+Do not save the generated bill in the database. Useful with return_bill
+
+=item return_bill
+
+A list reference on which the generated bill(s) will be returned.
+
=item invoice_terms
Optional terms to be printed on this invoice. Otherwise, customer-specific
'time' => $invoice_time,
'check_freq' => $options{'check_freq'},
'stage' => 'pre-bill',
- );
+ )
+ unless $options{no_commit};
if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return $error;
}
'options' => \%options,
);
if ($error) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return $error;
}
my $postal_pkg = $self->charge_postal_fee();
if ( $postal_pkg && !ref( $postal_pkg ) ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return "can't charge postal invoice fee for customer ".
$self->custnum. ": $postal_pkg";
'options' => \%postal_options,
);
if ($error) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return $error;
}
}
$self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time);
unless ( ref( $listref_or_error ) ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return $listref_or_error;
}
#my $balance_adjustments =
# sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
+ warn "creating the new invoice\n" if $DEBUG;
#create the new invoice
my $cust_bill = new FS::cust_bill ( {
'custnum' => $self->custnum,
'billing_balance' => $balance,
'previous_balance' => $previous_balance,
'invoice_terms' => $options{'invoice_terms'},
+ 'cust_bill_pkg' => \@cust_bill_pkg,
} );
- $error = $cust_bill->insert;
+ $error = $cust_bill->insert unless $options{no_commit};
if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
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";
- }
- }
+ push @{$options{return_bill}}, $cust_bill if $options{return_bill};
} #foreach my $pass ( keys %cust_bill_pkg )
foreach my $hook ( @precommit_hooks ) {
eval {
&{$hook}; #($self) ?
- };
+ } unless $options{no_commit};
if ( $@ ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return "$@ running precommit hook $hook\n";
}
}
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
+
''; #no error
}
)
)
)
+ and !$options{recurring_only}
)
{
# XXX should this be a package event? probably. events are called
# at collection time at the moment, though...
$part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
- if $part_pkg->can('reset_usage');
+ if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
#don't want to reset usage just cause we want a line item??
#&& $part_pkg->pkgpart == $real_pkgpart;
'increment_next_bill' => $increment_next_bill,
'discounts' => \@discounts,
'real_pkgpart' => $real_pkgpart,
+ 'freq_override' => $options{freq_override} || '',
);
my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
+
+ # There may be some part_pkg for which this is wrong. Only those
+ # which can_discount are supported.
+
$recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
return "$@ running $method for $cust_pkg\n"
if ( $@ );
if ( $increment_next_bill ) {
- my $next_bill = $part_pkg->add_freq($sdate);
+ my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
return "unparsable frequency: ". $part_pkg->freq
if $next_bill == -1;
my $error = $cust_pkg->replace( $old_cust_pkg,
'options' => { $cust_pkg->options },
- );
+ )
+ unless $options{no_commit};
return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
if $error; #just in case
}
'details' => \@details,
'discounts' => \@discounts,
'hidden' => $part_pkg->hidden,
+ 'freq' => $part_pkg->freq,
};
if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) {
}
+=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;
+ '';
+
+}
+
+=item do_cust_event [ HASHREF | OPTION => VALUE ... ]
+
+Runs billing events; see L<FS::part_event> and the billing events web
+interface.
+
+If there is an error, returns the error, otherwise returns false.
+
+Options are passed as name-value pairs.
+
+Currently available options are:
+
+=over 4
+
+=item time
+
+Use this time when deciding when to print invoices and late notices on those invoices. The default is now. It is specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=item check_freq
+
+"1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
+
+=item stage
+
+"collect" (the default) or "pre-bill"
+
+=item quiet
+
+set true to surpress email card/ACH decline notices.
+
+=item debug
+
+Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
+
+=back
+=cut
+
+# =item payby
+#
+# allows for one time override of normal customer billing method
+
+# =item retry
+#
+# Retry card/echeck/LEC transactions even when not scheduled by invoice events.
+
+sub do_cust_event {
+ my( $self, %options ) = @_;
+ my $time = $options{'time'} || time;
+
+ #put below somehow?
+ 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;
+
+ $self->select_for_update; #mutex
+
+ if ( $DEBUG ) {
+ my $balance = $self->balance;
+ warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
+ }
+
+# if ( exists($options{'retry_card'}) ) {
+# carp 'retry_card option passed to collect is deprecated; use retry';
+# $options{'retry'} ||= $options{'retry_card'};
+# }
+# if ( exists($options{'retry'}) && $options{'retry'} ) {
+# my $error = $self->retry_realtime;
+# if ( $error ) {
+# $dbh->rollback if $oldAutoCommit;
+# return $error;
+# }
+# }
+
+ # false laziness w/pay_batch::import_results
+
+ my $due_cust_event = $self->due_cust_event(
+ 'debug' => ( $options{'debug'} || 0 ),
+ 'time' => $time,
+ 'check_freq' => $options{'check_freq'},
+ 'stage' => ( $options{'stage'} || 'collect' ),
+ );
+ unless( ref($due_cust_event) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $due_cust_event;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ #never want to roll back an event just because it or a different one
+ # returned an error
+ local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+
+ foreach my $cust_event ( @$due_cust_event ) {
+
+ #XXX lock event
+
+ #re-eval event conditions (a previous event could have changed things)
+ unless ( $cust_event->test_conditions( 'time' => $time ) ) {
+ #don't leave stray "new/locked" records around
+ my $error = $cust_event->delete;
+ return $error if $error;
+ next;
+ }
+
+ {
+ local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
+ if $options{'quiet'};
+ warn " running cust_event ". $cust_event->eventnum. "\n"
+ if $DEBUG > 1;
+
+ #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
+ if ( my $error = $cust_event->do_event() ) {
+ #XXX wtf is this? figure out a proper dealio with return value
+ #from do_event
+ return $error;
+ }
+ }
+
+ }
+
+ '';
+
+}
+
+=item due_cust_event [ HASHREF | OPTION => VALUE ... ]
+
+Inserts database records for and returns an ordered listref of new events due
+for this customer, as FS::cust_event objects (see L<FS::cust_event>). If no
+events are due, an empty listref is returned. If there is an error, returns a
+scalar error message.
+
+To actually run the events, call each event's test_condition method, and if
+still true, call the event's do_event method.
+
+Options are passed as a hashref or as a list of name-value pairs. Available
+options are:
+
+=over 4
+
+=item check_freq
+
+Search only for events of this check frequency (how often events of this type are checked); currently "1d" (daily, the default) and "1m" (monthly) are recognized.
+
+=item stage
+
+"collect" (the default) or "pre-bill"
+
+=item time
+
+"Current time" for the events.
+
+=item debug
+
+Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
+
+=item eventtable
+
+Only return events for the specified eventtable (by default, events of all eventtables are returned)
+
+=item objects
+
+Explicitly pass the objects to be tested (typically used with eventtable).
+
+=item testonly
+
+Set to true to return the objects, but not actually insert them into the
+database.
+
+=item discount_terms
+
+Returns a list of lengths for term discounts
+
+=cut
+
+sub _discount_pkgs_and_bill {
+my $self = shift;
+
+ my @cust_bill = $self->cust_bill;
+ my $cust_bill = pop @cust_bill;
+ return () unless $cust_bill && $cust_bill->owed;
+
+ my @where = ();
+ push @where, "cust_bill_pkg.invnum = ". $cust_bill->invnum;
+ push @where, "cust_bill_pkg.pkgpart_override IS NULL";
+ push @where, "part_pkg.freq = 1";
+ push @where, "(cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0)";
+ push @where, "(cust_pkg.susp IS NULL OR cust_pkg.susp = 0)";
+ push @where, "0<(SELECT count(*) FROM part_pkg_discount
+ WHERE part_pkg.pkgpart = part_pkg_discount.pkgpart)";
+ push @where,
+ "0=(SELECT count(*) FROM cust_bill_pkg_discount
+ WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_discount.billpkgnum)";
+
+ my $extra_sql = 'WHERE '. join(' AND ', @where);
+
+ my @cust_pkg =
+ qsearch({
+ 'table' => 'cust_pkg',
+ 'select' => "DISTINCT cust_pkg.*",
+ 'addl_from' => 'JOIN cust_bill_pkg USING(pkgnum) '.
+ 'JOIN part_pkg USING(pkgpart)',
+ 'hashref' => {},
+ 'extra_sql' => $extra_sql,
+ });
+
+ ($cust_bill, @cust_pkg);
+}
+
+sub _discountable_pkgs_at_term {
+ my ($term, @pkgs) = @_;
+ my $part_pkg = new FS::part_pkg { freq => $term - 1 };
+ grep { ( !$_->adjourn || $_->adjourn > $part_pkg->add_freq($_->bill) ) &&
+ ( !$_->expire || $_->expire > $part_pkg->add_freq($_->bill) )
+ }
+ @pkgs;
+}
+
+=item discount_terms
+
+Returns a list of lengths for term discounts
+
+=cut
+
+sub discount_terms {
+my $self = shift;
+
+ my %terms = ();
+
+ my @discount_pkgs = $self->_discount_pkgs_and_bill;
+ shift @discount_pkgs; #discard bill;
+
+ map { $terms{$_->months} = 1 }
+ grep { $_->months && $_->months > 1 }
+ map { $_->discount }
+ map { $_->part_pkg->part_pkg_discount }
+ @discount_pkgs;
+
+ return sort { $a <=> $b } keys %terms;
+
+}
+
+=back
+
+=item discount_term_values MONTHS
+
+Returns a list with credit, dollar amount saved, and total bill acheived
+by prepaying the most recent invoice for MONTHS.
+
+=cut
+
+sub discount_term_values {
+ my $self = shift;
+ my $term = shift;
+ warn "$me discount_term_values called with $term\n" if $DEBUG;
+
+ my %result = ();
+
+ my @packages = $self->_discount_pkgs_and_bill;
+ my $cust_bill = shift(@packages);
+ @packages = _discountable_pkgs_at_term( $term, @packages );
+ return () unless scalar(@packages);
+
+ $_->bill($_->last_bill) foreach @packages;
+ my @final = map { new FS::cust_pkg { $_->hash } } @packages;
+
+ my %options = (
+ 'recurring_only' => 1,
+ 'no_usage_reset' => 1,
+ 'no_commit' => 1,
+ );
+
+ my %params = (
+ 'return_bill' => [],
+ 'pkg_list' => \@packages,
+ 'time' => $cust_bill->_date,
+ );
+
+ my $error = $self->bill(%options, %params);
+ die $error if $error; # XXX think about this a bit more
+
+ my $credit = 0;
+ $credit += $_->charged foreach @{$params{return_bill}};
+ $credit = sprintf('%.2f', $credit);
+ warn "$me discount_term_values $term credit: $credit\n" if $DEBUG;
+
+ %params = (
+ 'return_bill' => [],
+ 'pkg_list' => \@packages,
+ 'time' => $packages[0]->part_pkg->add_freq($cust_bill->_date)
+ );
+
+ $error = $self->bill(%options, %params);
+ die $error if $error; # XXX think about this a bit more
+
+ my $next = 0;
+ $next += $_->charged foreach @{$params{return_bill}};
+ warn "$me discount_term_values $term next: $next\n" if $DEBUG;
+
+ %params = (
+ 'return_bill' => [],
+ 'pkg_list' => \@final,
+ 'time' => $cust_bill->_date,
+ 'freq_override' => $term,
+ );
+
+ $error = $self->bill(%options, %params);
+ die $error if $error; # XXX think about this a bit more
+
+ my $final = $self->balance - $credit;
+ $final += $_->charged foreach @{$params{return_bill}};
+ $final = sprintf('%.2f', $final);
+ warn "$me discount_term_values $term final: $final\n" if $DEBUG;
+
+ my $savings = sprintf('%.2f', $self->balance + ($term - 1) * $next - $final);
+
+ ( $credit, $savings, $final );
+
+}
+
+sub discount_terms_hash {
+ my $self = shift;
+
+ my %result = ();
+ my @terms = $self->discount_terms;
+ foreach my $term (@terms) {
+ my @result = $self->discount_term_values($term);
+ $result{$term} = [ @result ] if scalar(@result);
+ }
+
+ return %result;
+
+}
+
+=back
+
+=cut
+
+sub due_cust_event {
+ my $self = shift;
+ my %opt = ref($_[0]) ? %{ $_[0] } : @_;
+
+ #???
+ #my $DEBUG = $opt{'debug'}
+ local($DEBUG) = $opt{'debug'}
+ if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
+
+ warn "$me due_cust_event called with options ".
+ join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
+ if $DEBUG;
+
+ $opt{'time'} ||= time;
+
+ 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;
+
+ $self->select_for_update #mutex
+ unless $opt{testonly};
+
+ ###
+ # find possible events (initial search)
+ ###
+
+ my @cust_event = ();
+
+ my @eventtable = $opt{'eventtable'}
+ ? ( $opt{'eventtable'} )
+ : FS::part_event->eventtables_runorder;
+
+ my $check_freq = $opt{'check_freq'} || '1d';
+
+ foreach my $eventtable ( @eventtable ) {
+
+ my @objects;
+ if ( $opt{'objects'} ) {
+
+ @objects = @{ $opt{'objects'} };
+
+ } else {
+
+ #my @objects = $self->$eventtable(); # sub cust_main { @{ [ $self ] }; }
+ if ( $eventtable eq 'cust_main' ) {
+ @objects = ( $self );
+ } else {
+
+ my $cm_join =
+ "LEFT JOIN cust_main USING ( custnum )";
+
+ #some false laziness w/Cron::bill bill_where
+
+ my $join = FS::part_event_condition->join_conditions_sql( $eventtable);
+ my $where = FS::part_event_condition->where_conditions_sql($eventtable,
+ 'time'=>$opt{'time'},
+ );
+ $where = $where ? "AND $where" : '';
+
+ my $are_part_event =
+ "EXISTS ( SELECT 1 FROM part_event $join
+ WHERE check_freq = '$check_freq'
+ AND eventtable = '$eventtable'
+ AND ( disabled = '' OR disabled IS NULL )
+ $where
+ )
+ ";
+ #eofalse
+
+ @objects = $self->$eventtable(
+ 'addl_from' => $cm_join,
+ 'extra_sql' => " AND $are_part_event",
+ );
+ }
+
+ }
+
+ my @e_cust_event = ();
+
+ my $cross = "CROSS JOIN $eventtable";
+ $cross .= ' LEFT JOIN cust_main USING ( custnum )'
+ unless $eventtable eq 'cust_main';
+
+ foreach my $object ( @objects ) {
+
+ #this first search uses the condition_sql magic for optimization.
+ #the more possible events we can eliminate in this step the better
+
+ my $cross_where = '';
+ my $pkey = $object->primary_key;
+ $cross_where = "$eventtable.$pkey = ". $object->$pkey();
+
+ my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
+ my $extra_sql =
+ FS::part_event_condition->where_conditions_sql( $eventtable,
+ 'time'=>$opt{'time'}
+ );
+ my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
+
+ $extra_sql = "AND $extra_sql" if $extra_sql;
+
+ #here is the agent virtualization
+ $extra_sql .= " AND ( part_event.agentnum IS NULL
+ OR part_event.agentnum = ". $self->agentnum. ' )';
+
+ $extra_sql .= " $order";
+
+ warn "searching for events for $eventtable ". $object->$pkey. "\n"
+ if $opt{'debug'} > 2;
+ my @part_event = qsearch( {
+ 'debug' => ( $opt{'debug'} > 3 ? 1 : 0 ),
+ 'select' => 'part_event.*',
+ 'table' => 'part_event',
+ 'addl_from' => "$cross $join",
+ 'hashref' => { 'check_freq' => $check_freq,
+ 'eventtable' => $eventtable,
+ 'disabled' => '',
+ },
+ 'extra_sql' => "AND $cross_where $extra_sql",
+ } );
+
+ if ( $DEBUG > 2 ) {
+ my $pkey = $object->primary_key;
+ warn " ". scalar(@part_event).
+ " possible events found for $eventtable ". $object->$pkey(). "\n";
+ }
+
+ push @e_cust_event, map { $_->new_cust_event($object) } @part_event;
+
+ }
+
+ warn " ". scalar(@e_cust_event).
+ " subtotal possible cust events found for $eventtable\n"
+ if $DEBUG > 1;
+
+ push @cust_event, @e_cust_event;
+
+ }
+
+ warn " ". scalar(@cust_event).
+ " total possible cust events found in initial search\n"
+ if $DEBUG; # > 1;
+
+
+ ##
+ # test stage
+ ##
+
+ $opt{stage} ||= 'collect';
+ @cust_event =
+ grep { my $stage = $_->part_event->event_stage;
+ $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
+ }
+ @cust_event;
+
+ ##
+ # test conditions
+ ##
+
+ my %unsat = ();
+
+ @cust_event = grep $_->test_conditions( 'time' => $opt{'time'},
+ 'stats_hashref' => \%unsat ),
+ @cust_event;
+
+ warn " ". scalar(@cust_event). " cust events left satisfying conditions\n"
+ if $DEBUG; # > 1;
+
+ warn " invalid conditions not eliminated with condition_sql:\n".
+ join('', map " $_: ".$unsat{$_}."\n", keys %unsat )
+ if keys %unsat && $DEBUG; # > 1;
+
+ ##
+ # insert
+ ##
+
+ unless( $opt{testonly} ) {
+ foreach my $cust_event ( @cust_event ) {
+
+ my $error = $cust_event->insert();
+ if ( $error ) {
+ $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 apply_payments_and_credits [ OPTION => VALUE ... ]
return $total_unapplied_payments;
}
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
+
+=cut
+
1;