fix discounts on one-time charges, #28956
[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                       'real_pkgpart' => $params{real_pkgpart}
1092                     );
1093   # Conditions for setting setup date and charging the setup fee:
1094   # - this is not a recurring-only billing run
1095   # - and the package is not currently being canceled
1096   # - and, unless we're specifically told otherwise via 'resetup':
1097   #   - it doesn't already HAVE a setup date
1098   #   - or a start date in the future
1099   #   - and it's not suspended
1100   #
1101   # The last condition used to check the "disable_setup_suspended" option but 
1102   # that's obsolete. We now never set the setup date on a suspended package.
1103   if (     ! $options{recurring_only}
1104        and ! $options{cancel}
1105        and ( $options{'resetup'}
1106              || ( ! $cust_pkg->setup
1107                   && ( ! $cust_pkg->start_date
1108                        || $cust_pkg->start_date <= $cmp_time
1109                      )
1110                   && ( ! $cust_pkg->getfield('susp') )
1111               )
1112            )
1113      )
1114   {
1115     
1116     warn "    bill setup\n" if $DEBUG > 1;
1117
1118     unless ( $cust_pkg->waive_setup ) {
1119         $lineitems++;
1120
1121         $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
1122         return "$@ running calc_setup for $cust_pkg\n"
1123           if $@;
1124
1125         $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
1126     }
1127
1128     $cust_pkg->setfield('setup', $time)
1129       unless $cust_pkg->setup;
1130           #do need it, but it won't get written to the db
1131           #|| $cust_pkg->pkgpart != $real_pkgpart;
1132
1133     $cust_pkg->setfield('start_date', '')
1134       if $cust_pkg->start_date;
1135
1136   }
1137
1138   ###
1139   # bill recurring fee
1140   ### 
1141
1142   my $recur = 0;
1143   my $unitrecur = 0;
1144   my @recur_discounts = ();
1145   my $sdate;
1146   if (     ! $cust_pkg->start_date
1147        and 
1148            ( ! $cust_pkg->susp
1149                || ( $cust_pkg->susp != $cust_pkg->order_date
1150                       && (    $cust_pkg->option('suspend_bill',1)
1151                            || ( $part_pkg->option('suspend_bill', 1)
1152                                  && ! $cust_pkg->option('no_suspend_bill',1)
1153                               )
1154                          )
1155                   )
1156            )
1157        and
1158             ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time )
1159          || ( $part_pkg->plan eq 'voip_cdr'
1160                && $part_pkg->option('bill_every_call')
1161             )
1162          || $options{cancel}
1163   ) {
1164
1165     # XXX should this be a package event?  probably.  events are called
1166     # at collection time at the moment, though...
1167     $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
1168       if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
1169       #don't want to reset usage just cause we want a line item??
1170       #&& $part_pkg->pkgpart == $real_pkgpart;
1171
1172     warn "    bill recur\n" if $DEBUG > 1;
1173     $lineitems++;
1174
1175     # XXX shared with $recur_prog
1176     $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
1177              || $cust_pkg->setup
1178              || $time;
1179
1180     #over two params!  lets at least switch to a hashref for the rest...
1181     my $increment_next_bill = ( $part_pkg->freq ne '0'
1182                                 && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time
1183                                 && !$options{cancel}
1184                               );
1185     my %param = ( %setup_param,
1186                   'precommit_hooks'     => $precommit_hooks,
1187                   'increment_next_bill' => $increment_next_bill,
1188                   'discounts'           => \@recur_discounts,
1189                   'real_pkgpart'        => $real_pkgpart,
1190                   'freq_override'       => $options{freq_override} || '',
1191                   'setup_fee'           => 0,
1192                 );
1193
1194     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
1195
1196     # There may be some part_pkg for which this is wrong.  Only those
1197     # which can_discount are supported.
1198     # (the UI should prevent adding discounts to these at the moment)
1199
1200     warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
1201          " for pkgpart ". $cust_pkg->pkgpart.
1202          " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
1203       if $DEBUG > 2;
1204            
1205     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
1206     return "$@ running $method for $cust_pkg\n"
1207       if ( $@ );
1208
1209     #base_cancel???
1210     $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
1211
1212     if ( $increment_next_bill ) {
1213
1214       my $next_bill;
1215
1216       if ( my $main_pkg = $cust_pkg->main_pkg ) {
1217         # supplemental package
1218         # to keep in sync with the main package, simulate billing at 
1219         # its frequency
1220         my $main_pkg_freq = $main_pkg->part_pkg->freq;
1221         my $supp_pkg_freq = $part_pkg->freq;
1222         my $ratio = $supp_pkg_freq / $main_pkg_freq;
1223         if ( $ratio != int($ratio) ) {
1224           # the UI should prevent setting up packages like this, but just
1225           # in case
1226           return "supplemental package period is not an integer multiple of main  package period";
1227         }
1228         $next_bill = $sdate;
1229         for (1..$ratio) {
1230           $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
1231         }
1232
1233       } else {
1234         # the normal case
1235       $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
1236       return "unparsable frequency: ". $part_pkg->freq
1237         if $next_bill == -1;
1238       }  
1239   
1240       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
1241       # only for figuring next bill date, nothing else, so, reset $sdate again
1242       # here
1243       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1244       #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
1245       $cust_pkg->last_bill($sdate);
1246
1247       $cust_pkg->setfield('bill', $next_bill );
1248
1249     }
1250
1251     if ( $param{'setup_fee'} ) {
1252       # Add an additional setup fee at the billing stage.
1253       # Used for prorate_defer_bill.
1254       $setup += $param{'setup_fee'};
1255       $unitsetup += $param{'setup_fee'};
1256       $lineitems++;
1257     }
1258
1259     if ( defined $param{'discount_left_setup'} ) {
1260         foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1261             $setup -= $discount_setup;
1262         }
1263     }
1264
1265   }
1266
1267   warn "\$setup is undefined" unless defined($setup);
1268   warn "\$recur is undefined" unless defined($recur);
1269   warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1270   
1271   ###
1272   # If there's line items, create em cust_bill_pkg records
1273   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1274   ###
1275
1276   if ( $lineitems ) {
1277
1278     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1279       # hmm.. and if just the options are modified in some weird price plan?
1280   
1281       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
1282         if $DEBUG >1;
1283   
1284       my $error = $cust_pkg->replace( $old_cust_pkg,
1285                                       'depend_jobnum'=>$options{depend_jobnum},
1286                                       'options' => { $cust_pkg->options },
1287                                     )
1288         unless $options{no_commit};
1289       return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1290         if $error; #just in case
1291     }
1292   
1293     $setup = sprintf( "%.2f", $setup );
1294     $recur = sprintf( "%.2f", $recur );
1295     if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1296       return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1297     }
1298     if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1299       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1300     }
1301
1302     my $discount_show_always = $conf->exists('discount-show-always')
1303                                && (    ($setup == 0 && scalar(@setup_discounts))
1304                                     || ($recur == 0 && scalar(@recur_discounts))
1305                                   );
1306
1307     if (    $setup != 0
1308          || $recur != 0
1309          || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1310          || $discount_show_always
1311          || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1312          || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1313        ) 
1314     {
1315
1316       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
1317         if $DEBUG > 1;
1318
1319       my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1320       if ( $DEBUG > 1 ) {
1321         warn "      adding customer package invoice detail: $_\n"
1322           foreach @cust_pkg_detail;
1323       }
1324       push @details, @cust_pkg_detail;
1325
1326       my $cust_bill_pkg = new FS::cust_bill_pkg {
1327         'pkgnum'    => $cust_pkg->pkgnum,
1328         'setup'     => $setup,
1329         'unitsetup' => $unitsetup,
1330         'recur'     => $recur,
1331         'unitrecur' => $unitrecur,
1332         'quantity'  => $cust_pkg->quantity,
1333         'details'   => \@details,
1334         'discounts' => [ @setup_discounts, @recur_discounts ],
1335         'hidden'    => $part_pkg->hidden,
1336         'freq'      => $part_pkg->freq,
1337       };
1338
1339       if ( $part_pkg->option('prorate_defer_bill',1) 
1340            and !$hash{last_bill} ) {
1341         # both preceding and upcoming, technically
1342         $cust_bill_pkg->sdate( $cust_pkg->setup );
1343         $cust_bill_pkg->edate( $cust_pkg->bill );
1344       } elsif ( $part_pkg->recur_temporality eq 'preceding' ) {
1345         $cust_bill_pkg->sdate( $hash{last_bill} );
1346         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
1347         $cust_bill_pkg->edate( $time ) if $options{cancel};
1348       } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) {
1349         $cust_bill_pkg->sdate( $sdate );
1350         $cust_bill_pkg->edate( $cust_pkg->bill );
1351         #$cust_bill_pkg->edate( $time ) if $options{cancel};
1352       }
1353
1354       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1355         unless $part_pkg->pkgpart == $real_pkgpart;
1356
1357       $$total_setup += $setup;
1358       $$total_recur += $recur;
1359
1360       ###
1361       # handle taxes
1362       ###
1363
1364       my $error = $self->_handle_taxes( $taxlisthash, $cust_bill_pkg,
1365         cancel => $options{cancel} );
1366       return $error if $error;
1367
1368       $cust_bill_pkg->set_display(
1369         part_pkg     => $part_pkg,
1370         real_pkgpart => $real_pkgpart,
1371       );
1372
1373       push @$cust_bill_pkgs, $cust_bill_pkg;
1374
1375     } #if $setup != 0 || $recur != 0
1376       
1377   } #if $line_items
1378
1379   '';
1380
1381 }
1382
1383 =item _transfer_balance TO_PKG [ FROM_PKGNUM ]
1384
1385 Takes one argument, a cust_pkg object that is being billed.  This will 
1386 be called only if the package was created by a package change, and has
1387 not been billed since the package change, and package balance tracking
1388 is enabled.  The second argument can be an alternate package number to 
1389 transfer the balance from; this should not be used externally.
1390
1391 Transfers the balance from the previous package (now canceled) to
1392 this package, by crediting one package and creating an invoice item for 
1393 the other.  Inserts the credit and returns the invoice item (so that it 
1394 can be added to an invoice that's being built).
1395
1396 If the previous package was never billed, and was also created by a package
1397 change, then this will also transfer the balance from I<its> previous 
1398 package, and so on, until reaching a package that either has been billed
1399 or was not created by a package change.
1400
1401 =cut
1402
1403 my $balance_transfer_reason;
1404
1405 sub _transfer_balance {
1406   my $self = shift;
1407   my $cust_pkg = shift;
1408   my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
1409   my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
1410
1411   my @transfers;
1412
1413   # if $from_pkg is not the first package in the chain, and it was never 
1414   # billed, walk back
1415   if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
1416     @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
1417   }
1418
1419   my $prev_balance = $self->balance_pkgnum($from_pkgnum);
1420   if ( $prev_balance != 0 ) {
1421     $balance_transfer_reason ||= FS::reason->new_or_existing(
1422       'reason' => 'Package balance transfer',
1423       'type'   => 'Internal adjustment',
1424       'class'  => 'R'
1425     );
1426
1427     my $credit = FS::cust_credit->new({
1428         'custnum'   => $self->custnum,
1429         'amount'    => abs($prev_balance),
1430         'reasonnum' => $balance_transfer_reason->reasonnum,
1431         '_date'     => $cust_pkg->change_date,
1432     });
1433
1434     my $cust_bill_pkg = FS::cust_bill_pkg->new({
1435         'setup'     => 0,
1436         'recur'     => abs($prev_balance),
1437         #'sdate'     => $from_pkg->last_bill, # not sure about this
1438         #'edate'     => $cust_pkg->change_date,
1439         'itemdesc'  => $self->mt('Previous Balance, [_1]',
1440                                  $from_pkg->part_pkg->pkg),
1441     });
1442
1443     if ( $prev_balance > 0 ) {
1444       # credit the old package, charge the new one
1445       $credit->set('pkgnum', $from_pkgnum);
1446       $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
1447     } else {
1448       # the reverse
1449       $credit->set('pkgnum', $cust_pkg->pkgnum);
1450       $cust_bill_pkg->set('pkgnum', $from_pkgnum);
1451     }
1452     my $error = $credit->insert;
1453     die "error transferring package balance from #".$from_pkgnum.
1454         " to #".$cust_pkg->pkgnum.": $error\n" if $error;
1455
1456     push @transfers, $cust_bill_pkg;
1457   } # $prev_balance != 0
1458
1459   return @transfers;
1460 }
1461
1462 =item handle_taxes TAXLISTHASH CUST_BILL_PKG [ OPTIONS ]
1463
1464 This is _handle_taxes.  It's called once for each cust_bill_pkg generated
1465 from _make_lines.
1466
1467 TAXLISTHASH is a hashref shared across the entire invoice.  It looks like 
1468 this:
1469 {
1470   'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
1471   'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
1472 }
1473
1474 'cust_main_county' can also be 'tax_rate'.  The first object in the array
1475 is always the cust_main_county or tax_rate identified by the key.
1476
1477 That "..." is a list of FS::cust_bill_pkg objects that will be fed to 
1478 the 'taxline' method to calculate the amount of the tax.  This doesn't
1479 happen until calculate_taxes, though.
1480
1481 OPTIONS may include:
1482 - part_item: a part_pkg or part_fee object to be used as the package/fee 
1483   definition.
1484 - location: a cust_location to be used as the billing location.
1485 - cancel: true if this package is being billed on cancellation.  This 
1486   allows tax to be calculated on usage charges only.
1487
1488 If not supplied, part_item will be inferred from the pkgnum or feepart of the
1489 cust_bill_pkg, and location from the pkgnum (or, for fees, the invnum and 
1490 the customer's default service location).
1491
1492 =cut
1493
1494 sub _handle_taxes {
1495   my $self = shift;
1496   my $taxlisthash = shift;
1497   my $cust_bill_pkg = shift;
1498   my %options = @_;
1499
1500   # at this point I realize that we have enough information to infer all this
1501   # stuff, instead of passing around giant honking argument lists
1502   my $location = $options{location} || $cust_bill_pkg->tax_location;
1503   my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
1504
1505   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1506
1507   return if ( $self->payby eq 'COMP' ); #dubious
1508
1509   if ( $conf->exists('enable_taxproducts')
1510        && ( scalar($part_item->part_pkg_taxoverride)
1511             || $part_item->has_taxproduct
1512           )
1513      )
1514     {
1515
1516     # EXTERNAL TAX RATES (via tax_rate)
1517     my %cust_bill_pkg = ();
1518     my %taxes = ();
1519
1520     my @classes;
1521     push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
1522     push @classes, 'setup' if $cust_bill_pkg->setup and !$options{cancel};
1523     push @classes, 'recur' if $cust_bill_pkg->recur and !$options{cancel};
1524
1525     my $exempt = $conf->exists('cust_class-tax_exempt')
1526                    ? ( $self->cust_class ? $self->cust_class->tax : '' )
1527                    : $self->tax;
1528     # standardize this just to be sure
1529     $exempt = ($exempt eq 'Y') ? 'Y' : '';
1530   
1531     if ( !$exempt ) {
1532
1533       foreach my $class (@classes) {
1534         my $err_or_ref = $self->_gather_taxes($part_item, $class, $location);
1535         return $err_or_ref unless ref($err_or_ref);
1536         $taxes{$class} = $err_or_ref;
1537       }
1538
1539       unless (exists $taxes{''}) {
1540         my $err_or_ref = $self->_gather_taxes($part_item, '', $location);
1541         return $err_or_ref unless ref($err_or_ref);
1542         $taxes{''} = $err_or_ref;
1543       }
1544
1545     }
1546
1547     my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; # grrr
1548     foreach my $key (keys %tax_cust_bill_pkg) {
1549       # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
1550       # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of 
1551       # the line item.
1552       # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
1553       # apply to $key-class charges.
1554       my @taxes = @{ $taxes{$key} || [] };
1555       my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1556
1557       my %localtaxlisthash = ();
1558       foreach my $tax ( @taxes ) {
1559
1560         # this is the tax identifier, not the taxname
1561         my $taxname = ref( $tax ). ' '. $tax->taxnum;
1562         # $taxlisthash: keys are "setup", "recur", and usage classes.
1563         # Values are arrayrefs, first the tax object (cust_main_county
1564         # or tax_rate) and then any cust_bill_pkg objects that the 
1565         # tax applies to.
1566         $taxlisthash->{ $taxname } ||= [ $tax ];
1567         push @{ $taxlisthash->{ $taxname  } }, $tax_cust_bill_pkg;
1568
1569         $localtaxlisthash{ $taxname } ||= [ $tax ];
1570         push @{ $localtaxlisthash{ $taxname  } }, $tax_cust_bill_pkg;
1571
1572       }
1573
1574       warn "finding taxed taxes...\n" if $DEBUG > 2;
1575       foreach my $tax ( keys %localtaxlisthash ) {
1576         my $tax_object = shift @{ $localtaxlisthash{$tax} };
1577         warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1578           if $DEBUG > 2;
1579         next unless $tax_object->can('tax_on_tax');
1580
1581         foreach my $tot ( $tax_object->tax_on_tax( $location ) ) {
1582           my $totname = ref( $tot ). ' '. $tot->taxnum;
1583
1584           warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1585             if $DEBUG > 2;
1586           next unless exists( $localtaxlisthash{ $totname } ); # only increase
1587                                                                # existing taxes
1588           warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1589           # calculate the tax amount that the tax_on_tax will apply to
1590           my $hashref_or_error = 
1591             $tax_object->taxline( $localtaxlisthash{$tax} );
1592           return $hashref_or_error
1593             unless ref($hashref_or_error);
1594           
1595           # and append it to the list of taxable items
1596           $taxlisthash->{ $totname } ||= [ $tot ];
1597           push @{ $taxlisthash->{ $totname  } }, $hashref_or_error->{amount};
1598
1599         }
1600       }
1601     }
1602
1603   } else {
1604
1605     # INTERNAL TAX RATES (cust_main_county)
1606
1607     # We fetch taxes even if the customer is completely exempt,
1608     # because we need to record that fact.
1609
1610     my @loc_keys = qw( district city county state country );
1611     my %taxhash = map { $_ => $location->$_ } @loc_keys;
1612
1613     $taxhash{'taxclass'} = $part_item->taxclass;
1614
1615     warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
1616
1617     my @taxes = (); # entries are cust_main_county objects
1618     my %taxhash_elim = %taxhash;
1619     my @elim = qw( district city county state );
1620     do { 
1621
1622       #first try a match with taxclass
1623       @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1624
1625       if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1626         #then try a match without taxclass
1627         my %no_taxclass = %taxhash_elim;
1628         $no_taxclass{ 'taxclass' } = '';
1629         @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1630       }
1631
1632       $taxhash_elim{ shift(@elim) } = '';
1633
1634     } while ( !scalar(@taxes) && scalar(@elim) );
1635
1636     foreach (@taxes) {
1637       my $tax_id = 'cust_main_county '.$_->taxnum;
1638       $taxlisthash->{$tax_id} ||= [ $_ ];
1639       push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
1640     }
1641
1642   }
1643   '';
1644 }
1645
1646 =item _gather_taxes PART_ITEM CLASS CUST_LOCATION
1647
1648 Internal method used with vendor-provided tax tables.  PART_ITEM is a part_pkg
1649 or part_fee (which will define the tax eligibility of the product), CLASS is
1650 'setup', 'recur', null, or a C<usage_class> number, and CUST_LOCATION is the 
1651 location where the service was provided (or billed, depending on 
1652 configuration).  Returns an arrayref of L<FS::tax_rate> objects that 
1653 can apply to this line item.
1654
1655 =cut
1656
1657 sub _gather_taxes {
1658   my $self = shift;
1659   my $part_item = shift;
1660   my $class = shift;
1661   my $location = shift;
1662
1663   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1664
1665   my $geocode = $location->geocode('cch');
1666
1667   [ $part_item->tax_rates('cch', $geocode, $class) ]
1668
1669 }
1670
1671 =item collect [ HASHREF | OPTION => VALUE ... ]
1672
1673 (Attempt to) collect money for this customer's outstanding invoices (see
1674 L<FS::cust_bill>).  Usually used after the bill method.
1675
1676 Actions are now triggered by billing events; see L<FS::part_event> and the
1677 billing events web interface.  Old-style invoice events (see
1678 L<FS::part_bill_event>) have been deprecated.
1679
1680 If there is an error, returns the error, otherwise returns false.
1681
1682 Options are passed as name-value pairs.
1683
1684 Currently available options are:
1685
1686 =over 4
1687
1688 =item invoice_time
1689
1690 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.
1691
1692 =item retry
1693
1694 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1695
1696 =item check_freq
1697
1698 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1699
1700 =item quiet
1701
1702 set true to surpress email card/ACH decline notices.
1703
1704 =item debug
1705
1706 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)
1707
1708 =back
1709
1710 # =item payby
1711 #
1712 # allows for one time override of normal customer billing method
1713
1714 =cut
1715
1716 sub collect {
1717   my( $self, %options ) = @_;
1718
1719   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1720
1721   my $invoice_time = $options{'invoice_time'} || time;
1722
1723   #put below somehow?
1724   local $SIG{HUP} = 'IGNORE';
1725   local $SIG{INT} = 'IGNORE';
1726   local $SIG{QUIT} = 'IGNORE';
1727   local $SIG{TERM} = 'IGNORE';
1728   local $SIG{TSTP} = 'IGNORE';
1729   local $SIG{PIPE} = 'IGNORE';
1730
1731   my $oldAutoCommit = $FS::UID::AutoCommit;
1732   local $FS::UID::AutoCommit = 0;
1733   my $dbh = dbh;
1734
1735   $self->select_for_update; #mutex
1736
1737   if ( $DEBUG ) {
1738     my $balance = $self->balance;
1739     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1740   }
1741
1742   if ( exists($options{'retry_card'}) ) {
1743     carp 'retry_card option passed to collect is deprecated; use retry';
1744     $options{'retry'} ||= $options{'retry_card'};
1745   }
1746   if ( exists($options{'retry'}) && $options{'retry'} ) {
1747     my $error = $self->retry_realtime;
1748     if ( $error ) {
1749       $dbh->rollback if $oldAutoCommit;
1750       return $error;
1751     }
1752   }
1753
1754   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1755
1756   #never want to roll back an event just because it returned an error
1757   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1758
1759   $self->do_cust_event(
1760     'debug'      => ( $options{'debug'} || 0 ),
1761     'time'       => $invoice_time,
1762     'check_freq' => $options{'check_freq'},
1763     'stage'      => 'collect',
1764   );
1765
1766 }
1767
1768 =item retry_realtime
1769
1770 Schedules realtime / batch  credit card / electronic check / LEC billing
1771 events for for retry.  Useful if card information has changed or manual
1772 retry is desired.  The 'collect' method must be called to actually retry
1773 the transaction.
1774
1775 Implementation details: For either this customer, or for each of this
1776 customer's open invoices, changes the status of the first "done" (with
1777 statustext error) realtime processing event to "failed".
1778
1779 =cut
1780
1781 sub retry_realtime {
1782   my $self = shift;
1783
1784   local $SIG{HUP} = 'IGNORE';
1785   local $SIG{INT} = 'IGNORE';
1786   local $SIG{QUIT} = 'IGNORE';
1787   local $SIG{TERM} = 'IGNORE';
1788   local $SIG{TSTP} = 'IGNORE';
1789   local $SIG{PIPE} = 'IGNORE';
1790
1791   my $oldAutoCommit = $FS::UID::AutoCommit;
1792   local $FS::UID::AutoCommit = 0;
1793   my $dbh = dbh;
1794
1795   #a little false laziness w/due_cust_event (not too bad, really)
1796
1797   # I guess this is always as of now?
1798   my $join = FS::part_event_condition->join_conditions_sql('', 'time' => time);
1799   my $order = FS::part_event_condition->order_conditions_sql;
1800   my $mine = 
1801   '( '
1802    . join ( ' OR ' , map { 
1803     my $cust_join = FS::part_event->eventtables_cust_join->{$_} || '';
1804     my $custnum = FS::part_event->eventtables_custnum->{$_};
1805     "( part_event.eventtable = " . dbh->quote($_) 
1806     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key 
1807     . " from $_ $cust_join"
1808     . " where $custnum = " . dbh->quote( $self->custnum ) . "))" ;
1809    } FS::part_event->eventtables)
1810    . ') ';
1811
1812   #here is the agent virtualization
1813   my $agent_virt = " (    part_event.agentnum IS NULL
1814                        OR part_event.agentnum = ". $self->agentnum. ' )';
1815
1816   #XXX this shouldn't be hardcoded, actions should declare it...
1817   my @realtime_events = qw(
1818     cust_bill_realtime_card
1819     cust_bill_realtime_check
1820     cust_bill_realtime_lec
1821     cust_bill_batch
1822   );
1823
1824   my $is_realtime_event =
1825     ' part_event.action IN ( '.
1826         join(',', map "'$_'", @realtime_events ).
1827     ' ) ';
1828
1829   my $batch_or_statustext =
1830     "( part_event.action = 'cust_bill_batch'
1831        OR ( statustext IS NOT NULL AND statustext != '' )
1832      )";
1833
1834
1835   my @cust_event = qsearch({
1836     'table'     => 'cust_event',
1837     'select'    => 'cust_event.*',
1838     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1839     'hashref'   => { 'status' => 'done' },
1840     'extra_sql' => " AND $batch_or_statustext ".
1841                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1842   });
1843
1844   my %seen_invnum = ();
1845   foreach my $cust_event (@cust_event) {
1846
1847     #max one for the customer, one for each open invoice
1848     my $cust_X = $cust_event->cust_X;
1849     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1850                           ? $cust_X->invnum
1851                           : 0
1852                         }++
1853          or $cust_event->part_event->eventtable eq 'cust_bill'
1854             && ! $cust_X->owed;
1855
1856     my $error = $cust_event->retry;
1857     if ( $error ) {
1858       $dbh->rollback if $oldAutoCommit;
1859       return "error scheduling event for retry: $error";
1860     }
1861
1862   }
1863
1864   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1865   '';
1866
1867 }
1868
1869 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1870
1871 Runs billing events; see L<FS::part_event> and the billing events web
1872 interface.
1873
1874 If there is an error, returns the error, otherwise returns false.
1875
1876 Options are passed as name-value pairs.
1877
1878 Currently available options are:
1879
1880 =over 4
1881
1882 =item time
1883
1884 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.
1885
1886 =item check_freq
1887
1888 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1889
1890 =item stage
1891
1892 "collect" (the default) or "pre-bill"
1893
1894 =item quiet
1895  
1896 set true to surpress email card/ACH decline notices.
1897
1898 =item debug
1899
1900 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)
1901
1902 =back
1903 =cut
1904
1905 # =item payby
1906 #
1907 # allows for one time override of normal customer billing method
1908
1909 # =item retry
1910 #
1911 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1912
1913 sub do_cust_event {
1914   my( $self, %options ) = @_;
1915
1916   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1917
1918   my $time = $options{'time'} || time;
1919
1920   #put below somehow?
1921   local $SIG{HUP} = 'IGNORE';
1922   local $SIG{INT} = 'IGNORE';
1923   local $SIG{QUIT} = 'IGNORE';
1924   local $SIG{TERM} = 'IGNORE';
1925   local $SIG{TSTP} = 'IGNORE';
1926   local $SIG{PIPE} = 'IGNORE';
1927
1928   my $oldAutoCommit = $FS::UID::AutoCommit;
1929   local $FS::UID::AutoCommit = 0;
1930   my $dbh = dbh;
1931
1932   $self->select_for_update; #mutex
1933
1934   if ( $DEBUG ) {
1935     my $balance = $self->balance;
1936     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1937   }
1938
1939 #  if ( exists($options{'retry_card'}) ) {
1940 #    carp 'retry_card option passed to collect is deprecated; use retry';
1941 #    $options{'retry'} ||= $options{'retry_card'};
1942 #  }
1943 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
1944 #    my $error = $self->retry_realtime;
1945 #    if ( $error ) {
1946 #      $dbh->rollback if $oldAutoCommit;
1947 #      return $error;
1948 #    }
1949 #  }
1950
1951   # false laziness w/pay_batch::import_results
1952
1953   my $due_cust_event = $self->due_cust_event(
1954     'debug'      => ( $options{'debug'} || 0 ),
1955     'time'       => $time,
1956     'check_freq' => $options{'check_freq'},
1957     'stage'      => ( $options{'stage'} || 'collect' ),
1958   );
1959   unless( ref($due_cust_event) ) {
1960     $dbh->rollback if $oldAutoCommit;
1961     return $due_cust_event;
1962   }
1963
1964   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1965   #never want to roll back an event just because it or a different one
1966   # returned an error
1967   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1968
1969   foreach my $cust_event ( @$due_cust_event ) {
1970
1971     #XXX lock event
1972     
1973     #re-eval event conditions (a previous event could have changed things)
1974     unless ( $cust_event->test_conditions ) {
1975       #don't leave stray "new/locked" records around
1976       my $error = $cust_event->delete;
1977       return $error if $error;
1978       next;
1979     }
1980
1981     {
1982       local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1983         if $options{'quiet'};
1984       warn "  running cust_event ". $cust_event->eventnum. "\n"
1985         if $DEBUG > 1;
1986
1987       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1988       if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1989         #XXX wtf is this?  figure out a proper dealio with return value
1990         #from do_event
1991         return $error;
1992       }
1993     }
1994
1995   }
1996
1997   '';
1998
1999 }
2000
2001 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
2002
2003 Inserts database records for and returns an ordered listref of new events due
2004 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
2005 events are due, an empty listref is returned.  If there is an error, returns a
2006 scalar error message.
2007
2008 To actually run the events, call each event's test_condition method, and if
2009 still true, call the event's do_event method.
2010
2011 Options are passed as a hashref or as a list of name-value pairs.  Available
2012 options are:
2013
2014 =over 4
2015
2016 =item check_freq
2017
2018 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.
2019
2020 =item stage
2021
2022 "collect" (the default) or "pre-bill"
2023
2024 =item time
2025
2026 "Current time" for the events.
2027
2028 =item debug
2029
2030 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)
2031
2032 =item eventtable
2033
2034 Only return events for the specified eventtable (by default, events of all eventtables are returned)
2035
2036 =item objects
2037
2038 Explicitly pass the objects to be tested (typically used with eventtable).
2039
2040 =item testonly
2041
2042 Set to true to return the objects, but not actually insert them into the
2043 database.
2044
2045 =back
2046
2047 =cut
2048
2049 sub due_cust_event {
2050   my $self = shift;
2051   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
2052
2053   #???
2054   #my $DEBUG = $opt{'debug'}
2055   $opt{'debug'} ||= 0; # silence some warnings
2056   local($DEBUG) = $opt{'debug'}
2057     if $opt{'debug'} > $DEBUG;
2058   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2059
2060   warn "$me due_cust_event called with options ".
2061        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
2062     if $DEBUG;
2063
2064   $opt{'time'} ||= time;
2065
2066   local $SIG{HUP} = 'IGNORE';
2067   local $SIG{INT} = 'IGNORE';
2068   local $SIG{QUIT} = 'IGNORE';
2069   local $SIG{TERM} = 'IGNORE';
2070   local $SIG{TSTP} = 'IGNORE';
2071   local $SIG{PIPE} = 'IGNORE';
2072
2073   my $oldAutoCommit = $FS::UID::AutoCommit;
2074   local $FS::UID::AutoCommit = 0;
2075   my $dbh = dbh;
2076
2077   $self->select_for_update #mutex
2078     unless $opt{testonly};
2079
2080   ###
2081   # find possible events (initial search)
2082   ###
2083   
2084   my @cust_event = ();
2085
2086   my @eventtable = $opt{'eventtable'}
2087                      ? ( $opt{'eventtable'} )
2088                      : FS::part_event->eventtables_runorder;
2089
2090   my $check_freq = $opt{'check_freq'} || '1d';
2091
2092   foreach my $eventtable ( @eventtable ) {
2093
2094     my @objects;
2095     if ( $opt{'objects'} ) {
2096
2097       @objects = @{ $opt{'objects'} };
2098
2099     } elsif ( $eventtable eq 'cust_main' ) {
2100
2101       @objects = ( $self );
2102
2103     } else {
2104
2105       my $cm_join = " LEFT JOIN cust_main USING ( custnum )";
2106       # linkage not needed here because FS::cust_main->$eventtable will 
2107       # already supply it
2108
2109       #some false laziness w/Cron::bill bill_where
2110
2111       my $join  = FS::part_event_condition->join_conditions_sql( $eventtable,
2112         'time' => $opt{'time'});
2113       my $where = FS::part_event_condition->where_conditions_sql($eventtable,
2114         'time'=>$opt{'time'},
2115       );
2116       $where = $where ? "AND $where" : '';
2117
2118       my $are_part_event = 
2119       "EXISTS ( SELECT 1 FROM part_event $join
2120         WHERE check_freq = '$check_freq'
2121         AND eventtable = '$eventtable'
2122         AND ( disabled = '' OR disabled IS NULL )
2123         $where
2124         )
2125       ";
2126       #eofalse
2127
2128       @objects = $self->$eventtable(
2129         'addl_from' => $cm_join,
2130         'extra_sql' => " AND $are_part_event",
2131       );
2132     } # if ( !$opt{objects} and $eventtable ne 'cust_main' )
2133
2134     my @e_cust_event = ();
2135
2136     my $linkage = FS::part_event->eventtables_cust_join->{$eventtable} || '';
2137
2138     my $cross = "CROSS JOIN $eventtable $linkage";
2139     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2140       unless $eventtable eq 'cust_main';
2141
2142     foreach my $object ( @objects ) {
2143
2144       #this first search uses the condition_sql magic for optimization.
2145       #the more possible events we can eliminate in this step the better
2146
2147       my $cross_where = '';
2148       my $pkey = $object->primary_key;
2149       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2150
2151       my $join = FS::part_event_condition->join_conditions_sql( $eventtable,
2152         'time' => $opt{'time'});
2153       my $extra_sql =
2154         FS::part_event_condition->where_conditions_sql( $eventtable,
2155                                                         'time'=>$opt{'time'}
2156                                                       );
2157       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2158
2159       $extra_sql = "AND $extra_sql" if $extra_sql;
2160
2161       #here is the agent virtualization
2162       $extra_sql .= " AND (    part_event.agentnum IS NULL
2163                             OR part_event.agentnum = ". $self->agentnum. ' )';
2164
2165       $extra_sql .= " $order";
2166
2167       warn "searching for events for $eventtable ". $object->$pkey. "\n"
2168         if $opt{'debug'} > 2;
2169       my @part_event = qsearch( {
2170         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
2171         'select'    => 'part_event.*',
2172         'table'     => 'part_event',
2173         'addl_from' => "$cross $join",
2174         'hashref'   => { 'check_freq' => $check_freq,
2175                          'eventtable' => $eventtable,
2176                          'disabled'   => '',
2177                        },
2178         'extra_sql' => "AND $cross_where $extra_sql",
2179       } );
2180
2181       if ( $DEBUG > 2 ) {
2182         my $pkey = $object->primary_key;
2183         warn "      ". scalar(@part_event).
2184              " possible events found for $eventtable ". $object->$pkey(). "\n";
2185       }
2186
2187       push @e_cust_event, map { 
2188         $_->new_cust_event($object, 'time' => $opt{'time'}) 
2189       } @part_event;
2190
2191     }
2192
2193     warn "    ". scalar(@e_cust_event).
2194          " subtotal possible cust events found for $eventtable\n"
2195       if $DEBUG > 1;
2196
2197     push @cust_event, @e_cust_event;
2198
2199   }
2200
2201   warn "  ". scalar(@cust_event).
2202        " total possible cust events found in initial search\n"
2203     if $DEBUG; # > 1;
2204
2205
2206   ##
2207   # test stage
2208   ##
2209
2210   $opt{stage} ||= 'collect';
2211   @cust_event =
2212     grep { my $stage = $_->part_event->event_stage;
2213            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
2214          }
2215          @cust_event;
2216
2217   ##
2218   # test conditions
2219   ##
2220   
2221   my %unsat = ();
2222
2223   @cust_event = grep $_->test_conditions( 'stats_hashref' => \%unsat ),
2224                      @cust_event;
2225
2226   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
2227     if $DEBUG; # > 1;
2228
2229   warn "    invalid conditions not eliminated with condition_sql:\n".
2230        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
2231     if keys %unsat && $DEBUG; # > 1;
2232
2233   ##
2234   # insert
2235   ##
2236
2237   unless( $opt{testonly} ) {
2238     foreach my $cust_event ( @cust_event ) {
2239
2240       my $error = $cust_event->insert();
2241       if ( $error ) {
2242         $dbh->rollback if $oldAutoCommit;
2243         return $error;
2244       }
2245                                        
2246     }
2247   }
2248
2249   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2250
2251   ##
2252   # return
2253   ##
2254
2255   warn "  returning events: ". Dumper(@cust_event). "\n"
2256     if $DEBUG > 2;
2257
2258   \@cust_event;
2259
2260 }
2261
2262 =item apply_payments_and_credits [ OPTION => VALUE ... ]
2263
2264 Applies unapplied payments and credits.
2265
2266 In most cases, this new method should be used in place of sequential
2267 apply_payments and apply_credits methods.
2268
2269 A hash of optional arguments may be passed.  Currently "manual" is supported.
2270 If true, a payment receipt is sent instead of a statement when
2271 'payment_receipt_email' configuration option is set.
2272
2273 If there is an error, returns the error, otherwise returns false.
2274
2275 =cut
2276
2277 sub apply_payments_and_credits {
2278   my( $self, %options ) = @_;
2279
2280   local $SIG{HUP} = 'IGNORE';
2281   local $SIG{INT} = 'IGNORE';
2282   local $SIG{QUIT} = 'IGNORE';
2283   local $SIG{TERM} = 'IGNORE';
2284   local $SIG{TSTP} = 'IGNORE';
2285   local $SIG{PIPE} = 'IGNORE';
2286
2287   my $oldAutoCommit = $FS::UID::AutoCommit;
2288   local $FS::UID::AutoCommit = 0;
2289   my $dbh = dbh;
2290
2291   $self->select_for_update; #mutex
2292
2293   foreach my $cust_bill ( $self->open_cust_bill ) {
2294     my $error = $cust_bill->apply_payments_and_credits(%options);
2295     if ( $error ) {
2296       $dbh->rollback if $oldAutoCommit;
2297       return "Error applying: $error";
2298     }
2299   }
2300
2301   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2302   ''; #no error
2303
2304 }
2305
2306 =item apply_credits OPTION => VALUE ...
2307
2308 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2309 to outstanding invoice balances in chronological order (or reverse
2310 chronological order if the I<order> option is set to B<newest>) and returns the
2311 value of any remaining unapplied credits available for refund (see
2312 L<FS::cust_refund>).
2313
2314 Dies if there is an error.
2315
2316 =cut
2317
2318 sub apply_credits {
2319   my $self = shift;
2320   my %opt = @_;
2321
2322   local $SIG{HUP} = 'IGNORE';
2323   local $SIG{INT} = 'IGNORE';
2324   local $SIG{QUIT} = 'IGNORE';
2325   local $SIG{TERM} = 'IGNORE';
2326   local $SIG{TSTP} = 'IGNORE';
2327   local $SIG{PIPE} = 'IGNORE';
2328
2329   my $oldAutoCommit = $FS::UID::AutoCommit;
2330   local $FS::UID::AutoCommit = 0;
2331   my $dbh = dbh;
2332
2333   $self->select_for_update; #mutex
2334
2335   unless ( $self->total_unapplied_credits ) {
2336     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2337     return 0;
2338   }
2339
2340   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2341       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2342
2343   my @invoices = $self->open_cust_bill;
2344   @invoices = sort { $b->_date <=> $a->_date } @invoices
2345     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2346
2347   if ( $conf->exists('pkg-balances') ) {
2348     # limit @credits to those w/ a pkgnum grepped from $self
2349     my %pkgnums = ();
2350     foreach my $i (@invoices) {
2351       foreach my $li ( $i->cust_bill_pkg ) {
2352         $pkgnums{$li->pkgnum} = 1;
2353       }
2354     }
2355     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2356   }
2357
2358   my $credit;
2359
2360   foreach my $cust_bill ( @invoices ) {
2361
2362     if ( !defined($credit) || $credit->credited == 0) {
2363       $credit = pop @credits or last;
2364     }
2365
2366     my $owed;
2367     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2368       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2369     } else {
2370       $owed = $cust_bill->owed;
2371     }
2372     unless ( $owed > 0 ) {
2373       push @credits, $credit;
2374       next;
2375     }
2376
2377     my $amount = min( $credit->credited, $owed );
2378     
2379     my $cust_credit_bill = new FS::cust_credit_bill ( {
2380       'crednum' => $credit->crednum,
2381       'invnum'  => $cust_bill->invnum,
2382       'amount'  => $amount,
2383     } );
2384     $cust_credit_bill->pkgnum( $credit->pkgnum )
2385       if $conf->exists('pkg-balances') && $credit->pkgnum;
2386     my $error = $cust_credit_bill->insert;
2387     if ( $error ) {
2388       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2389       die $error;
2390     }
2391     
2392     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2393
2394   }
2395
2396   my $total_unapplied_credits = $self->total_unapplied_credits;
2397
2398   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2399
2400   return $total_unapplied_credits;
2401 }
2402
2403 =item apply_payments  [ OPTION => VALUE ... ]
2404
2405 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2406 to outstanding invoice balances in chronological order.
2407
2408  #and returns the value of any remaining unapplied payments.
2409
2410 A hash of optional arguments may be passed.  Currently "manual" is supported.
2411 If true, a payment receipt is sent instead of a statement when
2412 'payment_receipt_email' configuration option is set.
2413
2414 Dies if there is an error.
2415
2416 =cut
2417
2418 sub apply_payments {
2419   my( $self, %options ) = @_;
2420
2421   local $SIG{HUP} = 'IGNORE';
2422   local $SIG{INT} = 'IGNORE';
2423   local $SIG{QUIT} = 'IGNORE';
2424   local $SIG{TERM} = 'IGNORE';
2425   local $SIG{TSTP} = 'IGNORE';
2426   local $SIG{PIPE} = 'IGNORE';
2427
2428   my $oldAutoCommit = $FS::UID::AutoCommit;
2429   local $FS::UID::AutoCommit = 0;
2430   my $dbh = dbh;
2431
2432   $self->select_for_update; #mutex
2433
2434   #return 0 unless
2435
2436   my @payments = $self->unapplied_cust_pay;
2437
2438   my @invoices = $self->open_cust_bill;
2439
2440   if ( $conf->exists('pkg-balances') ) {
2441     # limit @payments to those w/ a pkgnum grepped from $self
2442     my %pkgnums = ();
2443     foreach my $i (@invoices) {
2444       foreach my $li ( $i->cust_bill_pkg ) {
2445         $pkgnums{$li->pkgnum} = 1;
2446       }
2447     }
2448     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2449   }
2450
2451   my $payment;
2452
2453   foreach my $cust_bill ( @invoices ) {
2454
2455     if ( !defined($payment) || $payment->unapplied == 0 ) {
2456       $payment = pop @payments or last;
2457     }
2458
2459     my $owed;
2460     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2461       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2462     } else {
2463       $owed = $cust_bill->owed;
2464     }
2465     unless ( $owed > 0 ) {
2466       push @payments, $payment;
2467       next;
2468     }
2469
2470     my $amount = min( $payment->unapplied, $owed );
2471
2472     my $cbp = {
2473       'paynum' => $payment->paynum,
2474       'invnum' => $cust_bill->invnum,
2475       'amount' => $amount,
2476     };
2477     $cbp->{_date} = $payment->_date 
2478         if $options{'manual'} && $options{'backdate_application'};
2479     my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2480     $cust_bill_pay->pkgnum( $payment->pkgnum )
2481       if $conf->exists('pkg-balances') && $payment->pkgnum;
2482     my $error = $cust_bill_pay->insert(%options);
2483     if ( $error ) {
2484       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2485       die $error;
2486     }
2487
2488     redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2489
2490   }
2491
2492   my $total_unapplied_payments = $self->total_unapplied_payments;
2493
2494   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2495
2496   return $total_unapplied_payments;
2497 }
2498
2499 =back
2500
2501 =head1 FLOW
2502
2503   bill_and_collect
2504
2505     cancel_expired_pkgs
2506     suspend_adjourned_pkgs
2507     unsuspend_resumed_pkgs
2508
2509     bill
2510       (do_cust_event pre-bill)
2511       _make_lines
2512         _handle_taxes
2513           (vendor-only) _gather_taxes
2514       _omit_zero_value_bundles
2515       _handle_taxes (for fees)
2516       calculate_taxes
2517
2518     apply_payments_and_credits
2519     collect
2520       do_cust_event
2521         due_cust_event
2522
2523 =head1 BUGS
2524
2525 =head1 SEE ALSO
2526
2527 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
2528
2529 =cut
2530
2531 1;