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