per-package flags to override package def level suspend_bill flag, RT#18376
[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 || $cust_pkg->option('suspend_bill',1)
957                                || ( $part_pkg->option('suspend_bill', 1) )
958                                      && ! $cust_pkg->option('no_suspend_bill',1)
959                                   )
960        and
961             ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) )
962          || ( $part_pkg->plan eq 'voip_cdr'
963                && $part_pkg->option('bill_every_call')
964             )
965          || $options{cancel}
966   ) {
967
968     # XXX should this be a package event?  probably.  events are called
969     # at collection time at the moment, though...
970     $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
971       if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
972       #don't want to reset usage just cause we want a line item??
973       #&& $part_pkg->pkgpart == $real_pkgpart;
974
975     warn "    bill recur\n" if $DEBUG > 1;
976     $lineitems++;
977
978     # XXX shared with $recur_prog
979     $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
980              || $cust_pkg->setup
981              || $time;
982
983     #over two params!  lets at least switch to a hashref for the rest...
984     my $increment_next_bill = ( $part_pkg->freq ne '0'
985                                 && ( $cust_pkg->getfield('bill') || 0 ) <= day_end($time)
986                                 && !$options{cancel}
987                               );
988     my %param = ( %setup_param,
989                   'precommit_hooks'     => $precommit_hooks,
990                   'increment_next_bill' => $increment_next_bill,
991                   'discounts'           => \@recur_discounts,
992                   'real_pkgpart'        => $real_pkgpart,
993                   'freq_override'       => $options{freq_override} || '',
994                   'setup_fee'           => 0,
995                 );
996
997     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
998
999     # There may be some part_pkg for which this is wrong.  Only those
1000     # which can_discount are supported.
1001     # (the UI should prevent adding discounts to these at the moment)
1002
1003     warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
1004          " for pkgpart ". $cust_pkg->pkgpart.
1005          " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
1006       if $DEBUG > 2;
1007            
1008     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
1009     return "$@ running $method for $cust_pkg\n"
1010       if ( $@ );
1011
1012     if ( $increment_next_bill ) {
1013
1014       my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
1015       return "unparsable frequency: ". $part_pkg->freq
1016         if $next_bill == -1;
1017   
1018       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
1019       # only for figuring next bill date, nothing else, so, reset $sdate again
1020       # here
1021       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1022       #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
1023       $cust_pkg->last_bill($sdate);
1024
1025       $cust_pkg->setfield('bill', $next_bill );
1026
1027     }
1028
1029     if ( $param{'setup_fee'} ) {
1030       # Add an additional setup fee at the billing stage.
1031       # Used for prorate_defer_bill.
1032       $setup += $param{'setup_fee'};
1033       $unitsetup += $param{'setup_fee'};
1034       $lineitems++;
1035     }
1036
1037     if ( defined $param{'discount_left_setup'} ) {
1038         foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1039             $setup -= $discount_setup;
1040         }
1041     }
1042
1043   }
1044
1045   warn "\$setup is undefined" unless defined($setup);
1046   warn "\$recur is undefined" unless defined($recur);
1047   warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1048   
1049   ###
1050   # If there's line items, create em cust_bill_pkg records
1051   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1052   ###
1053
1054   if ( $lineitems ) {
1055
1056     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1057       # hmm.. and if just the options are modified in some weird price plan?
1058   
1059       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
1060         if $DEBUG >1;
1061   
1062       my $error = $cust_pkg->replace( $old_cust_pkg,
1063                                       'depend_jobnum'=>$options{depend_jobnum},
1064                                       'options' => { $cust_pkg->options },
1065                                     )
1066         unless $options{no_commit};
1067       return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1068         if $error; #just in case
1069     }
1070   
1071     $setup = sprintf( "%.2f", $setup );
1072     $recur = sprintf( "%.2f", $recur );
1073     if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1074       return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1075     }
1076     if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1077       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1078     }
1079
1080     my $discount_show_always = $conf->exists('discount-show-always')
1081                                && (    ($setup == 0 && scalar(@setup_discounts))
1082                                     || ($recur == 0 && scalar(@recur_discounts))
1083                                   );
1084
1085     if (    $setup != 0
1086          || $recur != 0
1087          || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1088          || $discount_show_always
1089          || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1090          || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1091        ) 
1092     {
1093
1094       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
1095         if $DEBUG > 1;
1096
1097       my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1098       if ( $DEBUG > 1 ) {
1099         warn "      adding customer package invoice detail: $_\n"
1100           foreach @cust_pkg_detail;
1101       }
1102       push @details, @cust_pkg_detail;
1103
1104       my $cust_bill_pkg = new FS::cust_bill_pkg {
1105         'pkgnum'    => $cust_pkg->pkgnum,
1106         'setup'     => $setup,
1107         'unitsetup' => $unitsetup,
1108         'recur'     => $recur,
1109         'unitrecur' => $unitrecur,
1110         'quantity'  => $cust_pkg->quantity,
1111         'details'   => \@details,
1112         'discounts' => [ @setup_discounts, @recur_discounts ],
1113         'hidden'    => $part_pkg->hidden,
1114         'freq'      => $part_pkg->freq,
1115       };
1116
1117       if ( $part_pkg->option('prorate_defer_bill',1) 
1118            and !$hash{last_bill} ) {
1119         # both preceding and upcoming, technically
1120         $cust_bill_pkg->sdate( $cust_pkg->setup );
1121         $cust_bill_pkg->edate( $cust_pkg->bill );
1122       } elsif ( $part_pkg->recur_temporality eq 'preceding' ) {
1123         $cust_bill_pkg->sdate( $hash{last_bill} );
1124         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
1125         $cust_bill_pkg->edate( $time ) if $options{cancel};
1126       } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) {
1127         $cust_bill_pkg->sdate( $sdate );
1128         $cust_bill_pkg->edate( $cust_pkg->bill );
1129         #$cust_bill_pkg->edate( $time ) if $options{cancel};
1130       }
1131
1132       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1133         unless $part_pkg->pkgpart == $real_pkgpart;
1134
1135       $$total_setup += $setup;
1136       $$total_recur += $recur;
1137
1138       ###
1139       # handle taxes
1140       ###
1141
1142       unless ( $discount_show_always ) {
1143           my $error = 
1144             $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
1145           return $error if $error;
1146       }
1147
1148       push @$cust_bill_pkgs, $cust_bill_pkg;
1149
1150     } #if $setup != 0 || $recur != 0
1151       
1152   } #if $line_items
1153
1154   '';
1155
1156 }
1157
1158 sub _handle_taxes {
1159   my $self = shift;
1160   my $part_pkg = shift;
1161   my $taxlisthash = shift;
1162   my $cust_bill_pkg = shift;
1163   my $cust_pkg = shift;
1164   my $invoice_time = shift;
1165   my $real_pkgpart = shift;
1166   my $options = shift;
1167
1168   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1169
1170   my %cust_bill_pkg = ();
1171   my %taxes = ();
1172     
1173   my @classes;
1174   #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
1175   push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
1176   push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
1177   push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
1178
1179   if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
1180
1181     if ( $conf->exists('enable_taxproducts')
1182          && ( scalar($part_pkg->part_pkg_taxoverride)
1183               || $part_pkg->has_taxproduct
1184             )
1185        )
1186     {
1187
1188       foreach my $class (@classes) {
1189         my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
1190         return $err_or_ref unless ref($err_or_ref);
1191         $taxes{$class} = $err_or_ref;
1192       }
1193
1194       unless (exists $taxes{''}) {
1195         my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
1196         return $err_or_ref unless ref($err_or_ref);
1197         $taxes{''} = $err_or_ref;
1198       }
1199
1200     } else {
1201
1202       my @loc_keys = qw( district city county state country );
1203       my %taxhash;
1204       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1205         my $cust_location = $cust_pkg->cust_location;
1206         %taxhash = map { $_ => $cust_location->$_()    } @loc_keys;
1207       } else {
1208         my $prefix = 
1209           ( $conf->exists('tax-ship_address') && length($self->ship_last) )
1210           ? 'ship_'
1211           : '';
1212         %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
1213       }
1214
1215       $taxhash{'taxclass'} = $part_pkg->taxclass;
1216
1217       my @taxes = ();
1218       my %taxhash_elim = %taxhash;
1219       my @elim = qw( district city county state );
1220       do { 
1221
1222         #first try a match with taxclass
1223         @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1224
1225         if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1226           #then try a match without taxclass
1227           my %no_taxclass = %taxhash_elim;
1228           $no_taxclass{ 'taxclass' } = '';
1229           @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1230         }
1231
1232         $taxhash_elim{ shift(@elim) } = '';
1233
1234       } while ( !scalar(@taxes) && scalar(@elim) );
1235
1236       @taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
1237                     @taxes
1238         if $self->cust_main_exemption; #just to be safe
1239
1240       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1241         foreach (@taxes) {
1242           $_->set('pkgnum',      $cust_pkg->pkgnum );
1243           $_->set('locationnum', $cust_pkg->locationnum );
1244         }
1245       }
1246
1247       $taxes{''} = [ @taxes ];
1248       $taxes{'setup'} = [ @taxes ];
1249       $taxes{'recur'} = [ @taxes ];
1250       $taxes{$_} = [ @taxes ] foreach (@classes);
1251
1252       # # maybe eliminate this entirely, along with all the 0% records
1253       # unless ( @taxes ) {
1254       #   return
1255       #     "fatal: can't find tax rate for state/county/country/taxclass ".
1256       #     join('/', map $taxhash{$_}, qw(state county country taxclass) );
1257       # }
1258
1259     } #if $conf->exists('enable_taxproducts') ...
1260
1261   }
1262
1263   #what's this doing in the middle of _handle_taxes?  probably should split
1264   #this into three parts above in _make_lines
1265   $cust_bill_pkg->set_display(   part_pkg     => $part_pkg,
1266                                  real_pkgpart => $real_pkgpart,
1267                              );
1268
1269   my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1270   foreach my $key (keys %tax_cust_bill_pkg) {
1271     my @taxes = @{ $taxes{$key} || [] };
1272     my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1273
1274     my %localtaxlisthash = ();
1275     foreach my $tax ( @taxes ) {
1276
1277       my $taxname = ref( $tax ). ' '. $tax->taxnum;
1278 #      $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
1279 #                  ' locationnum'. $cust_pkg->locationnum
1280 #        if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
1281
1282       $taxlisthash->{ $taxname } ||= [ $tax ];
1283       push @{ $taxlisthash->{ $taxname  } }, $tax_cust_bill_pkg;
1284
1285       $localtaxlisthash{ $taxname } ||= [ $tax ];
1286       push @{ $localtaxlisthash{ $taxname  } }, $tax_cust_bill_pkg;
1287
1288     }
1289
1290     warn "finding taxed taxes...\n" if $DEBUG > 2;
1291     foreach my $tax ( keys %localtaxlisthash ) {
1292       my $tax_object = shift @{ $localtaxlisthash{$tax} };
1293       warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1294         if $DEBUG > 2;
1295       next unless $tax_object->can('tax_on_tax');
1296
1297       foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
1298         my $totname = ref( $tot ). ' '. $tot->taxnum;
1299
1300         warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1301           if $DEBUG > 2;
1302         next unless exists( $localtaxlisthash{ $totname } ); # only increase
1303                                                              # existing taxes
1304         warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1305         my $hashref_or_error = 
1306           $tax_object->taxline( $localtaxlisthash{$tax},
1307                                 'custnum'      => $self->custnum,
1308                                 'invoice_time' => $invoice_time,
1309                               );
1310         return $hashref_or_error
1311           unless ref($hashref_or_error);
1312         
1313         $taxlisthash->{ $totname } ||= [ $tot ];
1314         push @{ $taxlisthash->{ $totname  } }, $hashref_or_error->{amount};
1315
1316       }
1317     }
1318
1319   }
1320
1321   '';
1322 }
1323
1324 sub _gather_taxes {
1325   my $self = shift;
1326   my $part_pkg = shift;
1327   my $class = shift;
1328   my $cust_pkg = shift;
1329
1330   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1331
1332   my $geocode;
1333   if ( $cust_pkg->locationnum && $conf->exists('tax-pkg_address') ) {
1334     $geocode = $cust_pkg->cust_location->geocode('cch');
1335   } else {
1336     $geocode = $self->geocode('cch');
1337   }
1338
1339   my @taxes = ();
1340
1341   my @taxclassnums = map { $_->taxclassnum }
1342                      $part_pkg->part_pkg_taxoverride($class);
1343
1344   unless (@taxclassnums) {
1345     @taxclassnums = map { $_->taxclassnum }
1346                     grep { $_->taxable eq 'Y' }
1347                     $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
1348   }
1349   warn "Found taxclassnum values of ". join(',', @taxclassnums)
1350     if $DEBUG;
1351
1352   my $extra_sql =
1353     "AND (".
1354     join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
1355
1356   @taxes = qsearch({ 'table' => 'tax_rate',
1357                      'hashref' => { 'geocode' => $geocode, },
1358                      'extra_sql' => $extra_sql,
1359                   })
1360     if scalar(@taxclassnums);
1361
1362   warn "Found taxes ".
1363        join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n" 
1364    if $DEBUG;
1365
1366   [ @taxes ];
1367
1368 }
1369
1370 =item collect [ HASHREF | OPTION => VALUE ... ]
1371
1372 (Attempt to) collect money for this customer's outstanding invoices (see
1373 L<FS::cust_bill>).  Usually used after the bill method.
1374
1375 Actions are now triggered by billing events; see L<FS::part_event> and the
1376 billing events web interface.  Old-style invoice events (see
1377 L<FS::part_bill_event>) have been deprecated.
1378
1379 If there is an error, returns the error, otherwise returns false.
1380
1381 Options are passed as name-value pairs.
1382
1383 Currently available options are:
1384
1385 =over 4
1386
1387 =item invoice_time
1388
1389 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.
1390
1391 =item retry
1392
1393 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1394
1395 =item check_freq
1396
1397 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1398
1399 =item quiet
1400
1401 set true to surpress email card/ACH decline notices.
1402
1403 =item debug
1404
1405 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)
1406
1407 =back
1408
1409 # =item payby
1410 #
1411 # allows for one time override of normal customer billing method
1412
1413 =cut
1414
1415 sub collect {
1416   my( $self, %options ) = @_;
1417
1418   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1419
1420   my $invoice_time = $options{'invoice_time'} || time;
1421
1422   #put below somehow?
1423   local $SIG{HUP} = 'IGNORE';
1424   local $SIG{INT} = 'IGNORE';
1425   local $SIG{QUIT} = 'IGNORE';
1426   local $SIG{TERM} = 'IGNORE';
1427   local $SIG{TSTP} = 'IGNORE';
1428   local $SIG{PIPE} = 'IGNORE';
1429
1430   my $oldAutoCommit = $FS::UID::AutoCommit;
1431   local $FS::UID::AutoCommit = 0;
1432   my $dbh = dbh;
1433
1434   $self->select_for_update; #mutex
1435
1436   if ( $DEBUG ) {
1437     my $balance = $self->balance;
1438     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1439   }
1440
1441   if ( exists($options{'retry_card'}) ) {
1442     carp 'retry_card option passed to collect is deprecated; use retry';
1443     $options{'retry'} ||= $options{'retry_card'};
1444   }
1445   if ( exists($options{'retry'}) && $options{'retry'} ) {
1446     my $error = $self->retry_realtime;
1447     if ( $error ) {
1448       $dbh->rollback if $oldAutoCommit;
1449       return $error;
1450     }
1451   }
1452
1453   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1454
1455   #never want to roll back an event just because it returned an error
1456   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1457
1458   $self->do_cust_event(
1459     'debug'      => ( $options{'debug'} || 0 ),
1460     'time'       => $invoice_time,
1461     'check_freq' => $options{'check_freq'},
1462     'stage'      => 'collect',
1463   );
1464
1465 }
1466
1467 =item retry_realtime
1468
1469 Schedules realtime / batch  credit card / electronic check / LEC billing
1470 events for for retry.  Useful if card information has changed or manual
1471 retry is desired.  The 'collect' method must be called to actually retry
1472 the transaction.
1473
1474 Implementation details: For either this customer, or for each of this
1475 customer's open invoices, changes the status of the first "done" (with
1476 statustext error) realtime processing event to "failed".
1477
1478 =cut
1479
1480 sub retry_realtime {
1481   my $self = shift;
1482
1483   local $SIG{HUP} = 'IGNORE';
1484   local $SIG{INT} = 'IGNORE';
1485   local $SIG{QUIT} = 'IGNORE';
1486   local $SIG{TERM} = 'IGNORE';
1487   local $SIG{TSTP} = 'IGNORE';
1488   local $SIG{PIPE} = 'IGNORE';
1489
1490   my $oldAutoCommit = $FS::UID::AutoCommit;
1491   local $FS::UID::AutoCommit = 0;
1492   my $dbh = dbh;
1493
1494   #a little false laziness w/due_cust_event (not too bad, really)
1495
1496   my $join = FS::part_event_condition->join_conditions_sql;
1497   my $order = FS::part_event_condition->order_conditions_sql;
1498   my $mine = 
1499   '( '
1500    . join ( ' OR ' , map { 
1501     "( part_event.eventtable = " . dbh->quote($_) 
1502     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ;
1503    } FS::part_event->eventtables)
1504    . ') ';
1505
1506   #here is the agent virtualization
1507   my $agent_virt = " (    part_event.agentnum IS NULL
1508                        OR part_event.agentnum = ". $self->agentnum. ' )';
1509
1510   #XXX this shouldn't be hardcoded, actions should declare it...
1511   my @realtime_events = qw(
1512     cust_bill_realtime_card
1513     cust_bill_realtime_check
1514     cust_bill_realtime_lec
1515     cust_bill_batch
1516   );
1517
1518   my $is_realtime_event =
1519     ' part_event.action IN ( '.
1520         join(',', map "'$_'", @realtime_events ).
1521     ' ) ';
1522
1523   my $batch_or_statustext =
1524     "( part_event.action = 'cust_bill_batch'
1525        OR ( statustext IS NOT NULL AND statustext != '' )
1526      )";
1527
1528
1529   my @cust_event = qsearch({
1530     'table'     => 'cust_event',
1531     'select'    => 'cust_event.*',
1532     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1533     'hashref'   => { 'status' => 'done' },
1534     'extra_sql' => " AND $batch_or_statustext ".
1535                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1536   });
1537
1538   my %seen_invnum = ();
1539   foreach my $cust_event (@cust_event) {
1540
1541     #max one for the customer, one for each open invoice
1542     my $cust_X = $cust_event->cust_X;
1543     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1544                           ? $cust_X->invnum
1545                           : 0
1546                         }++
1547          or $cust_event->part_event->eventtable eq 'cust_bill'
1548             && ! $cust_X->owed;
1549
1550     my $error = $cust_event->retry;
1551     if ( $error ) {
1552       $dbh->rollback if $oldAutoCommit;
1553       return "error scheduling event for retry: $error";
1554     }
1555
1556   }
1557
1558   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1559   '';
1560
1561 }
1562
1563 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1564
1565 Runs billing events; see L<FS::part_event> and the billing events web
1566 interface.
1567
1568 If there is an error, returns the error, otherwise returns false.
1569
1570 Options are passed as name-value pairs.
1571
1572 Currently available options are:
1573
1574 =over 4
1575
1576 =item time
1577
1578 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.
1579
1580 =item check_freq
1581
1582 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1583
1584 =item stage
1585
1586 "collect" (the default) or "pre-bill"
1587
1588 =item quiet
1589  
1590 set true to surpress email card/ACH decline notices.
1591
1592 =item debug
1593
1594 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)
1595
1596 =back
1597 =cut
1598
1599 # =item payby
1600 #
1601 # allows for one time override of normal customer billing method
1602
1603 # =item retry
1604 #
1605 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1606
1607 sub do_cust_event {
1608   my( $self, %options ) = @_;
1609
1610   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1611
1612   my $time = $options{'time'} || time;
1613
1614   #put below somehow?
1615   local $SIG{HUP} = 'IGNORE';
1616   local $SIG{INT} = 'IGNORE';
1617   local $SIG{QUIT} = 'IGNORE';
1618   local $SIG{TERM} = 'IGNORE';
1619   local $SIG{TSTP} = 'IGNORE';
1620   local $SIG{PIPE} = 'IGNORE';
1621
1622   my $oldAutoCommit = $FS::UID::AutoCommit;
1623   local $FS::UID::AutoCommit = 0;
1624   my $dbh = dbh;
1625
1626   $self->select_for_update; #mutex
1627
1628   if ( $DEBUG ) {
1629     my $balance = $self->balance;
1630     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1631   }
1632
1633 #  if ( exists($options{'retry_card'}) ) {
1634 #    carp 'retry_card option passed to collect is deprecated; use retry';
1635 #    $options{'retry'} ||= $options{'retry_card'};
1636 #  }
1637 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
1638 #    my $error = $self->retry_realtime;
1639 #    if ( $error ) {
1640 #      $dbh->rollback if $oldAutoCommit;
1641 #      return $error;
1642 #    }
1643 #  }
1644
1645   # false laziness w/pay_batch::import_results
1646
1647   my $due_cust_event = $self->due_cust_event(
1648     'debug'      => ( $options{'debug'} || 0 ),
1649     'time'       => $time,
1650     'check_freq' => $options{'check_freq'},
1651     'stage'      => ( $options{'stage'} || 'collect' ),
1652   );
1653   unless( ref($due_cust_event) ) {
1654     $dbh->rollback if $oldAutoCommit;
1655     return $due_cust_event;
1656   }
1657
1658   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1659   #never want to roll back an event just because it or a different one
1660   # returned an error
1661   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1662
1663   foreach my $cust_event ( @$due_cust_event ) {
1664
1665     #XXX lock event
1666     
1667     #re-eval event conditions (a previous event could have changed things)
1668     unless ( $cust_event->test_conditions( 'time' => $time ) ) {
1669       #don't leave stray "new/locked" records around
1670       my $error = $cust_event->delete;
1671       return $error if $error;
1672       next;
1673     }
1674
1675     {
1676       local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1677         if $options{'quiet'};
1678       warn "  running cust_event ". $cust_event->eventnum. "\n"
1679         if $DEBUG > 1;
1680
1681       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1682       if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1683         #XXX wtf is this?  figure out a proper dealio with return value
1684         #from do_event
1685         return $error;
1686       }
1687     }
1688
1689   }
1690
1691   '';
1692
1693 }
1694
1695 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
1696
1697 Inserts database records for and returns an ordered listref of new events due
1698 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
1699 events are due, an empty listref is returned.  If there is an error, returns a
1700 scalar error message.
1701
1702 To actually run the events, call each event's test_condition method, and if
1703 still true, call the event's do_event method.
1704
1705 Options are passed as a hashref or as a list of name-value pairs.  Available
1706 options are:
1707
1708 =over 4
1709
1710 =item check_freq
1711
1712 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.
1713
1714 =item stage
1715
1716 "collect" (the default) or "pre-bill"
1717
1718 =item time
1719
1720 "Current time" for the events.
1721
1722 =item debug
1723
1724 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)
1725
1726 =item eventtable
1727
1728 Only return events for the specified eventtable (by default, events of all eventtables are returned)
1729
1730 =item objects
1731
1732 Explicitly pass the objects to be tested (typically used with eventtable).
1733
1734 =item testonly
1735
1736 Set to true to return the objects, but not actually insert them into the
1737 database.
1738
1739 =back
1740
1741 =cut
1742
1743 sub due_cust_event {
1744   my $self = shift;
1745   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
1746
1747   #???
1748   #my $DEBUG = $opt{'debug'}
1749   local($DEBUG) = $opt{'debug'}
1750     if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
1751   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1752
1753   warn "$me due_cust_event called with options ".
1754        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
1755     if $DEBUG;
1756
1757   $opt{'time'} ||= time;
1758
1759   local $SIG{HUP} = 'IGNORE';
1760   local $SIG{INT} = 'IGNORE';
1761   local $SIG{QUIT} = 'IGNORE';
1762   local $SIG{TERM} = 'IGNORE';
1763   local $SIG{TSTP} = 'IGNORE';
1764   local $SIG{PIPE} = 'IGNORE';
1765
1766   my $oldAutoCommit = $FS::UID::AutoCommit;
1767   local $FS::UID::AutoCommit = 0;
1768   my $dbh = dbh;
1769
1770   $self->select_for_update #mutex
1771     unless $opt{testonly};
1772
1773   ###
1774   # find possible events (initial search)
1775   ###
1776   
1777   my @cust_event = ();
1778
1779   my @eventtable = $opt{'eventtable'}
1780                      ? ( $opt{'eventtable'} )
1781                      : FS::part_event->eventtables_runorder;
1782
1783   my $check_freq = $opt{'check_freq'} || '1d';
1784
1785   foreach my $eventtable ( @eventtable ) {
1786
1787     my @objects;
1788     if ( $opt{'objects'} ) {
1789
1790       @objects = @{ $opt{'objects'} };
1791
1792     } else {
1793
1794       #my @objects = $self->$eventtable(); # sub cust_main { @{ [ $self ] }; }
1795       if ( $eventtable eq 'cust_main' ) {
1796         @objects = ( $self );
1797       } else {
1798
1799         my $cm_join =
1800           "LEFT JOIN cust_main USING ( custnum )";
1801
1802         #some false laziness w/Cron::bill bill_where
1803
1804         my $join  = FS::part_event_condition->join_conditions_sql( $eventtable);
1805         my $where = FS::part_event_condition->where_conditions_sql($eventtable,
1806                                                            'time'=>$opt{'time'},
1807                                                                   );
1808         $where = $where ? "AND $where" : '';
1809
1810         my $are_part_event = 
1811           "EXISTS ( SELECT 1 FROM part_event $join
1812                       WHERE check_freq = '$check_freq'
1813                         AND eventtable = '$eventtable'
1814                         AND ( disabled = '' OR disabled IS NULL )
1815                         $where
1816                   )
1817           ";
1818         #eofalse
1819
1820         @objects = $self->$eventtable(
1821                      'addl_from' => $cm_join,
1822                      'extra_sql' => " AND $are_part_event",
1823                    );
1824       }
1825
1826     }
1827
1828     my @e_cust_event = ();
1829
1830     my $cross = "CROSS JOIN $eventtable";
1831     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
1832       unless $eventtable eq 'cust_main';
1833
1834     foreach my $object ( @objects ) {
1835
1836       #this first search uses the condition_sql magic for optimization.
1837       #the more possible events we can eliminate in this step the better
1838
1839       my $cross_where = '';
1840       my $pkey = $object->primary_key;
1841       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
1842
1843       my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
1844       my $extra_sql =
1845         FS::part_event_condition->where_conditions_sql( $eventtable,
1846                                                         'time'=>$opt{'time'}
1847                                                       );
1848       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
1849
1850       $extra_sql = "AND $extra_sql" if $extra_sql;
1851
1852       #here is the agent virtualization
1853       $extra_sql .= " AND (    part_event.agentnum IS NULL
1854                             OR part_event.agentnum = ". $self->agentnum. ' )';
1855
1856       $extra_sql .= " $order";
1857
1858       warn "searching for events for $eventtable ". $object->$pkey. "\n"
1859         if $opt{'debug'} > 2;
1860       my @part_event = qsearch( {
1861         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
1862         'select'    => 'part_event.*',
1863         'table'     => 'part_event',
1864         'addl_from' => "$cross $join",
1865         'hashref'   => { 'check_freq' => $check_freq,
1866                          'eventtable' => $eventtable,
1867                          'disabled'   => '',
1868                        },
1869         'extra_sql' => "AND $cross_where $extra_sql",
1870       } );
1871
1872       if ( $DEBUG > 2 ) {
1873         my $pkey = $object->primary_key;
1874         warn "      ". scalar(@part_event).
1875              " possible events found for $eventtable ". $object->$pkey(). "\n";
1876       }
1877
1878       push @e_cust_event, map { $_->new_cust_event($object) } @part_event;
1879
1880     }
1881
1882     warn "    ". scalar(@e_cust_event).
1883          " subtotal possible cust events found for $eventtable\n"
1884       if $DEBUG > 1;
1885
1886     push @cust_event, @e_cust_event;
1887
1888   }
1889
1890   warn "  ". scalar(@cust_event).
1891        " total possible cust events found in initial search\n"
1892     if $DEBUG; # > 1;
1893
1894
1895   ##
1896   # test stage
1897   ##
1898
1899   $opt{stage} ||= 'collect';
1900   @cust_event =
1901     grep { my $stage = $_->part_event->event_stage;
1902            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
1903          }
1904          @cust_event;
1905
1906   ##
1907   # test conditions
1908   ##
1909   
1910   my %unsat = ();
1911
1912   @cust_event = grep $_->test_conditions( 'time'          => $opt{'time'},
1913                                           'stats_hashref' => \%unsat ),
1914                      @cust_event;
1915
1916   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
1917     if $DEBUG; # > 1;
1918
1919   warn "    invalid conditions not eliminated with condition_sql:\n".
1920        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
1921     if keys %unsat && $DEBUG; # > 1;
1922
1923   ##
1924   # insert
1925   ##
1926
1927   unless( $opt{testonly} ) {
1928     foreach my $cust_event ( @cust_event ) {
1929
1930       my $error = $cust_event->insert();
1931       if ( $error ) {
1932         $dbh->rollback if $oldAutoCommit;
1933         return $error;
1934       }
1935                                        
1936     }
1937   }
1938
1939   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1940
1941   ##
1942   # return
1943   ##
1944
1945   warn "  returning events: ". Dumper(@cust_event). "\n"
1946     if $DEBUG > 2;
1947
1948   \@cust_event;
1949
1950 }
1951
1952 =item apply_payments_and_credits [ OPTION => VALUE ... ]
1953
1954 Applies unapplied payments and credits.
1955
1956 In most cases, this new method should be used in place of sequential
1957 apply_payments and apply_credits methods.
1958
1959 A hash of optional arguments may be passed.  Currently "manual" is supported.
1960 If true, a payment receipt is sent instead of a statement when
1961 'payment_receipt_email' configuration option is set.
1962
1963 If there is an error, returns the error, otherwise returns false.
1964
1965 =cut
1966
1967 sub apply_payments_and_credits {
1968   my( $self, %options ) = @_;
1969
1970   local $SIG{HUP} = 'IGNORE';
1971   local $SIG{INT} = 'IGNORE';
1972   local $SIG{QUIT} = 'IGNORE';
1973   local $SIG{TERM} = 'IGNORE';
1974   local $SIG{TSTP} = 'IGNORE';
1975   local $SIG{PIPE} = 'IGNORE';
1976
1977   my $oldAutoCommit = $FS::UID::AutoCommit;
1978   local $FS::UID::AutoCommit = 0;
1979   my $dbh = dbh;
1980
1981   $self->select_for_update; #mutex
1982
1983   foreach my $cust_bill ( $self->open_cust_bill ) {
1984     my $error = $cust_bill->apply_payments_and_credits(%options);
1985     if ( $error ) {
1986       $dbh->rollback if $oldAutoCommit;
1987       return "Error applying: $error";
1988     }
1989   }
1990
1991   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1992   ''; #no error
1993
1994 }
1995
1996 =item apply_credits OPTION => VALUE ...
1997
1998 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1999 to outstanding invoice balances in chronological order (or reverse
2000 chronological order if the I<order> option is set to B<newest>) and returns the
2001 value of any remaining unapplied credits available for refund (see
2002 L<FS::cust_refund>).
2003
2004 Dies if there is an error.
2005
2006 =cut
2007
2008 sub apply_credits {
2009   my $self = shift;
2010   my %opt = @_;
2011
2012   local $SIG{HUP} = 'IGNORE';
2013   local $SIG{INT} = 'IGNORE';
2014   local $SIG{QUIT} = 'IGNORE';
2015   local $SIG{TERM} = 'IGNORE';
2016   local $SIG{TSTP} = 'IGNORE';
2017   local $SIG{PIPE} = 'IGNORE';
2018
2019   my $oldAutoCommit = $FS::UID::AutoCommit;
2020   local $FS::UID::AutoCommit = 0;
2021   my $dbh = dbh;
2022
2023   $self->select_for_update; #mutex
2024
2025   unless ( $self->total_unapplied_credits ) {
2026     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2027     return 0;
2028   }
2029
2030   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2031       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2032
2033   my @invoices = $self->open_cust_bill;
2034   @invoices = sort { $b->_date <=> $a->_date } @invoices
2035     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2036
2037   if ( $conf->exists('pkg-balances') ) {
2038     # limit @credits to those w/ a pkgnum grepped from $self
2039     my %pkgnums = ();
2040     foreach my $i (@invoices) {
2041       foreach my $li ( $i->cust_bill_pkg ) {
2042         $pkgnums{$li->pkgnum} = 1;
2043       }
2044     }
2045     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2046   }
2047
2048   my $credit;
2049
2050   foreach my $cust_bill ( @invoices ) {
2051
2052     if ( !defined($credit) || $credit->credited == 0) {
2053       $credit = pop @credits or last;
2054     }
2055
2056     my $owed;
2057     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2058       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2059     } else {
2060       $owed = $cust_bill->owed;
2061     }
2062     unless ( $owed > 0 ) {
2063       push @credits, $credit;
2064       next;
2065     }
2066
2067     my $amount = min( $credit->credited, $owed );
2068     
2069     my $cust_credit_bill = new FS::cust_credit_bill ( {
2070       'crednum' => $credit->crednum,
2071       'invnum'  => $cust_bill->invnum,
2072       'amount'  => $amount,
2073     } );
2074     $cust_credit_bill->pkgnum( $credit->pkgnum )
2075       if $conf->exists('pkg-balances') && $credit->pkgnum;
2076     my $error = $cust_credit_bill->insert;
2077     if ( $error ) {
2078       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2079       die $error;
2080     }
2081     
2082     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2083
2084   }
2085
2086   my $total_unapplied_credits = $self->total_unapplied_credits;
2087
2088   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2089
2090   return $total_unapplied_credits;
2091 }
2092
2093 =item apply_payments  [ OPTION => VALUE ... ]
2094
2095 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2096 to outstanding invoice balances in chronological order.
2097
2098  #and returns the value of any remaining unapplied payments.
2099
2100 A hash of optional arguments may be passed.  Currently "manual" is supported.
2101 If true, a payment receipt is sent instead of a statement when
2102 'payment_receipt_email' configuration option is set.
2103
2104 Dies if there is an error.
2105
2106 =cut
2107
2108 sub apply_payments {
2109   my( $self, %options ) = @_;
2110
2111   local $SIG{HUP} = 'IGNORE';
2112   local $SIG{INT} = 'IGNORE';
2113   local $SIG{QUIT} = 'IGNORE';
2114   local $SIG{TERM} = 'IGNORE';
2115   local $SIG{TSTP} = 'IGNORE';
2116   local $SIG{PIPE} = 'IGNORE';
2117
2118   my $oldAutoCommit = $FS::UID::AutoCommit;
2119   local $FS::UID::AutoCommit = 0;
2120   my $dbh = dbh;
2121
2122   $self->select_for_update; #mutex
2123
2124   #return 0 unless
2125
2126   my @payments = sort { $b->_date <=> $a->_date }
2127                  grep { $_->unapplied > 0 }
2128                  $self->cust_pay;
2129
2130   my @invoices = sort { $a->_date <=> $b->_date}
2131                  grep { $_->owed > 0 }
2132                  $self->cust_bill;
2133
2134   if ( $conf->exists('pkg-balances') ) {
2135     # limit @payments to those w/ a pkgnum grepped from $self
2136     my %pkgnums = ();
2137     foreach my $i (@invoices) {
2138       foreach my $li ( $i->cust_bill_pkg ) {
2139         $pkgnums{$li->pkgnum} = 1;
2140       }
2141     }
2142     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2143   }
2144
2145   my $payment;
2146
2147   foreach my $cust_bill ( @invoices ) {
2148
2149     if ( !defined($payment) || $payment->unapplied == 0 ) {
2150       $payment = pop @payments or last;
2151     }
2152
2153     my $owed;
2154     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2155       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2156     } else {
2157       $owed = $cust_bill->owed;
2158     }
2159     unless ( $owed > 0 ) {
2160       push @payments, $payment;
2161       next;
2162     }
2163
2164     my $amount = min( $payment->unapplied, $owed );
2165
2166     my $cbp = {
2167       'paynum' => $payment->paynum,
2168       'invnum' => $cust_bill->invnum,
2169       'amount' => $amount,
2170     };
2171     $cbp->{_date} = $payment->_date 
2172         if $options{'manual'} && $options{'backdate_application'};
2173     my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2174     $cust_bill_pay->pkgnum( $payment->pkgnum )
2175       if $conf->exists('pkg-balances') && $payment->pkgnum;
2176     my $error = $cust_bill_pay->insert(%options);
2177     if ( $error ) {
2178       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2179       die $error;
2180     }
2181
2182     redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2183
2184   }
2185
2186   my $total_unapplied_payments = $self->total_unapplied_payments;
2187
2188   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2189
2190   return $total_unapplied_payments;
2191 }
2192
2193 =back
2194
2195 =head1 FLOW
2196
2197   bill_and_collect
2198
2199     cancel_expired_pkgs
2200     suspend_adjourned_pkgs
2201     unsuspend_resumed_pkgs
2202
2203     bill
2204       (do_cust_event pre-bill)
2205       _make_lines
2206         _handle_taxes
2207           (vendor-only) _gather_taxes
2208       _omit_zero_value_bundles
2209       calculate_taxes
2210
2211     apply_payments_and_credits
2212     collect
2213       do_cust_event
2214         due_cust_event
2215
2216 =head1 BUGS
2217
2218 =head1 SEE ALSO
2219
2220 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
2221
2222 =cut
2223
2224 1;