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