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