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