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