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