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