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