calculate unitrecur on sql_external packages with quantity details, #40558
[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 # XXX we should reconsider whether we even need this
884 sub _omit_zero_value_bundles {
885   my @in = @_;
886
887   my @out = ();
888   my @bundle = ();
889   my $discount_show_always = $conf->exists('discount-show-always');
890   my $show_this = 0;
891
892   # Sort @in the same way we do during invoice rendering, so we can identify
893   # bundles.  See FS::Template_Mixin::_items_nontax.
894   @in = sort { $a->pkgnum <=> $b->pkgnum        or
895                $a->sdate  <=> $b->sdate         or
896                ($a->pkgpart_override ? 0 : -1)  or
897                ($b->pkgpart_override ? 0 : 1)   or
898                $b->hidden cmp $a->hidden        or
899                $a->pkgpart_override <=> $b->pkgpart_override
900              } @in;
901
902   # this is a pack-and-deliver pattern. every time there's a cust_bill_pkg
903   # _without_ pkgpart_override, that's the start of the new bundle. if there's
904   # an existing bundle, and it contains a nonzero amount (or a zero amount 
905   # that's displayable anyway), push all line items in the bundle.
906   foreach my $cust_bill_pkg ( @in ) {
907
908     if (scalar(@bundle) and !$cust_bill_pkg->pkgpart_override) {
909       # ship out this bundle and reset it
910       if ( $show_this ) {
911         push @out, @bundle;
912       }
913       @bundle = ();
914       $show_this = 0;
915     }
916
917     # add this item to the current bundle
918     push @bundle, $cust_bill_pkg;
919
920     # determine if it makes the bundle displayable
921     if (   $cust_bill_pkg->setup > 0
922         or $cust_bill_pkg->recur > 0
923         or $cust_bill_pkg->setup_show_zero
924         or $cust_bill_pkg->recur_show_zero
925         or ($discount_show_always 
926           and scalar(@{ $cust_bill_pkg->get('discounts')}) 
927           )
928     ) {
929       $show_this++;
930     }
931   }
932
933   # last bundle
934   if ( $show_this) {
935     push @out, @bundle;
936   }
937
938   warn "  _omit_zero_value_bundles: ". scalar(@in).
939        '->'. scalar(@out). "\n" #. Dumper(@out). "\n"
940     if $DEBUG > 2;
941
942   @out;
943 }
944
945 sub _make_lines {
946   my ($self, %params) = @_;
947
948   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
949
950   my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
951   my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
952   my $cust_location = $cust_pkg->tax_location;
953   my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
954   my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
955   my $total_setup = $params{setup} or die "no setup accumulator specified";
956   my $total_recur = $params{recur} or die "no recur accumulator specified";
957   my $time = $params{'time'} or die "no time specified";
958   my (%options) = %{$params{options}};
959
960   my $tax_engine = $params{tax_engine};
961
962   if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
963     # this should never happen
964     die 'freq_override billing attempted on non-monthly package '.
965       $cust_pkg->pkgnum;
966   }
967
968   my $dbh = dbh;
969   my $real_pkgpart = $params{real_pkgpart};
970   my %hash = $cust_pkg->hash;
971   my $old_cust_pkg = new FS::cust_pkg \%hash;
972
973   my @details = ();
974   my $lineitems = 0;
975
976   $cust_pkg->pkgpart($part_pkg->pkgpart);
977
978   my $cmp_time = ( $conf->exists('next-bill-ignore-time')
979                      ? day_end( $time )
980                      : $time
981                  );
982
983   ###
984   # bill setup
985   ###
986
987   my $setup = 0;
988   my $unitsetup = 0;
989   my @setup_discounts = ();
990   my %setup_param = ( 'discounts'     => \@setup_discounts,
991                       'real_pkgpart'  => $params{real_pkgpart}
992                     );
993   my $setup_billed_currency = '';
994   my $setup_billed_amount = 0;
995   # Conditions for setting setup date and charging the setup fee:
996   # - this is not a recurring-only billing run
997   # - and the package is not currently being canceled
998   # - and, unless we're specifically told otherwise via 'resetup':
999   #   - it doesn't already HAVE a setup date
1000   #   - or a start date in the future
1001   #   - and it's not suspended
1002   # - and it doesn't have an expire date in the past
1003   #
1004   # The "disable_setup_suspended" option is now obsolete; we never set the
1005   # setup date on a suspended package.
1006   if (     ! $options{recurring_only}
1007        and ! $options{cancel}
1008        and ( $options{'resetup'}
1009              || ( ! $cust_pkg->setup
1010                   && ( ! $cust_pkg->start_date
1011                        || $cust_pkg->start_date <= $cmp_time
1012                      )
1013                   && ( ! $cust_pkg->getfield('susp') )
1014                 )
1015            )
1016        and ( ! $cust_pkg->expire
1017              || $cust_pkg->expire > $cmp_time )
1018      )
1019   {
1020     
1021     warn "    bill setup\n" if $DEBUG > 1;
1022
1023     unless ( $cust_pkg->waive_setup ) {
1024         $lineitems++;
1025
1026         $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
1027         return "$@ running calc_setup for $cust_pkg\n"
1028           if $@;
1029
1030         # Only increment unitsetup here if there IS a setup fee.
1031         # prorate_defer_bill may cause calc_setup on a setup-stage package
1032         # to return zero, and the setup fee to be charged later. (This happens
1033         # when it's first billed on the prorate cutoff day. RT#31276.)
1034         if ( $setup ) {
1035           $unitsetup = $cust_pkg->base_setup()
1036                          || $setup; #XXX uuh
1037         }
1038
1039         if ( $setup_param{'billed_currency'} ) {
1040           $setup_billed_currency = delete $setup_param{'billed_currency'};
1041           $setup_billed_amount   = delete $setup_param{'billed_amount'};
1042         }
1043     }
1044
1045     if ( $cust_pkg->get('setup') ) {
1046       # don't change it
1047     } elsif ( $cust_pkg->get('start_date') ) {
1048       # this allows start_date to be used to set the first bill date
1049       $cust_pkg->set('setup', $cust_pkg->get('start_date'));
1050     } else {
1051       # if unspecified, start it right now
1052       $cust_pkg->set('setup', $time);
1053     }
1054
1055     $cust_pkg->setfield('start_date', '')
1056       if $cust_pkg->start_date;
1057
1058   }
1059
1060   ###
1061   # bill recurring fee
1062   ### 
1063
1064   my $recur = 0;
1065   my $unitrecur = 0;
1066   my @recur_discounts = ();
1067   my $recur_billed_currency = '';
1068   my $recur_billed_amount = 0;
1069   my $sdate;
1070
1071   my $override_quantity;
1072
1073   # Conditions for billing the recurring fee:
1074   # - the package doesn't have a future start date
1075   # - and it's not suspended
1076   #   - unless suspend_bill is enabled on the package or package def
1077   #     - but still not, if the package is on hold
1078   #   - or it's suspended for a delayed cancellation
1079   # - and its next bill date is in the past
1080   #   - or it doesn't have a next bill date yet
1081   #   - or it's a one-time charge
1082   #   - or it's a CDR plan with the "bill_every_call" option
1083   #   - or it's being canceled
1084   # - and it doesn't have an expire date in the past (this can happen with
1085   #   advance billing)
1086   #   - again, unless it's being canceled
1087   if (     ! $cust_pkg->start_date
1088        and 
1089            ( ! $cust_pkg->susp
1090                || ( $cust_pkg->susp != $cust_pkg->order_date
1091                       && (    $cust_pkg->option('suspend_bill',1)
1092                            || ( $part_pkg->option('suspend_bill', 1)
1093                                  && ! $cust_pkg->option('no_suspend_bill',1)
1094                               )
1095                          )
1096                   )
1097                || $cust_pkg->is_status_delay_cancel
1098            )
1099        and
1100             ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time )
1101          || ( $part_pkg->plan eq 'voip_cdr'
1102                && $part_pkg->option('bill_every_call')
1103             )
1104          || $options{cancel}
1105
1106        and
1107           ( ! $cust_pkg->expire
1108             || $cust_pkg->expire > $cmp_time
1109             || $options{cancel}
1110           )
1111   ) {
1112
1113     # XXX should this be a package event?  probably.  events are called
1114     # at collection time at the moment, though...
1115     $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
1116       if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
1117       #don't want to reset usage just cause we want a line item??
1118       #&& $part_pkg->pkgpart == $real_pkgpart;
1119
1120     warn "    bill recur\n" if $DEBUG > 1;
1121     $lineitems++;
1122
1123     # XXX shared with $recur_prog
1124     $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
1125              || $cust_pkg->setup
1126              || $time;
1127
1128     #over two params!  lets at least switch to a hashref for the rest...
1129     my $increment_next_bill = ( $part_pkg->freq ne '0'
1130                                 && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time
1131                                 && !$options{cancel}
1132                               );
1133     my %param = ( %setup_param,
1134                   'precommit_hooks'     => $precommit_hooks,
1135                   'increment_next_bill' => $increment_next_bill,
1136                   'discounts'           => \@recur_discounts,
1137                   'real_pkgpart'        => $real_pkgpart,
1138                   'freq_override'       => $options{freq_override} || '',
1139                   'setup_fee'           => 0,
1140                 );
1141
1142     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
1143
1144     # There may be some part_pkg for which this is wrong.  Only those
1145     # which can_discount are supported.
1146     # (the UI should prevent adding discounts to these at the moment)
1147
1148     warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
1149          " for pkgpart ". $cust_pkg->pkgpart.
1150          " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
1151       if $DEBUG > 2;
1152            
1153     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
1154     return "$@ running $method for $cust_pkg\n"
1155       if ( $@ );
1156
1157     if ($recur eq 'NOTHING') {
1158       # then calc_cancel (or calc_recur but that's not used) has declined to
1159       # generate a recurring lineitem at all. treat this as zero, but also 
1160       # try not to generate a lineitem.
1161       $recur = 0;
1162       $lineitems--;
1163     }
1164
1165     #base_cancel???
1166     $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
1167
1168     if ( $param{'billed_currency'} ) {
1169       $recur_billed_currency = delete $param{'billed_currency'};
1170       $recur_billed_amount   = delete $param{'billed_amount'};
1171     }
1172
1173     if ( $param{'override_quantity'} ) {
1174       $override_quantity = $param{'override_quantity'};
1175       $unitrecur = $recur / $override_quantity;
1176     }
1177
1178     if ( $increment_next_bill ) {
1179
1180       my $next_bill;
1181
1182       if ( my $main_pkg = $cust_pkg->main_pkg ) {
1183         # supplemental package
1184         # to keep in sync with the main package, simulate billing at 
1185         # its frequency
1186         my $main_pkg_freq = $main_pkg->part_pkg->freq;
1187         my $supp_pkg_freq = $part_pkg->freq;
1188         if ( $supp_pkg_freq == 0 or $main_pkg_freq == 0 ) {
1189           # the UI should prevent setting up packages like this, but just
1190           # in case
1191           return "unable to calculate supplemental package period ratio";
1192         }
1193         my $ratio = $supp_pkg_freq / $main_pkg_freq;
1194         if ( $ratio == int($ratio) ) {
1195           # simple case: main package is X months, supp package is X*A months,
1196           # advance supp package to where the main package will be in A cycles.
1197           $next_bill = $sdate;
1198           for (1..$ratio) {
1199             $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
1200           }
1201         } else {
1202           # harder case: main package is X months, supp package is Y months.
1203           # advance supp package by Y months. then if they're within half a 
1204           # month of each other, resync them. this may result in the period
1205           # not being exactly Y months.
1206           $next_bill = $part_pkg->add_freq( $sdate, $supp_pkg_freq );
1207           my $main_next_bill = $main_pkg->bill;
1208           if ( $main_pkg->bill <= $time ) {
1209             # then the main package has not yet been billed on this cycle;
1210             # predict what its bill date will be.
1211             $main_next_bill =
1212               $part_pkg->add_freq( $main_next_bill, $main_pkg_freq );
1213           }
1214           if ( abs($main_next_bill - $next_bill) < 86400*15 ) {
1215             $next_bill = $main_next_bill;
1216           }
1217         }
1218
1219       } else {
1220       # the normal case, not a supplemental package
1221       $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
1222       return "unparsable frequency: ".
1223         ($options{freq_override} || $part_pkg->freq)
1224         if $next_bill == -1;
1225       }  
1226   
1227       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
1228       # only for figuring next bill date, nothing else, so, reset $sdate again
1229       # here
1230       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1231       #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
1232       $cust_pkg->last_bill($sdate);
1233
1234       $cust_pkg->setfield('bill', $next_bill );
1235
1236     }
1237
1238     if ( $param{'setup_fee'} ) {
1239       # Add an additional setup fee at the billing stage.
1240       # Used for prorate_defer_bill.
1241       $setup += $param{'setup_fee'};
1242       $unitsetup = $cust_pkg->base_setup();
1243       $lineitems++;
1244     }
1245
1246     if ( defined $param{'discount_left_setup'} ) {
1247         foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1248             $setup -= $discount_setup;
1249         }
1250     }
1251
1252   } # end of recurring fee
1253
1254   warn "\$setup is undefined" unless defined($setup);
1255   warn "\$recur is undefined" unless defined($recur);
1256   warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1257   
1258   ###
1259   # If there's line items, create em cust_bill_pkg records
1260   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1261   ###
1262
1263   if ( $lineitems ) {
1264
1265     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1266       # hmm.. and if just the options are modified in some weird price plan?
1267   
1268       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
1269         if $DEBUG >1;
1270   
1271       my $error = $cust_pkg->replace( $old_cust_pkg,
1272                                       'depend_jobnum'=>$options{depend_jobnum},
1273                                       'options' => { $cust_pkg->options },
1274                                     )
1275         unless $options{no_commit};
1276       return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1277         if $error; #just in case
1278     }
1279   
1280     $setup = sprintf( "%.2f", $setup );
1281     $recur = sprintf( "%.2f", $recur );
1282     if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1283       return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1284     }
1285     if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1286       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1287     }
1288
1289     my $discount_show_always = $conf->exists('discount-show-always')
1290                                && (    ($setup == 0 && scalar(@setup_discounts))
1291                                     || ($recur == 0 && scalar(@recur_discounts))
1292                                   );
1293
1294     if (    $setup != 0
1295          || $recur != 0
1296          || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1297          || $discount_show_always
1298          || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1299          || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1300        ) 
1301     {
1302
1303       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
1304         if $DEBUG > 1;
1305
1306       my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1307       if ( $DEBUG > 1 ) {
1308         warn "      adding customer package invoice detail: $_\n"
1309           foreach @cust_pkg_detail;
1310       }
1311       push @details, @cust_pkg_detail;
1312
1313       my $cust_bill_pkg = new FS::cust_bill_pkg {
1314         'pkgnum'                => $cust_pkg->pkgnum,
1315         'setup'                 => $setup,
1316         'unitsetup'             => sprintf('%.2f', $unitsetup),
1317         'setup_billed_currency' => $setup_billed_currency,
1318         'setup_billed_amount'   => $setup_billed_amount,
1319         'recur'                 => $recur,
1320         'unitrecur'             => sprintf('%.2f', $unitrecur),
1321         'recur_billed_currency' => $recur_billed_currency,
1322         'recur_billed_amount'   => $recur_billed_amount,
1323         'quantity'              => $override_quantity || $cust_pkg->quantity,
1324         'details'               => \@details,
1325         'discounts'             => [ @setup_discounts, @recur_discounts ],
1326         'hidden'                => $part_pkg->hidden,
1327         'freq'                  => $part_pkg->freq,
1328       };
1329
1330       if ( $part_pkg->option('prorate_defer_bill',1) 
1331            and !$hash{last_bill} ) {
1332         # both preceding and upcoming, technically
1333         $cust_bill_pkg->sdate( $cust_pkg->setup );
1334         $cust_bill_pkg->edate( $cust_pkg->bill );
1335       } elsif ( $part_pkg->recur_temporality eq 'preceding' ) {
1336         $cust_bill_pkg->sdate( $hash{last_bill} );
1337         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
1338         $cust_bill_pkg->edate( $time ) if $options{cancel};
1339       } else { #if ( $part_pkg->recur_temporality eq 'upcoming' )
1340         $cust_bill_pkg->sdate( $sdate );
1341         $cust_bill_pkg->edate( $cust_pkg->bill );
1342         #$cust_bill_pkg->edate( $time ) if $options{cancel};
1343       }
1344
1345       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1346         unless $part_pkg->pkgpart == $real_pkgpart;
1347
1348       $$total_setup += $setup;
1349       $$total_recur += $recur;
1350
1351       ###
1352       # handle taxes
1353       ###
1354       
1355       my $error = $tax_engine->add_sale($cust_bill_pkg);
1356       return $error if $error;
1357
1358       $cust_bill_pkg->set_display(
1359         part_pkg     => $part_pkg,
1360         real_pkgpart => $real_pkgpart,
1361       );
1362
1363       push @$cust_bill_pkgs, $cust_bill_pkg;
1364
1365     } #if $setup != 0 || $recur != 0
1366       
1367   } #if $line_items
1368
1369   '';
1370
1371 }
1372
1373 =item _transfer_balance TO_PKG [ FROM_PKGNUM ]
1374
1375 Takes one argument, a cust_pkg object that is being billed.  This will 
1376 be called only if the package was created by a package change, and has
1377 not been billed since the package change, and package balance tracking
1378 is enabled.  The second argument can be an alternate package number to 
1379 transfer the balance from; this should not be used externally.
1380
1381 Transfers the balance from the previous package (now canceled) to
1382 this package, by crediting one package and creating an invoice item for 
1383 the other.  Inserts the credit and returns the invoice item (so that it 
1384 can be added to an invoice that's being built).
1385
1386 If the previous package was never billed, and was also created by a package
1387 change, then this will also transfer the balance from I<its> previous 
1388 package, and so on, until reaching a package that either has been billed
1389 or was not created by a package change.
1390
1391 =cut
1392
1393 my $balance_transfer_reason;
1394
1395 sub _transfer_balance {
1396   my $self = shift;
1397   my $cust_pkg = shift;
1398   my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
1399   my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
1400
1401   my @transfers;
1402
1403   # if $from_pkg is not the first package in the chain, and it was never 
1404   # billed, walk back
1405   if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
1406     @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
1407   }
1408
1409   my $prev_balance = $self->balance_pkgnum($from_pkgnum);
1410   if ( $prev_balance != 0 ) {
1411     $balance_transfer_reason ||= FS::reason->new_or_existing(
1412       'reason' => 'Package balance transfer',
1413       'type'   => 'Internal adjustment',
1414       'class'  => 'R'
1415     );
1416
1417     my $credit = FS::cust_credit->new({
1418         'custnum'   => $self->custnum,
1419         'amount'    => abs($prev_balance),
1420         'reasonnum' => $balance_transfer_reason->reasonnum,
1421         '_date'     => $cust_pkg->change_date,
1422     });
1423
1424     my $cust_bill_pkg = FS::cust_bill_pkg->new({
1425         'setup'     => 0,
1426         'recur'     => abs($prev_balance),
1427         #'sdate'     => $from_pkg->last_bill, # not sure about this
1428         #'edate'     => $cust_pkg->change_date,
1429         'itemdesc'  => $self->mt('Previous Balance, [_1]',
1430                                  $from_pkg->part_pkg->pkg),
1431     });
1432
1433     if ( $prev_balance > 0 ) {
1434       # credit the old package, charge the new one
1435       $credit->set('pkgnum', $from_pkgnum);
1436       $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
1437     } else {
1438       # the reverse
1439       $credit->set('pkgnum', $cust_pkg->pkgnum);
1440       $cust_bill_pkg->set('pkgnum', $from_pkgnum);
1441     }
1442     my $error = $credit->insert;
1443     die "error transferring package balance from #".$from_pkgnum.
1444         " to #".$cust_pkg->pkgnum.": $error\n" if $error;
1445
1446     push @transfers, $cust_bill_pkg;
1447   } # $prev_balance != 0
1448
1449   return @transfers;
1450 }
1451
1452 #### vestigial code ####
1453
1454 =item handle_taxes TAXLISTHASH CUST_BILL_PKG [ OPTIONS ]
1455
1456 This is _handle_taxes.  It's called once for each cust_bill_pkg generated
1457 from _make_lines.
1458
1459 TAXLISTHASH is a hashref shared across the entire invoice.  It looks like 
1460 this:
1461 {
1462   'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
1463   'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
1464 }
1465
1466 'cust_main_county' can also be 'tax_rate'.  The first object in the array
1467 is always the cust_main_county or tax_rate identified by the key.
1468
1469 That "..." is a list of FS::cust_bill_pkg objects that will be fed to 
1470 the 'taxline' method to calculate the amount of the tax.  This doesn't
1471 happen until calculate_taxes, though.
1472
1473 OPTIONS may include:
1474 - part_item: a part_pkg or part_fee object to be used as the package/fee 
1475   definition.
1476 - location: a cust_location to be used as the billing location.
1477 - cancel: true if this package is being billed on cancellation.  This 
1478   allows tax to be calculated on usage charges only.
1479
1480 If not supplied, part_item will be inferred from the pkgnum or feepart of the
1481 cust_bill_pkg, and location from the pkgnum (or, for fees, the invnum and 
1482 the customer's default service location).
1483
1484 This method will also calculate exemptions for any taxes that apply to the
1485 line item (using the C<set_exemptions> method of L<FS::cust_bill_pkg>) and
1486 attach them.  This is the only place C<set_exemptions> is called in normal
1487 invoice processing.
1488
1489 =cut
1490
1491 sub _handle_taxes {
1492   my $self = shift;
1493   my $taxlisthash = shift;
1494   my $cust_bill_pkg = shift;
1495   my %options = @_;
1496
1497   # at this point I realize that we have enough information to infer all this
1498   # stuff, instead of passing around giant honking argument lists
1499   my $location = $options{location} || $cust_bill_pkg->tax_location;
1500   my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
1501
1502   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1503
1504   return if ( $self->payby eq 'COMP' ); #dubious
1505
1506   if ( $conf->config('enable_taxproducts')
1507        && ( scalar($part_item->part_pkg_taxoverride)
1508             || $part_item->has_taxproduct
1509           )
1510      )
1511     {
1512
1513     # EXTERNAL TAX RATES (via tax_rate)
1514     my %cust_bill_pkg = ();
1515     my %taxes = ();
1516
1517     my @classes;
1518     my $usage = $cust_bill_pkg->usage || 0;
1519     push @classes, $cust_bill_pkg->usage_classes if $usage;
1520     push @classes, 'setup' if $cust_bill_pkg->setup and !$options{cancel};
1521     push @classes, 'recur' if ($cust_bill_pkg->recur - $usage)
1522         and !$options{cancel};
1523     # that's better--probably don't even need $options{cancel} now
1524     # but leave it for now, just to be safe
1525     #
1526     # About $options{cancel}: This protects against charging per-line or
1527     # per-customer or other flat-rate surcharges on a package that's being
1528     # billed on cancellation (which is an out-of-cycle bill and should only
1529     # have usage charges).  See RT#29443.
1530
1531     # customer exemption is now handled in the 'taxline' method
1532     #my $exempt = $conf->exists('cust_class-tax_exempt')
1533     #               ? ( $self->cust_class ? $self->cust_class->tax : '' )
1534     #               : $self->tax;
1535     # standardize this just to be sure
1536     #$exempt = ($exempt eq 'Y') ? 'Y' : '';
1537     #
1538     #if ( !$exempt ) {
1539
1540     unless (exists $taxes{''}) {
1541       # unsure what purpose this serves, but last time I deleted something
1542       # from here just because I didn't see the point, it actually did
1543       # something important.
1544       my $err_or_ref = $self->_gather_taxes($part_item, '', $location);
1545       return $err_or_ref unless ref($err_or_ref);
1546       $taxes{''} = $err_or_ref;
1547     }
1548
1549     # NO DISINTEGRATIONS.
1550     # my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1551     #
1552     # do not call taxline() with any argument except the entire set of
1553     # cust_bill_pkgs on an invoice that are eligible for the tax.
1554
1555     # only calculate exemptions once for each tax rate, even if it's used
1556     # for multiple classes
1557     my %tax_seen = ();
1558  
1559     foreach my $class (@classes) {
1560       my $err_or_ref = $self->_gather_taxes($part_item, $class, $location);
1561       return $err_or_ref unless ref($err_or_ref);
1562       my @taxes = @$err_or_ref;
1563
1564       next if !@taxes;
1565
1566       foreach my $tax ( @taxes ) {
1567
1568         my $tax_id = ref( $tax ). ' '. $tax->taxnum;
1569         # $taxlisthash: keys are tax identifiers ('FS::tax_rate 123456').
1570         # Values are arrayrefs, first the tax object (cust_main_county
1571         # or tax_rate), then the cust_bill_pkg object that the 
1572         # tax applies to, then the tax class (setup, recur, usage classnum).
1573         $taxlisthash->{ $tax_id } ||= [ $tax ];
1574         push @{ $taxlisthash->{ $tax_id  } }, $cust_bill_pkg, $class;
1575
1576         # determine any exemptions that apply
1577         if (!$tax_seen{$tax_id}) {
1578           $cust_bill_pkg->set_exemptions( $tax, custnum => $self->custnum );
1579           $tax_seen{$tax_id} = 1;
1580         }
1581
1582         # tax on tax will be done later, when we actually create the tax
1583         # line items
1584
1585       }
1586     }
1587
1588   } else {
1589
1590     # INTERNAL TAX RATES (cust_main_county)
1591
1592     # We fetch taxes even if the customer is completely exempt,
1593     # because we need to record that fact.
1594
1595     my @loc_keys = qw( district city county state country );
1596     my %taxhash = map { $_ => $location->$_ } @loc_keys;
1597
1598     $taxhash{'taxclass'} = $part_item->taxclass;
1599
1600     warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
1601
1602     my @taxes = (); # entries are cust_main_county objects
1603     my %taxhash_elim = %taxhash;
1604     my @elim = qw( district city county state );
1605     do { 
1606
1607       #first try a match with taxclass
1608       @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1609
1610       if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1611         #then try a match without taxclass
1612         my %no_taxclass = %taxhash_elim;
1613         $no_taxclass{ 'taxclass' } = '';
1614         @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1615       }
1616
1617       $taxhash_elim{ shift(@elim) } = '';
1618
1619     } while ( !scalar(@taxes) && scalar(@elim) );
1620
1621     foreach (@taxes) {
1622       my $tax_id = 'cust_main_county '.$_->taxnum;
1623       $taxlisthash->{$tax_id} ||= [ $_ ];
1624       $cust_bill_pkg->set_exemptions($_, custnum => $self->custnum);
1625       push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
1626     }
1627
1628   }
1629   '';
1630 }
1631
1632 =item _gather_taxes PART_ITEM CLASS CUST_LOCATION
1633
1634 Internal method used with vendor-provided tax tables.  PART_ITEM is a part_pkg
1635 or part_fee (which will define the tax eligibility of the product), CLASS is
1636 'setup', 'recur', null, or a C<usage_class> number, and CUST_LOCATION is the 
1637 location where the service was provided (or billed, depending on 
1638 configuration).  Returns an arrayref of L<FS::tax_rate> objects that 
1639 can apply to this line item.
1640
1641 =cut
1642
1643 sub _gather_taxes {
1644   my $self = shift;
1645   my $part_item = shift;
1646   my $class = shift;
1647   my $location = shift;
1648
1649   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1650
1651   my $geocode = $location->geocode('cch');
1652
1653   [ $part_item->tax_rates('cch', $geocode, $class) ]
1654
1655 }
1656
1657 #### end vestigial code ####
1658
1659 =item collect [ HASHREF | OPTION => VALUE ... ]
1660
1661 (Attempt to) collect money for this customer's outstanding invoices (see
1662 L<FS::cust_bill>).  Usually used after the bill method.
1663
1664 Actions are now triggered by billing events; see L<FS::part_event> and the
1665 billing events web interface.  Old-style invoice events (see
1666 L<FS::part_bill_event>) have been deprecated.
1667
1668 If there is an error, returns the error, otherwise returns false.
1669
1670 Options are passed as name-value pairs.
1671
1672 Currently available options are:
1673
1674 =over 4
1675
1676 =item invoice_time
1677
1678 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.
1679
1680 =item retry
1681
1682 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1683
1684 =item check_freq
1685
1686 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1687
1688 =item quiet
1689
1690 set true to surpress email card/ACH decline notices.
1691
1692 =item debug
1693
1694 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)
1695
1696 =back
1697
1698 # =item payby
1699 #
1700 # allows for one time override of normal customer billing method
1701
1702 =cut
1703
1704 sub collect {
1705   my( $self, %options ) = @_;
1706
1707   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1708
1709   my $invoice_time = $options{'invoice_time'} || time;
1710
1711   #put below somehow?
1712   local $SIG{HUP} = 'IGNORE';
1713   local $SIG{INT} = 'IGNORE';
1714   local $SIG{QUIT} = 'IGNORE';
1715   local $SIG{TERM} = 'IGNORE';
1716   local $SIG{TSTP} = 'IGNORE';
1717   local $SIG{PIPE} = 'IGNORE';
1718
1719   my $oldAutoCommit = $FS::UID::AutoCommit;
1720   local $FS::UID::AutoCommit = 0;
1721   my $dbh = dbh;
1722
1723   $self->select_for_update; #mutex
1724
1725   if ( $DEBUG ) {
1726     my $balance = $self->balance;
1727     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1728   }
1729
1730   if ( exists($options{'retry_card'}) ) {
1731     carp 'retry_card option passed to collect is deprecated; use retry';
1732     $options{'retry'} ||= $options{'retry_card'};
1733   }
1734   if ( exists($options{'retry'}) && $options{'retry'} ) {
1735     my $error = $self->retry_realtime;
1736     if ( $error ) {
1737       $dbh->rollback if $oldAutoCommit;
1738       return $error;
1739     }
1740   }
1741
1742   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1743
1744   #never want to roll back an event just because it returned an error
1745   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1746
1747   $self->do_cust_event(
1748     'debug'      => ( $options{'debug'} || 0 ),
1749     'time'       => $invoice_time,
1750     'check_freq' => $options{'check_freq'},
1751     'stage'      => 'collect',
1752   );
1753
1754 }
1755
1756 =item retry_realtime
1757
1758 Schedules realtime / batch  credit card / electronic check / LEC billing
1759 events for for retry.  Useful if card information has changed or manual
1760 retry is desired.  The 'collect' method must be called to actually retry
1761 the transaction.
1762
1763 Implementation details: For either this customer, or for each of this
1764 customer's open invoices, changes the status of the first "done" (with
1765 statustext error) realtime processing event to "failed".
1766
1767 =cut
1768
1769 sub retry_realtime {
1770   my $self = shift;
1771
1772   local $SIG{HUP} = 'IGNORE';
1773   local $SIG{INT} = 'IGNORE';
1774   local $SIG{QUIT} = 'IGNORE';
1775   local $SIG{TERM} = 'IGNORE';
1776   local $SIG{TSTP} = 'IGNORE';
1777   local $SIG{PIPE} = 'IGNORE';
1778
1779   my $oldAutoCommit = $FS::UID::AutoCommit;
1780   local $FS::UID::AutoCommit = 0;
1781   my $dbh = dbh;
1782
1783   #a little false laziness w/due_cust_event (not too bad, really)
1784
1785   # I guess this is always as of now?
1786   my $join = FS::part_event_condition->join_conditions_sql('', 'time' => time);
1787   my $order = FS::part_event_condition->order_conditions_sql;
1788   my $mine = 
1789   '( '
1790    . join ( ' OR ' , map { 
1791     my $cust_join = FS::part_event->eventtables_cust_join->{$_} || '';
1792     my $custnum = FS::part_event->eventtables_custnum->{$_};
1793     "( part_event.eventtable = " . dbh->quote($_) 
1794     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key 
1795     . " from $_ $cust_join"
1796     . " where $custnum = " . dbh->quote( $self->custnum ) . "))" ;
1797    } FS::part_event->eventtables)
1798    . ') ';
1799
1800   #here is the agent virtualization
1801   my $agent_virt = " (    part_event.agentnum IS NULL
1802                        OR part_event.agentnum = ". $self->agentnum. ' )';
1803
1804   #XXX this shouldn't be hardcoded, actions should declare it...
1805   my @realtime_events = qw(
1806     cust_bill_realtime_card
1807     cust_bill_realtime_check
1808     cust_bill_realtime_lec
1809     cust_bill_batch
1810   );
1811
1812   my $is_realtime_event =
1813     ' part_event.action IN ( '.
1814         join(',', map "'$_'", @realtime_events ).
1815     ' ) ';
1816
1817   my $batch_or_statustext =
1818     "( part_event.action = 'cust_bill_batch'
1819        OR ( statustext IS NOT NULL AND statustext != '' )
1820      )";
1821
1822
1823   my @cust_event = qsearch({
1824     'table'     => 'cust_event',
1825     'select'    => 'cust_event.*',
1826     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1827     'hashref'   => { 'status' => 'done' },
1828     'extra_sql' => " AND $batch_or_statustext ".
1829                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1830   });
1831
1832   my %seen_invnum = ();
1833   foreach my $cust_event (@cust_event) {
1834
1835     #max one for the customer, one for each open invoice
1836     my $cust_X = $cust_event->cust_X;
1837     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1838                           ? $cust_X->invnum
1839                           : 0
1840                         }++
1841          or $cust_event->part_event->eventtable eq 'cust_bill'
1842             && ! $cust_X->owed;
1843
1844     my $error = $cust_event->retry;
1845     if ( $error ) {
1846       $dbh->rollback if $oldAutoCommit;
1847       return "error scheduling event for retry: $error";
1848     }
1849
1850   }
1851
1852   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1853   '';
1854
1855 }
1856
1857 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1858
1859 Runs billing events; see L<FS::part_event> and the billing events web
1860 interface.
1861
1862 If there is an error, returns the error, otherwise returns false.
1863
1864 Options are passed as name-value pairs.
1865
1866 Currently available options are:
1867
1868 =over 4
1869
1870 =item time
1871
1872 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.
1873
1874 =item check_freq
1875
1876 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1877
1878 =item stage
1879
1880 "collect" (the default) or "pre-bill"
1881
1882 =item quiet
1883  
1884 set true to surpress email card/ACH decline notices.
1885
1886 =item debug
1887
1888 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)
1889
1890 =back
1891 =cut
1892
1893 # =item payby
1894 #
1895 # allows for one time override of normal customer billing method
1896
1897 # =item retry
1898 #
1899 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1900
1901 sub do_cust_event {
1902   my( $self, %options ) = @_;
1903
1904   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1905
1906   my $time = $options{'time'} || time;
1907
1908   #put below somehow?
1909   local $SIG{HUP} = 'IGNORE';
1910   local $SIG{INT} = 'IGNORE';
1911   local $SIG{QUIT} = 'IGNORE';
1912   local $SIG{TERM} = 'IGNORE';
1913   local $SIG{TSTP} = 'IGNORE';
1914   local $SIG{PIPE} = 'IGNORE';
1915
1916   my $oldAutoCommit = $FS::UID::AutoCommit;
1917   local $FS::UID::AutoCommit = 0;
1918   my $dbh = dbh;
1919
1920   $self->select_for_update; #mutex
1921
1922   if ( $DEBUG ) {
1923     my $balance = $self->balance;
1924     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1925   }
1926
1927 #  if ( exists($options{'retry_card'}) ) {
1928 #    carp 'retry_card option passed to collect is deprecated; use retry';
1929 #    $options{'retry'} ||= $options{'retry_card'};
1930 #  }
1931 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
1932 #    my $error = $self->retry_realtime;
1933 #    if ( $error ) {
1934 #      $dbh->rollback if $oldAutoCommit;
1935 #      return $error;
1936 #    }
1937 #  }
1938
1939   # false laziness w/pay_batch::import_results
1940
1941   my $due_cust_event = $self->due_cust_event(
1942     'debug'      => ( $options{'debug'} || 0 ),
1943     'time'       => $time,
1944     'check_freq' => $options{'check_freq'},
1945     'stage'      => ( $options{'stage'} || 'collect' ),
1946   );
1947   unless( ref($due_cust_event) ) {
1948     $dbh->rollback if $oldAutoCommit;
1949     return $due_cust_event;
1950   }
1951
1952   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1953   #never want to roll back an event just because it or a different one
1954   # returned an error
1955   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1956
1957   foreach my $cust_event ( @$due_cust_event ) {
1958
1959     #XXX lock event
1960     
1961     #re-eval event conditions (a previous event could have changed things)
1962     unless ( $cust_event->test_conditions ) {
1963       #don't leave stray "new/locked" records around
1964       my $error = $cust_event->delete;
1965       return $error if $error;
1966       next;
1967     }
1968
1969     {
1970       local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1971         if $options{'quiet'};
1972       warn "  running cust_event ". $cust_event->eventnum. "\n"
1973         if $DEBUG > 1;
1974
1975       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1976       if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1977         #XXX wtf is this?  figure out a proper dealio with return value
1978         #from do_event
1979         return $error;
1980       }
1981     }
1982
1983   }
1984
1985   '';
1986
1987 }
1988
1989 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
1990
1991 Inserts database records for and returns an ordered listref of new events due
1992 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
1993 events are due, an empty listref is returned.  If there is an error, returns a
1994 scalar error message.
1995
1996 To actually run the events, call each event's test_condition method, and if
1997 still true, call the event's do_event method.
1998
1999 Options are passed as a hashref or as a list of name-value pairs.  Available
2000 options are:
2001
2002 =over 4
2003
2004 =item check_freq
2005
2006 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.
2007
2008 =item stage
2009
2010 "collect" (the default) or "pre-bill"
2011
2012 =item time
2013
2014 "Current time" for the events.
2015
2016 =item debug
2017
2018 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)
2019
2020 =item eventtable
2021
2022 Only return events for the specified eventtable (by default, events of all eventtables are returned)
2023
2024 =item objects
2025
2026 Explicitly pass the objects to be tested (typically used with eventtable).
2027
2028 =item testonly
2029
2030 Set to true to return the objects, but not actually insert them into the
2031 database.
2032
2033 =back
2034
2035 =cut
2036
2037 sub due_cust_event {
2038   my $self = shift;
2039   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
2040
2041   #???
2042   #my $DEBUG = $opt{'debug'}
2043   $opt{'debug'} ||= 0; # silence some warnings
2044   local($DEBUG) = $opt{'debug'}
2045     if $opt{'debug'} > $DEBUG;
2046   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2047
2048   warn "$me due_cust_event called with options ".
2049        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
2050     if $DEBUG;
2051
2052   $opt{'time'} ||= time;
2053
2054   local $SIG{HUP} = 'IGNORE';
2055   local $SIG{INT} = 'IGNORE';
2056   local $SIG{QUIT} = 'IGNORE';
2057   local $SIG{TERM} = 'IGNORE';
2058   local $SIG{TSTP} = 'IGNORE';
2059   local $SIG{PIPE} = 'IGNORE';
2060
2061   my $oldAutoCommit = $FS::UID::AutoCommit;
2062   local $FS::UID::AutoCommit = 0;
2063   my $dbh = dbh;
2064
2065   $self->select_for_update #mutex
2066     unless $opt{testonly};
2067
2068   ###
2069   # find possible events (initial search)
2070   ###
2071   
2072   my @cust_event = ();
2073
2074   my @eventtable = $opt{'eventtable'}
2075                      ? ( $opt{'eventtable'} )
2076                      : FS::part_event->eventtables_runorder;
2077
2078   my $check_freq = $opt{'check_freq'} || '1d';
2079
2080   foreach my $eventtable ( @eventtable ) {
2081
2082     my @objects;
2083     if ( $opt{'objects'} ) {
2084
2085       @objects = @{ $opt{'objects'} };
2086
2087     } elsif ( $eventtable eq 'cust_main' ) {
2088
2089       @objects = ( $self );
2090
2091     } else {
2092
2093       my $cm_join = " LEFT JOIN cust_main USING ( custnum )";
2094       # linkage not needed here because FS::cust_main->$eventtable will 
2095       # already supply it
2096
2097       #some false laziness w/Cron::bill bill_where
2098
2099       my $join  = FS::part_event_condition->join_conditions_sql( $eventtable,
2100         'time' => $opt{'time'});
2101       my $where = FS::part_event_condition->where_conditions_sql($eventtable,
2102         'time'=>$opt{'time'},
2103       );
2104       $where = $where ? "AND $where" : '';
2105
2106       my $are_part_event = 
2107       "EXISTS ( SELECT 1 FROM part_event $join
2108         WHERE check_freq = '$check_freq'
2109         AND eventtable = '$eventtable'
2110         AND ( disabled = '' OR disabled IS NULL )
2111         $where
2112         )
2113       ";
2114       #eofalse
2115
2116       @objects = $self->$eventtable(
2117         'addl_from' => $cm_join,
2118         'extra_sql' => " AND $are_part_event",
2119       );
2120     } # if ( !$opt{objects} and $eventtable ne 'cust_main' )
2121
2122     my @e_cust_event = ();
2123
2124     my $linkage = FS::part_event->eventtables_cust_join->{$eventtable} || '';
2125
2126     my $cross = "CROSS JOIN $eventtable $linkage";
2127     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2128       unless $eventtable eq 'cust_main';
2129
2130     foreach my $object ( @objects ) {
2131
2132       #this first search uses the condition_sql magic for optimization.
2133       #the more possible events we can eliminate in this step the better
2134
2135       my $cross_where = '';
2136       my $pkey = $object->primary_key;
2137       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2138
2139       my $join = FS::part_event_condition->join_conditions_sql( $eventtable,
2140         'time' => $opt{'time'});
2141       my $extra_sql =
2142         FS::part_event_condition->where_conditions_sql( $eventtable,
2143                                                         'time'=>$opt{'time'}
2144                                                       );
2145       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2146
2147       $extra_sql = "AND $extra_sql" if $extra_sql;
2148
2149       #here is the agent virtualization
2150       $extra_sql .= " AND (    part_event.agentnum IS NULL
2151                             OR part_event.agentnum = ". $self->agentnum. ' )';
2152
2153       $extra_sql .= " $order";
2154
2155       warn "searching for events for $eventtable ". $object->$pkey. "\n"
2156         if $opt{'debug'} > 2;
2157       my @part_event = qsearch( {
2158         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
2159         'select'    => 'part_event.*',
2160         'table'     => 'part_event',
2161         'addl_from' => "$cross $join",
2162         'hashref'   => { 'check_freq' => $check_freq,
2163                          'eventtable' => $eventtable,
2164                          'disabled'   => '',
2165                        },
2166         'extra_sql' => "AND $cross_where $extra_sql",
2167       } );
2168
2169       if ( $DEBUG > 2 ) {
2170         my $pkey = $object->primary_key;
2171         warn "      ". scalar(@part_event).
2172              " possible events found for $eventtable ". $object->$pkey(). "\n";
2173       }
2174
2175       push @e_cust_event, map { 
2176         $_->new_cust_event($object, 'time' => $opt{'time'}) 
2177       } @part_event;
2178
2179     }
2180
2181     warn "    ". scalar(@e_cust_event).
2182          " subtotal possible cust events found for $eventtable\n"
2183       if $DEBUG > 1;
2184
2185     push @cust_event, @e_cust_event;
2186
2187   }
2188
2189   warn "  ". scalar(@cust_event).
2190        " total possible cust events found in initial search\n"
2191     if $DEBUG; # > 1;
2192
2193
2194   ##
2195   # test stage
2196   ##
2197
2198   $opt{stage} ||= 'collect';
2199   @cust_event =
2200     grep { my $stage = $_->part_event->event_stage;
2201            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
2202          }
2203          @cust_event;
2204
2205   ##
2206   # test conditions
2207   ##
2208   
2209   my %unsat = ();
2210
2211   @cust_event = grep $_->test_conditions( 'stats_hashref' => \%unsat ),
2212                      @cust_event;
2213
2214   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
2215     if $DEBUG; # > 1;
2216
2217   warn "    invalid conditions not eliminated with condition_sql:\n".
2218        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
2219     if keys %unsat && $DEBUG; # > 1;
2220
2221   ##
2222   # insert
2223   ##
2224
2225   unless( $opt{testonly} ) {
2226     foreach my $cust_event ( @cust_event ) {
2227
2228       my $error = $cust_event->insert();
2229       if ( $error ) {
2230         $dbh->rollback if $oldAutoCommit;
2231         return $error;
2232       }
2233                                        
2234     }
2235   }
2236
2237   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2238
2239   ##
2240   # return
2241   ##
2242
2243   warn "  returning events: ". Dumper(@cust_event). "\n"
2244     if $DEBUG > 2;
2245
2246   \@cust_event;
2247
2248 }
2249
2250 =item apply_payments_and_credits [ OPTION => VALUE ... ]
2251
2252 Applies unapplied payments and credits.
2253 Payments with the no_auto_apply flag set will not be applied.
2254
2255 In most cases, this new method should be used in place of sequential
2256 apply_payments and apply_credits methods.
2257
2258 A hash of optional arguments may be passed.  Currently "manual" is supported.
2259 If true, a payment receipt is sent instead of a statement when
2260 'payment_receipt_email' configuration option is set.
2261
2262 If there is an error, returns the error, otherwise returns false.
2263
2264 =cut
2265
2266 sub apply_payments_and_credits {
2267   my( $self, %options ) = @_;
2268
2269   local $SIG{HUP} = 'IGNORE';
2270   local $SIG{INT} = 'IGNORE';
2271   local $SIG{QUIT} = 'IGNORE';
2272   local $SIG{TERM} = 'IGNORE';
2273   local $SIG{TSTP} = 'IGNORE';
2274   local $SIG{PIPE} = 'IGNORE';
2275
2276   my $oldAutoCommit = $FS::UID::AutoCommit;
2277   local $FS::UID::AutoCommit = 0;
2278   my $dbh = dbh;
2279
2280   $self->select_for_update; #mutex
2281
2282   foreach my $cust_bill ( $self->open_cust_bill ) {
2283     my $error = $cust_bill->apply_payments_and_credits(%options);
2284     if ( $error ) {
2285       $dbh->rollback if $oldAutoCommit;
2286       return "Error applying: $error";
2287     }
2288   }
2289
2290   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2291   ''; #no error
2292
2293 }
2294
2295 =item apply_credits OPTION => VALUE ...
2296
2297 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2298 to outstanding invoice balances in chronological order (or reverse
2299 chronological order if the I<order> option is set to B<newest>) and returns the
2300 value of any remaining unapplied credits available for refund (see
2301 L<FS::cust_refund>).
2302
2303 Dies if there is an error.
2304
2305 =cut
2306
2307 sub apply_credits {
2308   my $self = shift;
2309   my %opt = @_;
2310
2311   local $SIG{HUP} = 'IGNORE';
2312   local $SIG{INT} = 'IGNORE';
2313   local $SIG{QUIT} = 'IGNORE';
2314   local $SIG{TERM} = 'IGNORE';
2315   local $SIG{TSTP} = 'IGNORE';
2316   local $SIG{PIPE} = 'IGNORE';
2317
2318   my $oldAutoCommit = $FS::UID::AutoCommit;
2319   local $FS::UID::AutoCommit = 0;
2320   my $dbh = dbh;
2321
2322   $self->select_for_update; #mutex
2323
2324   unless ( $self->total_unapplied_credits ) {
2325     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2326     return 0;
2327   }
2328
2329   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2330       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2331
2332   my @invoices = $self->open_cust_bill;
2333   @invoices = sort { $b->_date <=> $a->_date } @invoices
2334     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2335
2336   if ( $conf->exists('pkg-balances') ) {
2337     # limit @credits to those w/ a pkgnum grepped from $self
2338     my %pkgnums = ();
2339     foreach my $i (@invoices) {
2340       foreach my $li ( $i->cust_bill_pkg ) {
2341         $pkgnums{$li->pkgnum} = 1;
2342       }
2343     }
2344     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2345   }
2346
2347   my $credit;
2348
2349   foreach my $cust_bill ( @invoices ) {
2350
2351     if ( !defined($credit) || $credit->credited == 0) {
2352       $credit = pop @credits or last;
2353     }
2354
2355     my $owed;
2356     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2357       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2358     } else {
2359       $owed = $cust_bill->owed;
2360     }
2361     unless ( $owed > 0 ) {
2362       push @credits, $credit;
2363       next;
2364     }
2365
2366     my $amount = min( $credit->credited, $owed );
2367     
2368     my $cust_credit_bill = new FS::cust_credit_bill ( {
2369       'crednum' => $credit->crednum,
2370       'invnum'  => $cust_bill->invnum,
2371       'amount'  => $amount,
2372     } );
2373     $cust_credit_bill->pkgnum( $credit->pkgnum )
2374       if $conf->exists('pkg-balances') && $credit->pkgnum;
2375     my $error = $cust_credit_bill->insert;
2376     if ( $error ) {
2377       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2378       die $error;
2379     }
2380     
2381     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2382
2383   }
2384
2385   my $total_unapplied_credits = $self->total_unapplied_credits;
2386
2387   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2388
2389   return $total_unapplied_credits;
2390 }
2391
2392 =item apply_payments  [ OPTION => VALUE ... ]
2393
2394 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2395 to outstanding invoice balances in chronological order.
2396 Payments with the no_auto_apply flag set will not be applied.
2397
2398  #and returns the value of any remaining unapplied payments.
2399
2400 A hash of optional arguments may be passed.  Currently "manual" is supported.
2401 If true, a payment receipt is sent instead of a statement when
2402 'payment_receipt_email' configuration option is set.
2403
2404 Dies if there is an error.
2405
2406 =cut
2407
2408 sub apply_payments {
2409   my( $self, %options ) = @_;
2410
2411   local $SIG{HUP} = 'IGNORE';
2412   local $SIG{INT} = 'IGNORE';
2413   local $SIG{QUIT} = 'IGNORE';
2414   local $SIG{TERM} = 'IGNORE';
2415   local $SIG{TSTP} = 'IGNORE';
2416   local $SIG{PIPE} = 'IGNORE';
2417
2418   my $oldAutoCommit = $FS::UID::AutoCommit;
2419   local $FS::UID::AutoCommit = 0;
2420   my $dbh = dbh;
2421
2422   $self->select_for_update; #mutex
2423
2424   #return 0 unless
2425
2426   my @payments = grep { !$_->no_auto_apply } $self->unapplied_cust_pay;
2427
2428   my @invoices = $self->open_cust_bill;
2429
2430   if ( $conf->exists('pkg-balances') ) {
2431     # limit @payments to those w/ a pkgnum grepped from $self
2432     my %pkgnums = ();
2433     foreach my $i (@invoices) {
2434       foreach my $li ( $i->cust_bill_pkg ) {
2435         $pkgnums{$li->pkgnum} = 1;
2436       }
2437     }
2438     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2439   }
2440
2441   my $payment;
2442
2443   foreach my $cust_bill ( @invoices ) {
2444
2445     if ( !defined($payment) || $payment->unapplied == 0 ) {
2446       $payment = pop @payments or last;
2447     }
2448
2449     my $owed;
2450     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2451       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2452     } else {
2453       $owed = $cust_bill->owed;
2454     }
2455     unless ( $owed > 0 ) {
2456       push @payments, $payment;
2457       next;
2458     }
2459
2460     my $amount = min( $payment->unapplied, $owed );
2461
2462     my $cbp = {
2463       'paynum' => $payment->paynum,
2464       'invnum' => $cust_bill->invnum,
2465       'amount' => $amount,
2466     };
2467     $cbp->{_date} = $payment->_date 
2468         if $options{'manual'} && $options{'backdate_application'};
2469     my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2470     $cust_bill_pay->pkgnum( $payment->pkgnum )
2471       if $conf->exists('pkg-balances') && $payment->pkgnum;
2472     my $error = $cust_bill_pay->insert(%options);
2473     if ( $error ) {
2474       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2475       die $error;
2476     }
2477
2478     redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2479
2480   }
2481
2482   my $total_unapplied_payments = $self->total_unapplied_payments;
2483
2484   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2485
2486   return $total_unapplied_payments;
2487 }
2488
2489 =back
2490
2491 =head1 FLOW
2492
2493   bill_and_collect
2494
2495     cancel_expired_pkgs
2496     suspend_adjourned_pkgs
2497     unsuspend_resumed_pkgs
2498
2499     bill
2500       (do_cust_event pre-bill)
2501       _make_lines
2502       _omit_zero_value_bundles
2503       calculate_taxes
2504
2505     apply_payments_and_credits
2506     collect
2507       do_cust_event
2508         due_cust_event
2509
2510 =head1 BUGS
2511
2512 =head1 SEE ALSO
2513
2514 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
2515
2516 =cut
2517
2518 1;