obey summarize_usage, usage_mandate and usage_section for bundled packages, RT#13908
[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. " sum $sum, ".
620          "setup_show_zero ". $cust_bill_pkg->setup_show_zero.
621          "recur_show_zero ". $cust_bill_pkg->recur_show_zero. "\n"
622       if $DEBUG > 0;
623
624     if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
625       push @cust_bill_pkg, @cust_bill_pkg_bundle 
626         if $sum > 0
627         || ($sum == 0 && (    $discount_show_always
628                            || grep {$_->recur_show_zero || $_->setup_show_zero}
629                                    @cust_bill_pkg_bundle
630                          )
631            );
632       @cust_bill_pkg_bundle = ();
633       $sum = 0;
634     }
635
636     $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
637     push @cust_bill_pkg_bundle, $cust_bill_pkg;
638
639   }
640
641   push @cust_bill_pkg, @cust_bill_pkg_bundle
642     if $sum > 0
643     || ($sum == 0 && (    $discount_show_always
644                        || grep {$_->recur_show_zero || $_->setup_show_zero}
645                                @cust_bill_pkg_bundle
646                      )
647        );
648
649   warn "  _omit_zero_value_bundles: ". scalar(@in).
650        '->'. scalar(@cust_bill_pkg). "\n" #. Dumper(@cust_bill_pkg). "\n"
651     if $DEBUG > 2;
652
653   (@cust_bill_pkg);
654
655 }
656
657 =item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
658
659 This is a weird one.  Perhaps it should not even be exposed.
660
661 Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
662 Usually used internally by bill method B<bill>.
663
664 If there is an error, returns the error, otherwise returns reference to a
665 list of line items suitable for insertion.
666
667 =over 4
668
669 =item LINEITEMREF
670
671 An array ref of the line items being billed.
672
673 =item TAXHASHREF
674
675 A strange beast.  The keys to this hash are internal identifiers consisting
676 of the name of the tax object type, a space, and its unique identifier ( e.g.
677  'cust_main_county 23' ).  The values of the hash are listrefs.  The first
678 item in the list is the tax object.  The remaining items are either line
679 items or floating point values (currency amounts).
680
681 The taxes are calculated on this entity.  Calculated exemption records are
682 transferred to the LINEITEMREF items on the assumption that they are related.
683
684 Read the source.
685
686 =item INVOICE_TIME
687
688 This specifies the date appearing on the associated invoice.  Some
689 jurisdictions (i.e. Texas) have tax exemptions which are date sensitive.
690
691 =back
692
693 =cut
694
695 sub calculate_taxes {
696   my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
697
698   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
699
700   warn "$me calculate_taxes\n"
701        #.Dumper($self, $cust_bill_pkg, $taxlisthash, $invoice_time). "\n"
702     if $DEBUG > 2;
703
704   my @tax_line_items = ();
705
706   # keys are tax names (as printed on invoices / itemdesc )
707   # values are listrefs of taxlisthash keys (internal identifiers)
708   my %taxname = ();
709
710   # keys are taxlisthash keys (internal identifiers)
711   # values are (cumulative) amounts
712   my %tax = ();
713
714   # keys are taxlisthash keys (internal identifiers)
715   # values are listrefs of cust_bill_pkg_tax_location hashrefs
716   my %tax_location = ();
717
718   # keys are taxlisthash keys (internal identifiers)
719   # values are listrefs of cust_bill_pkg_tax_rate_location hashrefs
720   my %tax_rate_location = ();
721
722   foreach my $tax ( keys %$taxlisthash ) {
723     my $tax_object = shift @{ $taxlisthash->{$tax} };
724     warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
725     warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
726     my $hashref_or_error =
727       $tax_object->taxline( $taxlisthash->{$tax},
728                             'custnum'      => $self->custnum,
729                             'invoice_time' => $invoice_time
730                           );
731     return $hashref_or_error unless ref($hashref_or_error);
732
733     unshift @{ $taxlisthash->{$tax} }, $tax_object;
734
735     my $name   = $hashref_or_error->{'name'};
736     my $amount = $hashref_or_error->{'amount'};
737
738     #warn "adding $amount as $name\n";
739     $taxname{ $name } ||= [];
740     push @{ $taxname{ $name } }, $tax;
741
742     $tax{ $tax } += $amount;
743
744     $tax_location{ $tax } ||= [];
745     if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
746       push @{ $tax_location{ $tax }  },
747         {
748           'taxnum'      => $tax_object->taxnum, 
749           'taxtype'     => ref($tax_object),
750           'pkgnum'      => $tax_object->get('pkgnum'),
751           'locationnum' => $tax_object->get('locationnum'),
752           'amount'      => sprintf('%.2f', $amount ),
753         };
754     }
755
756     $tax_rate_location{ $tax } ||= [];
757     if ( ref($tax_object) eq 'FS::tax_rate' ) {
758       my $taxratelocationnum =
759         $tax_object->tax_rate_location->taxratelocationnum;
760       push @{ $tax_rate_location{ $tax }  },
761         {
762           'taxnum'             => $tax_object->taxnum, 
763           'taxtype'            => ref($tax_object),
764           'amount'             => sprintf('%.2f', $amount ),
765           'locationtaxid'      => $tax_object->location,
766           'taxratelocationnum' => $taxratelocationnum,
767         };
768     }
769
770   }
771
772   #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
773   my %packagemap = map { $_->pkgnum => $_ } @$cust_bill_pkg;
774   foreach my $tax ( keys %$taxlisthash ) {
775     foreach ( @{ $taxlisthash->{$tax} }[1 ... scalar(@{ $taxlisthash->{$tax} })] ) {
776       next unless ref($_) eq 'FS::cust_bill_pkg';
777      
778       my @cust_tax_exempt_pkg = splice( @{ $_->_cust_tax_exempt_pkg } );
779
780       next unless @cust_tax_exempt_pkg; #just avoiding the prob when irrelevant?
781       die "can't distribute tax exemptions: no line item for ".  Dumper($_).
782           " in packagemap ". join(',', sort {$a<=>$b} keys %packagemap). "\n"
783         unless $packagemap{$_->pkgnum};
784
785       push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
786            @cust_tax_exempt_pkg;
787     }
788   }
789
790   #consolidate and create tax line items
791   warn "consolidating and generating...\n" if $DEBUG > 2;
792   foreach my $taxname ( keys %taxname ) {
793     my $tax = 0;
794     my %seen = ();
795     my @cust_bill_pkg_tax_location = ();
796     my @cust_bill_pkg_tax_rate_location = ();
797     warn "adding $taxname\n" if $DEBUG > 1;
798     foreach my $taxitem ( @{ $taxname{$taxname} } ) {
799       next if $seen{$taxitem}++;
800       warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
801       $tax += $tax{$taxitem};
802       push @cust_bill_pkg_tax_location,
803         map { new FS::cust_bill_pkg_tax_location $_ }
804             @{ $tax_location{ $taxitem } };
805       push @cust_bill_pkg_tax_rate_location,
806         map { new FS::cust_bill_pkg_tax_rate_location $_ }
807             @{ $tax_rate_location{ $taxitem } };
808     }
809     next unless $tax;
810
811     $tax = sprintf('%.2f', $tax );
812   
813     my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
814                                                    'disabled'     => '',
815                                                  },
816                                );
817
818     my @display = ();
819     if ( $pkg_category and
820          $conf->config('invoice_latexsummary') ||
821          $conf->config('invoice_htmlsummary')
822        )
823     {
824
825       my %hash = (  'section' => $pkg_category->categoryname );
826       push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
827
828     }
829
830     push @tax_line_items, new FS::cust_bill_pkg {
831       'pkgnum'   => 0,
832       'setup'    => $tax,
833       'recur'    => 0,
834       'sdate'    => '',
835       'edate'    => '',
836       'itemdesc' => $taxname,
837       'display'  => \@display,
838       'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
839       'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
840     };
841
842   }
843
844   \@tax_line_items;
845 }
846
847 sub _make_lines {
848   my ($self, %params) = @_;
849
850   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
851
852   my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
853   my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
854   my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
855   my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
856   my $total_setup = $params{setup} or die "no setup accumulator specified";
857   my $total_recur = $params{recur} or die "no recur accumulator specified";
858   my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
859   my $time = $params{'time'} or die "no time specified";
860   my (%options) = %{$params{options}};
861
862   my $dbh = dbh;
863   my $real_pkgpart = $params{real_pkgpart};
864   my %hash = $cust_pkg->hash;
865   my $old_cust_pkg = new FS::cust_pkg \%hash;
866
867   my @details = ();
868   my @discounts = ();
869   my $lineitems = 0;
870
871   $cust_pkg->pkgpart($part_pkg->pkgpart);
872
873   ###
874   # bill setup
875   ###
876
877   my $setup = 0;
878   my $unitsetup = 0;
879   my %setup_param = ();
880   if (     ! $options{recurring_only}
881        and ! $options{cancel}
882        and ( $options{'resetup'}
883              || ( ! $cust_pkg->setup
884                   && ( ! $cust_pkg->start_date
885                        || $cust_pkg->start_date <= $self->day_end($time)
886                      )
887                   && ( ! $conf->exists('disable_setup_suspended_pkgs')
888                        || ( $conf->exists('disable_setup_suspended_pkgs') &&
889                             ! $cust_pkg->getfield('susp')
890                           )
891                      )
892                 )
893            )
894      )
895   {
896     
897     warn "    bill setup\n" if $DEBUG > 1;
898
899     unless ( $cust_pkg->waive_setup ) {
900         $lineitems++;
901
902         $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
903         return "$@ running calc_setup for $cust_pkg\n"
904           if $@;
905
906         $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
907     }
908
909     $cust_pkg->setfield('setup', $time)
910       unless $cust_pkg->setup;
911           #do need it, but it won't get written to the db
912           #|| $cust_pkg->pkgpart != $real_pkgpart;
913
914     $cust_pkg->setfield('start_date', '')
915       if $cust_pkg->start_date;
916
917   }
918
919   ###
920   # bill recurring fee
921   ### 
922
923   #XXX unit stuff here too
924   my $recur = 0;
925   my $unitrecur = 0;
926   my $sdate;
927   if (     ! $cust_pkg->start_date
928        and ( ! $cust_pkg->susp || $part_pkg->option('suspend_bill', 1) )
929        and
930             ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $self->day_end($time) )
931          || ( $part_pkg->plan eq 'voip_cdr'
932                && $part_pkg->option('bill_every_call')
933             )
934          || $options{cancel}
935   ) {
936
937     # XXX should this be a package event?  probably.  events are called
938     # at collection time at the moment, though...
939     $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
940       if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
941       #don't want to reset usage just cause we want a line item??
942       #&& $part_pkg->pkgpart == $real_pkgpart;
943
944     warn "    bill recur\n" if $DEBUG > 1;
945     $lineitems++;
946
947     # XXX shared with $recur_prog
948     $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
949              || $cust_pkg->setup
950              || $time;
951
952     #over two params!  lets at least switch to a hashref for the rest...
953     my $increment_next_bill = ( $part_pkg->freq ne '0'
954                                 && ( $cust_pkg->getfield('bill') || 0 ) <= $self->day_end($time)
955                                 && !$options{cancel}
956                               );
957     my %param = ( 'precommit_hooks'     => $precommit_hooks,
958                   'increment_next_bill' => $increment_next_bill,
959                   'discounts'           => \@discounts,
960                   'real_pkgpart'        => $real_pkgpart,
961                   'freq_override'       => $options{freq_override} || '',
962                   'setup_fee'           => 0,
963                   %setup_param,
964                 );
965
966     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
967
968     # There may be some part_pkg for which this is wrong.  Only those
969     # which can_discount are supported.
970     # (the UI should prevent adding discounts to these at the moment)
971
972     warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
973          " for pkgpart ". $cust_pkg->pkgpart.
974          " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
975       if $DEBUG > 2;
976            
977     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
978     return "$@ running $method for $cust_pkg\n"
979       if ( $@ );
980
981     if ( $increment_next_bill ) {
982
983       my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
984       return "unparsable frequency: ". $part_pkg->freq
985         if $next_bill == -1;
986   
987       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
988       # only for figuring next bill date, nothing else, so, reset $sdate again
989       # here
990       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
991       #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
992       $cust_pkg->last_bill($sdate);
993
994       $cust_pkg->setfield('bill', $next_bill );
995
996     }
997
998     if ( $param{'setup_fee'} ) {
999       # Add an additional setup fee at the billing stage.
1000       # Used for prorate_defer_bill.
1001       $setup += $param{'setup_fee'};
1002       $unitsetup += $param{'setup_fee'};
1003       $lineitems++;
1004     }
1005
1006     if ( defined $param{'discount_left_setup'} ) {
1007         foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1008             $setup -= $discount_setup;
1009         }
1010     }
1011
1012   }
1013
1014   warn "\$setup is undefined" unless defined($setup);
1015   warn "\$recur is undefined" unless defined($recur);
1016   warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1017   
1018   ###
1019   # If there's line items, create em cust_bill_pkg records
1020   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1021   ###
1022
1023   if ( $lineitems ) {
1024
1025     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1026       # hmm.. and if just the options are modified in some weird price plan?
1027   
1028       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
1029         if $DEBUG >1;
1030   
1031       my $error = $cust_pkg->replace( $old_cust_pkg,
1032                                       'depend_jobnum'=>$options{depend_jobnum},
1033                                       'options' => { $cust_pkg->options },
1034                                     )
1035         unless $options{no_commit};
1036       return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1037         if $error; #just in case
1038     }
1039   
1040     $setup = sprintf( "%.2f", $setup );
1041     $recur = sprintf( "%.2f", $recur );
1042     if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1043       return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1044     }
1045     if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1046       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1047     }
1048
1049     my $discount_show_always = ($recur == 0 && scalar(@discounts) 
1050                                 && $conf->exists('discount-show-always'));
1051
1052     if (    $setup != 0
1053          || $recur != 0
1054          || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1055          || $discount_show_always
1056          || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1057          || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1058        ) 
1059     {
1060
1061       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
1062         if $DEBUG > 1;
1063
1064       my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1065       if ( $DEBUG > 1 ) {
1066         warn "      adding customer package invoice detail: $_\n"
1067           foreach @cust_pkg_detail;
1068       }
1069       push @details, @cust_pkg_detail;
1070
1071       my $cust_bill_pkg = new FS::cust_bill_pkg {
1072         'pkgnum'    => $cust_pkg->pkgnum,
1073         'setup'     => $setup,
1074         'unitsetup' => $unitsetup,
1075         'recur'     => $recur,
1076         'unitrecur' => $unitrecur,
1077         'quantity'  => $cust_pkg->quantity,
1078         'details'   => \@details,
1079         'discounts' => \@discounts,
1080         'hidden'    => $part_pkg->hidden,
1081         'freq'      => $part_pkg->freq,
1082       };
1083
1084       if ( $part_pkg->recur_temporality eq 'preceding' ) {
1085         $cust_bill_pkg->sdate( $hash{last_bill} );
1086         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
1087         $cust_bill_pkg->edate( $time ) if $options{cancel};
1088       } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) {
1089         $cust_bill_pkg->sdate( $sdate );
1090         $cust_bill_pkg->edate( $cust_pkg->bill );
1091         #$cust_bill_pkg->edate( $time ) if $options{cancel};
1092       }
1093
1094       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1095         unless $part_pkg->pkgpart == $real_pkgpart;
1096
1097       $$total_setup += $setup;
1098       $$total_recur += $recur;
1099
1100       ###
1101       # handle taxes
1102       ###
1103
1104       unless ( $discount_show_always ) {
1105           my $error = 
1106             $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
1107           return $error if $error;
1108       }
1109
1110       push @$cust_bill_pkgs, $cust_bill_pkg;
1111
1112     } #if $setup != 0 || $recur != 0
1113       
1114   } #if $line_items
1115
1116   '';
1117
1118 }
1119
1120 sub _handle_taxes {
1121   my $self = shift;
1122   my $part_pkg = shift;
1123   my $taxlisthash = shift;
1124   my $cust_bill_pkg = shift;
1125   my $cust_pkg = shift;
1126   my $invoice_time = shift;
1127   my $real_pkgpart = shift;
1128   my $options = shift;
1129
1130   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1131
1132   my %cust_bill_pkg = ();
1133   my %taxes = ();
1134     
1135   my @classes;
1136   #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
1137   push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
1138   push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
1139   push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
1140
1141   if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
1142
1143     if ( $conf->exists('enable_taxproducts')
1144          && ( scalar($part_pkg->part_pkg_taxoverride)
1145               || $part_pkg->has_taxproduct
1146             )
1147        )
1148     {
1149
1150       foreach my $class (@classes) {
1151         my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
1152         return $err_or_ref unless ref($err_or_ref);
1153         $taxes{$class} = $err_or_ref;
1154       }
1155
1156       unless (exists $taxes{''}) {
1157         my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
1158         return $err_or_ref unless ref($err_or_ref);
1159         $taxes{''} = $err_or_ref;
1160       }
1161
1162     } else {
1163
1164       my @loc_keys = qw( city county state country );
1165       my %taxhash;
1166       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1167         my $cust_location = $cust_pkg->cust_location;
1168         %taxhash = map { $_ => $cust_location->$_()    } @loc_keys;
1169       } else {
1170         my $prefix = 
1171           ( $conf->exists('tax-ship_address') && length($self->ship_last) )
1172           ? 'ship_'
1173           : '';
1174         %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
1175       }
1176
1177       $taxhash{'taxclass'} = $part_pkg->taxclass;
1178
1179       my @taxes = ();
1180       my %taxhash_elim = %taxhash;
1181       my @elim = qw( city county state );
1182       do { 
1183
1184         #first try a match with taxclass
1185         @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1186
1187         if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1188           #then try a match without taxclass
1189           my %no_taxclass = %taxhash_elim;
1190           $no_taxclass{ 'taxclass' } = '';
1191           @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1192         }
1193
1194         $taxhash_elim{ shift(@elim) } = '';
1195
1196       } while ( !scalar(@taxes) && scalar(@elim) );
1197
1198       @taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
1199                     @taxes
1200         if $self->cust_main_exemption; #just to be safe
1201
1202       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
1203         foreach (@taxes) {
1204           $_->set('pkgnum',      $cust_pkg->pkgnum );
1205           $_->set('locationnum', $cust_pkg->locationnum );
1206         }
1207       }
1208
1209       $taxes{''} = [ @taxes ];
1210       $taxes{'setup'} = [ @taxes ];
1211       $taxes{'recur'} = [ @taxes ];
1212       $taxes{$_} = [ @taxes ] foreach (@classes);
1213
1214       # # maybe eliminate this entirely, along with all the 0% records
1215       # unless ( @taxes ) {
1216       #   return
1217       #     "fatal: can't find tax rate for state/county/country/taxclass ".
1218       #     join('/', map $taxhash{$_}, qw(state county country taxclass) );
1219       # }
1220
1221     } #if $conf->exists('enable_taxproducts') ...
1222
1223   }
1224
1225   #what's this doing in the middle of _handle_taxes?  probably should split
1226   #this into three parts above in _make_lines
1227   $cust_bill_pkg->set_display(   part_pkg     => $part_pkg,
1228                                  real_pkgpart => $real_pkgpart,
1229                              );
1230
1231   my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1232   foreach my $key (keys %tax_cust_bill_pkg) {
1233     my @taxes = @{ $taxes{$key} || [] };
1234     my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1235
1236     my %localtaxlisthash = ();
1237     foreach my $tax ( @taxes ) {
1238
1239       my $taxname = ref( $tax ). ' '. $tax->taxnum;
1240 #      $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
1241 #                  ' locationnum'. $cust_pkg->locationnum
1242 #        if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
1243
1244       $taxlisthash->{ $taxname } ||= [ $tax ];
1245       push @{ $taxlisthash->{ $taxname  } }, $tax_cust_bill_pkg;
1246
1247       $localtaxlisthash{ $taxname } ||= [ $tax ];
1248       push @{ $localtaxlisthash{ $taxname  } }, $tax_cust_bill_pkg;
1249
1250     }
1251
1252     warn "finding taxed taxes...\n" if $DEBUG > 2;
1253     foreach my $tax ( keys %localtaxlisthash ) {
1254       my $tax_object = shift @{ $localtaxlisthash{$tax} };
1255       warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1256         if $DEBUG > 2;
1257       next unless $tax_object->can('tax_on_tax');
1258
1259       foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
1260         my $totname = ref( $tot ). ' '. $tot->taxnum;
1261
1262         warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1263           if $DEBUG > 2;
1264         next unless exists( $localtaxlisthash{ $totname } ); # only increase
1265                                                              # existing taxes
1266         warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1267         my $hashref_or_error = 
1268           $tax_object->taxline( $localtaxlisthash{$tax},
1269                                 'custnum'      => $self->custnum,
1270                                 'invoice_time' => $invoice_time,
1271                               );
1272         return $hashref_or_error
1273           unless ref($hashref_or_error);
1274         
1275         $taxlisthash->{ $totname } ||= [ $tot ];
1276         push @{ $taxlisthash->{ $totname  } }, $hashref_or_error->{amount};
1277
1278       }
1279     }
1280
1281   }
1282
1283   '';
1284 }
1285
1286 sub _gather_taxes {
1287   my $self = shift;
1288   my $part_pkg = shift;
1289   my $class = shift;
1290   my $cust_pkg = shift;
1291
1292   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1293
1294   my $geocode;
1295   if ( $cust_pkg->locationnum && $conf->exists('tax-pkg_address') ) {
1296     $geocode = $cust_pkg->cust_location->geocode('cch');
1297   } else {
1298     $geocode = $self->geocode('cch');
1299   }
1300
1301   my @taxes = ();
1302
1303   my @taxclassnums = map { $_->taxclassnum }
1304                      $part_pkg->part_pkg_taxoverride($class);
1305
1306   unless (@taxclassnums) {
1307     @taxclassnums = map { $_->taxclassnum }
1308                     grep { $_->taxable eq 'Y' }
1309                     $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
1310   }
1311   warn "Found taxclassnum values of ". join(',', @taxclassnums)
1312     if $DEBUG;
1313
1314   my $extra_sql =
1315     "AND (".
1316     join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
1317
1318   @taxes = qsearch({ 'table' => 'tax_rate',
1319                      'hashref' => { 'geocode' => $geocode, },
1320                      'extra_sql' => $extra_sql,
1321                   })
1322     if scalar(@taxclassnums);
1323
1324   warn "Found taxes ".
1325        join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n" 
1326    if $DEBUG;
1327
1328   [ @taxes ];
1329
1330 }
1331
1332 =item collect [ HASHREF | OPTION => VALUE ... ]
1333
1334 (Attempt to) collect money for this customer's outstanding invoices (see
1335 L<FS::cust_bill>).  Usually used after the bill method.
1336
1337 Actions are now triggered by billing events; see L<FS::part_event> and the
1338 billing events web interface.  Old-style invoice events (see
1339 L<FS::part_bill_event>) have been deprecated.
1340
1341 If there is an error, returns the error, otherwise returns false.
1342
1343 Options are passed as name-value pairs.
1344
1345 Currently available options are:
1346
1347 =over 4
1348
1349 =item invoice_time
1350
1351 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.
1352
1353 =item retry
1354
1355 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1356
1357 =item check_freq
1358
1359 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1360
1361 =item quiet
1362
1363 set true to surpress email card/ACH decline notices.
1364
1365 =item debug
1366
1367 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)
1368
1369 =back
1370
1371 # =item payby
1372 #
1373 # allows for one time override of normal customer billing method
1374
1375 =cut
1376
1377 sub collect {
1378   my( $self, %options ) = @_;
1379
1380   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1381
1382   my $invoice_time = $options{'invoice_time'} || time;
1383
1384   #put below somehow?
1385   local $SIG{HUP} = 'IGNORE';
1386   local $SIG{INT} = 'IGNORE';
1387   local $SIG{QUIT} = 'IGNORE';
1388   local $SIG{TERM} = 'IGNORE';
1389   local $SIG{TSTP} = 'IGNORE';
1390   local $SIG{PIPE} = 'IGNORE';
1391
1392   my $oldAutoCommit = $FS::UID::AutoCommit;
1393   local $FS::UID::AutoCommit = 0;
1394   my $dbh = dbh;
1395
1396   $self->select_for_update; #mutex
1397
1398   if ( $DEBUG ) {
1399     my $balance = $self->balance;
1400     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1401   }
1402
1403   if ( exists($options{'retry_card'}) ) {
1404     carp 'retry_card option passed to collect is deprecated; use retry';
1405     $options{'retry'} ||= $options{'retry_card'};
1406   }
1407   if ( exists($options{'retry'}) && $options{'retry'} ) {
1408     my $error = $self->retry_realtime;
1409     if ( $error ) {
1410       $dbh->rollback if $oldAutoCommit;
1411       return $error;
1412     }
1413   }
1414
1415   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1416
1417   #never want to roll back an event just because it returned an error
1418   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1419
1420   $self->do_cust_event(
1421     'debug'      => ( $options{'debug'} || 0 ),
1422     'time'       => $invoice_time,
1423     'check_freq' => $options{'check_freq'},
1424     'stage'      => 'collect',
1425   );
1426
1427 }
1428
1429 =item retry_realtime
1430
1431 Schedules realtime / batch  credit card / electronic check / LEC billing
1432 events for for retry.  Useful if card information has changed or manual
1433 retry is desired.  The 'collect' method must be called to actually retry
1434 the transaction.
1435
1436 Implementation details: For either this customer, or for each of this
1437 customer's open invoices, changes the status of the first "done" (with
1438 statustext error) realtime processing event to "failed".
1439
1440 =cut
1441
1442 sub retry_realtime {
1443   my $self = shift;
1444
1445   local $SIG{HUP} = 'IGNORE';
1446   local $SIG{INT} = 'IGNORE';
1447   local $SIG{QUIT} = 'IGNORE';
1448   local $SIG{TERM} = 'IGNORE';
1449   local $SIG{TSTP} = 'IGNORE';
1450   local $SIG{PIPE} = 'IGNORE';
1451
1452   my $oldAutoCommit = $FS::UID::AutoCommit;
1453   local $FS::UID::AutoCommit = 0;
1454   my $dbh = dbh;
1455
1456   #a little false laziness w/due_cust_event (not too bad, really)
1457
1458   my $join = FS::part_event_condition->join_conditions_sql;
1459   my $order = FS::part_event_condition->order_conditions_sql;
1460   my $mine = 
1461   '( '
1462    . join ( ' OR ' , map { 
1463     "( part_event.eventtable = " . dbh->quote($_) 
1464     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ;
1465    } FS::part_event->eventtables)
1466    . ') ';
1467
1468   #here is the agent virtualization
1469   my $agent_virt = " (    part_event.agentnum IS NULL
1470                        OR part_event.agentnum = ". $self->agentnum. ' )';
1471
1472   #XXX this shouldn't be hardcoded, actions should declare it...
1473   my @realtime_events = qw(
1474     cust_bill_realtime_card
1475     cust_bill_realtime_check
1476     cust_bill_realtime_lec
1477     cust_bill_batch
1478   );
1479
1480   my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'",
1481                                                   @realtime_events
1482                                      ).
1483                           ' ) ';
1484
1485   my @cust_event = qsearchs({
1486     'table'     => 'cust_event',
1487     'select'    => 'cust_event.*',
1488     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1489     'hashref'   => { 'status' => 'done' },
1490     'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ".
1491                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1492   });
1493
1494   my %seen_invnum = ();
1495   foreach my $cust_event (@cust_event) {
1496
1497     #max one for the customer, one for each open invoice
1498     my $cust_X = $cust_event->cust_X;
1499     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1500                           ? $cust_X->invnum
1501                           : 0
1502                         }++
1503          or $cust_event->part_event->eventtable eq 'cust_bill'
1504             && ! $cust_X->owed;
1505
1506     my $error = $cust_event->retry;
1507     if ( $error ) {
1508       $dbh->rollback if $oldAutoCommit;
1509       return "error scheduling event for retry: $error";
1510     }
1511
1512   }
1513
1514   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1515   '';
1516
1517 }
1518
1519 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1520
1521 Runs billing events; see L<FS::part_event> and the billing events web
1522 interface.
1523
1524 If there is an error, returns the error, otherwise returns false.
1525
1526 Options are passed as name-value pairs.
1527
1528 Currently available options are:
1529
1530 =over 4
1531
1532 =item time
1533
1534 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.
1535
1536 =item check_freq
1537
1538 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1539
1540 =item stage
1541
1542 "collect" (the default) or "pre-bill"
1543
1544 =item quiet
1545  
1546 set true to surpress email card/ACH decline notices.
1547
1548 =item debug
1549
1550 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)
1551
1552 =back
1553 =cut
1554
1555 # =item payby
1556 #
1557 # allows for one time override of normal customer billing method
1558
1559 # =item retry
1560 #
1561 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1562
1563 sub do_cust_event {
1564   my( $self, %options ) = @_;
1565
1566   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1567
1568   my $time = $options{'time'} || time;
1569
1570   #put below somehow?
1571   local $SIG{HUP} = 'IGNORE';
1572   local $SIG{INT} = 'IGNORE';
1573   local $SIG{QUIT} = 'IGNORE';
1574   local $SIG{TERM} = 'IGNORE';
1575   local $SIG{TSTP} = 'IGNORE';
1576   local $SIG{PIPE} = 'IGNORE';
1577
1578   my $oldAutoCommit = $FS::UID::AutoCommit;
1579   local $FS::UID::AutoCommit = 0;
1580   my $dbh = dbh;
1581
1582   $self->select_for_update; #mutex
1583
1584   if ( $DEBUG ) {
1585     my $balance = $self->balance;
1586     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1587   }
1588
1589 #  if ( exists($options{'retry_card'}) ) {
1590 #    carp 'retry_card option passed to collect is deprecated; use retry';
1591 #    $options{'retry'} ||= $options{'retry_card'};
1592 #  }
1593 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
1594 #    my $error = $self->retry_realtime;
1595 #    if ( $error ) {
1596 #      $dbh->rollback if $oldAutoCommit;
1597 #      return $error;
1598 #    }
1599 #  }
1600
1601   # false laziness w/pay_batch::import_results
1602
1603   my $due_cust_event = $self->due_cust_event(
1604     'debug'      => ( $options{'debug'} || 0 ),
1605     'time'       => $time,
1606     'check_freq' => $options{'check_freq'},
1607     'stage'      => ( $options{'stage'} || 'collect' ),
1608   );
1609   unless( ref($due_cust_event) ) {
1610     $dbh->rollback if $oldAutoCommit;
1611     return $due_cust_event;
1612   }
1613
1614   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1615   #never want to roll back an event just because it or a different one
1616   # returned an error
1617   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1618
1619   foreach my $cust_event ( @$due_cust_event ) {
1620
1621     #XXX lock event
1622     
1623     #re-eval event conditions (a previous event could have changed things)
1624     unless ( $cust_event->test_conditions( 'time' => $time ) ) {
1625       #don't leave stray "new/locked" records around
1626       my $error = $cust_event->delete;
1627       return $error if $error;
1628       next;
1629     }
1630
1631     {
1632       local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1633         if $options{'quiet'};
1634       warn "  running cust_event ". $cust_event->eventnum. "\n"
1635         if $DEBUG > 1;
1636
1637       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1638       if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1639         #XXX wtf is this?  figure out a proper dealio with return value
1640         #from do_event
1641         return $error;
1642       }
1643     }
1644
1645   }
1646
1647   '';
1648
1649 }
1650
1651 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
1652
1653 Inserts database records for and returns an ordered listref of new events due
1654 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
1655 events are due, an empty listref is returned.  If there is an error, returns a
1656 scalar error message.
1657
1658 To actually run the events, call each event's test_condition method, and if
1659 still true, call the event's do_event method.
1660
1661 Options are passed as a hashref or as a list of name-value pairs.  Available
1662 options are:
1663
1664 =over 4
1665
1666 =item check_freq
1667
1668 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.
1669
1670 =item stage
1671
1672 "collect" (the default) or "pre-bill"
1673
1674 =item time
1675
1676 "Current time" for the events.
1677
1678 =item debug
1679
1680 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)
1681
1682 =item eventtable
1683
1684 Only return events for the specified eventtable (by default, events of all eventtables are returned)
1685
1686 =item objects
1687
1688 Explicitly pass the objects to be tested (typically used with eventtable).
1689
1690 =item testonly
1691
1692 Set to true to return the objects, but not actually insert them into the
1693 database.
1694
1695 =back
1696
1697 =cut
1698
1699 sub due_cust_event {
1700   my $self = shift;
1701   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
1702
1703   #???
1704   #my $DEBUG = $opt{'debug'}
1705   local($DEBUG) = $opt{'debug'}
1706     if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
1707   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1708
1709   warn "$me due_cust_event called with options ".
1710        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
1711     if $DEBUG;
1712
1713   $opt{'time'} ||= time;
1714
1715   local $SIG{HUP} = 'IGNORE';
1716   local $SIG{INT} = 'IGNORE';
1717   local $SIG{QUIT} = 'IGNORE';
1718   local $SIG{TERM} = 'IGNORE';
1719   local $SIG{TSTP} = 'IGNORE';
1720   local $SIG{PIPE} = 'IGNORE';
1721
1722   my $oldAutoCommit = $FS::UID::AutoCommit;
1723   local $FS::UID::AutoCommit = 0;
1724   my $dbh = dbh;
1725
1726   $self->select_for_update #mutex
1727     unless $opt{testonly};
1728
1729   ###
1730   # find possible events (initial search)
1731   ###
1732   
1733   my @cust_event = ();
1734
1735   my @eventtable = $opt{'eventtable'}
1736                      ? ( $opt{'eventtable'} )
1737                      : FS::part_event->eventtables_runorder;
1738
1739   my $check_freq = $opt{'check_freq'} || '1d';
1740
1741   foreach my $eventtable ( @eventtable ) {
1742
1743     my @objects;
1744     if ( $opt{'objects'} ) {
1745
1746       @objects = @{ $opt{'objects'} };
1747
1748     } else {
1749
1750       #my @objects = $self->$eventtable(); # sub cust_main { @{ [ $self ] }; }
1751       if ( $eventtable eq 'cust_main' ) {
1752         @objects = ( $self );
1753       } else {
1754
1755         my $cm_join =
1756           "LEFT JOIN cust_main USING ( custnum )";
1757
1758         #some false laziness w/Cron::bill bill_where
1759
1760         my $join  = FS::part_event_condition->join_conditions_sql( $eventtable);
1761         my $where = FS::part_event_condition->where_conditions_sql($eventtable,
1762                                                            'time'=>$opt{'time'},
1763                                                                   );
1764         $where = $where ? "AND $where" : '';
1765
1766         my $are_part_event = 
1767           "EXISTS ( SELECT 1 FROM part_event $join
1768                       WHERE check_freq = '$check_freq'
1769                         AND eventtable = '$eventtable'
1770                         AND ( disabled = '' OR disabled IS NULL )
1771                         $where
1772                   )
1773           ";
1774         #eofalse
1775
1776         @objects = $self->$eventtable(
1777                      'addl_from' => $cm_join,
1778                      'extra_sql' => " AND $are_part_event",
1779                    );
1780       }
1781
1782     }
1783
1784     my @e_cust_event = ();
1785
1786     my $cross = "CROSS JOIN $eventtable";
1787     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
1788       unless $eventtable eq 'cust_main';
1789
1790     foreach my $object ( @objects ) {
1791
1792       #this first search uses the condition_sql magic for optimization.
1793       #the more possible events we can eliminate in this step the better
1794
1795       my $cross_where = '';
1796       my $pkey = $object->primary_key;
1797       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
1798
1799       my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
1800       my $extra_sql =
1801         FS::part_event_condition->where_conditions_sql( $eventtable,
1802                                                         'time'=>$opt{'time'}
1803                                                       );
1804       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
1805
1806       $extra_sql = "AND $extra_sql" if $extra_sql;
1807
1808       #here is the agent virtualization
1809       $extra_sql .= " AND (    part_event.agentnum IS NULL
1810                             OR part_event.agentnum = ". $self->agentnum. ' )';
1811
1812       $extra_sql .= " $order";
1813
1814       warn "searching for events for $eventtable ". $object->$pkey. "\n"
1815         if $opt{'debug'} > 2;
1816       my @part_event = qsearch( {
1817         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
1818         'select'    => 'part_event.*',
1819         'table'     => 'part_event',
1820         'addl_from' => "$cross $join",
1821         'hashref'   => { 'check_freq' => $check_freq,
1822                          'eventtable' => $eventtable,
1823                          'disabled'   => '',
1824                        },
1825         'extra_sql' => "AND $cross_where $extra_sql",
1826       } );
1827
1828       if ( $DEBUG > 2 ) {
1829         my $pkey = $object->primary_key;
1830         warn "      ". scalar(@part_event).
1831              " possible events found for $eventtable ". $object->$pkey(). "\n";
1832       }
1833
1834       push @e_cust_event, map { $_->new_cust_event($object) } @part_event;
1835
1836     }
1837
1838     warn "    ". scalar(@e_cust_event).
1839          " subtotal possible cust events found for $eventtable\n"
1840       if $DEBUG > 1;
1841
1842     push @cust_event, @e_cust_event;
1843
1844   }
1845
1846   warn "  ". scalar(@cust_event).
1847        " total possible cust events found in initial search\n"
1848     if $DEBUG; # > 1;
1849
1850
1851   ##
1852   # test stage
1853   ##
1854
1855   $opt{stage} ||= 'collect';
1856   @cust_event =
1857     grep { my $stage = $_->part_event->event_stage;
1858            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
1859          }
1860          @cust_event;
1861
1862   ##
1863   # test conditions
1864   ##
1865   
1866   my %unsat = ();
1867
1868   @cust_event = grep $_->test_conditions( 'time'          => $opt{'time'},
1869                                           'stats_hashref' => \%unsat ),
1870                      @cust_event;
1871
1872   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
1873     if $DEBUG; # > 1;
1874
1875   warn "    invalid conditions not eliminated with condition_sql:\n".
1876        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
1877     if keys %unsat && $DEBUG; # > 1;
1878
1879   ##
1880   # insert
1881   ##
1882
1883   unless( $opt{testonly} ) {
1884     foreach my $cust_event ( @cust_event ) {
1885
1886       my $error = $cust_event->insert();
1887       if ( $error ) {
1888         $dbh->rollback if $oldAutoCommit;
1889         return $error;
1890       }
1891                                        
1892     }
1893   }
1894
1895   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1896
1897   ##
1898   # return
1899   ##
1900
1901   warn "  returning events: ". Dumper(@cust_event). "\n"
1902     if $DEBUG > 2;
1903
1904   \@cust_event;
1905
1906 }
1907
1908 =item apply_payments_and_credits [ OPTION => VALUE ... ]
1909
1910 Applies unapplied payments and credits.
1911
1912 In most cases, this new method should be used in place of sequential
1913 apply_payments and apply_credits methods.
1914
1915 A hash of optional arguments may be passed.  Currently "manual" is supported.
1916 If true, a payment receipt is sent instead of a statement when
1917 'payment_receipt_email' configuration option is set.
1918
1919 If there is an error, returns the error, otherwise returns false.
1920
1921 =cut
1922
1923 sub apply_payments_and_credits {
1924   my( $self, %options ) = @_;
1925
1926   local $SIG{HUP} = 'IGNORE';
1927   local $SIG{INT} = 'IGNORE';
1928   local $SIG{QUIT} = 'IGNORE';
1929   local $SIG{TERM} = 'IGNORE';
1930   local $SIG{TSTP} = 'IGNORE';
1931   local $SIG{PIPE} = 'IGNORE';
1932
1933   my $oldAutoCommit = $FS::UID::AutoCommit;
1934   local $FS::UID::AutoCommit = 0;
1935   my $dbh = dbh;
1936
1937   $self->select_for_update; #mutex
1938
1939   foreach my $cust_bill ( $self->open_cust_bill ) {
1940     my $error = $cust_bill->apply_payments_and_credits(%options);
1941     if ( $error ) {
1942       $dbh->rollback if $oldAutoCommit;
1943       return "Error applying: $error";
1944     }
1945   }
1946
1947   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1948   ''; #no error
1949
1950 }
1951
1952 =item apply_credits OPTION => VALUE ...
1953
1954 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1955 to outstanding invoice balances in chronological order (or reverse
1956 chronological order if the I<order> option is set to B<newest>) and returns the
1957 value of any remaining unapplied credits available for refund (see
1958 L<FS::cust_refund>).
1959
1960 Dies if there is an error.
1961
1962 =cut
1963
1964 sub apply_credits {
1965   my $self = shift;
1966   my %opt = @_;
1967
1968   local $SIG{HUP} = 'IGNORE';
1969   local $SIG{INT} = 'IGNORE';
1970   local $SIG{QUIT} = 'IGNORE';
1971   local $SIG{TERM} = 'IGNORE';
1972   local $SIG{TSTP} = 'IGNORE';
1973   local $SIG{PIPE} = 'IGNORE';
1974
1975   my $oldAutoCommit = $FS::UID::AutoCommit;
1976   local $FS::UID::AutoCommit = 0;
1977   my $dbh = dbh;
1978
1979   $self->select_for_update; #mutex
1980
1981   unless ( $self->total_unapplied_credits ) {
1982     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1983     return 0;
1984   }
1985
1986   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1987       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1988
1989   my @invoices = $self->open_cust_bill;
1990   @invoices = sort { $b->_date <=> $a->_date } @invoices
1991     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
1992
1993   if ( $conf->exists('pkg-balances') ) {
1994     # limit @credits to those w/ a pkgnum grepped from $self
1995     my %pkgnums = ();
1996     foreach my $i (@invoices) {
1997       foreach my $li ( $i->cust_bill_pkg ) {
1998         $pkgnums{$li->pkgnum} = 1;
1999       }
2000     }
2001     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2002   }
2003
2004   my $credit;
2005
2006   foreach my $cust_bill ( @invoices ) {
2007
2008     if ( !defined($credit) || $credit->credited == 0) {
2009       $credit = pop @credits or last;
2010     }
2011
2012     my $owed;
2013     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2014       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2015     } else {
2016       $owed = $cust_bill->owed;
2017     }
2018     unless ( $owed > 0 ) {
2019       push @credits, $credit;
2020       next;
2021     }
2022
2023     my $amount = min( $credit->credited, $owed );
2024     
2025     my $cust_credit_bill = new FS::cust_credit_bill ( {
2026       'crednum' => $credit->crednum,
2027       'invnum'  => $cust_bill->invnum,
2028       'amount'  => $amount,
2029     } );
2030     $cust_credit_bill->pkgnum( $credit->pkgnum )
2031       if $conf->exists('pkg-balances') && $credit->pkgnum;
2032     my $error = $cust_credit_bill->insert;
2033     if ( $error ) {
2034       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2035       die $error;
2036     }
2037     
2038     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2039
2040   }
2041
2042   my $total_unapplied_credits = $self->total_unapplied_credits;
2043
2044   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2045
2046   return $total_unapplied_credits;
2047 }
2048
2049 =item apply_payments  [ OPTION => VALUE ... ]
2050
2051 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2052 to outstanding invoice balances in chronological order.
2053
2054  #and returns the value of any remaining unapplied payments.
2055
2056 A hash of optional arguments may be passed.  Currently "manual" is supported.
2057 If true, a payment receipt is sent instead of a statement when
2058 'payment_receipt_email' configuration option is set.
2059
2060 Dies if there is an error.
2061
2062 =cut
2063
2064 sub apply_payments {
2065   my( $self, %options ) = @_;
2066
2067   local $SIG{HUP} = 'IGNORE';
2068   local $SIG{INT} = 'IGNORE';
2069   local $SIG{QUIT} = 'IGNORE';
2070   local $SIG{TERM} = 'IGNORE';
2071   local $SIG{TSTP} = 'IGNORE';
2072   local $SIG{PIPE} = 'IGNORE';
2073
2074   my $oldAutoCommit = $FS::UID::AutoCommit;
2075   local $FS::UID::AutoCommit = 0;
2076   my $dbh = dbh;
2077
2078   $self->select_for_update; #mutex
2079
2080   #return 0 unless
2081
2082   my @payments = sort { $b->_date <=> $a->_date }
2083                  grep { $_->unapplied > 0 }
2084                  $self->cust_pay;
2085
2086   my @invoices = sort { $a->_date <=> $b->_date}
2087                  grep { $_->owed > 0 }
2088                  $self->cust_bill;
2089
2090   if ( $conf->exists('pkg-balances') ) {
2091     # limit @payments to those w/ a pkgnum grepped from $self
2092     my %pkgnums = ();
2093     foreach my $i (@invoices) {
2094       foreach my $li ( $i->cust_bill_pkg ) {
2095         $pkgnums{$li->pkgnum} = 1;
2096       }
2097     }
2098     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2099   }
2100
2101   my $payment;
2102
2103   foreach my $cust_bill ( @invoices ) {
2104
2105     if ( !defined($payment) || $payment->unapplied == 0 ) {
2106       $payment = pop @payments or last;
2107     }
2108
2109     my $owed;
2110     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2111       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2112     } else {
2113       $owed = $cust_bill->owed;
2114     }
2115     unless ( $owed > 0 ) {
2116       push @payments, $payment;
2117       next;
2118     }
2119
2120     my $amount = min( $payment->unapplied, $owed );
2121
2122     my $cbp = {
2123       'paynum' => $payment->paynum,
2124       'invnum' => $cust_bill->invnum,
2125       'amount' => $amount,
2126     };
2127     $cbp->{_date} = $payment->_date 
2128         if $options{'manual'} && $options{'backdate_application'};
2129     my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2130     $cust_bill_pay->pkgnum( $payment->pkgnum )
2131       if $conf->exists('pkg-balances') && $payment->pkgnum;
2132     my $error = $cust_bill_pay->insert(%options);
2133     if ( $error ) {
2134       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2135       die $error;
2136     }
2137
2138     redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2139
2140   }
2141
2142   my $total_unapplied_payments = $self->total_unapplied_payments;
2143
2144   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2145
2146   return $total_unapplied_payments;
2147 }
2148
2149 =back
2150
2151 =head1 FLOW
2152
2153   bill_and_collect
2154
2155     cancel_expired_pkgs
2156     suspend_adjourned_pkgs
2157
2158     bill
2159       (do_cust_event pre-bill)
2160       _make_lines
2161         _handle_taxes
2162           (vendor-only) _gather_taxes
2163       _omit_zero_value_bundles
2164       calculate_taxes
2165
2166     apply_payments_and_credits
2167     collect
2168       do_cust_event
2169         due_cust_event
2170
2171 =head1 BUGS
2172
2173 =head1 SEE ALSO
2174
2175 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
2176
2177 =cut
2178
2179 1;