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