this is a quick hack to rebill customers when a cdr didn't happen
authorjeff <jeff>
Mon, 4 May 2009 18:33:48 +0000 (18:33 +0000)
committerjeff <jeff>
Mon, 4 May 2009 18:33:48 +0000 (18:33 +0000)
bin/cust_main_special.pm [new file with mode: 0644]
bin/rebill [new file with mode: 0755]

diff --git a/bin/cust_main_special.pm b/bin/cust_main_special.pm
new file mode 100644 (file)
index 0000000..1f3203e
--- /dev/null
@@ -0,0 +1,776 @@
+package cust_main_special;
+
+require 5.006;
+use strict;
+use vars qw( @ISA $DEBUG $me $conf );
+use Safe;
+use Carp;
+use Data::Dumper;
+use Date::Format;
+use FS::UID qw( dbh );
+use FS::Record qw( qsearchs qsearch );
+use FS::payby;
+use FS::cust_pkg;
+use FS::cust_bill;
+use FS::cust_bill_pkg;
+use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_tax_location;
+use FS::cust_main_county;
+use FS::cust_location;
+use FS::tax_rate;
+use FS::cust_tax_location;
+use FS::part_pkg_taxrate;
+use FS::queue;
+use FS::part_pkg;
+
+@ISA = qw ( FS::cust_main );
+
+$DEBUG = 0;
+$me = '[emergency billing program]';
+
+$conf = new FS::Conf;
+
+=head1 METHODS
+
+=over 4
+
+=item bill_and_collect 
+
+Cancels and suspends any packages due, generates bills, applies payments and
+cred
+
+Warns on errors (Does not currently: If there is an error, returns the error, otherwise returns false.)
+
+Options are passed as name-value pairs.  Currently available options are:
+
+=over 4
+
+=item time
+
+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:
+
+ use Date::Parse;
+ ...
+ $cust_main->bill( 'time' => str2time('April 20th, 2001') );
+
+=item invoice_time
+
+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.
+
+=item check_freq
+
+"1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
+
+=item resetup
+
+If set true, re-charges setup fees.
+
+=item debug
+
+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)
+
+=back
+
+=cut
+
+sub bill_and_collect {
+  my( $self, %options ) = @_;
+
+  #$options{actual_time} not $options{time} because freeside-daily -d is for
+  #pre-printing invoices
+  $self->cancel_expired_pkgs( $options{actual_time} );
+  $self->suspend_adjourned_pkgs( $options{actual_time} );
+
+  my $error = $self->bill( %options );
+  warn "Error billing, custnum ". $self->custnum. ": $error" if $error;
+
+  $self->apply_payments_and_credits;
+
+  unless ( $conf->exists('cancelled_cust-noevents')
+           && ! $self->num_ncancelled_pkgs
+  ) {
+
+    $error = $self->collect( %options );
+    warn "Error collecting, custnum". $self->custnum. ": $error" if $error;
+
+  }
+
+}
+
+=item bill OPTIONS
+
+Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
+conjunction with the collect method by calling B<bill_and_collect>.
+
+If there is an error, returns the error, otherwise returns false.
+
+Options are passed as name-value pairs.  Currently available options are:
+
+=over 4
+
+=item resetup
+
+If set true, re-charges setup fees.
+
+=item time
+
+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:
+
+ use Date::Parse;
+ ...
+ $cust_main->bill( 'time' => str2time('April 20th, 2001') );
+
+=item pkg_list
+
+An array ref of specific packages (objects) to attempt billing, instead trying all of them.
+
+ $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
+
+=item invoice_time
+
+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.
+
+=item backbill
+
+Used to specify the period starting date and preventing normal billing.  Instead all outstanding cdrs/usage are processed as if from the unix timestamp in backbill and without changing the dates in the customer packages.  Useful in those situations when cdrs were not imported before a billing run
+
+=back
+
+=cut
+
+sub bill {
+  my( $self, %options ) = @_;
+
+  bless $self, 'cust_main_special';
+  return '' if $self->payby eq 'COMP';
+  warn "$me backbill usage for customer ". $self->custnum. "\n"
+    if $DEBUG;
+
+  my $time = $options{'time'} || time;
+  my $invoice_time = $options{'invoice_time'} || $time;
+
+  #put below somehow?
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $self->select_for_update; #mutex
+
+  my @cust_bill_pkg = ();
+
+  ###
+  # find the packages which are due for billing, find out how much they are
+  # & generate invoice database.
+  ###
+
+  my( $total_setup, $total_recur, $postal_charge ) = ( 0, 0, 0 );
+  my %taxlisthash;
+  my @precommit_hooks = ();
+
+  my @cust_pkgs = qsearch('cust_pkg', { 'custnum' => $self->custnum } );
+  foreach my $cust_pkg (@cust_pkgs) {
+
+    #NO!! next if $cust_pkg->cancel;  
+    next if $cust_pkg->getfield('cancel');  
+
+    warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
+
+    #? to avoid use of uninitialized value errors... ?
+    $cust_pkg->setfield('bill', '')
+      unless defined($cust_pkg->bill);
+    #my $part_pkg = $cust_pkg->part_pkg;
+
+    my $real_pkgpart = $cust_pkg->pkgpart;
+    my %hash = $cust_pkg->hash;
+
+    foreach my $part_pkg ( $cust_pkg->part_pkg->self_and_bill_linked ) {
+
+      $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
+
+      my $error =
+        $self->_make_lines( 'part_pkg'            => $part_pkg,
+                            'cust_pkg'            => $cust_pkg,
+                            'precommit_hooks'     => \@precommit_hooks,
+                            'line_items'          => \@cust_bill_pkg,
+                            'setup'               => \$total_setup,
+                            'recur'               => \$total_recur,
+                            'tax_matrix'          => \%taxlisthash,
+                            'time'                => $time,
+                            'options'             => \%options,
+                          );
+      if ($error) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+
+    } #foreach my $part_pkg
+
+  } #foreach my $cust_pkg
+
+  unless ( @cust_bill_pkg ) { #don't create an invoice w/o line items
+    unless ( $options{backbill} ) {
+      #but do commit any package date cycling that happened
+      $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    } else {
+      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+    }
+    return '';
+  }
+
+  my $postal_pkg = $self->charge_postal_fee();
+  if ( $postal_pkg && !ref( $postal_pkg ) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "can't charge postal invoice fee for customer ".
+      $self->custnum. ": $postal_pkg";
+  }
+  if ( !$options{backbill} && $postal_pkg &&
+       ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
+         !$conf->exists('postal_invoice-recurring_only')
+       )
+     )
+  {
+    foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) {
+      my $error =
+        $self->_make_lines( 'part_pkg'            => $part_pkg,
+                            'cust_pkg'            => $postal_pkg,
+                            'precommit_hooks'     => \@precommit_hooks,
+                            'line_items'          => \@cust_bill_pkg,
+                            'setup'               => \$total_setup,
+                            'recur'               => \$total_recur,
+                            'tax_matrix'          => \%taxlisthash,
+                            'time'                => $time,
+                            'options'             => \%options,
+                          );
+      if ($error) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  warn "having a look at the taxes we found...\n" if $DEBUG > 2;
+
+  # keys are tax names (as printed on invoices / itemdesc )
+  # values are listrefs of taxlisthash keys (internal identifiers)
+  my %taxname = ();
+
+  # keys are taxlisthash keys (internal identifiers)
+  # values are (cumulative) amounts
+  my %tax = ();
+
+  # keys are taxlisthash keys (internal identifiers)
+  # values are listrefs of cust_bill_pkg_tax_location hashrefs
+  my %tax_location = ();
+
+  foreach my $tax ( keys %taxlisthash ) {
+    my $tax_object = shift @{ $taxlisthash{$tax} };
+    warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
+    warn " ". join('/', @{ $taxlisthash{$tax} } ). "\n" if $DEBUG > 2;
+    my $hashref_or_error =
+      $tax_object->taxline( $taxlisthash{$tax},
+                            'custnum'      => $self->custnum,
+                            'invoice_time' => $invoice_time
+                          );
+    unless ( ref($hashref_or_error) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $hashref_or_error;
+    }
+    unshift @{ $taxlisthash{$tax} }, $tax_object;
+
+    my $name   = $hashref_or_error->{'name'};
+    my $amount = $hashref_or_error->{'amount'};
+
+    #warn "adding $amount as $name\n";
+    $taxname{ $name } ||= [];
+    push @{ $taxname{ $name } }, $tax;
+
+    $tax{ $tax } += $amount;
+
+    $tax_location{ $tax } ||= [];
+    if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
+      push @{ $tax_location{ $tax }  },
+        {
+          'taxnum'      => $tax_object->taxnum, 
+          'taxtype'     => ref($tax_object),
+          'pkgnum'      => $tax_object->get('pkgnum'),
+          'locationnum' => $tax_object->get('locationnum'),
+          'amount'      => sprintf('%.2f', $amount ),
+        };
+    }
+
+  }
+
+  #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
+  my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg;
+  foreach my $tax ( keys %taxlisthash ) {
+    foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
+      next unless ref($_) eq 'FS::cust_bill_pkg';
+
+      push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg }, 
+        splice( @{ $_->_cust_tax_exempt_pkg } );
+    }
+  }
+
+  #consolidate and create tax line items
+  warn "consolidating and generating...\n" if $DEBUG > 2;
+  foreach my $taxname ( keys %taxname ) {
+    my $tax = 0;
+    my %seen = ();
+    my @cust_bill_pkg_tax_location = ();
+    warn "adding $taxname\n" if $DEBUG > 1;
+    foreach my $taxitem ( @{ $taxname{$taxname} } ) {
+      next if $seen{$taxitem}++;
+      warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
+      $tax += $tax{$taxitem};
+      push @cust_bill_pkg_tax_location,
+        map { new FS::cust_bill_pkg_tax_location $_ }
+            @{ $tax_location{ $taxitem } };
+    }
+    next unless $tax;
+
+    $tax = sprintf('%.2f', $tax );
+    $total_setup = sprintf('%.2f', $total_setup+$tax );
+  
+    push @cust_bill_pkg, new FS::cust_bill_pkg {
+      'pkgnum'   => 0,
+      'setup'    => $tax,
+      'recur'    => 0,
+      'sdate'    => '',
+      'edate'    => '',
+      'itemdesc' => $taxname,
+      'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
+    };
+
+  }
+
+  my $charged = sprintf('%.2f', $total_setup + $total_recur );
+
+  #create the new invoice
+  my $cust_bill = new FS::cust_bill ( {
+    'custnum' => $self->custnum,
+    '_date'   => ( $invoice_time ),
+    'charged' => $charged,
+  } );
+  my $error = $cust_bill->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "can't create invoice for customer #". $self->custnum. ": $error";
+  }
+
+  foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
+    $cust_bill_pkg->invnum($cust_bill->invnum); 
+    my $error = $cust_bill_pkg->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't create invoice line item: $error";
+    }
+  }
+    
+
+  #foreach my $hook ( @precommit_hooks ) { 
+  #  eval {
+  #    &{$hook}; #($self) ?
+  #  };
+  #  if ( $@ ) {
+  #    $dbh->rollback if $oldAutoCommit;
+  #    return "$@ running precommit hook $hook\n";
+  #  }
+  #}
+  
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+}
+
+
+sub _make_lines {
+  my ($self, %params) = @_;
+
+  warn "    making lines\n" if $DEBUG > 1;
+  my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
+  my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
+  my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
+  my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
+  my $total_setup = $params{setup} or die "no setup accumulator specified";
+  my $total_recur = $params{recur} or die "no recur accumulator specified";
+  my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
+  my $time = $params{'time'} or die "no time specified";
+  my (%options) = %{$params{options}};
+
+  my $dbh = dbh;
+  my $real_pkgpart = $cust_pkg->pkgpart;
+  my %hash = $cust_pkg->hash;
+  my $old_cust_pkg = new FS::cust_pkg \%hash;
+  my $backbill = $options{backbill} || 0;
+
+  my @details = ();
+
+  my $lineitems = 0;
+
+  $cust_pkg->pkgpart($part_pkg->pkgpart);
+
+  ###
+  # bill setup
+  ###
+
+  my $setup = 0;
+  my $unitsetup = 0;
+  if ( ! $cust_pkg->setup &&
+       (
+         ( $conf->exists('disable_setup_suspended_pkgs') &&
+          ! $cust_pkg->getfield('susp')
+        ) || ! $conf->exists('disable_setup_suspended_pkgs')
+       )
+    || $options{'resetup'}
+  ) {
+    
+    warn "    bill setup\n" if $DEBUG > 1;
+    $lineitems++;
+
+    $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
+    return "$@ running calc_setup for $cust_pkg\n"
+      if $@;
+
+    $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
+
+    $cust_pkg->setfield('setup', $time)
+      unless $cust_pkg->setup;
+          #do need it, but it won't get written to the db
+          #|| $cust_pkg->pkgpart != $real_pkgpart;
+
+  }
+
+  ###
+  # bill recurring fee
+  ### 
+
+  #XXX unit stuff here too
+  my $recur = 0;
+  my $unitrecur = 0;
+  my $sdate;
+  if ( ! $cust_pkg->getfield('susp') and
+           ( $part_pkg->getfield('freq') ne '0' &&
+             ( $cust_pkg->getfield('bill') || 0 ) <= $time
+           )
+        || ( $part_pkg->plan eq 'voip_cdr'
+              && $part_pkg->option('bill_every_call')
+           )
+        || $backbill
+  ) {
+
+    # XXX should this be a package event?  probably.  events are called
+    # at collection time at the moment, though...
+    $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
+      if $part_pkg->can('reset_usage');
+      #don't want to reset usage just cause we want a line item??
+      #&& $part_pkg->pkgpart == $real_pkgpart;
+
+    warn "    bill recur\n" if $DEBUG > 1;
+    $lineitems++;
+
+    # XXX shared with $recur_prog
+    $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+    $sdate = $cust_pkg->lastbill || $backbill if $backbill;
+
+    #over two params!  lets at least switch to a hashref for the rest...
+    my $increment_next_bill = ( $part_pkg->freq ne '0'
+                                && ( $cust_pkg->getfield('bill') || 0 ) <= $time
+                              );
+    my %param = ( 'precommit_hooks'     => $precommit_hooks,
+                  'increment_next_bill' => $increment_next_bill,
+                );
+
+    $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
+    return "$@ running calc_recur for $cust_pkg\n"
+      if ( $@ );
+
+
+    warn "details is now: \n" if $DEBUG > 2;
+    warn Dumper(\@details) if $DEBUG > 2;
+
+    if ( $increment_next_bill ) {
+
+      my $next_bill = $part_pkg->add_freq($sdate);
+      return "unparsable frequency: ". $part_pkg->freq
+        if $next_bill == -1;
+  
+      #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
+      # only for figuring next bill date, nothing else, so, reset $sdate again
+      # here
+      $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+      $sdate = $cust_pkg->lastbill || $backbill if $backbill;
+      #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
+      $cust_pkg->last_bill($sdate);
+
+      $cust_pkg->setfield('bill', $next_bill );
+
+    }
+
+  }
+
+  warn "\$setup is undefined" unless defined($setup);
+  warn "\$recur is undefined" unless defined($recur);
+  warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
+  
+  ###
+  # If there's line items, create em cust_bill_pkg records
+  # If $cust_pkg has been modified, update it (if we're a real pkgpart)
+  ###
+
+  if ( $lineitems ) {
+
+    if ( !$backbill && $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
+      # hmm.. and if just the options are modified in some weird price plan?
+  
+      warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
+        if $DEBUG >1;
+  
+      my $error = $cust_pkg->replace( $old_cust_pkg,
+                                      'options' => { $cust_pkg->options },
+                                    );
+      return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
+        if $error; #just in case
+    }
+  
+    my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
+    if ( $DEBUG > 1 ) {
+      warn "      tentatively adding customer package invoice detail: $_\n"
+        foreach @cust_pkg_detail;
+    }
+    push @details, @cust_pkg_detail;
+
+    $setup = sprintf( "%.2f", $setup );
+    $recur = sprintf( "%.2f", $recur );
+    my $cust_bill_pkg = new FS::cust_bill_pkg {
+      'pkgnum'    => $cust_pkg->pkgnum,
+      'setup'     => $setup,
+      'unitsetup' => $unitsetup,
+      'recur'     => $recur,
+      'unitrecur' => $unitrecur,
+      'quantity'  => $cust_pkg->quantity,
+      'details'   => \@details,
+    };
+
+    warn "created cust_bill_pkg which looks like:\n" if $DEBUG > 2;
+    warn Dumper($cust_bill_pkg) if $DEBUG > 2;
+    if ($backbill) {
+      my %usage_cust_bill_pkg = $cust_bill_pkg->disintegrate;
+      $recur = 0;
+      foreach my $key (keys %usage_cust_bill_pkg) {
+        next if ($key eq 'setup' || $key eq 'recur');
+        $recur += $usage_cust_bill_pkg{$key}->recur;
+      }
+      $setup = 0;
+    }
+
+    $setup = sprintf( "%.2f", $setup );
+    $recur = sprintf( "%.2f", $recur );
+    if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
+      return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
+    }
+    if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
+      return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
+    }
+
+
+    if ( $setup != 0 || $recur != 0 ) {
+
+      warn "    charges (setup=$setup, recur=$recur); adding line items\n"
+        if $DEBUG > 1;
+
+      $cust_bill_pkg->setup($setup);
+      $cust_bill_pkg->recur($recur);
+
+      warn "cust_bill_pkg now looks like:\n" if $DEBUG > 2;
+      warn Dumper($cust_bill_pkg) if $DEBUG > 2;
+
+      if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) {
+        $cust_bill_pkg->sdate( $hash{last_bill} );
+        $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
+      } else { #if ( $part_pkg->option('recur_temporality', 1) eq 'upcoming' ) {
+        $cust_bill_pkg->sdate( $sdate );
+        $cust_bill_pkg->edate( $cust_pkg->bill );
+      }
+
+      $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
+        unless $part_pkg->pkgpart == $real_pkgpart;
+
+      $$total_setup += $setup;
+      $$total_recur += $recur;
+
+      ###
+      # handle taxes
+      ###
+
+      my $error = 
+        $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time});
+      return $error if $error;
+
+      push @$cust_bill_pkgs, $cust_bill_pkg;
+
+    } #if $setup != 0 || $recur != 0
+      
+  } #if $line_items
+
+  '';
+
+}
+
+=item collect OPTIONS
+
+(Attempt to) collect money for this customer's outstanding invoices (see
+L<FS::cust_bill>).  Usually used after the bill method.
+
+Actions are now triggered by billing events; see L<FS::part_event> and the
+billing events web interface.  Old-style invoice events (see
+L<FS::part_bill_event>) have been deprecated.
+
+If there is an error, returns the error, otherwise returns false.
+
+Options are passed as name-value pairs.
+
+Currently available options are:
+
+=over 4
+
+=item invoice_time
+
+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.
+
+=item retry
+
+Retry card/echeck/LEC transactions even when not scheduled by invoice events.
+
+=item quiet
+
+set true to surpress email card/ACH decline notices.
+
+=item check_freq
+
+"1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
+
+=item payby
+
+allows for one time override of normal customer billing method
+
+=item debug
+
+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)
+
+
+=back
+
+=cut
+
+sub collect {
+  my( $self, %options ) = @_;
+  my $invoice_time = $options{'invoice_time'} || time;
+
+  #put below somehow?
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $self->select_for_update; #mutex
+
+  if ( $DEBUG ) {
+    my $balance = $self->balance;
+    warn "$me collect customer ". $self->custnum. ": balance $balance\n"
+  }
+
+  if ( exists($options{'retry_card'}) ) {
+    carp 'retry_card option passed to collect is deprecated; use retry';
+    $options{'retry'} ||= $options{'retry_card'};
+  }
+  if ( exists($options{'retry'}) && $options{'retry'} ) {
+    my $error = $self->retry_realtime;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  # false laziness w/pay_batch::import_results
+
+  my $due_cust_event = $self->due_cust_event(
+    'debug'      => ( $options{'debug'} || 0 ),
+    'time'       => $invoice_time,
+    'check_freq' => $options{'check_freq'},
+  );
+  unless( ref($due_cust_event) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $due_cust_event;
+  }
+
+  foreach my $cust_event ( @$due_cust_event ) {
+
+    #XXX lock event
+    
+    #re-eval event conditions (a previous event could have changed things)
+    unless ( $cust_event->test_conditions( 'time' => $invoice_time ) ) {
+      #don't leave stray "new/locked" records around
+      my $error = $cust_event->delete;
+      if ( $error ) {
+        #gah, even with transactions
+        $dbh->commit if $oldAutoCommit; #well.
+        return $error;
+      }
+      next;
+    }
+
+    {
+      local $FS::cust_main::realtime_bop_decline_quiet = 1 if $options{'quiet'};
+      warn "  running cust_event ". $cust_event->eventnum. "\n"
+        if $DEBUG > 1;
+
+      
+      #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
+      if ( my $error = $cust_event->do_event() ) {
+        #XXX wtf is this?  figure out a proper dealio with return value
+        #from do_event
+         # gah, even with transactions.
+         $dbh->commit if $oldAutoCommit; #well.
+         return $error;
+       }
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+
+
+sub queued_bill {
+  ## actual sub, not a method, designed to be called from the queue.
+  ## sets up the customer, and calls the bill_and_collect
+  my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
+  my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
+      $cust_main->bill_and_collect(
+        %args,
+      );
+}
+
+=back
+
+
+=cut
+
+1;
+
diff --git a/bin/rebill b/bin/rebill
new file mode 100755 (executable)
index 0000000..4f05238
--- /dev/null
@@ -0,0 +1,132 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Getopt::Std;
+use Date::Parse;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw( qsearch );
+use cust_main_special;
+
+&untaint_argv; #what it sounds like  (eww)
+use vars qw(%opt);
+getopts("p:a:d:sy:n", \%opt);
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my (@custnums) = @ARGV;
+
+my $time = $opt{d} ? str2time($opt{d}) : $^T;
+$time += $opt{y} * 86400 if $opt{y};
+my $invoice_time = $opt{n} ? $^T : $time;
+
+my %args = (
+  'time'         => $time,
+  'invoice_time' => $invoice_time,
+  'actual_time'  => $^T, #when freeside-bill was started
+                         #(not, when using -m, freeside-queued)
+  'resetup'      => ( $opt{'s'} ? $opt{'s'} : 0 ),
+  'backbill'     => $time,
+);
+
+my $extra_sql = ( $opt{a} || $opt{p} ) ? ' AND ' : ' WHERE ';
+$extra_sql .= "( ". join( ' OR ', map{ "custnum = $_" } @custnums ). " )";
+$extra_sql = '' unless scalar @custnums;
+
+my @cust = qsearch( { table   => 'cust_main',
+                      hashref => { $opt{a} ? ( 'agentnum' => $opt{a} ) : (),
+                                   $opt{p} ? ( 'payby'    => $opt{p} ) : (),
+                                 },
+                      extra_sql => $extra_sql,
+                    }
+                  );
+
+foreach my $cust ( @cust ) {
+  my $balance = $cust->balance;
+  cust_main_special::bill($cust, %args);
+  if ($balance != $cust->balance){
+    $cust->apply_payments_and_credits;
+    my $error = $cust->collect(%args);
+    warn "Error collecting, custnum ". $cust->custnum. ": $error" if $error;
+  }
+}
+
+
+###
+# subroutines
+###
+
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    # Date::Parse
+    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] user [ custnum custnum ... ]\n";
+}
+
+###
+# documentation
+###
+
+=head1 NAME
+
+freeside-daily - Run daily billing and invoice collection events.
+
+=head1 SYNOPSIS
+
+  freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] user [ custnum custnum ... ]
+
+=head1 DESCRIPTION
+
+Bills customers and runs invoice collection events.  Should be run from
+crontab daily.
+
+Bills customers.  Searches for customers who are due for billing and calls
+the bill and collect methods of a cust_main object.  See L<FS::cust_main>.
+
+  -d: Pretend it's 'date'.  Date is in any format Date::Parse is happy with,
+      but be careful.
+
+  -y: In addition to -d, which specifies an absolute date, the -y switch
+      specifies an offset, in days.  For example, "-y 15" would increment the
+      "pretend date" 15 days from whatever was specified by the -d switch
+      (or now, if no -d switch was given).
+
+  -n: When used with "-d" and/or "-y", specifies that invoices should be dated
+      with today's date, irregardless of the pretend date used to pre-generate
+      the invoices.
+
+  -p: Only process customers with the specified payby (I<CARD>, I<DCRD>, I<CHEK>, I<DCHK>, I<BILL>, I<COMP>, I<LECB>)
+
+  -a: Only process customers with the specified agentnum
+
+  -s: re-charge setup fees
+
+  -v: enable debugging
+
+  -l: debugging level
+
+  -m: Experimental multi-process mode uses the job queue for multi-process and/or multi-machine billing.
+
+  -r: Multi-process mode dry run option
+
+  -k: skip notify_flat_delay and vacuum
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+custnum: if one or more customer numbers are specified, only bills those
+customers.  Otherwise, bills all customers.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=cut
+