add_full_period fix, RT#9874
[freeside.git] / FS / FS / part_pkg / prorate_Mixin.pm
1 package FS::part_pkg::prorate_Mixin;
2
3 use strict;
4 use vars qw(@ISA %info);
5 use Time::Local qw(timelocal);
6
7 @ISA = qw(FS::part_pkg);
8 %info = ( 
9   'disabled'  => 1,
10 );
11
12 =head1 NAME
13
14 FS::part_pkg::prorate_Mixin - Mixin class for part_pkg:: classes that 
15 need to prorate partial months
16
17 =head1 SYNOPSIS
18
19 package FS::part_pkg::...;
20 use base qw( FS::part_pkg::prorate_Mixin );
21
22 sub calc_recur {
23   ...
24   if( conditions that trigger prorate ) {
25     # sets $$sdate and $param->{'months'}, returns the prorated charge
26     $charges = $self->calc_prorate($cust_pkg, $sdate, $param, $cutoff_day);
27   } 
28   ...
29 }
30
31 =head METHODS
32
33 =item calc_prorate CUST_PKG
34
35 Takes all the arguments of calc_recur, followed by a day of the month 
36 to prorate to (which must be <= 28).  Calculates a prorated charge from 
37 the $sdate to that day, and sets the $sdate and $param->{months} accordingly.
38 base_recur() will be called to determine the base price per billing cycle.
39
40 Options:
41 - add_full_period: Bill for the time up to the prorate day plus one full
42 billing period after that.
43 - prorate_round_day: Round the current time to the nearest full day, 
44 instead of using the exact time.
45
46 =cut
47
48 sub calc_prorate {
49   my $self  = shift;
50   my ($cust_pkg, $sdate, $details, $param, $cutoff_day) = @_;
51  
52   my $charge = $self->base_recur($cust_pkg, $sdate) || 0;
53   if($cutoff_day) {
54     # only works for freq >= 1 month; probably can't be fixed
55     my $mnow = $$sdate;
56     my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($mnow))[0..5];
57     if( $self->option('prorate_round_day',1) ) {
58       $mday++ if $hour >= 12;
59       $mnow = timelocal(0,0,0,$mday,$mon,$year);
60     }
61     my $mend;
62     my $mstart;
63     # if cutoff day > 28, force it to the 1st of next month
64     if ( $cutoff_day > 28 ) {
65       $cutoff_day = 1;
66       # and if we are currently after the 28th, roll the current day 
67       # forward to that day
68       if ( $mday > 28 ) {
69         $mday = 1;
70         #set $mnow = $mend so the amount billed will be zero
71         $mnow = timelocal(0,0,0,1,$mon == 11 ? 0 : $mon + 1,$year+($mon==11));
72       }
73     }
74     if ( $mday >= $cutoff_day ) {
75       $mend = 
76         timelocal(0,0,0,$cutoff_day,$mon == 11 ? 0 : $mon + 1,$year+($mon==11));
77       $mstart =
78         timelocal(0,0,0,$cutoff_day,$mon,$year);
79     }
80     else {
81       $mend = 
82         timelocal(0,0,0,$cutoff_day,$mon,$year);
83       $mstart = 
84         timelocal(0,0,0,$cutoff_day,$mon == 0 ? 11 : $mon - 1,$year-($mon==0));
85     }
86    
87     # next bill date will be figured as $$sdate + one period
88     $$sdate = $mstart;
89
90     my $permonth = $charge / $self->freq;
91     my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) );
92
93     # add a full period if currently billing for a partial period
94     if ( $self->option('add_full_period',1) and $months < $self->freq ) {
95       $months += $self->freq;
96       $$sdate = $self->add_freq($mstart);
97     }
98
99     $param->{'months'} = $months;
100     $charge = sprintf('%.2f', $permonth * $months);
101   }
102   return $charge;
103 }
104
105 1;