1 package FS::part_pkg::prorate_Mixin;
5 use Time::Local qw( timelocal );
9 # define all fields that are referenced in this code
11 'add_full_period' => {
12 'name' => 'When prorating first month, also bill for one full '.
16 'prorate_round_day' => {
17 'name' => 'When prorating, round to the nearest full day',
20 'prorate_defer_bill' => {
21 'name' => 'When prorating, defer the first bill until the '.
26 'fieldorder' => [ qw(prorate_defer_bill prorate_round_day add_full_period) ],
30 @{ $info{'fieldorder'} }
35 FS::part_pkg::prorate_Mixin - Mixin class for part_pkg:: classes that
36 need to prorate partial months
40 package FS::part_pkg::...;
41 use base qw( FS::part_pkg::prorate_Mixin );
45 if( conditions that trigger prorate ) {
46 # sets $$sdate and $param->{'months'}, returns the prorated charge
47 $charges = $self->calc_prorate($cust_pkg, $sdate, $param, $cutoff_day);
54 =item calc_prorate CUST_PKG SDATE DETAILS PARAM CUTOFF_DAY
56 Takes all the arguments of calc_recur. Calculates a prorated charge from
57 the $sdate to the cutoff day for this package definition, and sets the $sdate
58 and $param->{months} accordingly. base_recur() will be called to determine
59 the base price per billing cycle.
62 - add_full_period: Bill for the time up to the prorate day plus one full
63 billing period after that.
64 - prorate_round_day: Round the current time to the nearest full day,
65 instead of using the exact time.
66 - prorate_defer_bill: Don't bill the prorate interval until the prorate
72 my ($self, $cust_pkg, $sdate, $details, $param, $cutoff_day) = @_;
73 die "no cutoff_day" unless $cutoff_day;
74 die "can't prorate non-monthly package\n" if $self->freq =~ /\D/;
76 my $charge = $self->base_recur($cust_pkg, $sdate) || 0;
80 # if this is the first bill but the bill date has been set
81 # (by prorate_defer_bill), calculate from the setup date,
82 # and append the setup fee to @$details.
83 if ( $self->option('prorate_defer_bill',1)
84 and ! $cust_pkg->getfield('last_bill')
85 and $cust_pkg->setup ) {
86 #warn "[calc_prorate] #".$cust_pkg->pkgnum.": running deferred setup\n";
87 $param->{'setup_fee'} = $self->calc_setup($cust_pkg, $$sdate, $details);
88 $mnow = $cust_pkg->setup;
92 ($mnow, $mend, $mstart) = $self->_endpoints($mnow, $cutoff_day);
94 # next bill date will be figured as $$sdate + one period
97 my $permonth = $charge / $self->freq;
98 my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) );
100 # add a full period if currently billing for a partial period
101 if ( ( $self->option('add_full_period',1)
102 or $self->option('prorate_defer_bill',1) ) # necessary
103 and $months < $self->freq ) {
104 $months += $self->freq;
105 $$sdate = $self->add_freq($mstart);
108 $param->{'months'} = $months;
109 $charge = sprintf('%.2f', $permonth * $months);
114 =item prorate_setup CUST_PKG SDATE
116 Set up the package. This only has an effect if prorate_defer_bill is
117 set, in which case it postpones the next bill to the cutoff day.
123 my ($cust_pkg, $sdate) = @_;
124 my $cutoff_day = $self->cutoff_day($cust_pkg);
125 if ( ! $cust_pkg->bill
126 and $self->option('prorate_defer_bill',1)
129 my ($mnow, $mend, $mstart) = $self->_endpoints($sdate, $cutoff_day);
130 # if today is the cutoff day, set the next bill to right now instead
131 # of waiting a month.
132 if ( $mnow - $mstart < 86400 ) {
133 $cust_pkg->bill($mnow);
136 $cust_pkg->bill($mend);
143 =item _endpoints TIME CUTOFF_DAY
145 Given a current time and a day of the month to prorate to, return three
146 times: the start of the prorate interval (usually the current time), the
147 end of the prorate interval (i.e. the cutoff date), and the time one month
148 before the end of the prorate interval.
153 my ($self, $mnow, $cutoff_day) = @_;
155 # only works for freq >= 1 month; probably can't be fixed
156 my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($mnow))[0..5];
157 if( $self->option('prorate_round_day',1) ) {
158 # If the time is 12:00-23:59, move to the next day by adding 18
159 # hours to $mnow. Because of DST this can end up from 05:00 to 18:59
160 # but it's always within the next day.
161 $mnow += 64800 if $hour >= 12;
162 # Get the new day, month, and year.
163 ($mday,$mon,$year) = (localtime($mnow))[3..5];
164 # Then set $mnow to midnight on that day.
165 $mnow = timelocal(0,0,0,$mday,$mon,$year);
169 # if cutoff day > 28, force it to the 1st of next month
170 if ( $cutoff_day > 28 ) {
172 # and if we are currently after the 28th, roll the current day
173 # forward to that day
176 #set $mnow = $mend so the amount billed will be zero
177 $mnow = timelocal(0,0,0,1,$mon == 11 ? 0 : $mon + 1,$year+($mon==11));
180 if ( $mday >= $cutoff_day ) {
182 timelocal(0,0,0,$cutoff_day,$mon == 11 ? 0 : $mon + 1,$year+($mon==11));
184 timelocal(0,0,0,$cutoff_day,$mon,$year);
188 timelocal(0,0,0,$cutoff_day,$mon,$year);
190 timelocal(0,0,0,$cutoff_day,$mon == 0 ? 11 : $mon - 1,$year-($mon==0));
192 return ($mnow, $mend, $mstart);