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