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