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