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