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