71d5c9b81f565d9794aae74e8e3ab3b88656fcd9
[freeside.git] / FS / FS / cust_main / Billing.pm
1 package FS::cust_main::Billing;
2
3 use strict;
4 use feature 'state';
5 use vars qw( $conf $DEBUG $me );
6 use Carp;
7 use Data::Dumper;
8 use List::Util qw( min );
9 use FS::UID qw( dbh );
10 use FS::Record qw( qsearch qsearchs dbdef );
11 use FS::Misc::DateTime qw( day_end );
12 use Tie::RefHash;
13 use FS::cust_bill;
14 use FS::cust_bill_pkg;
15 use FS::cust_bill_pkg_display;
16 use FS::cust_bill_pay;
17 use FS::cust_credit_bill;
18 use FS::cust_tax_adjustment;
19 use FS::tax_rate;
20 use FS::tax_rate_location;
21 use FS::cust_bill_pkg_tax_location;
22 use FS::cust_bill_pkg_tax_rate_location;
23 use FS::part_event;
24 use FS::part_event_condition;
25 use FS::pkg_category;
26 use FS::FeeOrigin_Mixin;
27 use FS::Log;
28 use FS::TaxEngine;
29
30 # 1 is mostly method/subroutine entry and options
31 # 2 traces progress of some operations
32 # 3 is even more information including possibly sensitive data
33 $DEBUG = 0;
34 $me = '[FS::cust_main::Billing]';
35
36 install_callback FS::UID sub { 
37   $conf = new FS::Conf;
38   #yes, need it for stuff below (prolly should be cached)
39 };
40
41 =head1 NAME
42
43 FS::cust_main::Billing - Billing mixin for cust_main
44
45 =head1 SYNOPSIS
46
47 =head1 DESCRIPTION
48
49 These methods are available on FS::cust_main objects.
50
51 =head1 METHODS
52
53 =over 4
54
55 =item bill_and_collect 
56
57 Cancels and suspends any packages due, generates bills, applies payments and
58 credits, and applies collection events to run cards, send bills and notices,
59 etc.
60
61 Any errors prevent subsequent operations from continuing and die (but see the
62 "fatal" flag below).
63
64 Options are passed as name-value pairs.  Currently available options are:
65
66 =over 4
67
68 =item time
69
70 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:
71
72  use Date::Parse;
73  ...
74  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
75
76 =item invoice_time
77
78 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.
79
80 =item check_freq
81
82 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
83
84 =item resetup
85
86 If set true, re-charges setup fees.
87
88 =item fatal
89
90 If set any errors prevent subsequent operations from continusing.  If set
91 specifically to "return", returns the error (or false, if there is no error).
92 Any other true value causes errors to die.
93
94 =item debug
95
96 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)
97
98 =item job
99
100 Optional FS::queue entry to receive status updates.
101
102 =back
103
104 Options are passed to the B<bill> and B<collect> methods verbatim, so all
105 options of those methods are also available.
106
107 =cut
108
109 sub bill_and_collect {
110   my( $self, %options ) = @_;
111
112   my $log = FS::Log->new('FS::cust_main::Billing::bill_and_collect');
113   my %logopt = (object => $self);
114   $log->debug('start', %logopt);
115
116   my $error;
117
118   #$options{actual_time} not $options{time} because freeside-daily -d is for
119   #pre-printing invoices
120
121   $options{'actual_time'} ||= time;
122   my $job = $options{'job'};
123
124   my $actual_time = ( $conf->exists('next-bill-ignore-time')
125                         ? day_end( $options{actual_time} )
126                         : $options{actual_time}
127                     );
128
129   $job->update_statustext('0,cleaning expired packages') if $job;
130   $log->debug('canceling expired packages', %logopt);
131   $error = $self->cancel_expired_pkgs( $actual_time );
132   if ( $error ) {
133     $error = "Error expiring custnum ". $self->custnum. ": $error";
134     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
135     else                                                     { die    $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     else                                                     { die    $error; }
144   }
145
146   $log->debug('unsuspending resumed packages', %logopt);
147   $error = $self->unsuspend_resumed_pkgs( $actual_time );
148   if ( $error ) {
149     $error = "Error resuming custnum ".$self->custnum. ": $error";
150     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
151     else                                                     { die    $error; }
152   }
153
154   $job->update_statustext('20,billing packages') if $job;
155   $log->debug('billing packages', %logopt);
156   $error = $self->bill( %options );
157   if ( $error ) {
158     $error = "Error billing custnum ". $self->custnum. ": $error";
159     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
160     else                                                     { die    $error; }
161   }
162
163   $job->update_statustext('50,applying payments and credits') if $job;
164   $log->debug('applying payments and credits', %logopt);
165   $error = $self->apply_payments_and_credits;
166   if ( $error ) {
167     $error = "Error applying custnum ". $self->custnum. ": $error";
168     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
169     else                                                     { die    $error; }
170   }
171
172   # In a batch tax environment, do not run collection if any pending 
173   # invoices were created.  Collection will run after the next tax batch.
174   state $is_batch_tax = FS::TaxEngine->new->info->{batch} ? 1 : 0;
175   if ( $is_batch_tax && $self->pending_invoice_count ) {
176     warn "skipped collection for custnum ".$self->custnum.
177          " due to pending invoices\n" if $DEBUG;
178   } elsif ( $conf->exists('cancelled_cust-noevents')
179              && ! $self->num_ncancelled_pkgs )
180   {
181     warn "skipped collection for custnum ".$self->custnum.
182          " because they have no active packages\n" if $DEBUG;
183   } else {
184     # run collection normally
185     $job->update_statustext('70,running collection events') if $job;
186     $log->debug('running collection events', %logopt);
187     $error = $self->collect( %options );
188     if ( $error ) {
189       $error = "Error collecting custnum ". $self->custnum. ": $error";
190       if    ($options{fatal} && $options{fatal} eq 'return') { return $error; }
191       else                                                   { die    $error; }
192     }
193   }
194
195   $job->update_statustext('100,finished') if $job;
196   $log->debug('finish', %logopt);
197
198   '';
199
200 }
201
202 sub cancel_expired_pkgs {
203   my ( $self, $time, %options ) = @_;
204   
205   my @cancel_pkgs = $self->ncancelled_pkgs( { 
206     'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
207   } );
208
209   my @errors = ();
210
211   my @really_cancel_pkgs = ();
212   my @cancel_reasons = ();
213
214   CUST_PKG: foreach my $cust_pkg ( @cancel_pkgs ) {
215     my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
216
217     if ( $cust_pkg->change_to_pkgnum ) {
218
219       my $new_pkg = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
220       if ( !$new_pkg ) {
221         push @errors, 'can\'t change pkgnum '.$cust_pkg->pkgnum.' to pkgnum '.
222                       $cust_pkg->change_to_pkgnum.'; not expiring';
223         next CUST_PKG;
224       }
225       my $error = $cust_pkg->change( 'cust_pkg'        => $new_pkg,
226                                      'unprotect_svcs'  => 1,
227                                    );
228       push @errors, $error if $error && ref($error) ne 'FS::cust_pkg';
229
230     } else { # just cancel it
231
232       push @really_cancel_pkgs, $cust_pkg;
233       push @cancel_reasons, $cpr;
234
235     }
236   }
237
238   if (@really_cancel_pkgs) {
239
240     my %cancel_opt = ( 'cust_pkg' => \@really_cancel_pkgs,
241                        'cust_pkg_reason' => \@cancel_reasons,
242                        'time' => $time,
243                      );
244
245     push @errors, $self->cancel_pkgs(%cancel_opt);
246
247   }
248
249   join(' / ', @errors);
250
251 }
252
253 sub suspend_adjourned_pkgs {
254   my ( $self, $time, %options ) = @_;
255   
256   my @susp_pkgs = $self->ncancelled_pkgs( {
257     'extra_sql' =>
258       " AND ( susp IS NULL OR susp = 0 )
259         AND (    ( bill    IS NOT NULL AND bill    != 0 AND bill    <  $time )
260               OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
261             )
262       ",
263   } );
264
265   #only because there's no SQL test for is_prepaid :/
266   @susp_pkgs = 
267     grep {     (    $_->part_pkg->is_prepaid
268                  && $_->bill
269                  && $_->bill < $time
270                )
271             || (    $_->adjourn
272                  && $_->adjourn <= $time
273                )
274            
275          }
276          @susp_pkgs;
277
278   my @errors = ();
279
280   foreach my $cust_pkg ( @susp_pkgs ) {
281     my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
282       if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
283     my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
284                                             'reason_otaker' => $cpr->otaker
285                                           )
286                                         : ()
287                                   );
288     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
289   }
290
291   join(' / ', @errors);
292
293 }
294
295 sub unsuspend_resumed_pkgs {
296   my ( $self, $time, %options ) = @_;
297   
298   my @unsusp_pkgs = $self->ncancelled_pkgs( { 
299     'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time "
300   } );
301
302   my @errors = ();
303
304   foreach my $cust_pkg ( @unsusp_pkgs ) {
305     my $error = $cust_pkg->unsuspend( 'time' => $time );
306     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
307   }
308
309   join(' / ', @errors);
310
311 }
312
313 =item bill OPTIONS
314
315 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
316 conjunction with the collect method by calling B<bill_and_collect>.
317
318 If there is an error, returns the error, otherwise returns false.
319
320 Options are passed as name-value pairs.  Currently available options are:
321
322 =over 4
323
324 =item resetup
325
326 If set true, re-charges setup fees.
327
328 =item recurring_only
329
330 If set true then only bill recurring charges, not setup, usage, one time
331 charges, etc.
332
333 =item freq_override
334
335 If set, then override the normal frequency and look for a part_pkg_discount
336 to take at that frequency.  This is appropriate only when the normal 
337 frequency for all packages is monthly, and is an error otherwise.  Use
338 C<pkg_list> to limit the set of packages included in billing.
339
340 =item time
341
342 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:
343
344  use Date::Parse;
345  ...
346  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
347
348 =item pkg_list
349
350 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
351
352  $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
353
354 =item not_pkgpart
355
356 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
357
358 =item no_prepaid
359
360 Do not bill prepaid packages.  Used by freeside-daily.
361
362 =item invoice_time
363
364 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.
365
366 =item cancel
367
368 This boolean value informs the us that the package is being cancelled.  This
369 typically might mean not charging the normal recurring fee but only usage
370 fees since the last billing. Setup charges may be charged.  Not all package
371 plans support this feature (they tend to charge 0).
372
373 =item no_usage_reset
374
375 Prevent the resetting of usage limits during this call.
376
377 =item no_commit
378
379 Do not save the generated bill in the database.  Useful with return_bill
380
381 =item return_bill
382
383 A list reference on which the generated bill(s) will be returned.
384
385 =item estimate
386
387 Boolean value; indicates that this is an estimate rather than a "tax invoice".
388 This will be passed through to the tax engine, as online tax services 
389 sometimes need to know it for reporting purposes. Otherwise it has no effect.
390
391 =item invoice_terms
392
393 Optional terms to be printed on this invoice.  Otherwise, customer-specific
394 terms or the default terms are used.
395
396 =back
397
398 =cut
399
400 sub bill {
401   my( $self, %options ) = @_;
402
403   return '' if $self->complimentary eq 'Y';
404
405   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
406   my $log = FS::Log->new('FS::cust_main::Billing::bill');
407   my %logopt = (object => $self);
408
409   $log->debug('start', %logopt);
410   warn "$me bill customer ". $self->custnum. "\n"
411     if $DEBUG;
412
413   my $time = $options{'time'} || time;
414   my $invoice_time = $options{'invoice_time'} || $time;
415
416   my $cmp_time = ( $conf->exists('next-bill-ignore-time')
417                      ? day_end( $time )
418                      : $time
419                  );
420
421   $options{'not_pkgpart'} ||= {};
422   $options{'not_pkgpart'} = { map { $_ => 1 }
423                                   split(/\s*,\s*/, $options{'not_pkgpart'})
424                             }
425     unless ref($options{'not_pkgpart'});
426
427   local $SIG{HUP} = 'IGNORE';
428   local $SIG{INT} = 'IGNORE';
429   local $SIG{QUIT} = 'IGNORE';
430   local $SIG{TERM} = 'IGNORE';
431   local $SIG{TSTP} = 'IGNORE';
432   local $SIG{PIPE} = 'IGNORE';
433
434   my $oldAutoCommit = $FS::UID::AutoCommit;
435   local $FS::UID::AutoCommit = 0;
436   my $dbh = dbh;
437
438   $log->debug('acquiring lock', %logopt);
439   warn "$me acquiring lock on customer ". $self->custnum. "\n"
440     if $DEBUG;
441
442   $self->select_for_update; #mutex
443
444   $log->debug('running pre-bill events', %logopt);
445   warn "$me running pre-bill events for customer ". $self->custnum. "\n"
446     if $DEBUG;
447
448   my $error = $self->do_cust_event(
449     'debug'      => ( $options{'debug'} || 0 ),
450     'time'       => $invoice_time,
451     'check_freq' => $options{'check_freq'},
452     'stage'      => 'pre-bill',
453   )
454     unless $options{no_commit};
455   if ( $error ) {
456     $dbh->rollback if $oldAutoCommit && !$options{no_commit};
457     return $error;
458   }
459
460   $log->debug('done running pre-bill events', %logopt);
461   warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
462     if $DEBUG;
463
464   #keep auto-charge and non-auto-charge line items separate
465   my @passes = ( '', 'no_auto' );
466
467   my %cust_bill_pkg = map { $_ => [] } @passes;
468
469   ###
470   # find the packages which are due for billing, find out how much they are
471   # & generate invoice database.
472   ###
473
474   my %total_setup   = map { my $z = 0; $_ => \$z; } @passes;
475   my %total_recur   = map { my $z = 0; $_ => \$z; } @passes;
476
477   my @precommit_hooks = ();
478
479   $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ];  #param checks?
480   
481   my %tax_engines;
482   my $tax_is_batch = '';
483   foreach (@passes) {
484     $tax_engines{$_} = FS::TaxEngine->new(cust_main    => $self,
485                                           invoice_time => $invoice_time,
486                                           cancel       => $options{cancel},
487                                           estimate     => $options{estimate},
488                                          );
489     $tax_is_batch ||= $tax_engines{$_}->info->{batch};
490   }
491
492   foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
493
494     next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
495
496     my $part_pkg = $cust_pkg->part_pkg;
497
498     next if $options{'no_prepaid'} && $part_pkg->is_prepaid;
499
500     $log->debug('bill package '. $cust_pkg->pkgnum, %logopt);
501     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
502
503     #? to avoid use of uninitialized value errors... ?
504     $cust_pkg->setfield('bill', '')
505       unless defined($cust_pkg->bill);
506  
507     my $real_pkgpart = $cust_pkg->pkgpart;
508     my %hash = $cust_pkg->hash;
509
510     # we could implement this bit as FS::part_pkg::has_hidden, but we already
511     # suffer from performance issues
512     $options{has_hidden} = 0;
513     my @part_pkg = $part_pkg->self_and_bill_linked;
514     $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
515  
516     # if this package was changed from another package,
517     # and it hasn't been billed since then,
518     # and package balances are enabled,
519     if ( $cust_pkg->change_pkgnum
520         and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0)
521         and $cust_pkg->change_date <  $invoice_time
522       and $conf->exists('pkg-balances') )
523     {
524       # _transfer_balance will also create the appropriate credit
525       my @transfer_items = $self->_transfer_balance($cust_pkg);
526       # $part_pkg[0] is the "real" part_pkg
527       my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ? 
528                   'no_auto' : '';
529       push @{ $cust_bill_pkg{$pass} }, @transfer_items;
530       # treating this as recur, just because most charges are recur...
531       ${$total_recur{$pass}} += $_->recur foreach @transfer_items;
532
533       # currently not considering separate_bill here, as it's for 
534       # one-time charges only
535     }
536
537     foreach my $part_pkg ( @part_pkg ) {
538
539       my $this_cust_pkg = $cust_pkg;
540       # for add-on packages, copy the object to avoid leaking changes back to
541       # the caller if pkg_list is in use; see RT#73607
542       if ( $part_pkg->get('pkgpart') != $real_pkgpart ) {
543         $this_cust_pkg = FS::cust_pkg->new({ %hash });
544       }
545
546       my $pass = '';
547       if ( $this_cust_pkg->separate_bill ) {
548         # if no_auto is also set, that's fine. we just need to not have
549         # invoices that are both auto and no_auto, and since the package
550         # gets an invoice all to itself, it will only be one or the other.
551         $pass = $this_cust_pkg->pkgnum;
552         if (!exists $cust_bill_pkg{$pass}) { # it may not exist yet
553           push @passes, $pass;
554           $total_setup{$pass} = do { my $z = 0; \$z };
555           $total_recur{$pass} = do { my $z = 0; \$z };
556           # it also needs its own tax context
557           $tax_engines{$pass} = FS::TaxEngine->new(
558                                   cust_main    => $self,
559                                   invoice_time => $invoice_time,
560                                   cancel       => $options{cancel},
561                                   estimate     => $options{estimate},
562                                 );
563           $cust_bill_pkg{$pass} = [];
564         }
565       } elsif ( ($this_cust_pkg->no_auto || $part_pkg->no_auto) ) {
566         $pass = 'no_auto';
567       }
568
569       my $next_bill = $this_cust_pkg->getfield('bill') || 0;
570       my $error;
571       # let this run once if this is the last bill upon cancellation
572       while ( $next_bill <= $cmp_time or $options{cancel} ) {
573         $error =
574           $self->_make_lines( 'part_pkg'            => $part_pkg,
575                               'cust_pkg'            => $this_cust_pkg,
576                               'precommit_hooks'     => \@precommit_hooks,
577                               'line_items'          => $cust_bill_pkg{$pass},
578                               'setup'               => $total_setup{$pass},
579                               'recur'               => $total_recur{$pass},
580                               'tax_engine'          => $tax_engines{$pass},
581                               'time'                => $time,
582                               'real_pkgpart'        => $real_pkgpart,
583                               'options'             => \%options,
584                             );
585
586         # Stop if anything goes wrong
587         last if $error;
588
589         # or if we're not incrementing the bill date.
590         last if ($this_cust_pkg->getfield('bill') || 0) == $next_bill;
591
592         # or if we're letting it run only once
593         last if $options{cancel};
594
595         $next_bill = $this_cust_pkg->getfield('bill') || 0;
596
597         #stop if -o was passed to freeside-daily
598         last if $options{'one_recur'};
599       }
600       if ($error) {
601         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
602         return $error;
603       }
604
605     } #foreach my $part_pkg
606
607   } #foreach my $cust_pkg
608
609   foreach my $pass (@passes) { # keys %cust_bill_pkg )
610
611     my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
612
613     warn "$me billing pass $pass\n"
614            #.Dumper(\@cust_bill_pkg)."\n"
615       if $DEBUG > 2;
616
617     ###
618     # process fees
619     ###
620
621     my @pending_fees = FS::FeeOrigin_Mixin->by_cust($self->custnum,
622       hashref => { 'billpkgnum' => '' }
623     );
624     warn "$me found pending fees:\n".Dumper(\@pending_fees)."\n"
625       if @pending_fees and $DEBUG > 1;
626
627     # determine whether to generate an invoice
628     my $generate_bill = scalar(@cust_bill_pkg) > 0;
629
630     foreach my $fee (@pending_fees) {
631       $generate_bill = 1 unless $fee->nextbill;
632     }
633     
634     # don't create an invoice with no line items, or where the only line 
635     # items are fees that are supposed to be held until the next invoice
636     next if !$generate_bill;
637
638     # calculate fees...
639     my @fee_items;
640     foreach my $fee_origin (@pending_fees) {
641       my $part_fee = $fee_origin->part_fee;
642
643       # check whether the fee is applicable before doing anything expensive:
644       #
645       # if the fee def belongs to a different agent, don't charge the fee.
646       # event conditions should prevent this, but just in case they don't,
647       # skip the fee.
648       if ( $part_fee->agentnum and $part_fee->agentnum != $self->agentnum ) {
649         warn "tried to charge fee#".$part_fee->feepart .
650              " on customer#".$self->custnum." from a different agent.\n";
651         next;
652       }
653       # also skip if it's disabled
654       next if $part_fee->disabled eq 'Y';
655
656       # Decide which invoice to base the fee on.
657       my $cust_bill = $fee_origin->cust_bill;
658       if (!$cust_bill) {
659         # Then link it to the current invoice. This isn't the real cust_bill
660         # object that will be inserted--in particular there are no taxes yet.
661         # If you want to charge a fee on the total invoice amount including
662         # taxes, you have to put the fee on the next invoice.
663         $cust_bill = FS::cust_bill->new({
664             'custnum'       => $self->custnum,
665             'cust_bill_pkg' => \@cust_bill_pkg,
666             'charged'       => ${ $total_setup{$pass} } +
667                                ${ $total_recur{$pass} },
668         });
669
670         # If the origin is for a specific package, then only apply the fee to
671         # line items from that package.
672         if ( my $cust_pkg = $fee_origin->cust_pkg ) {
673           my @charge_fee_on_item;
674           my $charge_fee_on_amount = 0;
675           foreach (@cust_bill_pkg) {
676             if ($_->pkgnum == $cust_pkg->pkgnum) {
677               push @charge_fee_on_item, $_;
678               $charge_fee_on_amount += $_->setup + $_->recur;
679             }
680           }
681           $cust_bill->set('cust_bill_pkg', \@charge_fee_on_item);
682           $cust_bill->set('charged', $charge_fee_on_amount);
683         }
684
685       } # $cust_bill is now set
686       # calculate the fee
687       my $fee_item = $part_fee->lineitem($cust_bill) or next;
688       # link this so that we can clear the marker on inserting the line item
689       $fee_item->set('fee_origin', $fee_origin);
690       push @fee_items, $fee_item;
691
692     }
693     
694     # add fees to the invoice
695     foreach my $fee_item (@fee_items) {
696
697       push @cust_bill_pkg, $fee_item;
698       ${ $total_setup{$pass} } += $fee_item->setup;
699       ${ $total_recur{$pass} } += $fee_item->recur;
700
701       my $part_fee = $fee_item->part_fee;
702       my $fee_location = $self->ship_location; # I think?
703       
704       my $error = $tax_engines{''}->add_sale($fee_item);
705
706       return $error if $error;
707
708     }
709
710     # XXX implementation of fees is supposed to make this go away...
711     if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
712            !$conf->exists('postal_invoice-recurring_only')
713        )
714     {
715
716       my $postal_pkg = $self->charge_postal_fee();
717       if ( $postal_pkg && !ref( $postal_pkg ) ) {
718
719         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
720         return "can't charge postal invoice fee for customer ".
721           $self->custnum. ": $postal_pkg";
722
723       } elsif ( $postal_pkg ) {
724
725         my $real_pkgpart = $postal_pkg->pkgpart;
726         # we could implement this bit as FS::part_pkg::has_hidden, but we already
727         # suffer from performance issues
728         $options{has_hidden} = 0;
729         my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
730         $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
731
732         foreach my $part_pkg ( @part_pkg ) {
733           my %postal_options = %options;
734           delete $postal_options{cancel};
735           my $error =
736             $self->_make_lines( 'part_pkg'            => $part_pkg,
737                                 'cust_pkg'            => $postal_pkg,
738                                 'precommit_hooks'     => \@precommit_hooks,
739                                 'line_items'          => \@cust_bill_pkg,
740                                 'setup'               => $total_setup{$pass},
741                                 'recur'               => $total_recur{$pass},
742                                 'tax_engine'          => $tax_engines{$pass},
743                                 'time'                => $time,
744                                 'real_pkgpart'        => $real_pkgpart,
745                                 'options'             => \%postal_options,
746                               );
747           if ($error) {
748             $dbh->rollback if $oldAutoCommit && !$options{no_commit};
749             return $error;
750           }
751         }
752
753         # it's silly to have a zero value postal_pkg, but....
754         @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
755
756       }
757
758     }
759
760     #add tax adjustments
761     #XXX does this work with batch tax engines?
762     warn "adding tax adjustments...\n" if $DEBUG > 2;
763     foreach my $cust_tax_adjustment (
764       qsearch('cust_tax_adjustment', { 'custnum'    => $self->custnum,
765                                        'billpkgnum' => '',
766                                      }
767              )
768     ) {
769
770       my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
771
772       my $itemdesc = $cust_tax_adjustment->taxname;
773       $itemdesc = '' if $itemdesc eq 'Tax';
774
775       push @cust_bill_pkg, new FS::cust_bill_pkg {
776         'pkgnum'      => 0,
777         'setup'       => $tax,
778         'recur'       => 0,
779         'sdate'       => '',
780         'edate'       => '',
781         'itemdesc'    => $itemdesc,
782         'itemcomment' => $cust_tax_adjustment->comment,
783         'cust_tax_adjustment' => $cust_tax_adjustment,
784         #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
785       };
786
787     }
788
789     my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
790
791     my $balance = $self->balance;
792
793     my $previous_bill = qsearchs({ 'table'     => 'cust_bill',
794                                    'hashref'   => { custnum=>$self->custnum },
795                                    'extra_sql' => 'ORDER BY _date DESC LIMIT 1',
796                                 });
797     my $previous_balance =
798       $previous_bill
799         ? ( $previous_bill->billing_balance + $previous_bill->charged )
800         : 0;
801
802     $log->debug('creating the new invoice', %logopt);
803     warn "creating the new invoice\n" if $DEBUG;
804     #create the new invoice
805     my $cust_bill = new FS::cust_bill ( {
806       'custnum'             => $self->custnum,
807       '_date'               => $invoice_time,
808       'charged'             => $charged,
809       'billing_balance'     => $balance,
810       'previous_balance'    => $previous_balance,
811       'invoice_terms'       => $options{'invoice_terms'},
812       'cust_bill_pkg'       => \@cust_bill_pkg,
813       'pending'             => 'Y', # clear this after doing taxes
814     } );
815
816     if (!$options{no_commit}) {
817       # probably we ought to insert it as pending, and then rollback
818       # without ever un-pending it
819       $error = $cust_bill->insert;
820       if ( $error ) {
821         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
822         return "can't create invoice for customer #". $self->custnum. ": $error";
823       }
824
825     }
826
827     # calculate and append taxes
828     if ( ! $tax_is_batch) {
829       local $@;
830       my $arrayref = eval { $tax_engines{$pass}->calculate_taxes($cust_bill) };
831
832       if ( $@ ) {
833         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
834         return $@;
835       }
836
837       # or should this be in TaxEngine?
838       my $total_tax = 0;
839       foreach my $taxline ( @$arrayref ) {
840         $total_tax += $taxline->setup;
841         $taxline->set('invnum' => $cust_bill->invnum); # just to be sure
842         push @cust_bill_pkg, $taxline; # for return_bill
843
844         if (!$options{no_commit}) {
845           my $error = $taxline->insert;
846           if ( $error ) {
847             $dbh->rollback if $oldAutoCommit;
848             return $error;
849           }
850         }
851
852       }
853
854       # add tax to the invoice amount and finalize it
855       ${ $total_setup{$pass} } = sprintf('%.2f', ${ $total_setup{$pass} } + $total_tax);
856       $charged = sprintf('%.2f', $charged + $total_tax);
857       $cust_bill->set('charged', $charged);
858       $cust_bill->set('pending', '');
859
860       if (!$options{no_commit}) {
861         my $error = $cust_bill->replace;
862         if ( $error ) {
863           $dbh->rollback if $oldAutoCommit;
864           return $error;
865         }
866       }
867
868     } # if !$tax_is_batch
869       # if it IS batch, then we'll do all this in process_tax_batch
870
871     push @{$options{return_bill}}, $cust_bill if $options{return_bill};
872
873   } #foreach my $pass ( keys %cust_bill_pkg )
874
875   foreach my $hook ( @precommit_hooks ) { 
876     eval {
877       &{$hook}; #($self) ?
878     } unless $options{no_commit};
879     if ( $@ ) {
880       $dbh->rollback if $oldAutoCommit && !$options{no_commit};
881       return "$@ running precommit hook $hook\n";
882     }
883   }
884   
885   $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
886
887   ''; #no error
888 }
889
890 #discard bundled packages of 0 value
891 # XXX we should reconsider whether we even need this
892 sub _omit_zero_value_bundles {
893   my @in = @_;
894
895   my @out = ();
896   my @bundle = ();
897   my $discount_show_always = $conf->exists('discount-show-always');
898   my $show_this = 0;
899
900   # Sort @in the same way we do during invoice rendering, so we can identify
901   # bundles.  See FS::Template_Mixin::_items_nontax.
902   @in = sort { $a->pkgnum <=> $b->pkgnum        or
903                $a->sdate  <=> $b->sdate         or
904                ($a->pkgpart_override ? 0 : -1)  or
905                ($b->pkgpart_override ? 0 : 1)   or
906                $b->hidden cmp $a->hidden        or
907                $a->pkgpart_override <=> $b->pkgpart_override
908              } @in;
909
910   # this is a pack-and-deliver pattern. every time there's a cust_bill_pkg
911   # _without_ pkgpart_override, that's the start of the new bundle. if there's
912   # an existing bundle, and it contains a nonzero amount (or a zero amount 
913   # that's displayable anyway), push all line items in the bundle.
914   foreach my $cust_bill_pkg ( @in ) {
915
916     if (scalar(@bundle) and !$cust_bill_pkg->pkgpart_override) {
917       # ship out this bundle and reset it
918       if ( $show_this ) {
919         push @out, @bundle;
920       }
921       @bundle = ();
922       $show_this = 0;
923     }
924
925     # add this item to the current bundle
926     push @bundle, $cust_bill_pkg;
927
928     # determine if it makes the bundle displayable
929     if (   $cust_bill_pkg->setup > 0
930         or $cust_bill_pkg->recur > 0
931         or $cust_bill_pkg->setup_show_zero
932         or $cust_bill_pkg->recur_show_zero
933         or ($discount_show_always 
934           and scalar(@{ $cust_bill_pkg->get('discounts')}) 
935           )
936     ) {
937       $show_this++;
938     }
939   }
940
941   # last bundle
942   if ( $show_this) {
943     push @out, @bundle;
944   }
945
946   warn "  _omit_zero_value_bundles: ". scalar(@in).
947        '->'. scalar(@out). "\n" #. Dumper(@out). "\n"
948     if $DEBUG > 2;
949
950   @out;
951 }
952
953 sub _make_lines {
954   my ($self, %params) = @_;
955
956   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
957
958   my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
959   my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
960   my $cust_location = $cust_pkg->tax_location;
961   my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
962   my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
963   my $total_setup = $params{setup} or die "no setup accumulator specified";
964   my $total_recur = $params{recur} or die "no recur accumulator specified";
965   my $time = $params{'time'} or die "no time specified";
966   my (%options) = %{$params{options}};
967
968   my $tax_engine = $params{tax_engine};
969
970   if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
971     # this should never happen
972     die 'freq_override billing attempted on non-monthly package '.
973       $cust_pkg->pkgnum;
974   }
975
976   my $dbh = dbh;
977   my $real_pkgpart = $params{real_pkgpart};
978   my %hash = $cust_pkg->hash;
979   my $old_cust_pkg = new FS::cust_pkg \%hash;
980
981   my @details = ();
982   my $lineitems = 0;
983
984   $cust_pkg->pkgpart($part_pkg->pkgpart);
985
986   my $cmp_time = ( $conf->exists('next-bill-ignore-time')
987                      ? day_end( $time )
988                      : $time
989                  );
990
991   ###
992   # bill setup
993   ###
994
995   my $setup = 0;
996   my $unitsetup = 0;
997   my @setup_discounts = ();
998   my %setup_param = ( 'discounts'     => \@setup_discounts,
999                       'real_pkgpart'  => $params{real_pkgpart}
1000                     );
1001   my $setup_billed_currency = '';
1002   my $setup_billed_amount = 0;
1003   # Conditions for setting setup date and charging the setup fee:
1004   # - this is not a recurring-only billing run
1005   # - and the package is not currently being canceled
1006   # - and, unless we're specifically told otherwise via 'resetup':
1007   #   - it doesn't already HAVE a setup date
1008   #   - or a start date in the future
1009   #   - and it's not suspended
1010   # - and it doesn't have an expire date in the past
1011   #
1012   # The "disable_setup_suspended" option is now obsolete; we never set the
1013   # setup date on a suspended package.
1014   if (     ! $options{recurring_only}
1015        and ! $options{cancel}
1016        and ( $options{'resetup'}
1017              || ( ! $cust_pkg->setup
1018                   && ( ! $cust_pkg->start_date
1019                        || $cust_pkg->start_date <= $cmp_time
1020                      )
1021                   && ( ! $cust_pkg->getfield('susp') )
1022                 )
1023            )
1024        and ( ! $cust_pkg->expire
1025              || $cust_pkg->expire > $cmp_time )
1026      )
1027   {
1028     
1029     warn "    bill setup\n" if $DEBUG > 1;
1030
1031     unless ( $cust_pkg->waive_setup ) {
1032         $lineitems++;
1033
1034         $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
1035         return "$@ running calc_setup for $cust_pkg\n"
1036           if $@;
1037
1038         # Only increment unitsetup here if there IS a setup fee.
1039         # prorate_defer_bill may cause calc_setup on a setup-stage package
1040         # to return zero, and the setup fee to be charged later. (This happens
1041         # when it's first billed on the prorate cutoff day. RT#31276.)
1042         if ( $setup ) {
1043           $unitsetup = $cust_pkg->base_setup()
1044                          || $setup; #XXX uuh
1045         }
1046
1047         if ( $setup_param{'billed_currency'} ) {
1048           $setup_billed_currency = delete $setup_param{'billed_currency'};
1049           $setup_billed_amount   = delete $setup_param{'billed_amount'};
1050         }
1051     }
1052
1053     $lineitems++
1054     if $cust_pkg->waive_setup && $part_pkg->can('prorate_setup') && $part_pkg->prorate_setup($cust_pkg, $time);
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;