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