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