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