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