discount_Mixin
authormark <mark>
Sat, 30 Oct 2010 23:22:31 +0000 (23:22 +0000)
committermark <mark>
Sat, 30 Oct 2010 23:22:31 +0000 (23:22 +0000)
FS/FS/part_pkg/discount_Mixin.pm [new file with mode: 0644]
FS/FS/part_pkg/flat.pm
FS/FS/part_pkg/prorate.pm
FS/FS/part_pkg/prorate_Mixin.pm
FS/FS/part_pkg/recur_Common.pm
FS/FS/part_pkg/subscription.pm

diff --git a/FS/FS/part_pkg/discount_Mixin.pm b/FS/FS/part_pkg/discount_Mixin.pm
new file mode 100644 (file)
index 0000000..df65e97
--- /dev/null
@@ -0,0 +1,128 @@
+package FS::part_pkg::discount_Mixin;
+
+use strict;
+use vars qw(@ISA %info);
+use FS::part_pkg;
+use FS::cust_pkg;
+use FS::cust_bill_pkg_discount;
+use Time::Local qw(timelocal);
+use List::Util 'min';
+
+@ISA = qw(FS::part_pkg);
+%info = ( 'disabled' => 1 );
+
+=head1 NAME
+
+FS::part_pkg::discount_Mixin - Mixin class for part_pkg:: classes that 
+can be discounted.
+
+=head1 SYNOPSIS
+
+package FS::part_pkg::...;
+use base qw( FS::part_pkg::discount_Mixin );
+
+sub calc_recur {
+  ...
+  my $discount = $self->calc_discount($cust_pkg, $$sdate, $details, $param);
+  $charge -= $discount;
+  ...
+}
+
+=head METHODS
+
+=item calc_discount
+
+Takes all the arguments of calc_recur.  Calculates and returns  the amount 
+by which to reduce the recurring fee; also increments months used on the 
+discount and generates an invoice detail describing it.
+
+=cut
+
+sub calc_discount {
+  my($self, $cust_pkg, $sdate, $details, $param ) = @_;
+
+  my $br = $self->base_recur($cust_pkg);
+
+  my $tot_discount = 0;
+  #UI enforces just 1 for now, will need ordering when they can be stacked
+
+  if ( $param->{freq_override} ) {
+    # When a customer pays for more than one month at a time to receive a 
+    # term discount, freq_override is set to the number of months.
+    my $real_part_pkg = new FS::part_pkg { $self->hash };
+    $real_part_pkg->pkgpart($param->{real_pkgpart} || $self->pkgpart);
+    # Find a discount with that duration...
+    my @discount = grep { $_->months == $param->{freq_override} }
+                    map { $_->discount } $real_part_pkg->part_pkg_discount;
+    my $discount = shift @discount;
+    # and default to bill that many months at once.
+    $param->{months} = $param->{freq_override} unless $param->{months};
+    my $error;
+    if ($discount) {
+      # Then set the cust_pkg discount.
+      if ($discount->months == $param->{months}) {
+        $cust_pkg->discountnum($discount->discountnum);
+        $error = $cust_pkg->insert_discount;
+      } else {
+        $cust_pkg->discountnum(-1);
+        foreach ( qw( amount percent months ) ) {
+          my $method = "discountnum_$_";
+          $cust_pkg->$method($discount->$_);
+        }
+        $error = $cust_pkg->insert_discount;
+      }
+      die "error discounting using part_pkg_discount: $error" if $error;
+    }
+  }
+
+  my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active;
+  foreach my $cust_pkg_discount ( @cust_pkg_discount ) {
+    my $discount = $cust_pkg_discount->discount;
+    #UI enforces one or the other (for now?  probably for good)
+    my $amount = 0;
+    $amount += $discount->amount
+    if $cust_pkg->pkgpart == $param->{real_pkgpart};
+    $amount += sprintf('%.2f', $discount->percent * $br / 100 );
+    my $chg_months = $param->{'months'} || $cust_pkg->part_pkg->freq;
+
+    my $months = $discount->months
+    ? min( $chg_months,
+      $discount->months - $cust_pkg_discount->months_used )
+    : $chg_months;
+
+    my $error = $cust_pkg_discount->increment_months_used($months)
+    if $cust_pkg->pkgpart == $param->{real_pkgpart};
+    die "error discounting: $error" if $error;
+
+    $amount *= $months;
+    $amount = sprintf('%.2f', $amount);
+
+    next unless $amount > 0;
+
+    #record details in cust_bill_pkg_discount
+    my $cust_bill_pkg_discount = new FS::cust_bill_pkg_discount {
+      'pkgdiscountnum' => $cust_pkg_discount->pkgdiscountnum,
+      'amount'         => $amount,
+      'months'         => $months,
+    };
+    push @{ $param->{'discounts'} }, $cust_bill_pkg_discount;
+
+    #add details on discount to invoice
+    my $conf = new FS::Conf;
+    my $money_char = $conf->config('money_char') || '$';
+    $months = sprintf('%.2f', $months) if $months =~ /\./;
+
+    my $d = 'Includes ';
+    $d .= $discount->name. ' ' if $discount->name;
+    $d .= 'discount of '. $discount->description_short;
+    $d .= " for $months month". ( $months!=1 ? 's' : '' );
+    $d .= ": $money_char$amount" if $months != 1 || $discount->percent;
+    push @$details, $d;
+
+    $tot_discount += $amount;
+  }
+
+  sprintf('%.2f', $tot_discount);
+}
+
+1;
index e69c10c..a17813b 100644 (file)
@@ -11,9 +11,10 @@ use List::Util qw(min); # max);
 use FS::UI::bytecount;
 use FS::Conf;
 use FS::part_pkg;
-use FS::cust_bill_pkg_discount;
 
-@ISA = qw(FS::part_pkg FS::part_pkg::prorate_Mixin);
+@ISA = qw(FS::part_pkg 
+          FS::part_pkg::prorate_Mixin
+          FS::part_pkg::discount_Mixin);
 
 tie my %temporalities, 'Tie::IxHash',
   'upcoming'  => "Upcoming (future)",
@@ -194,100 +195,23 @@ sub calc_recur {
   return 0
     if $self->option('recur_temporality', 1) eq 'preceding' && $last_bill == 0;
 
-  if( $self->option('sync_bill_date',1) ) {
-    return $self->calc_prorate(@_);
-  }
-  else {
-    my $charge = $self->base_recur($cust_pkg);
-    $charge *= $param->{freq_override} if $param->{freq_override};
-    my $discount = $self->calc_discount($cust_pkg, $sdate, $details, $param);
-
-    return sprintf('%.2f', $charge - $discount);
-  }
-}
-
-sub calc_discount {
-  my($self, $cust_pkg, $sdate, $details, $param ) = @_;
-
-  my $br = $self->base_recur($cust_pkg);
-
-  my $tot_discount = 0;
-  #UI enforces just 1 for now, will need ordering when they can be stacked
-
-  if ( $param->{freq_override} ) {
-    my $real_part_pkg = new FS::part_pkg { $self->hash };
-    $real_part_pkg->pkgpart($param->{real_pkgpart} || $self->pkgpart);
-    my @discount = grep { $_->months == $param->{freq_override} }
-                   map { $_->discount }
-                   $real_part_pkg->part_pkg_discount;
-    my $discount = shift @discount;
-    $param->{months} = $param->{freq_override} unless $param->{months};
-    my $error;
-    if ($discount) {
-      if ($discount->months == $param->{months}) {
-        $cust_pkg->discountnum($discount->discountnum);
-        $error = $cust_pkg->insert_discount;
-      } else {
-        $cust_pkg->discountnum(-1);
-        foreach ( qw( amount percent months ) ) {
-          my $method = "discountnum_$_";
-          $cust_pkg->$method($discount->$_);
-        }
-        $error = $cust_pkg->insert_discount;
-      }
-      die "error discounting using part_pkg_discount: $error" if $error;
+  my $charge = $self->base_recur($cust_pkg);
+  if ( $self->option('sync_bill_date',1) ) {
+    my $next_bill = $cust_pkg->cust_main->next_bill_date;
+    if ( defined($next_bill) and $next_bill != $$sdate ) {
+      my $cutoff_day = (localtime($next_bill))[3];
+      $charge = $self->calc_prorate(@_, $cutoff_day);
     }
   }
-
-  my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active;
-  foreach my $cust_pkg_discount ( @cust_pkg_discount ) {
-     my $discount = $cust_pkg_discount->discount;
-     #UI enforces one or the other (for now?  probably for good)
-     my $amount = 0;
-     $amount += $discount->amount
-       if $cust_pkg->pkgpart == $param->{real_pkgpart};
-     $amount += sprintf('%.2f', $discount->percent * $br / 100 );
-
-     my $chg_months = $param->{'months'} || $cust_pkg->part_pkg->freq;
-     
-     my $months = $discount->months
-                    ? min( $chg_months,
-                           $discount->months - $cust_pkg_discount->months_used )
-                    : $chg_months;
-
-     my $error = $cust_pkg_discount->increment_months_used($months)
-       if $cust_pkg->pkgpart == $param->{real_pkgpart};
-     die "error discounting: $error" if $error;
-
-     $amount *= $months;
-     $amount = sprintf('%.2f', $amount);
-
-     next unless $amount > 0;
-
-     #record details in cust_bill_pkg_discount
-     my $cust_bill_pkg_discount = new FS::cust_bill_pkg_discount {
-       'pkgdiscountnum' => $cust_pkg_discount->pkgdiscountnum,
-       'amount'         => $amount,
-       'months'         => $months,
-     };
-     push @{ $param->{'discounts'} }, $cust_bill_pkg_discount;
-
-     #add details on discount to invoice
-     my $conf = new FS::Conf;
-     my $money_char = $conf->config('money_char') || '$';  
-     $months = sprintf('%.2f', $months) if $months =~ /\./;
-
-     my $d = 'Includes ';
-     $d .= $discount->name. ' ' if $discount->name;
-     $d .= 'discount of '. $discount->description_short;
-     $d .= " for $months month". ( $months!=1 ? 's' : '' );
-     $d .= ": $money_char$amount" if $months != 1 || $discount->percent;
-     push @$details, $d;
-
-     $tot_discount += $amount;
+  elsif ( $param->{freq_override} ) {
+    # XXX not sure if this should be mutually exclusive with sync_bill_date.
+    # Given the very specific problem that freq_override is meant to 'solve',
+    # it probably should.
+    $charge *= $param->{freq_override} if $param->{freq_override};
   }
 
-  sprintf('%.2f', $tot_discount);
+  my $discount = $self->calc_discount($cust_pkg, $sdate, $details, $param);
+  return sprintf('%.2f', $charge - $discount);
 }
 
 sub base_recur {
index 218f80f..4abdb8d 100644 (file)
@@ -106,7 +106,8 @@ use FS::part_pkg::flat;
 
 sub calc_recur {
   my $self = shift;
-  $self->calc_prorate(@_);
+  my $cutoff_day = $self->option('cutoff_day') || 1;
+  return $self->calc_prorate(@_, $cutoff_day) - $self->calc_discount(@_);
 }
 
 1;
index b77d898..9c0c266 100644 (file)
@@ -5,7 +5,9 @@ use vars qw(@ISA %info);
 use Time::Local qw(timelocal);
 
 @ISA = qw(FS::part_pkg);
-%info = ( 'disabled' => 1 );
+%info = ( 
+  'disabled'  => 1,
+);
 
 =head1 NAME
 
@@ -28,45 +30,31 @@ sub calc_recur {
 
 =head METHODS
 
-=item calc_prorate
+=item calc_prorate CUST_PKG
 
-Takes all the arguments of calc_recur, and calculates a prorated charge 
-in one of two ways:
+Takes all the arguments of calc_recur, followed by a day of the month 
+to prorate to.  Calculates a prorated charge from the $sdate to that day, 
+and sets the $sdate and $param->{months} accordingly.
 
-- If 'sync_bill_date' is set: Charge for a number of days to synchronize 
-  this package to the customer's next bill date.  If this is their only 
-  package (or they're already synchronized), that will take them through 
-  one billing cycle.
-- If 'cutoff_day' is set: Prorate this package so that its next bill date 
-  falls on that day of the month.
+Options:
+- recur_fee: The charge to use for a complete billing period.
+- add_full_period: Bill for the time up to the prorate day plus one full
+billing period after that.
+- prorate_round_day: Round the current time to the nearest full day, 
+instead of using the exact time.
 
 =cut
 
 sub calc_prorate {
   my $self  = shift;
-  my ($cust_pkg, $sdate, $details, $param) = @_;
+  my ($cust_pkg, $sdate, $details, $param, $cutoff_day) = @_;
  
-  my $charge = $self->option('recur_fee') || 0;
-  my $cutoff_day;
-  if( $self->option('sync_bill_date',1) ) {
-    my $next_bill = $cust_pkg->cust_main->next_bill_date;
-    if( defined($next_bill) and $next_bill != $$sdate ) {
-      $cutoff_day = (localtime($next_bill))[3];
-    }
-    else {
-      # don't prorate, assume a full month
-      $param->{'months'} = $self->freq;
-    }
-  }
-  else { # no sync, use cutoff_day or day 1
-    $cutoff_day = $self->option('cutoff_day') || 1;
-  }
-
+  my $charge = $self->option('recur_fee',1) || 0;
   if($cutoff_day) {
     # only works for freq >= 1 month; probably can't be fixed
     my $mnow = $$sdate;
     my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($mnow))[0..5];
-    if ( $self->option('prorate_round_day',1) ) {
+    if( $self->option('prorate_round_day',1) ) {
       $mday++ if $hour >= 12;
       $mnow = timelocal(0,0,0,$mday,$mon,$year);
     }
@@ -88,7 +76,7 @@ sub calc_prorate {
     # next bill date will be figured as $$sdate + one period
     $$sdate = $mstart;
 
-    my $permonth = $self->option('recur_fee', 1) / $self->freq;
+    my $permonth = $charge / $self->freq;
     my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) );
 
     if ( $self->option('add_full_period',1) ) {
@@ -100,8 +88,7 @@ sub calc_prorate {
     $param->{'months'} = $months;
     $charge = sprintf('%.2f', $permonth * $months);
   }
-  my $discount =  $self->calc_discount(@_);
-  return ($charge - $discount);
+  return $charge;
 }
 
 1;
index ec17c16..7614d7a 100644 (file)
@@ -4,9 +4,9 @@ use strict;
 use vars qw( @ISA %info %recur_method );
 use Tie::IxHash;
 use Time::Local;
-use FS::part_pkg::prorate;
+use FS::part_pkg::flat;
 
-@ISA = qw(FS::part_pkg::prorate);
+@ISA = qw(FS::part_pkg::flat);
 
 %info = ( 'disabled' => 1 ); #recur_Common not a usable price plan directly
 
@@ -16,6 +16,11 @@ tie %recur_method, 'Tie::IxHash',
   'subscription' => 'Charge the full fee for the first partial period (selectable billing date)',
 ;
 
+sub base_recur {
+  my $self = shift;
+  $self->option('recur_fee', 1) || 0;
+}
+
 sub calc_recur_Common {
   my $self = shift;
   my($cust_pkg, $sdate, $details, $param ) = @_; #only need $sdate & $param
@@ -25,32 +30,37 @@ sub calc_recur_Common {
   if ( $param->{'increment_next_bill'} ) {
 
     my $recur_method = $self->option('recur_method', 1) || 'anniversary';
-                  
-    if ( $recur_method eq 'prorate' 
-        or ($recur_method eq 'anniversary' and $self->option('sync_bill_date',1))
-      ) {
-      $charges = $self->calc_prorate(@_);
+    
+    $charges = $self->base_recur;
+
+    if ( $recur_method eq 'prorate' ) {
+      my $cutoff_day = $self->option('cutoff_day') || 1;
+      $charges = $self->calc_prorate(@_, $cutoff_day);
+    }
+    elsif ( $recur_method eq 'anniversary' and 
+            $self->option('sync_bill_date',1) ) {
+      my $next_bill = $cust_pkg->cust_main->next_bill_date;
+      if ( defined($next_bill) ) {
+        my $cutoff_day = (localtime($next_bill))[3];
+        $charges = $self->calc_prorate(@_, $cutoff_day);
+      }
     } 
-    else {
-
-      $charges = $self->option('recur_fee');
+    elsif ( $recur_method eq 'subscription' ) {
 
-      if ( $recur_method eq 'subscription' ) {
+      my $cutoff_day = $self->option('cutoff_day', 1) || 1;
+      my ($day, $mon, $year) = ( localtime($$sdate) )[ 3..5 ];
 
-        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--; }
+      }
 
-        if ( $day < $cutoff_day ) {
-          if ( $mon == 0 ) { $mon=11; $year--; }
-          else { $mon--; }
-        }
+      $$sdate = timelocal(0, 0, 0, $cutoff_day, $mon, $year);
 
-        $$sdate = timelocal(0, 0, 0, $cutoff_day, $mon, $year);
+    }#$recur_method eq 'subscription'
 
-      }#$recur_method eq 'subscription'
     $charges -= $self->calc_discount( $cust_pkg, $sdate, $details, $param );
 
-    }#$recur_method eq 'prorate' or ...
   }#increment_next_bill
 
   return $charges;
index a5e0262..5495e3a 100644 (file)
@@ -18,7 +18,7 @@ use FS::part_pkg::flat;
     'recur_fee' => { 'name' => 'Recurring fee for this package',
                      'default' => 0,
                           },
-    'cutoff_day' => { 'name' => 'billing day',
+    'cutoff_day' => { 'name' => 'Billing day',
                       'default' => 1,
                     },
     'seconds'       => { 'name' => 'Time limit for this package',