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