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