bill usage when cancelling package
authorjeff <jeff>
Fri, 17 Jul 2009 01:44:24 +0000 (01:44 +0000)
committerjeff <jeff>
Fri, 17 Jul 2009 01:44:24 +0000 (01:44 +0000)
FS/FS/Conf.pm
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
FS/FS/part_pkg/voip_cdr.pm

index eb046ee..1d937b7 100644 (file)
@@ -1494,6 +1494,13 @@ worry that config_items is freeside-specific and icky.
   },
 
   {
   },
 
   {
+    'key'         => 'bill_usage_on_cancel',
+    'section'     => 'billing',
+    'description' => 'Enable automatic generation of an invoice for usage when a package is cancelled.  Not all packages can do this.  Usage data must already be available.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'require_cardname',
     'section'     => 'billing',
     'description' => 'Require an "Exact name on card" to be entered explicitly; don\'t default to using the first and last name.',
     'key'         => 'require_cardname',
     'section'     => 'billing',
     'description' => 'Require an "Exact name on card" to be entered explicitly; don\'t default to using the first and last name.',
index 286d333..1f063d9 100644 (file)
@@ -2201,12 +2201,16 @@ Available options are:
 
 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
 
 
 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
 
+=item nobill - can be set true to skip billing if it might otherwise be done.
+
 =back
 
 Always returns a list: an empty list on success or a list of errors.
 
 =cut
 
 =back
 
 Always returns a list: an empty list on success or a list of errors.
 
 =cut
 
+# nb that dates are not specified as valid options to this method
+
 sub cancel {
   my( $self, %opt ) = @_;
 
 sub cancel {
   my( $self, %opt ) = @_;
 
@@ -2232,6 +2236,13 @@ sub cancel {
 
   my @pkgs = $self->ncancelled_pkgs;
 
 
   my @pkgs = $self->ncancelled_pkgs;
 
+  if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
+    $opt{nobill} = 1;
+    my $error = $self->bill( pkg_list => [ @pkgs ], cancel => 1 );
+    warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
+      if $error;
+  }
+
   warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
        scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
     if $DEBUG;
   warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
        scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
     if $DEBUG;
@@ -2441,6 +2452,13 @@ An array ref of specific packages (objects) to attempt billing, instead trying a
 
 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.
 
 
 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 cancel
+
+This boolean value informs the us that the package is being cancelled.  This
+typically might mean not charging the normal recurring fee but only usage
+fees since the last billing. Setup charges may be charged.  Not all package
+plans support this feature (they tend to charge 0).
+
 =back
 
 =cut
 =back
 
 =cut
@@ -2479,7 +2497,8 @@ sub bill {
   my %taxlisthash;
   my @precommit_hooks = ();
 
   my %taxlisthash;
   my @precommit_hooks = ();
 
-  foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
+  $options{ pkg_list } ||= [ $self->ncancelled_pkgs ];  #param checks?
+  foreach my $cust_pkg ( @{ $options{ pkg_list } } ) {
 
     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
 
 
     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
 
@@ -2537,6 +2556,8 @@ sub bill {
     } elsif ( $postal_pkg ) {
 
       foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) {
     } elsif ( $postal_pkg ) {
 
       foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) {
+        my %postal_options = %options;
+        delete $postal_options{cancel};
         my $error =
           $self->_make_lines( 'part_pkg'            => $part_pkg,
                               'cust_pkg'            => $postal_pkg,
         my $error =
           $self->_make_lines( 'part_pkg'            => $part_pkg,
                               'cust_pkg'            => $postal_pkg,
@@ -2546,7 +2567,7 @@ sub bill {
                               'recur'               => \$total_recur,
                               'tax_matrix'          => \%taxlisthash,
                               'time'                => $time,
                               'recur'               => \$total_recur,
                               'tax_matrix'          => \%taxlisthash,
                               'time'                => $time,
-                              'options'             => \%options,
+                              'options'             => \%postal_options,
                             );
         if ($error) {
           $dbh->rollback if $oldAutoCommit;
                             );
         if ($error) {
           $dbh->rollback if $oldAutoCommit;
@@ -2822,6 +2843,7 @@ sub _make_lines {
         || ( $part_pkg->plan eq 'voip_cdr'
               && $part_pkg->option('bill_every_call')
            )
         || ( $part_pkg->plan eq 'voip_cdr'
               && $part_pkg->option('bill_every_call')
            )
+        || ( $options{cancel} )
   ) {
 
     # XXX should this be a package event?  probably.  events are called
   ) {
 
     # XXX should this be a package event?  probably.  events are called
@@ -2835,18 +2857,22 @@ sub _make_lines {
     $lineitems++;
 
     # XXX shared with $recur_prog
     $lineitems++;
 
     # XXX shared with $recur_prog
-    $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+    $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
+             || $cust_pkg->setup
+             || $time;
 
     #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
 
     #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
+                                && !$options{cancel}
                               );
     my %param = ( 'precommit_hooks'     => $precommit_hooks,
                   'increment_next_bill' => $increment_next_bill,
                 );
 
                               );
     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"
+    my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
+    $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
+    return "$@ running $method for $cust_pkg\n"
       if ( $@ );
 
     if ( $increment_next_bill ) {
       if ( $@ );
 
     if ( $increment_next_bill ) {
@@ -2926,9 +2952,11 @@ sub _make_lines {
       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
       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
+        $cust_bill_pkg->edate( $time ) if $options{cancel};
       } else { #if ( $part_pkg->option('recur_temporality', 1) eq 'upcoming' ) {
         $cust_bill_pkg->sdate( $sdate );
         $cust_bill_pkg->edate( $cust_pkg->bill );
       } 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->edate( $time ) if $options{cancel};
       }
 
       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
       }
 
       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
index d2f0690..f564023 100644 (file)
@@ -560,6 +560,8 @@ Available options are:
 
 =item date - can be set to a unix style timestamp to specify when to cancel (expire)
 
 
 =item date - can be set to a unix style timestamp to specify when to cancel (expire)
 
+=item nobill - can be set true to skip billing if it might otherwise be done.
+
 =back
 
 If there is an error, returns the error, otherwise returns false.
 =back
 
 If there is an error, returns the error, otherwise returns false.
@@ -570,6 +572,8 @@ sub cancel {
   my( $self, %options ) = @_;
   my $error;
 
   my( $self, %options ) = @_;
   my $error;
 
+  my $conf = new FS::Conf;
+
   warn "cust_pkg::cancel called with options".
        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
     if $DEBUG;
   warn "cust_pkg::cancel called with options".
        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
     if $DEBUG;
@@ -595,6 +599,19 @@ sub cancel {
   my $date = $options{date} if $options{date}; # expire/cancel later
   $date = '' if ($date && $date <= time);      # complain instead?
 
   my $date = $options{date} if $options{date}; # expire/cancel later
   $date = '' if ($date && $date <= time);      # complain instead?
 
+  #race condition: usage could be ongoing until unprovisioned
+  #resolved by performing a change package instead (which unprovisions) and
+  #later cancelling
+  if ( !$options{nobill} && !$date && $conf->exists('bill_usage_on_cancel') ) {
+      my $error =
+        $self->cust_main->bill( pkg_list => [ $self ], cancel => 1 );
+      warn "Error billing during cancel, custnum ".
+        #$self->cust_main->custnum. ": $error"
+        ": $error"
+        if $error;
+  }
+
+
   my $cancel_time = $options{'time'} || time;
 
   if ( $options{'reason'} ) {
   my $cancel_time = $options{'time'} || time;
 
   if ( $options{'reason'} ) {
index e6044b1..5a43403 100644 (file)
@@ -12,6 +12,7 @@ use FS::cdr;
 use FS::rate;
 use FS::rate_prefix;
 use FS::rate_detail;
 use FS::rate;
 use FS::rate_prefix;
 use FS::rate_detail;
+use FS::part_pkg::recur_Common;
 
 @ISA = qw(FS::part_pkg::prorate);
 
 
 @ISA = qw(FS::part_pkg::prorate);
 
@@ -24,12 +25,6 @@ tie my %rating_method, 'Tie::IxHash',
   'single_price' => 'A single price per minute for all calls.',
 ;
 
   'single_price' => 'A single price per minute for all calls.',
 ;
 
-tie my %recur_method, 'Tie::IxHash',
-  'anniversary' => 'Charge the recurring fee at the frequency specified above',
-  'prorate' => 'Charge a prorated fee the first time (selectable billing date)',
-  'subscription' => 'Charge the full fee for the first partial period (selectable billing date)',
-;
-
 #tie my %cdr_location, 'Tie::IxHash',
 #  'internal' => 'Internal: CDR records imported into the internal CDR table',
 #  'external' => 'External: CDR records queried directly from an external '.
 #tie my %cdr_location, 'Tie::IxHash',
 #  'internal' => 'Internal: CDR records imported into the internal CDR table',
 #  'external' => 'External: CDR records queried directly from an external '.
@@ -72,7 +67,7 @@ tie my %temporalities, 'Tie::IxHash',
                          #'type' => 'radio',
                          #'options' => \%recur_method,
                          'type' => 'select',
                          #'type' => 'radio',
                          #'options' => \%recur_method,
                          'type' => 'select',
-                         'select_options' => \%recur_method,
+                         'select_options' => \%FS::part_pkg::recur_common::recur_method,
                        },
 
     'rating_method' => { 'name' => 'Rating method',
                        },
 
     'rating_method' => { 'name' => 'Rating method',
@@ -228,11 +223,32 @@ sub calc_setup {
   $self->option('setup_fee');
 }
 
   $self->option('setup_fee');
 }
 
-#false laziness w/voip_sqlradacct calc_recur resolve it if that one ever gets used again
 sub calc_recur {
   my $self = shift;
   my($cust_pkg, $sdate, $details, $param ) = @_;
 
 sub calc_recur {
   my $self = shift;
   my($cust_pkg, $sdate, $details, $param ) = @_;
 
+  my $charges = 0;
+
+  $charges += $self->calc_usage(@_);
+  $charges += $self->calc_recur_Common(@_);
+
+  $charges;
+
+}
+
+sub calc_cancel {
+  my $self = shift;
+  my($cust_pkg, $sdate, $details, $param ) = @_;
+
+  $self->calc_usage(@_);
+}
+
+#false laziness w/voip_sqlradacct calc_recur resolve it if that one ever gets used again
+
+sub calc_usage {
+  my $self = shift;
+  my($cust_pkg, $sdate, $details, $param ) = @_;
+
   #my $last_bill = $cust_pkg->last_bill;
   my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup
 
   #my $last_bill = $cust_pkg->last_bill;
   my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup
 
@@ -599,33 +615,6 @@ sub calc_recur {
 #
 #  } #if ( $spool_cdr && length($downstream_cdr) )
 
 #
 #  } #if ( $spool_cdr && length($downstream_cdr) )
 
-  if ($param->{'increment_next_bill'}) {
-    my $recur_method = $self->option('recur_method', 1) || 'anniversary';
-                  
-    if ( $recur_method eq 'prorate' ) {
-
-      $charges += $self->SUPER::calc_recur(@_);
-
-    } else {
-
-      $charges += $self->option('recur_fee');
-
-      if ( $recur_method eq 'subscription' ) {
-
-        my $cutoff_day = $self->option('cutoff_day', 1) || 1;
-        my ($day, $mon, $year) = ( localtime($$sdate) )[ 3..5 ];
-
-        if ( $day < $cutoff_day ) {
-          if ( $mon == 0 ) { $mon=11; $year--; }
-          else { $mon--; }
-        }
-
-        $$sdate = timelocal(0, 0, 0, $cutoff_day, $mon, $year);
-
-      }#$recur_method eq 'subscription'
-    }#$recur_method eq 'prorate'
-  }#increment_next_bill
-
   $charges;
 }
 
   $charges;
 }