fix selfservice display when there are term discounts defined, looks like fallout...
[freeside.git] / FS / FS / cust_main / Billing.pm
1 package FS::cust_main::Billing;
2
3 use strict;
4 use vars qw( $conf $DEBUG $me );
5 use Carp;
6 use Data::Dumper;
7 use List::Util qw( min );
8 use FS::UID qw( dbh );
9 use FS::Record qw( qsearch qsearchs dbdef );
10 use FS::Misc::DateTime qw( day_end );
11 use Tie::RefHash;
12 use FS::cust_bill;
13 use FS::cust_bill_pkg;
14 use FS::cust_bill_pkg_display;
15 use FS::cust_bill_pay;
16 use FS::cust_credit_bill;
17 use FS::cust_tax_adjustment;
18 use FS::tax_rate;
19 use FS::tax_rate_location;
20 use FS::cust_bill_pkg_tax_location;
21 use FS::cust_bill_pkg_tax_rate_location;
22 use FS::part_event;
23 use FS::part_event_condition;
24 use FS::pkg_category;
25 use FS::FeeOrigin_Mixin;
26 use FS::Log;
27 use FS::TaxEngine;
28
29 # 1 is mostly method/subroutine entry and options
30 # 2 traces progress of some operations
31 # 3 is even more information including possibly sensitive data
32 $DEBUG = 0;
33 $me = '[FS::cust_main::Billing]';
34
35 install_callback FS::UID sub { 
36   $conf = new FS::Conf;
37   #yes, need it for stuff below (prolly should be cached)
38 };
39
40 =head1 NAME
41
42 FS::cust_main::Billing - Billing mixin for cust_main
43
44 =head1 SYNOPSIS
45
46 =head1 DESCRIPTION
47
48 These methods are available on FS::cust_main objects.
49
50 =head1 METHODS
51
52 =over 4
53
54 =item bill_and_collect 
55
56 Cancels and suspends any packages due, generates bills, applies payments and
57 credits, and applies collection events to run cards, send bills and notices,
58 etc.
59
60 By default, warns on errors and continues with the next operation (but see the
61 "fatal" flag below).
62
63 Options are passed as name-value pairs.  Currently available options are:
64
65 =over 4
66
67 =item time
68
69 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:
70
71  use Date::Parse;
72  ...
73  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
74
75 =item invoice_time
76
77 Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices.  Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
78
79 =item check_freq
80
81 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
82
83 =item resetup
84
85 If set true, re-charges setup fees.
86
87 =item fatal
88
89 If set any errors prevent subsequent operations from continusing.  If set
90 specifically to "return", returns the error (or false, if there is no error).
91 Any other true value causes errors to die.
92
93 =item debug
94
95 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)
96
97 =item job
98
99 Optional FS::queue entry to receive status updates.
100
101 =back
102
103 Options are passed to the B<bill> and B<collect> methods verbatim, so all
104 options of those methods are also available.
105
106 =cut
107
108 sub bill_and_collect {
109   my( $self, %options ) = @_;
110
111   my $log = FS::Log->new('FS::cust_main::Billing::bill_and_collect');
112   my %logopt = (object => $self);
113   $log->debug('start', %logopt);
114
115   my $error;
116
117   #$options{actual_time} not $options{time} because freeside-daily -d is for
118   #pre-printing invoices
119
120   $options{'actual_time'} ||= time;
121   my $job = $options{'job'};
122
123   my $actual_time = ( $conf->exists('next-bill-ignore-time')
124                         ? day_end( $options{actual_time} )
125                         : $options{actual_time}
126                     );
127
128   $job->update_statustext('0,cleaning expired packages') if $job;
129   $log->debug('canceling expired packages', %logopt);
130   $error = $self->cancel_expired_pkgs( $actual_time );
131   if ( $error ) {
132     $error = "Error expiring custnum ". $self->custnum. ": $error";
133     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
134     elsif ( $options{fatal}                                ) { die    $error; }
135     else                                                     { warn   $error; }
136   }
137
138   $log->debug('suspending adjourned packages', %logopt);
139   $error = $self->suspend_adjourned_pkgs( $actual_time );
140   if ( $error ) {
141     $error = "Error adjourning custnum ". $self->custnum. ": $error";
142     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
143     elsif ( $options{fatal}                                ) { die    $error; }
144     else                                                     { warn   $error; }
145   }
146
147   $log->debug('unsuspending resumed packages', %logopt);
148   $error = $self->unsuspend_resumed_pkgs( $actual_time );
149   if ( $error ) {
150     $error = "Error resuming custnum ".$self->custnum. ": $error";
151     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
152     elsif ( $options{fatal}                                ) { die    $error; }
153     else                                                     { warn   $error; }
154   }
155
156   $job->update_statustext('20,billing packages') if $job;
157   $log->debug('billing packages', %logopt);
158   $error = $self->bill( %options );
159   if ( $error ) {
160     $error = "Error billing custnum ". $self->custnum. ": $error";
161     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
162     elsif ( $options{fatal}                                ) { die    $error; }
163     else                                                     { warn   $error; }
164   }
165
166   $job->update_statustext('50,applying payments and credits') if $job;
167   $log->debug('applying payments and credits', %logopt);
168   $error = $self->apply_payments_and_credits;
169   if ( $error ) {
170     $error = "Error applying custnum ". $self->custnum. ": $error";
171     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
172     elsif ( $options{fatal}                                ) { die    $error; }
173     else                                                     { warn   $error; }
174   }
175
176   # In a batch tax environment, do not run collection if any pending 
177   # invoices were created.  Collection will run after the next tax batch.
178   my $tax = FS::TaxEngine->new;
179   if ( $tax->info->{batch} and 
180        qsearch('cust_bill', { custnum => $self->custnum, pending => 'Y' })
181      )
182   {
183     warn "skipped collection for custnum ".$self->custnum.
184          " due to pending invoices\n" if $DEBUG;
185   } elsif ( $conf->exists('cancelled_cust-noevents')
186              && ! $self->num_ncancelled_pkgs )
187   {
188     warn "skipped collection for custnum ".$self->custnum.
189          " because they have no active packages\n" if $DEBUG;
190   } else {
191     # run collection normally
192     $job->update_statustext('70,running collection events') if $job;
193     $log->debug('running collection events', %logopt);
194     $error = $self->collect( %options );
195     if ( $error ) {
196       $error = "Error collecting custnum ". $self->custnum. ": $error";
197       if    ($options{fatal} && $options{fatal} eq 'return') { return $error; }
198       elsif ($options{fatal}                               ) { die    $error; }
199       else                                                   { warn   $error; }
200     }
201   }
202
203   $job->update_statustext('100,finished') if $job;
204   $log->debug('finish', %logopt);
205
206   '';
207
208 }
209
210 sub cancel_expired_pkgs {
211   my ( $self, $time, %options ) = @_;
212   
213   my @cancel_pkgs = $self->ncancelled_pkgs( { 
214     'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
215   } );
216
217   my @errors = ();
218
219   CUST_PKG: foreach my $cust_pkg ( @cancel_pkgs ) {
220     my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
221     my $error;
222
223     if ( $cust_pkg->change_to_pkgnum ) {
224
225       my $new_pkg = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
226       if ( !$new_pkg ) {
227         push @errors, 'can\'t change pkgnum '.$cust_pkg->pkgnum.' to pkgnum '.
228                       $cust_pkg->change_to_pkgnum.'; not expiring';
229         next CUST_PKG;
230       }
231       $error = $cust_pkg->change( 'cust_pkg'        => $new_pkg,
232                                   'unprotect_svcs'  => 1 );
233       $error = '' if ref $error eq 'FS::cust_pkg';
234
235     } else { # just cancel it
236        $error = $cust_pkg->cancel($cpr ? ( 'reason'        => $cpr->reasonnum,
237                                            'reason_otaker' => $cpr->otaker,
238                                            'time'          => $time,
239                                          )
240                                        : ()
241                                  );
242     }
243     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
244   }
245
246   join(' / ', @errors);
247
248 }
249
250 sub suspend_adjourned_pkgs {
251   my ( $self, $time, %options ) = @_;
252   
253   my @susp_pkgs = $self->ncancelled_pkgs( {
254     'extra_sql' =>
255       " AND ( susp IS NULL OR susp = 0 )
256         AND (    ( bill    IS NOT NULL AND bill    != 0 AND bill    <  $time )
257               OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
258             )
259       ",
260   } );
261
262   #only because there's no SQL test for is_prepaid :/
263   @susp_pkgs = 
264     grep {     (    $_->part_pkg->is_prepaid
265                  && $_->bill
266                  && $_->bill < $time
267                )
268             || (    $_->adjourn
269                  && $_->adjourn <= $time
270                )
271            
272          }
273          @susp_pkgs;
274
275   my @errors = ();
276
277   foreach my $cust_pkg ( @susp_pkgs ) {
278     my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
279       if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
280     my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
281                                             'reason_otaker' => $cpr->otaker
282                                           )
283                                         : ()
284                                   );
285     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
286   }
287
288   join(' / ', @errors);
289
290 }
291
292 sub unsuspend_resumed_pkgs {
293   my ( $self, $time, %options ) = @_;
294   
295   my @unsusp_pkgs = $self->ncancelled_pkgs( { 
296     'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time "
297   } );
298
299   my @errors = ();
300
301   foreach my $cust_pkg ( @unsusp_pkgs ) {
302     my $error = $cust_pkg->unsuspend( 'time' => $time );
303     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
304   }
305
306   join(' / ', @errors);
307
308 }
309
310 =item bill OPTIONS
311
312 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
313 conjunction with the collect method by calling B<bill_and_collect>.
314
315 If there is an error, returns the error, otherwise returns false.
316
317 Options are passed as name-value pairs.  Currently available options are:
318
319 =over 4
320
321 =item resetup
322
323 If set true, re-charges setup fees.
324
325 =item recurring_only
326
327 If set true then only bill recurring charges, not setup, usage, one time
328 charges, etc.
329
330 =item freq_override
331
332 If set, then override the normal frequency and look for a part_pkg_discount
333 to take at that frequency.  This is appropriate only when the normal 
334 frequency for all packages is monthly, and is an error otherwise.  Use
335 C<pkg_list> to limit the set of packages included in billing.
336
337 =item time
338
339 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:
340
341  use Date::Parse;
342  ...
343  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
344
345 =item pkg_list
346
347 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
348
349  $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
350
351 =item not_pkgpart
352
353 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
354
355 =item no_prepaid
356
357 Do not bill prepaid packages.  Used by freeside-daily.
358
359 =item invoice_time
360
361 Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices.  Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
362
363 =item cancel
364
365 This boolean value informs the us that the package is being cancelled.  This
366 typically might mean not charging the normal recurring fee but only usage
367 fees since the last billing. Setup charges may be charged.  Not all package
368 plans support this feature (they tend to charge 0).
369
370 =item no_usage_reset
371
372 Prevent the resetting of usage limits during this call.
373
374 =item no_commit
375
376 Do not save the generated bill in the database.  Useful with return_bill
377
378 =item return_bill
379
380 A list reference on which the generated bill(s) will be returned.
381
382 =item estimate
383
384 Boolean value; indicates that this is an estimate rather than a "tax invoice".
385 This will be passed through to the tax engine, as online tax services 
386 sometimes need to know it for reporting purposes. Otherwise it has no effect.
387
388 =item invoice_terms
389
390 Optional terms to be printed on this invoice.  Otherwise, customer-specific
391 terms or the default terms are used.
392
393 =back
394
395 =cut
396
397 sub bill {
398   my( $self, %options ) = @_;
399
400   return '' if $self->complimentary eq 'Y';
401
402   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
403   my $log = FS::Log->new('FS::cust_main::Billing::bill');
404   my %logopt = (object => $self);
405
406   $log->debug('start', %logopt);
407   warn "$me bill customer ". $self->custnum. "\n"
408     if $DEBUG;
409
410   my $time = $options{'time'} || time;
411   my $invoice_time = $options{'invoice_time'} || $time;
412
413   my $cmp_time = ( $conf->exists('next-bill-ignore-time')
414                      ? day_end( $time )
415                      : $time
416                  );
417
418   $options{'not_pkgpart'} ||= {};
419   $options{'not_pkgpart'} = { map { $_ => 1 }
420                                   split(/\s*,\s*/, $options{'not_pkgpart'})
421                             }
422     unless ref($options{'not_pkgpart'});
423
424   local $SIG{HUP} = 'IGNORE';
425   local $SIG{INT} = 'IGNORE';
426   local $SIG{QUIT} = 'IGNORE';
427   local $SIG{TERM} = 'IGNORE';
428   local $SIG{TSTP} = 'IGNORE';
429   local $SIG{PIPE} = 'IGNORE';
430
431   my $oldAutoCommit = $FS::UID::AutoCommit;
432   local $FS::UID::AutoCommit = 0;
433   my $dbh = dbh;
434
435   $log->debug('acquiring lock', %logopt);
436   warn "$me acquiring lock on customer ". $self->custnum. "\n"
437     if $DEBUG;
438
439   $self->select_for_update; #mutex
440
441   $log->debug('running pre-bill events', %logopt);
442   warn "$me running pre-bill events for customer ". $self->custnum. "\n"
443     if $DEBUG;
444
445   my $error = $self->do_cust_event(
446     'debug'      => ( $options{'debug'} || 0 ),
447     'time'       => $invoice_time,
448     'check_freq' => $options{'check_freq'},
449     'stage'      => 'pre-bill',
450   )
451     unless $options{no_commit};
452   if ( $error ) {
453     $dbh->rollback if $oldAutoCommit && !$options{no_commit};
454     return $error;
455   }
456
457   $log->debug('done running pre-bill events', %logopt);
458   warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
459     if $DEBUG;
460
461   #keep auto-charge and non-auto-charge line items separate
462   my @passes = ( '', 'no_auto' );
463
464   my %cust_bill_pkg = map { $_ => [] } @passes;
465
466   ###
467   # find the packages which are due for billing, find out how much they are
468   # & generate invoice database.
469   ###
470
471   my %total_setup   = map { my $z = 0; $_ => \$z; } @passes;
472   my %total_recur   = map { my $z = 0; $_ => \$z; } @passes;
473
474   my @precommit_hooks = ();
475
476   $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ];  #param checks?
477   
478   my %tax_engines;
479   my $tax_is_batch = '';
480   foreach (@passes) {
481     $tax_engines{$_} = FS::TaxEngine->new(cust_main    => $self,
482                                           invoice_time => $invoice_time,
483                                           cancel       => $options{cancel},
484                                           estimate     => $options{estimate},
485                                          );
486     $tax_is_batch ||= $tax_engines{$_}->info->{batch};
487   }
488
489   foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
490
491     next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
492
493     my $part_pkg = $cust_pkg->part_pkg;
494
495     next if $options{'no_prepaid'} && $part_pkg->is_prepaid;
496
497     $log->debug('bill package '. $cust_pkg->pkgnum, %logopt);
498     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
499
500     #? to avoid use of uninitialized value errors... ?
501     $cust_pkg->setfield('bill', '')
502       unless defined($cust_pkg->bill);
503  
504     my $real_pkgpart = $cust_pkg->pkgpart;
505     my %hash = $cust_pkg->hash;
506
507     # we could implement this bit as FS::part_pkg::has_hidden, but we already
508     # suffer from performance issues
509     $options{has_hidden} = 0;
510     my @part_pkg = $part_pkg->self_and_bill_linked;
511     $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
512  
513     # if this package was changed from another package,
514     # and it hasn't been billed since then,
515     # and package balances are enabled,
516     if ( $cust_pkg->change_pkgnum
517         and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0)
518         and $cust_pkg->change_date <  $invoice_time
519       and $conf->exists('pkg-balances') )
520     {
521       # _transfer_balance will also create the appropriate credit
522       my @transfer_items = $self->_transfer_balance($cust_pkg);
523       # $part_pkg[0] is the "real" part_pkg
524       my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ? 
525                   'no_auto' : '';
526       push @{ $cust_bill_pkg{$pass} }, @transfer_items;
527       # treating this as recur, just because most charges are recur...
528       ${$total_recur{$pass}} += $_->recur foreach @transfer_items;
529
530       # currently not considering separate_bill here, as it's for 
531       # one-time charges only
532     }
533
534     foreach my $part_pkg ( @part_pkg ) {
535
536       $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
537
538       my $pass = '';
539       if ( $cust_pkg->separate_bill ) {
540         # if no_auto is also set, that's fine. we just need to not have
541         # invoices that are both auto and no_auto, and since the package
542         # gets an invoice all to itself, it will only be one or the other.
543         $pass = $cust_pkg->pkgnum;
544         if (!exists $cust_bill_pkg{$pass}) { # it may not exist yet
545           push @passes, $pass;
546           $total_setup{$pass} = do { my $z = 0; \$z };
547           $total_recur{$pass} = do { my $z = 0; \$z };
548           # it also needs its own tax context
549           $tax_engines{$pass} = FS::TaxEngine->new(
550                                   cust_main    => $self,
551                                   invoice_time => $invoice_time,
552                                   cancel       => $options{cancel},
553                                   estimate     => $options{estimate},
554                                 );
555           $cust_bill_pkg{$pass} = [];
556         }
557       } elsif ( ($cust_pkg->no_auto || $part_pkg->no_auto) ) {
558         $pass = 'no_auto';
559       }
560
561       my $next_bill = $cust_pkg->getfield('bill') || 0;
562       my $error;
563       # let this run once if this is the last bill upon cancellation
564       while ( $next_bill <= $cmp_time or $options{cancel} ) {
565         $error =
566           $self->_make_lines( 'part_pkg'            => $part_pkg,
567                               'cust_pkg'            => $cust_pkg,
568                               'precommit_hooks'     => \@precommit_hooks,
569                               'line_items'          => $cust_bill_pkg{$pass},
570                               'setup'               => $total_setup{$pass},
571                               'recur'               => $total_recur{$pass},
572                               'tax_engine'          => $tax_engines{$pass},
573                               'time'                => $time,
574                               'real_pkgpart'        => $real_pkgpart,
575                               'options'             => \%options,
576                             );
577
578         # Stop if anything goes wrong
579         last if $error;
580
581         # or if we're not incrementing the bill date.
582         last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
583
584         # or if we're letting it run only once
585         last if $options{cancel};
586
587         $next_bill = $cust_pkg->getfield('bill') || 0;
588
589         #stop if -o was passed to freeside-daily
590         last if $options{'one_recur'};
591       }
592       if ($error) {
593         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
594         return $error;
595       }
596
597     } #foreach my $part_pkg
598
599   } #foreach my $cust_pkg
600
601   foreach my $pass (@passes) { # keys %cust_bill_pkg )
602
603     my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
604
605     warn "$me billing pass $pass\n"
606            #.Dumper(\@cust_bill_pkg)."\n"
607       if $DEBUG > 2;
608
609     ###
610     # process fees
611     ###
612
613     my @pending_fees = FS::FeeOrigin_Mixin->by_cust($self->custnum,
614       hashref => { 'billpkgnum' => '' }
615     );
616     warn "$me found pending fees:\n".Dumper(\@pending_fees)."\n"
617       if @pending_fees and $DEBUG > 1;
618
619     # determine whether to generate an invoice
620     my $generate_bill = scalar(@cust_bill_pkg) > 0;
621
622     foreach my $fee (@pending_fees) {
623       $generate_bill = 1 unless $fee->nextbill;
624     }
625     
626     # don't create an invoice with no line items, or where the only line 
627     # items are fees that are supposed to be held until the next invoice
628     next if !$generate_bill;
629
630     # calculate fees...
631     my @fee_items;
632     foreach my $fee_origin (@pending_fees) {
633       my $part_fee = $fee_origin->part_fee;
634
635       # check whether the fee is applicable before doing anything expensive:
636       #
637       # if the fee def belongs to a different agent, don't charge the fee.
638       # event conditions should prevent this, but just in case they don't,
639       # skip the fee.
640       if ( $part_fee->agentnum and $part_fee->agentnum != $self->agentnum ) {
641         warn "tried to charge fee#".$part_fee->feepart .
642              " on customer#".$self->custnum." from a different agent.\n";
643         next;
644       }
645       # also skip if it's disabled
646       next if $part_fee->disabled eq 'Y';
647
648       # Decide which invoice to base the fee on.
649       my $cust_bill = $fee_origin->cust_bill;
650       if (!$cust_bill) {
651         # Then link it to the current invoice. This isn't the real cust_bill
652         # object that will be inserted--in particular there are no taxes yet.
653         # If you want to charge a fee on the total invoice amount including
654         # taxes, you have to put the fee on the next invoice.
655         $cust_bill = FS::cust_bill->new({
656             'custnum'       => $self->custnum,
657             'cust_bill_pkg' => \@cust_bill_pkg,
658             'charged'       => ${ $total_setup{$pass} } +
659                                ${ $total_recur{$pass} },
660         });
661
662         # If the origin is for a specific package, then only apply the fee to
663         # line items from that package.
664         if ( my $cust_pkg = $fee_origin->cust_pkg ) {
665           my @charge_fee_on_item;
666           my $charge_fee_on_amount = 0;
667           foreach (@cust_bill_pkg) {
668             if ($_->pkgnum == $cust_pkg->pkgnum) {
669               push @charge_fee_on_item, $_;
670               $charge_fee_on_amount += $_->setup + $_->recur;
671             }
672           }
673           $cust_bill->set('cust_bill_pkg', \@charge_fee_on_item);
674           $cust_bill->set('charged', $charge_fee_on_amount);
675         }
676
677       } # $cust_bill is now set
678       # calculate the fee
679       my $fee_item = $part_fee->lineitem($cust_bill) or next;
680       # link this so that we can clear the marker on inserting the line item
681       $fee_item->set('fee_origin', $fee_origin);
682       push @fee_items, $fee_item;
683
684     }
685     
686     # add fees to the invoice
687     foreach my $fee_item (@fee_items) {
688
689       push @cust_bill_pkg, $fee_item;
690       ${ $total_setup{$pass} } += $fee_item->setup;
691       ${ $total_recur{$pass} } += $fee_item->recur;
692
693       my $part_fee = $fee_item->part_fee;
694       my $fee_location = $self->ship_location; # I think?
695       
696       my $error = $tax_engines{''}->add_sale($fee_item);
697
698       return $error if $error;
699
700     }
701
702     # XXX implementation of fees is supposed to make this go away...
703     if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
704            !$conf->exists('postal_invoice-recurring_only')
705        )
706     {
707
708       my $postal_pkg = $self->charge_postal_fee();
709       if ( $postal_pkg && !ref( $postal_pkg ) ) {
710
711         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
712         return "can't charge postal invoice fee for customer ".
713           $self->custnum. ": $postal_pkg";
714
715       } elsif ( $postal_pkg ) {
716
717         my $real_pkgpart = $postal_pkg->pkgpart;
718         # we could implement this bit as FS::part_pkg::has_hidden, but we already
719         # suffer from performance issues
720         $options{has_hidden} = 0;
721         my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
722         $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
723
724         foreach my $part_pkg ( @part_pkg ) {
725           my %postal_options = %options;
726           delete $postal_options{cancel};
727           my $error =
728             $self->_make_lines( 'part_pkg'            => $part_pkg,
729                                 'cust_pkg'            => $postal_pkg,
730                                 'precommit_hooks'     => \@precommit_hooks,
731                                 'line_items'          => \@cust_bill_pkg,
732                                 'setup'               => $total_setup{$pass},
733                                 'recur'               => $total_recur{$pass},
734                                 'tax_engine'          => $tax_engines{$pass},
735                                 'time'                => $time,
736                                 'real_pkgpart'        => $real_pkgpart,
737                                 'options'             => \%postal_options,
738                               );
739           if ($error) {
740             $dbh->rollback if $oldAutoCommit && !$options{no_commit};
741             return $error;
742           }
743         }
744
745         # it's silly to have a zero value postal_pkg, but....
746         @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
747
748       }
749
750     }
751
752     #add tax adjustments
753     #XXX does this work with batch tax engines?
754     warn "adding tax adjustments...\n" if $DEBUG > 2;
755     foreach my $cust_tax_adjustment (
756       qsearch('cust_tax_adjustment', { 'custnum'    => $self->custnum,
757                                        'billpkgnum' => '',
758                                      }
759              )
760     ) {
761
762       my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
763
764       my $itemdesc = $cust_tax_adjustment->taxname;
765       $itemdesc = '' if $itemdesc eq 'Tax';
766
767       push @cust_bill_pkg, new FS::cust_bill_pkg {
768         'pkgnum'      => 0,
769         'setup'       => $tax,
770         'recur'       => 0,
771         'sdate'       => '',
772         'edate'       => '',
773         'itemdesc'    => $itemdesc,
774         'itemcomment' => $cust_tax_adjustment->comment,
775         'cust_tax_adjustment' => $cust_tax_adjustment,
776         #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
777       };
778
779     }
780
781     my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
782
783     my $balance = $self->balance;
784
785     my $previous_bill = qsearchs({ 'table'     => 'cust_bill',
786                                    'hashref'   => { custnum=>$self->custnum },
787                                    'extra_sql' => 'ORDER BY _date DESC LIMIT 1',
788                                 });
789     my $previous_balance =
790       $previous_bill
791         ? ( $previous_bill->billing_balance + $previous_bill->charged )
792         : 0;
793
794     $log->debug('creating the new invoice', %logopt);
795     warn "creating the new invoice\n" if $DEBUG;
796     #create the new invoice
797     my $cust_bill = new FS::cust_bill ( {
798       'custnum'             => $self->custnum,
799       '_date'               => $invoice_time,
800       'charged'             => $charged,
801       'billing_balance'     => $balance,
802       'previous_balance'    => $previous_balance,
803       'invoice_terms'       => $options{'invoice_terms'},
804       'cust_bill_pkg'       => \@cust_bill_pkg,
805       'pending'             => 'Y', # clear this after doing taxes
806     } );
807
808     if (!$options{no_commit}) {
809       # probably we ought to insert it as pending, and then rollback
810       # without ever un-pending it
811       $error = $cust_bill->insert;
812       if ( $error ) {
813         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
814         return "can't create invoice for customer #". $self->custnum. ": $error";
815       }
816
817     }
818
819     # calculate and append taxes
820     if ( ! $tax_is_batch) {
821       local $@;
822       my $arrayref = eval { $tax_engines{$pass}->calculate_taxes($cust_bill) };
823
824       if ( $@ ) {
825         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
826         return $@;
827       }
828
829       # or should this be in TaxEngine?
830       my $total_tax = 0;
831       foreach my $taxline ( @$arrayref ) {
832         $total_tax += $taxline->setup;
833         $taxline->set('invnum' => $cust_bill->invnum); # just to be sure
834         push @cust_bill_pkg, $taxline; # for return_bill
835
836         if (!$options{no_commit}) {
837           my $error = $taxline->insert;
838           if ( $error ) {
839             $dbh->rollback if $oldAutoCommit;
840             return $error;
841           }
842         }
843
844       }
845
846       # add tax to the invoice amount and finalize it
847       ${ $total_setup{$pass} } = sprintf('%.2f', ${ $total_setup{$pass} } + $total_tax);
848       $charged = sprintf('%.2f', $charged + $total_tax);
849       $cust_bill->set('charged', $charged);
850       $cust_bill->set('pending', '');
851
852       if (!$options{no_commit}) {
853         my $error = $cust_bill->replace;
854         if ( $error ) {
855           $dbh->rollback if $oldAutoCommit;
856           return $error;
857         }
858       }
859
860     } # if !$tax_is_batch
861       # if it IS batch, then we'll do all this in process_tax_batch
862
863     push @{$options{return_bill}}, $cust_bill if $options{return_bill};
864
865   } #foreach my $pass ( keys %cust_bill_pkg )
866
867   foreach my $hook ( @precommit_hooks ) { 
868     eval {
869       &{$hook}; #($self) ?
870     } unless $options{no_commit};
871     if ( $@ ) {
872       $dbh->rollback if $oldAutoCommit && !$options{no_commit};
873       return "$@ running precommit hook $hook\n";
874     }
875   }
876   
877   $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
878
879   ''; #no error
880 }
881
882 #discard bundled packages of 0 value
883 # XXX we should reconsider whether we even need this
884 sub _omit_zero_value_bundles {
885   my @in = @_;
886
887   my @out = ();
888   my @bundle = ();
889   my $discount_show_always = $conf->exists('discount-show-always');
890   my $show_this = 0;
891
892   # Sort @in the same way we do during invoice rendering, so we can identify
893   # bundles.  See FS::Template_Mixin::_items_nontax.
894   @in = sort { $a->pkgnum <=> $b->pkgnum        or
895                $a->sdate  <=> $b->sdate         or
896                ($a->pkgpart_override ? 0 : -1)  or
897                ($b->pkgpart_override ? 0 : 1)   or
898                $b->hidden cmp $a->hidden        or
899                $a->pkgpart_override <=> $b->pkgpart_override
900              } @in;
901
902   # this is a pack-and-deliver pattern. every time there's a cust_bill_pkg
903   # _without_ pkgpart_override, that's the start of the new bundle. if there's
904   # an existing bundle, and it contains a nonzero amount (or a zero amount 
905   # that's displayable anyway), push all line items in the bundle.
906   foreach my $cust_bill_pkg ( @in ) {
907
908     if (scalar(@bundle) and !$cust_bill_pkg->pkgpart_override) {
909       # ship out this bundle and reset it
910       if ( $show_this ) {
911         push @out, @bundle;
912       }
913       @bundle = ();
914       $show_this = 0;
915     }
916
917     # add this item to the current bundle
918     push @bundle, $cust_bill_pkg;
919
920     # determine if it makes the bundle displayable
921     if (   $cust_bill_pkg->setup > 0
922         or $cust_bill_pkg->recur > 0
923         or $cust_bill_pkg->setup_show_zero
924         or $cust_bill_pkg->recur_show_zero
925         or ($discount_show_always 
926           and scalar(@{ $cust_bill_pkg->get('discounts')}) 
927           )
928     ) {
929       $show_this++;
930     }
931   }
932
933   # last bundle
934   if ( $show_this) {
935     push @out, @bundle;
936   }
937
938   warn "  _omit_zero_value_bundles: ". scalar(@in).
939        '->'. scalar(@out). "\n" #. Dumper(@out). "\n"
940     if $DEBUG > 2;
941
942   @out;
943 }
944
945 sub _make_lines {
946   my ($self, %params) = @_;
947
948   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
949
950   my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
951   my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
952   my $cust_location = $cust_pkg->tax_location;
953   my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
954   my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
955   my $total_setup = $params{setup} or die "no setup accumulator specified";
956   my $total_recur = $params{recur} or die "no recur accumulator specified";
957   my $time = $params{'time'} or die "no time specified";
958   my (%options) = %{$params{options}};
959
960   my $tax_engine = $params{tax_engine};
961
962   if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
963     # this should never happen
964     die 'freq_override billing attempted on non-monthly package '.
965       $cust_pkg->pkgnum;
966   }
967
968   my $dbh = dbh;
969   my $real_pkgpart = $params{real_pkgpart};
970   my %hash = $cust_pkg->hash;
971   my $old_cust_pkg = new FS::cust_pkg \%hash;
972
973   my @details = ();
974   my $lineitems = 0;
975
976   $cust_pkg->pkgpart($part_pkg->pkgpart);
977
978   my $cmp_time = ( $conf->exists('next-bill-ignore-time')
979                      ? day_end( $time )
980                      : $time
981                  );
982
983   ###
984   # bill setup
985   ###
986
987   my $setup = 0;
988   my $unitsetup = 0;
989   my @setup_discounts = ();
990   my %setup_param = ( 'discounts'     => \@setup_discounts,
991                       'real_pkgpart'  => $params{real_pkgpart}
992                     );
993   my $setup_billed_currency = '';
994   my $setup_billed_amount = 0;
995   # Conditions for setting setup date and charging the setup fee:
996   # - this is not a recurring-only billing run
997   # - and the package is not currently being canceled
998   # - and, unless we're specifically told otherwise via 'resetup':
999   #   - it doesn't already HAVE a setup date
1000   #   - or a start date in the future
1001   #   - and it's not suspended
1002   #
1003   # The last condition used to check the "disable_setup_suspended" option but 
1004   # that's obsolete. We now never set the setup date on a suspended package.
1005   if (     ! $options{recurring_only}
1006        and ! $options{cancel}
1007        and ( $options{'resetup'}
1008              || ( ! $cust_pkg->setup
1009                   && ( ! $cust_pkg->start_date
1010                        || $cust_pkg->start_date <= $cmp_time
1011                      )
1012                   && ( ! $cust_pkg->getfield('susp') )
1013                 )
1014            )
1015      )
1016   {
1017     
1018     warn "    bill setup\n" if $DEBUG > 1;
1019
1020     unless ( $cust_pkg->waive_setup ) {
1021         $lineitems++;
1022
1023         $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
1024         return "$@ running calc_setup for $cust_pkg\n"
1025           if $@;
1026
1027         # Only increment unitsetup here if there IS a setup fee.
1028         # prorate_defer_bill may cause calc_setup on a setup-stage package
1029         # to return zero, and the setup fee to be charged later. (This happens
1030         # when it's first billed on the prorate cutoff day. RT#31276.)
1031         if ( $setup ) {
1032           $unitsetup = $cust_pkg->base_setup()
1033                          || $setup; #XXX uuh
1034         }
1035
1036         if ( $setup_param{'billed_currency'} ) {
1037           $setup_billed_currency = delete $setup_param{'billed_currency'};
1038           $setup_billed_amount   = delete $setup_param{'billed_amount'};
1039         }
1040     }
1041
1042     $cust_pkg->setfield('setup', $time)
1043       unless $cust_pkg->setup;
1044           #do need it, but it won't get written to the db
1045           #|| $cust_pkg->pkgpart != $real_pkgpart;
1046
1047     $cust_pkg->setfield('start_date', '')
1048       if $cust_pkg->start_date;
1049
1050   }
1051
1052   ###
1053   # bill recurring fee
1054   ### 
1055
1056   my $recur = 0;
1057   my $unitrecur = 0;
1058   my @recur_discounts = ();
1059   my $recur_billed_currency = '';
1060   my $recur_billed_amount = 0;
1061   my $sdate;
1062   if (     ! $cust_pkg->start_date
1063        and 
1064            ( ! $cust_pkg->susp
1065                || ( $cust_pkg->susp != $cust_pkg->order_date
1066                       && (    $cust_pkg->option('suspend_bill',1)
1067                            || ( $part_pkg->option('suspend_bill', 1)
1068                                  && ! $cust_pkg->option('no_suspend_bill',1)
1069                               )
1070                          )
1071                   )
1072                || $cust_pkg->is_status_delay_cancel
1073            )
1074        and
1075             ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time )
1076          || ( $part_pkg->plan eq 'voip_cdr'
1077                && $part_pkg->option('bill_every_call')
1078             )
1079          || $options{cancel}
1080   ) {
1081
1082     # XXX should this be a package event?  probably.  events are called
1083     # at collection time at the moment, though...
1084     $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
1085       if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
1086       #don't want to reset usage just cause we want a line item??
1087       #&& $part_pkg->pkgpart == $real_pkgpart;
1088
1089     warn "    bill recur\n" if $DEBUG > 1;
1090     $lineitems++;
1091
1092     # XXX shared with $recur_prog
1093     $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
1094              || $cust_pkg->setup
1095              || $time;
1096
1097     #over two params!  lets at least switch to a hashref for the rest...
1098     my $increment_next_bill = ( $part_pkg->freq ne '0'
1099                                 && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time
1100                                 && !$options{cancel}
1101                               );
1102     my %param = ( %setup_param,
1103                   'precommit_hooks'     => $precommit_hooks,
1104                   'increment_next_bill' => $increment_next_bill,
1105                   'discounts'           => \@recur_discounts,
1106                   'real_pkgpart'        => $real_pkgpart,
1107                   'freq_override'       => $options{freq_override} || '',
1108                   'setup_fee'           => 0,
1109                 );
1110
1111     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
1112
1113     # There may be some part_pkg for which this is wrong.  Only those
1114     # which can_discount are supported.
1115     # (the UI should prevent adding discounts to these at the moment)
1116
1117     warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
1118          " for pkgpart ". $cust_pkg->pkgpart.
1119          " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
1120       if $DEBUG > 2;
1121            
1122     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
1123     return "$@ running $method for $cust_pkg\n"
1124       if ( $@ );
1125
1126     if ($recur eq 'NOTHING') {
1127       # then calc_cancel (or calc_recur but that's not used) has declined to
1128       # generate a recurring lineitem at all. treat this as zero, but also 
1129       # try not to generate a lineitem.
1130       $recur = 0;
1131       $lineitems--;
1132     }
1133
1134     #base_cancel???
1135     $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
1136
1137     if ( $param{'billed_currency'} ) {
1138       $recur_billed_currency = delete $param{'billed_currency'};
1139       $recur_billed_amount   = delete $param{'billed_amount'};
1140     }
1141
1142     if ( $increment_next_bill ) {
1143
1144       my $next_bill;
1145
1146       if ( my $main_pkg = $cust_pkg->main_pkg ) {
1147         # supplemental package
1148         # to keep in sync with the main package, simulate billing at 
1149         # its frequency
1150         my $main_pkg_freq = $main_pkg->part_pkg->freq;
1151         my $supp_pkg_freq = $part_pkg->freq;
1152         if ( $supp_pkg_freq == 0 or $main_pkg_freq == 0 ) {
1153           # the UI should prevent setting up packages like this, but just
1154           # in case
1155           return "unable to calculate supplemental package period ratio";
1156         }
1157         my $ratio = $supp_pkg_freq / $main_pkg_freq;
1158         if ( $ratio == int($ratio) ) {
1159           # simple case: main package is X months, supp package is X*A months,
1160           # advance supp package to where the main package will be in A cycles.
1161           $next_bill = $sdate;
1162           for (1..$ratio) {
1163             $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
1164           }
1165         } else {
1166           # harder case: main package is X months, supp package is Y months.
1167           # advance supp package by Y months. then if they're within half a 
1168           # month of each other, resync them. this may result in the period
1169           # not being exactly Y months.
1170           $next_bill = $part_pkg->add_freq( $sdate, $supp_pkg_freq );
1171           my $main_next_bill = $main_pkg->bill;
1172           if ( $main_pkg->bill <= $time ) {
1173             # then the main package has not yet been billed on this cycle;
1174             # predict what its bill date will be.
1175             $main_next_bill =
1176               $part_pkg->add_freq( $main_next_bill, $main_pkg_freq );
1177           }
1178           if ( abs($main_next_bill - $next_bill) < 86400*15 ) {
1179             $next_bill = $main_next_bill;
1180           }
1181         }
1182
1183       } else {
1184       # the normal case, not a supplemental package
1185       $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
1186       return "unparsable frequency: ".
1187         ($options{freq_override} || $part_pkg->freq)
1188         if $next_bill == -1;
1189       }  
1190   
1191       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
1192       # only for figuring next bill date, nothing else, so, reset $sdate again
1193       # here
1194       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1195       #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
1196       $cust_pkg->last_bill($sdate);
1197
1198       $cust_pkg->setfield('bill', $next_bill );
1199
1200     }
1201
1202     if ( $param{'setup_fee'} ) {
1203       # Add an additional setup fee at the billing stage.
1204       # Used for prorate_defer_bill.
1205       $setup += $param{'setup_fee'};
1206       $unitsetup = $cust_pkg->base_setup();
1207       $lineitems++;
1208     }
1209
1210     if ( defined $param{'discount_left_setup'} ) {
1211         foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1212             $setup -= $discount_setup;
1213         }
1214     }
1215
1216   }
1217
1218   warn "\$setup is undefined" unless defined($setup);
1219   warn "\$recur is undefined" unless defined($recur);
1220   warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1221   
1222   ###
1223   # If there's line items, create em cust_bill_pkg records
1224   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1225   ###
1226
1227   if ( $lineitems ) {
1228
1229     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1230       # hmm.. and if just the options are modified in some weird price plan?
1231   
1232       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
1233         if $DEBUG >1;
1234   
1235       my $error = $cust_pkg->replace( $old_cust_pkg,
1236                                       'depend_jobnum'=>$options{depend_jobnum},
1237                                       'options' => { $cust_pkg->options },
1238                                     )
1239         unless $options{no_commit};
1240       return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1241         if $error; #just in case
1242     }
1243   
1244     $setup = sprintf( "%.2f", $setup );
1245     $recur = sprintf( "%.2f", $recur );
1246     if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1247       return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1248     }
1249     if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1250       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1251     }
1252
1253     my $discount_show_always = $conf->exists('discount-show-always')
1254                                && (    ($setup == 0 && scalar(@setup_discounts))
1255                                     || ($recur == 0 && scalar(@recur_discounts))
1256                                   );
1257
1258     if (    $setup != 0
1259          || $recur != 0
1260          || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1261          || $discount_show_always
1262          || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1263          || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1264        ) 
1265     {
1266
1267       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
1268         if $DEBUG > 1;
1269
1270       my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1271       if ( $DEBUG > 1 ) {
1272         warn "      adding customer package invoice detail: $_\n"
1273           foreach @cust_pkg_detail;
1274       }
1275       push @details, @cust_pkg_detail;
1276
1277       my $cust_bill_pkg = new FS::cust_bill_pkg {
1278         'pkgnum'                => $cust_pkg->pkgnum,
1279         'setup'                 => $setup,
1280         'unitsetup'             => $unitsetup,
1281         'setup_billed_currency' => $setup_billed_currency,
1282         'setup_billed_amount'   => $setup_billed_amount,
1283         'recur'                 => $recur,
1284         'unitrecur'             => $unitrecur,
1285         'recur_billed_currency' => $recur_billed_currency,
1286         'recur_billed_amount'   => $recur_billed_amount,
1287         'quantity'              => $cust_pkg->quantity,
1288         'details'               => \@details,
1289         'discounts'             => [ @setup_discounts, @recur_discounts ],
1290         'hidden'                => $part_pkg->hidden,
1291         'freq'                  => $part_pkg->freq,
1292       };
1293
1294       if ( $part_pkg->option('prorate_defer_bill',1) 
1295            and !$hash{last_bill} ) {
1296         # both preceding and upcoming, technically
1297         $cust_bill_pkg->sdate( $cust_pkg->setup );
1298         $cust_bill_pkg->edate( $cust_pkg->bill );
1299       } elsif ( $part_pkg->recur_temporality eq 'preceding' ) {
1300         $cust_bill_pkg->sdate( $hash{last_bill} );
1301         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
1302         $cust_bill_pkg->edate( $time ) if $options{cancel};
1303       } else { #if ( $part_pkg->recur_temporality eq 'upcoming' )
1304         $cust_bill_pkg->sdate( $sdate );
1305         $cust_bill_pkg->edate( $cust_pkg->bill );
1306         #$cust_bill_pkg->edate( $time ) if $options{cancel};
1307       }
1308
1309       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1310         unless $part_pkg->pkgpart == $real_pkgpart;
1311
1312       $$total_setup += $setup;
1313       $$total_recur += $recur;
1314
1315       ###
1316       # handle taxes
1317       ###
1318       
1319       my $error = $tax_engine->add_sale($cust_bill_pkg);
1320       return $error if $error;
1321
1322       $cust_bill_pkg->set_display(
1323         part_pkg     => $part_pkg,
1324         real_pkgpart => $real_pkgpart,
1325       );
1326
1327       push @$cust_bill_pkgs, $cust_bill_pkg;
1328
1329     } #if $setup != 0 || $recur != 0
1330       
1331   } #if $line_items
1332
1333   '';
1334
1335 }
1336
1337 =item _transfer_balance TO_PKG [ FROM_PKGNUM ]
1338
1339 Takes one argument, a cust_pkg object that is being billed.  This will 
1340 be called only if the package was created by a package change, and has
1341 not been billed since the package change, and package balance tracking
1342 is enabled.  The second argument can be an alternate package number to 
1343 transfer the balance from; this should not be used externally.
1344
1345 Transfers the balance from the previous package (now canceled) to
1346 this package, by crediting one package and creating an invoice item for 
1347 the other.  Inserts the credit and returns the invoice item (so that it 
1348 can be added to an invoice that's being built).
1349
1350 If the previous package was never billed, and was also created by a package
1351 change, then this will also transfer the balance from I<its> previous 
1352 package, and so on, until reaching a package that either has been billed
1353 or was not created by a package change.
1354
1355 =cut
1356
1357 my $balance_transfer_reason;
1358
1359 sub _transfer_balance {
1360   my $self = shift;
1361   my $cust_pkg = shift;
1362   my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
1363   my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
1364
1365   my @transfers;
1366
1367   # if $from_pkg is not the first package in the chain, and it was never 
1368   # billed, walk back
1369   if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
1370     @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
1371   }
1372
1373   my $prev_balance = $self->balance_pkgnum($from_pkgnum);
1374   if ( $prev_balance != 0 ) {
1375     $balance_transfer_reason ||= FS::reason->new_or_existing(
1376       'reason' => 'Package balance transfer',
1377       'type'   => 'Internal adjustment',
1378       'class'  => 'R'
1379     );
1380
1381     my $credit = FS::cust_credit->new({
1382         'custnum'   => $self->custnum,
1383         'amount'    => abs($prev_balance),
1384         'reasonnum' => $balance_transfer_reason->reasonnum,
1385         '_date'     => $cust_pkg->change_date,
1386     });
1387
1388     my $cust_bill_pkg = FS::cust_bill_pkg->new({
1389         'setup'     => 0,
1390         'recur'     => abs($prev_balance),
1391         #'sdate'     => $from_pkg->last_bill, # not sure about this
1392         #'edate'     => $cust_pkg->change_date,
1393         'itemdesc'  => $self->mt('Previous Balance, [_1]',
1394                                  $from_pkg->part_pkg->pkg),
1395     });
1396
1397     if ( $prev_balance > 0 ) {
1398       # credit the old package, charge the new one
1399       $credit->set('pkgnum', $from_pkgnum);
1400       $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
1401     } else {
1402       # the reverse
1403       $credit->set('pkgnum', $cust_pkg->pkgnum);
1404       $cust_bill_pkg->set('pkgnum', $from_pkgnum);
1405     }
1406     my $error = $credit->insert;
1407     die "error transferring package balance from #".$from_pkgnum.
1408         " to #".$cust_pkg->pkgnum.": $error\n" if $error;
1409
1410     push @transfers, $cust_bill_pkg;
1411   } # $prev_balance != 0
1412
1413   return @transfers;
1414 }
1415
1416 #### vestigial code ####
1417
1418 =item handle_taxes TAXLISTHASH CUST_BILL_PKG [ OPTIONS ]
1419
1420 This is _handle_taxes.  It's called once for each cust_bill_pkg generated
1421 from _make_lines.
1422
1423 TAXLISTHASH is a hashref shared across the entire invoice.  It looks like 
1424 this:
1425 {
1426   'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
1427   'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
1428 }
1429
1430 'cust_main_county' can also be 'tax_rate'.  The first object in the array
1431 is always the cust_main_county or tax_rate identified by the key.
1432
1433 That "..." is a list of FS::cust_bill_pkg objects that will be fed to 
1434 the 'taxline' method to calculate the amount of the tax.  This doesn't
1435 happen until calculate_taxes, though.
1436
1437 OPTIONS may include:
1438 - part_item: a part_pkg or part_fee object to be used as the package/fee 
1439   definition.
1440 - location: a cust_location to be used as the billing location.
1441 - cancel: true if this package is being billed on cancellation.  This 
1442   allows tax to be calculated on usage charges only.
1443
1444 If not supplied, part_item will be inferred from the pkgnum or feepart of the
1445 cust_bill_pkg, and location from the pkgnum (or, for fees, the invnum and 
1446 the customer's default service location).
1447
1448 This method will also calculate exemptions for any taxes that apply to the
1449 line item (using the C<set_exemptions> method of L<FS::cust_bill_pkg>) and
1450 attach them.  This is the only place C<set_exemptions> is called in normal
1451 invoice processing.
1452
1453 =cut
1454
1455 sub _handle_taxes {
1456   my $self = shift;
1457   my $taxlisthash = shift;
1458   my $cust_bill_pkg = shift;
1459   my %options = @_;
1460
1461   # at this point I realize that we have enough information to infer all this
1462   # stuff, instead of passing around giant honking argument lists
1463   my $location = $options{location} || $cust_bill_pkg->tax_location;
1464   my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
1465
1466   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1467
1468   return if ( $self->payby eq 'COMP' ); #dubious
1469
1470   if ( $conf->config('enable_taxproducts')
1471        && ( scalar($part_item->part_pkg_taxoverride)
1472             || $part_item->has_taxproduct
1473           )
1474      )
1475     {
1476
1477     # EXTERNAL TAX RATES (via tax_rate)
1478     my %cust_bill_pkg = ();
1479     my %taxes = ();
1480
1481     my @classes;
1482     my $usage = $cust_bill_pkg->usage || 0;
1483     push @classes, $cust_bill_pkg->usage_classes if $usage;
1484     push @classes, 'setup' if $cust_bill_pkg->setup and !$options{cancel};
1485     push @classes, 'recur' if ($cust_bill_pkg->recur - $usage)
1486         and !$options{cancel};
1487     # that's better--probably don't even need $options{cancel} now
1488     # but leave it for now, just to be safe
1489     #
1490     # About $options{cancel}: This protects against charging per-line or
1491     # per-customer or other flat-rate surcharges on a package that's being
1492     # billed on cancellation (which is an out-of-cycle bill and should only
1493     # have usage charges).  See RT#29443.
1494
1495     # customer exemption is now handled in the 'taxline' method
1496     #my $exempt = $conf->exists('cust_class-tax_exempt')
1497     #               ? ( $self->cust_class ? $self->cust_class->tax : '' )
1498     #               : $self->tax;
1499     # standardize this just to be sure
1500     #$exempt = ($exempt eq 'Y') ? 'Y' : '';
1501     #
1502     #if ( !$exempt ) {
1503
1504     unless (exists $taxes{''}) {
1505       # unsure what purpose this serves, but last time I deleted something
1506       # from here just because I didn't see the point, it actually did
1507       # something important.
1508       my $err_or_ref = $self->_gather_taxes($part_item, '', $location);
1509       return $err_or_ref unless ref($err_or_ref);
1510       $taxes{''} = $err_or_ref;
1511     }
1512
1513     # NO DISINTEGRATIONS.
1514     # my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1515     #
1516     # do not call taxline() with any argument except the entire set of
1517     # cust_bill_pkgs on an invoice that are eligible for the tax.
1518
1519     # only calculate exemptions once for each tax rate, even if it's used
1520     # for multiple classes
1521     my %tax_seen = ();
1522  
1523     foreach my $class (@classes) {
1524       my $err_or_ref = $self->_gather_taxes($part_item, $class, $location);
1525       return $err_or_ref unless ref($err_or_ref);
1526       my @taxes = @$err_or_ref;
1527
1528       next if !@taxes;
1529
1530       foreach my $tax ( @taxes ) {
1531
1532         my $tax_id = ref( $tax ). ' '. $tax->taxnum;
1533         # $taxlisthash: keys are tax identifiers ('FS::tax_rate 123456').
1534         # Values are arrayrefs, first the tax object (cust_main_county
1535         # or tax_rate), then the cust_bill_pkg object that the 
1536         # tax applies to, then the tax class (setup, recur, usage classnum).
1537         $taxlisthash->{ $tax_id } ||= [ $tax ];
1538         push @{ $taxlisthash->{ $tax_id  } }, $cust_bill_pkg, $class;
1539
1540         # determine any exemptions that apply
1541         if (!$tax_seen{$tax_id}) {
1542           $cust_bill_pkg->set_exemptions( $tax, custnum => $self->custnum );
1543           $tax_seen{$tax_id} = 1;
1544         }
1545
1546         # tax on tax will be done later, when we actually create the tax
1547         # line items
1548
1549       }
1550     }
1551
1552   } else {
1553
1554     # INTERNAL TAX RATES (cust_main_county)
1555
1556     # We fetch taxes even if the customer is completely exempt,
1557     # because we need to record that fact.
1558
1559     my @loc_keys = qw( district city county state country );
1560     my %taxhash = map { $_ => $location->$_ } @loc_keys;
1561
1562     $taxhash{'taxclass'} = $part_item->taxclass;
1563
1564     warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
1565
1566     my @taxes = (); # entries are cust_main_county objects
1567     my %taxhash_elim = %taxhash;
1568     my @elim = qw( district city county state );
1569     do { 
1570
1571       #first try a match with taxclass
1572       @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1573
1574       if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1575         #then try a match without taxclass
1576         my %no_taxclass = %taxhash_elim;
1577         $no_taxclass{ 'taxclass' } = '';
1578         @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1579       }
1580
1581       $taxhash_elim{ shift(@elim) } = '';
1582
1583     } while ( !scalar(@taxes) && scalar(@elim) );
1584
1585     foreach (@taxes) {
1586       my $tax_id = 'cust_main_county '.$_->taxnum;
1587       $taxlisthash->{$tax_id} ||= [ $_ ];
1588       $cust_bill_pkg->set_exemptions($_, custnum => $self->custnum);
1589       push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
1590     }
1591
1592   }
1593   '';
1594 }
1595
1596 =item _gather_taxes PART_ITEM CLASS CUST_LOCATION
1597
1598 Internal method used with vendor-provided tax tables.  PART_ITEM is a part_pkg
1599 or part_fee (which will define the tax eligibility of the product), CLASS is
1600 'setup', 'recur', null, or a C<usage_class> number, and CUST_LOCATION is the 
1601 location where the service was provided (or billed, depending on 
1602 configuration).  Returns an arrayref of L<FS::tax_rate> objects that 
1603 can apply to this line item.
1604
1605 =cut
1606
1607 sub _gather_taxes {
1608   my $self = shift;
1609   my $part_item = shift;
1610   my $class = shift;
1611   my $location = shift;
1612
1613   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1614
1615   my $geocode = $location->geocode('cch');
1616
1617   [ $part_item->tax_rates('cch', $geocode, $class) ]
1618
1619 }
1620
1621 #### end vestigial code ####
1622
1623 =item collect [ HASHREF | OPTION => VALUE ... ]
1624
1625 (Attempt to) collect money for this customer's outstanding invoices (see
1626 L<FS::cust_bill>).  Usually used after the bill method.
1627
1628 Actions are now triggered by billing events; see L<FS::part_event> and the
1629 billing events web interface.  Old-style invoice events (see
1630 L<FS::part_bill_event>) have been deprecated.
1631
1632 If there is an error, returns the error, otherwise returns false.
1633
1634 Options are passed as name-value pairs.
1635
1636 Currently available options are:
1637
1638 =over 4
1639
1640 =item invoice_time
1641
1642 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.
1643
1644 =item retry
1645
1646 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1647
1648 =item check_freq
1649
1650 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1651
1652 =item quiet
1653
1654 set true to surpress email card/ACH decline notices.
1655
1656 =item debug
1657
1658 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)
1659
1660 =back
1661
1662 # =item payby
1663 #
1664 # allows for one time override of normal customer billing method
1665
1666 =cut
1667
1668 sub collect {
1669   my( $self, %options ) = @_;
1670
1671   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1672
1673   my $invoice_time = $options{'invoice_time'} || time;
1674
1675   #put below somehow?
1676   local $SIG{HUP} = 'IGNORE';
1677   local $SIG{INT} = 'IGNORE';
1678   local $SIG{QUIT} = 'IGNORE';
1679   local $SIG{TERM} = 'IGNORE';
1680   local $SIG{TSTP} = 'IGNORE';
1681   local $SIG{PIPE} = 'IGNORE';
1682
1683   my $oldAutoCommit = $FS::UID::AutoCommit;
1684   local $FS::UID::AutoCommit = 0;
1685   my $dbh = dbh;
1686
1687   $self->select_for_update; #mutex
1688
1689   if ( $DEBUG ) {
1690     my $balance = $self->balance;
1691     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1692   }
1693
1694   if ( exists($options{'retry_card'}) ) {
1695     carp 'retry_card option passed to collect is deprecated; use retry';
1696     $options{'retry'} ||= $options{'retry_card'};
1697   }
1698   if ( exists($options{'retry'}) && $options{'retry'} ) {
1699     my $error = $self->retry_realtime;
1700     if ( $error ) {
1701       $dbh->rollback if $oldAutoCommit;
1702       return $error;
1703     }
1704   }
1705
1706   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1707
1708   #never want to roll back an event just because it returned an error
1709   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1710
1711   $self->do_cust_event(
1712     'debug'      => ( $options{'debug'} || 0 ),
1713     'time'       => $invoice_time,
1714     'check_freq' => $options{'check_freq'},
1715     'stage'      => 'collect',
1716   );
1717
1718 }
1719
1720 =item retry_realtime
1721
1722 Schedules realtime / batch  credit card / electronic check / LEC billing
1723 events for for retry.  Useful if card information has changed or manual
1724 retry is desired.  The 'collect' method must be called to actually retry
1725 the transaction.
1726
1727 Implementation details: For either this customer, or for each of this
1728 customer's open invoices, changes the status of the first "done" (with
1729 statustext error) realtime processing event to "failed".
1730
1731 =cut
1732
1733 sub retry_realtime {
1734   my $self = shift;
1735
1736   local $SIG{HUP} = 'IGNORE';
1737   local $SIG{INT} = 'IGNORE';
1738   local $SIG{QUIT} = 'IGNORE';
1739   local $SIG{TERM} = 'IGNORE';
1740   local $SIG{TSTP} = 'IGNORE';
1741   local $SIG{PIPE} = 'IGNORE';
1742
1743   my $oldAutoCommit = $FS::UID::AutoCommit;
1744   local $FS::UID::AutoCommit = 0;
1745   my $dbh = dbh;
1746
1747   #a little false laziness w/due_cust_event (not too bad, really)
1748
1749   # I guess this is always as of now?
1750   my $join = FS::part_event_condition->join_conditions_sql('', 'time' => time);
1751   my $order = FS::part_event_condition->order_conditions_sql;
1752   my $mine = 
1753   '( '
1754    . join ( ' OR ' , map { 
1755     my $cust_join = FS::part_event->eventtables_cust_join->{$_} || '';
1756     my $custnum = FS::part_event->eventtables_custnum->{$_};
1757     "( part_event.eventtable = " . dbh->quote($_) 
1758     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key 
1759     . " from $_ $cust_join"
1760     . " where $custnum = " . dbh->quote( $self->custnum ) . "))" ;
1761    } FS::part_event->eventtables)
1762    . ') ';
1763
1764   #here is the agent virtualization
1765   my $agent_virt = " (    part_event.agentnum IS NULL
1766                        OR part_event.agentnum = ". $self->agentnum. ' )';
1767
1768   #XXX this shouldn't be hardcoded, actions should declare it...
1769   my @realtime_events = qw(
1770     cust_bill_realtime_card
1771     cust_bill_realtime_check
1772     cust_bill_realtime_lec
1773     cust_bill_batch
1774   );
1775
1776   my $is_realtime_event =
1777     ' part_event.action IN ( '.
1778         join(',', map "'$_'", @realtime_events ).
1779     ' ) ';
1780
1781   my $batch_or_statustext =
1782     "( part_event.action = 'cust_bill_batch'
1783        OR ( statustext IS NOT NULL AND statustext != '' )
1784      )";
1785
1786
1787   my @cust_event = qsearch({
1788     'table'     => 'cust_event',
1789     'select'    => 'cust_event.*',
1790     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1791     'hashref'   => { 'status' => 'done' },
1792     'extra_sql' => " AND $batch_or_statustext ".
1793                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1794   });
1795
1796   my %seen_invnum = ();
1797   foreach my $cust_event (@cust_event) {
1798
1799     #max one for the customer, one for each open invoice
1800     my $cust_X = $cust_event->cust_X;
1801     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1802                           ? $cust_X->invnum
1803                           : 0
1804                         }++
1805          or $cust_event->part_event->eventtable eq 'cust_bill'
1806             && ! $cust_X->owed;
1807
1808     my $error = $cust_event->retry;
1809     if ( $error ) {
1810       $dbh->rollback if $oldAutoCommit;
1811       return "error scheduling event for retry: $error";
1812     }
1813
1814   }
1815
1816   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1817   '';
1818
1819 }
1820
1821 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1822
1823 Runs billing events; see L<FS::part_event> and the billing events web
1824 interface.
1825
1826 If there is an error, returns the error, otherwise returns false.
1827
1828 Options are passed as name-value pairs.
1829
1830 Currently available options are:
1831
1832 =over 4
1833
1834 =item time
1835
1836 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.
1837
1838 =item check_freq
1839
1840 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1841
1842 =item stage
1843
1844 "collect" (the default) or "pre-bill"
1845
1846 =item quiet
1847  
1848 set true to surpress email card/ACH decline notices.
1849
1850 =item debug
1851
1852 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)
1853
1854 =back
1855 =cut
1856
1857 # =item payby
1858 #
1859 # allows for one time override of normal customer billing method
1860
1861 # =item retry
1862 #
1863 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1864
1865 sub do_cust_event {
1866   my( $self, %options ) = @_;
1867
1868   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1869
1870   my $time = $options{'time'} || time;
1871
1872   #put below somehow?
1873   local $SIG{HUP} = 'IGNORE';
1874   local $SIG{INT} = 'IGNORE';
1875   local $SIG{QUIT} = 'IGNORE';
1876   local $SIG{TERM} = 'IGNORE';
1877   local $SIG{TSTP} = 'IGNORE';
1878   local $SIG{PIPE} = 'IGNORE';
1879
1880   my $oldAutoCommit = $FS::UID::AutoCommit;
1881   local $FS::UID::AutoCommit = 0;
1882   my $dbh = dbh;
1883
1884   $self->select_for_update; #mutex
1885
1886   if ( $DEBUG ) {
1887     my $balance = $self->balance;
1888     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1889   }
1890
1891 #  if ( exists($options{'retry_card'}) ) {
1892 #    carp 'retry_card option passed to collect is deprecated; use retry';
1893 #    $options{'retry'} ||= $options{'retry_card'};
1894 #  }
1895 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
1896 #    my $error = $self->retry_realtime;
1897 #    if ( $error ) {
1898 #      $dbh->rollback if $oldAutoCommit;
1899 #      return $error;
1900 #    }
1901 #  }
1902
1903   # false laziness w/pay_batch::import_results
1904
1905   my $due_cust_event = $self->due_cust_event(
1906     'debug'      => ( $options{'debug'} || 0 ),
1907     'time'       => $time,
1908     'check_freq' => $options{'check_freq'},
1909     'stage'      => ( $options{'stage'} || 'collect' ),
1910   );
1911   unless( ref($due_cust_event) ) {
1912     $dbh->rollback if $oldAutoCommit;
1913     return $due_cust_event;
1914   }
1915
1916   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1917   #never want to roll back an event just because it or a different one
1918   # returned an error
1919   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1920
1921   foreach my $cust_event ( @$due_cust_event ) {
1922
1923     #XXX lock event
1924     
1925     #re-eval event conditions (a previous event could have changed things)
1926     unless ( $cust_event->test_conditions ) {
1927       #don't leave stray "new/locked" records around
1928       my $error = $cust_event->delete;
1929       return $error if $error;
1930       next;
1931     }
1932
1933     {
1934       local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1935         if $options{'quiet'};
1936       warn "  running cust_event ". $cust_event->eventnum. "\n"
1937         if $DEBUG > 1;
1938
1939       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1940       if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1941         #XXX wtf is this?  figure out a proper dealio with return value
1942         #from do_event
1943         return $error;
1944       }
1945     }
1946
1947   }
1948
1949   '';
1950
1951 }
1952
1953 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
1954
1955 Inserts database records for and returns an ordered listref of new events due
1956 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
1957 events are due, an empty listref is returned.  If there is an error, returns a
1958 scalar error message.
1959
1960 To actually run the events, call each event's test_condition method, and if
1961 still true, call the event's do_event method.
1962
1963 Options are passed as a hashref or as a list of name-value pairs.  Available
1964 options are:
1965
1966 =over 4
1967
1968 =item check_freq
1969
1970 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.
1971
1972 =item stage
1973
1974 "collect" (the default) or "pre-bill"
1975
1976 =item time
1977
1978 "Current time" for the events.
1979
1980 =item debug
1981
1982 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)
1983
1984 =item eventtable
1985
1986 Only return events for the specified eventtable (by default, events of all eventtables are returned)
1987
1988 =item objects
1989
1990 Explicitly pass the objects to be tested (typically used with eventtable).
1991
1992 =item testonly
1993
1994 Set to true to return the objects, but not actually insert them into the
1995 database.
1996
1997 =back
1998
1999 =cut
2000
2001 sub due_cust_event {
2002   my $self = shift;
2003   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
2004
2005   #???
2006   #my $DEBUG = $opt{'debug'}
2007   $opt{'debug'} ||= 0; # silence some warnings
2008   local($DEBUG) = $opt{'debug'}
2009     if $opt{'debug'} > $DEBUG;
2010   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2011
2012   warn "$me due_cust_event called with options ".
2013        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
2014     if $DEBUG;
2015
2016   $opt{'time'} ||= time;
2017
2018   local $SIG{HUP} = 'IGNORE';
2019   local $SIG{INT} = 'IGNORE';
2020   local $SIG{QUIT} = 'IGNORE';
2021   local $SIG{TERM} = 'IGNORE';
2022   local $SIG{TSTP} = 'IGNORE';
2023   local $SIG{PIPE} = 'IGNORE';
2024
2025   my $oldAutoCommit = $FS::UID::AutoCommit;
2026   local $FS::UID::AutoCommit = 0;
2027   my $dbh = dbh;
2028
2029   $self->select_for_update #mutex
2030     unless $opt{testonly};
2031
2032   ###
2033   # find possible events (initial search)
2034   ###
2035   
2036   my @cust_event = ();
2037
2038   my @eventtable = $opt{'eventtable'}
2039                      ? ( $opt{'eventtable'} )
2040                      : FS::part_event->eventtables_runorder;
2041
2042   my $check_freq = $opt{'check_freq'} || '1d';
2043
2044   foreach my $eventtable ( @eventtable ) {
2045
2046     my @objects;
2047     if ( $opt{'objects'} ) {
2048
2049       @objects = @{ $opt{'objects'} };
2050
2051     } elsif ( $eventtable eq 'cust_main' ) {
2052
2053       @objects = ( $self );
2054
2055     } else {
2056
2057       my $cm_join = " LEFT JOIN cust_main USING ( custnum )";
2058       # linkage not needed here because FS::cust_main->$eventtable will 
2059       # already supply it
2060
2061       #some false laziness w/Cron::bill bill_where
2062
2063       my $join  = FS::part_event_condition->join_conditions_sql( $eventtable,
2064         'time' => $opt{'time'});
2065       my $where = FS::part_event_condition->where_conditions_sql($eventtable,
2066         'time'=>$opt{'time'},
2067       );
2068       $where = $where ? "AND $where" : '';
2069
2070       my $are_part_event = 
2071       "EXISTS ( SELECT 1 FROM part_event $join
2072         WHERE check_freq = '$check_freq'
2073         AND eventtable = '$eventtable'
2074         AND ( disabled = '' OR disabled IS NULL )
2075         $where
2076         )
2077       ";
2078       #eofalse
2079
2080       @objects = $self->$eventtable(
2081         'addl_from' => $cm_join,
2082         'extra_sql' => " AND $are_part_event",
2083       );
2084     } # if ( !$opt{objects} and $eventtable ne 'cust_main' )
2085
2086     my @e_cust_event = ();
2087
2088     my $linkage = FS::part_event->eventtables_cust_join->{$eventtable} || '';
2089
2090     my $cross = "CROSS JOIN $eventtable $linkage";
2091     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2092       unless $eventtable eq 'cust_main';
2093
2094     foreach my $object ( @objects ) {
2095
2096       #this first search uses the condition_sql magic for optimization.
2097       #the more possible events we can eliminate in this step the better
2098
2099       my $cross_where = '';
2100       my $pkey = $object->primary_key;
2101       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2102
2103       my $join = FS::part_event_condition->join_conditions_sql( $eventtable,
2104         'time' => $opt{'time'});
2105       my $extra_sql =
2106         FS::part_event_condition->where_conditions_sql( $eventtable,
2107                                                         'time'=>$opt{'time'}
2108                                                       );
2109       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2110
2111       $extra_sql = "AND $extra_sql" if $extra_sql;
2112
2113       #here is the agent virtualization
2114       $extra_sql .= " AND (    part_event.agentnum IS NULL
2115                             OR part_event.agentnum = ". $self->agentnum. ' )';
2116
2117       $extra_sql .= " $order";
2118
2119       warn "searching for events for $eventtable ". $object->$pkey. "\n"
2120         if $opt{'debug'} > 2;
2121       my @part_event = qsearch( {
2122         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
2123         'select'    => 'part_event.*',
2124         'table'     => 'part_event',
2125         'addl_from' => "$cross $join",
2126         'hashref'   => { 'check_freq' => $check_freq,
2127                          'eventtable' => $eventtable,
2128                          'disabled'   => '',
2129                        },
2130         'extra_sql' => "AND $cross_where $extra_sql",
2131       } );
2132
2133       if ( $DEBUG > 2 ) {
2134         my $pkey = $object->primary_key;
2135         warn "      ". scalar(@part_event).
2136              " possible events found for $eventtable ". $object->$pkey(). "\n";
2137       }
2138
2139       push @e_cust_event, map { 
2140         $_->new_cust_event($object, 'time' => $opt{'time'}) 
2141       } @part_event;
2142
2143     }
2144
2145     warn "    ". scalar(@e_cust_event).
2146          " subtotal possible cust events found for $eventtable\n"
2147       if $DEBUG > 1;
2148
2149     push @cust_event, @e_cust_event;
2150
2151   }
2152
2153   warn "  ". scalar(@cust_event).
2154        " total possible cust events found in initial search\n"
2155     if $DEBUG; # > 1;
2156
2157
2158   ##
2159   # test stage
2160   ##
2161
2162   $opt{stage} ||= 'collect';
2163   @cust_event =
2164     grep { my $stage = $_->part_event->event_stage;
2165            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
2166          }
2167          @cust_event;
2168
2169   ##
2170   # test conditions
2171   ##
2172   
2173   my %unsat = ();
2174
2175   @cust_event = grep $_->test_conditions( 'stats_hashref' => \%unsat ),
2176                      @cust_event;
2177
2178   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
2179     if $DEBUG; # > 1;
2180
2181   warn "    invalid conditions not eliminated with condition_sql:\n".
2182        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
2183     if keys %unsat && $DEBUG; # > 1;
2184
2185   ##
2186   # insert
2187   ##
2188
2189   unless( $opt{testonly} ) {
2190     foreach my $cust_event ( @cust_event ) {
2191
2192       my $error = $cust_event->insert();
2193       if ( $error ) {
2194         $dbh->rollback if $oldAutoCommit;
2195         return $error;
2196       }
2197                                        
2198     }
2199   }
2200
2201   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2202
2203   ##
2204   # return
2205   ##
2206
2207   warn "  returning events: ". Dumper(@cust_event). "\n"
2208     if $DEBUG > 2;
2209
2210   \@cust_event;
2211
2212 }
2213
2214 =item apply_payments_and_credits [ OPTION => VALUE ... ]
2215
2216 Applies unapplied payments and credits.
2217 Payments with the no_auto_apply flag set will not be applied.
2218
2219 In most cases, this new method should be used in place of sequential
2220 apply_payments and apply_credits methods.
2221
2222 A hash of optional arguments may be passed.  Currently "manual" is supported.
2223 If true, a payment receipt is sent instead of a statement when
2224 'payment_receipt_email' configuration option is set.
2225
2226 If there is an error, returns the error, otherwise returns false.
2227
2228 =cut
2229
2230 sub apply_payments_and_credits {
2231   my( $self, %options ) = @_;
2232
2233   local $SIG{HUP} = 'IGNORE';
2234   local $SIG{INT} = 'IGNORE';
2235   local $SIG{QUIT} = 'IGNORE';
2236   local $SIG{TERM} = 'IGNORE';
2237   local $SIG{TSTP} = 'IGNORE';
2238   local $SIG{PIPE} = 'IGNORE';
2239
2240   my $oldAutoCommit = $FS::UID::AutoCommit;
2241   local $FS::UID::AutoCommit = 0;
2242   my $dbh = dbh;
2243
2244   $self->select_for_update; #mutex
2245
2246   foreach my $cust_bill ( $self->open_cust_bill ) {
2247     my $error = $cust_bill->apply_payments_and_credits(%options);
2248     if ( $error ) {
2249       $dbh->rollback if $oldAutoCommit;
2250       return "Error applying: $error";
2251     }
2252   }
2253
2254   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2255   ''; #no error
2256
2257 }
2258
2259 =item apply_credits OPTION => VALUE ...
2260
2261 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2262 to outstanding invoice balances in chronological order (or reverse
2263 chronological order if the I<order> option is set to B<newest>) and returns the
2264 value of any remaining unapplied credits available for refund (see
2265 L<FS::cust_refund>).
2266
2267 Dies if there is an error.
2268
2269 =cut
2270
2271 sub apply_credits {
2272   my $self = shift;
2273   my %opt = @_;
2274
2275   local $SIG{HUP} = 'IGNORE';
2276   local $SIG{INT} = 'IGNORE';
2277   local $SIG{QUIT} = 'IGNORE';
2278   local $SIG{TERM} = 'IGNORE';
2279   local $SIG{TSTP} = 'IGNORE';
2280   local $SIG{PIPE} = 'IGNORE';
2281
2282   my $oldAutoCommit = $FS::UID::AutoCommit;
2283   local $FS::UID::AutoCommit = 0;
2284   my $dbh = dbh;
2285
2286   $self->select_for_update; #mutex
2287
2288   unless ( $self->total_unapplied_credits ) {
2289     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2290     return 0;
2291   }
2292
2293   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2294       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2295
2296   my @invoices = $self->open_cust_bill;
2297   @invoices = sort { $b->_date <=> $a->_date } @invoices
2298     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2299
2300   if ( $conf->exists('pkg-balances') ) {
2301     # limit @credits to those w/ a pkgnum grepped from $self
2302     my %pkgnums = ();
2303     foreach my $i (@invoices) {
2304       foreach my $li ( $i->cust_bill_pkg ) {
2305         $pkgnums{$li->pkgnum} = 1;
2306       }
2307     }
2308     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2309   }
2310
2311   my $credit;
2312
2313   foreach my $cust_bill ( @invoices ) {
2314
2315     if ( !defined($credit) || $credit->credited == 0) {
2316       $credit = pop @credits or last;
2317     }
2318
2319     my $owed;
2320     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2321       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2322     } else {
2323       $owed = $cust_bill->owed;
2324     }
2325     unless ( $owed > 0 ) {
2326       push @credits, $credit;
2327       next;
2328     }
2329
2330     my $amount = min( $credit->credited, $owed );
2331     
2332     my $cust_credit_bill = new FS::cust_credit_bill ( {
2333       'crednum' => $credit->crednum,
2334       'invnum'  => $cust_bill->invnum,
2335       'amount'  => $amount,
2336     } );
2337     $cust_credit_bill->pkgnum( $credit->pkgnum )
2338       if $conf->exists('pkg-balances') && $credit->pkgnum;
2339     my $error = $cust_credit_bill->insert;
2340     if ( $error ) {
2341       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2342       die $error;
2343     }
2344     
2345     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2346
2347   }
2348
2349   my $total_unapplied_credits = $self->total_unapplied_credits;
2350
2351   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2352
2353   return $total_unapplied_credits;
2354 }
2355
2356 =item apply_payments  [ OPTION => VALUE ... ]
2357
2358 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2359 to outstanding invoice balances in chronological order.
2360 Payments with the no_auto_apply flag set will not be applied.
2361
2362  #and returns the value of any remaining unapplied payments.
2363
2364 A hash of optional arguments may be passed.  Currently "manual" is supported.
2365 If true, a payment receipt is sent instead of a statement when
2366 'payment_receipt_email' configuration option is set.
2367
2368 Dies if there is an error.
2369
2370 =cut
2371
2372 sub apply_payments {
2373   my( $self, %options ) = @_;
2374
2375   local $SIG{HUP} = 'IGNORE';
2376   local $SIG{INT} = 'IGNORE';
2377   local $SIG{QUIT} = 'IGNORE';
2378   local $SIG{TERM} = 'IGNORE';
2379   local $SIG{TSTP} = 'IGNORE';
2380   local $SIG{PIPE} = 'IGNORE';
2381
2382   my $oldAutoCommit = $FS::UID::AutoCommit;
2383   local $FS::UID::AutoCommit = 0;
2384   my $dbh = dbh;
2385
2386   $self->select_for_update; #mutex
2387
2388   #return 0 unless
2389
2390   my @payments = grep { !$_->no_auto_apply } $self->unapplied_cust_pay;
2391
2392   my @invoices = $self->open_cust_bill;
2393
2394   if ( $conf->exists('pkg-balances') ) {
2395     # limit @payments to those w/ a pkgnum grepped from $self
2396     my %pkgnums = ();
2397     foreach my $i (@invoices) {
2398       foreach my $li ( $i->cust_bill_pkg ) {
2399         $pkgnums{$li->pkgnum} = 1;
2400       }
2401     }
2402     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2403   }
2404
2405   my $payment;
2406
2407   foreach my $cust_bill ( @invoices ) {
2408
2409     if ( !defined($payment) || $payment->unapplied == 0 ) {
2410       $payment = pop @payments or last;
2411     }
2412
2413     my $owed;
2414     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2415       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2416     } else {
2417       $owed = $cust_bill->owed;
2418     }
2419     unless ( $owed > 0 ) {
2420       push @payments, $payment;
2421       next;
2422     }
2423
2424     my $amount = min( $payment->unapplied, $owed );
2425
2426     my $cbp = {
2427       'paynum' => $payment->paynum,
2428       'invnum' => $cust_bill->invnum,
2429       'amount' => $amount,
2430     };
2431     $cbp->{_date} = $payment->_date 
2432         if $options{'manual'} && $options{'backdate_application'};
2433     my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2434     $cust_bill_pay->pkgnum( $payment->pkgnum )
2435       if $conf->exists('pkg-balances') && $payment->pkgnum;
2436     my $error = $cust_bill_pay->insert(%options);
2437     if ( $error ) {
2438       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2439       die $error;
2440     }
2441
2442     redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2443
2444   }
2445
2446   my $total_unapplied_payments = $self->total_unapplied_payments;
2447
2448   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2449
2450   return $total_unapplied_payments;
2451 }
2452
2453 =back
2454
2455 =head1 FLOW
2456
2457   bill_and_collect
2458
2459     cancel_expired_pkgs
2460     suspend_adjourned_pkgs
2461     unsuspend_resumed_pkgs
2462
2463     bill
2464       (do_cust_event pre-bill)
2465       _make_lines
2466       _omit_zero_value_bundles
2467       calculate_taxes
2468
2469     apply_payments_and_credits
2470     collect
2471       do_cust_event
2472         due_cust_event
2473
2474 =head1 BUGS
2475
2476 =head1 SEE ALSO
2477
2478 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
2479
2480 =cut
2481
2482 1;