discounts, RT#6679
[freeside.git] / FS / FS / part_pkg / flat.pm
1 package FS::part_pkg::flat;
2
3 use strict;
4 use vars qw( @ISA %info
5              %usage_fields %usage_recharge_fields
6              @usage_fieldorder @usage_recharge_fieldorder
7            );
8 use Tie::IxHash;
9 use List::Util qw(min); # max);
10 #use FS::Record qw(qsearch);
11 use FS::UI::bytecount;
12 use FS::Conf;
13 use FS::part_pkg;
14
15 @ISA = qw(FS::part_pkg);
16
17 tie my %temporalities, 'Tie::IxHash',
18   'upcoming'  => "Upcoming (future)",
19   'preceding' => "Preceding (past)",
20 ;
21
22 %usage_fields = (
23
24     'seconds'       => { 'name' => 'Time limit for this package',
25                          'default' => '',
26                          'check' => sub { shift =~ /^\d*$/ },
27                        },
28     'upbytes'       => { 'name' => 'Upload limit for this package',
29                          'default' => '',
30                          'check' => sub { shift =~ /^\d*$/ },
31                          'format' => \&FS::UI::bytecount::display_bytecount,
32                          'parse' => \&FS::UI::bytecount::parse_bytecount,
33                        },
34     'downbytes'     => { 'name' => 'Download limit for this package',
35                          'default' => '',
36                          'check' => sub { shift =~ /^\d*$/ },
37                          'format' => \&FS::UI::bytecount::display_bytecount,
38                          'parse' => \&FS::UI::bytecount::parse_bytecount,
39                        },
40     'totalbytes'    => { 'name' => 'Transfer limit for this package',
41                          'default' => '',
42                          'check' => sub { shift =~ /^\d*$/ },
43                          'format' => \&FS::UI::bytecount::display_bytecount,
44                          'parse' => \&FS::UI::bytecount::parse_bytecount,
45                        },
46 );
47
48 %usage_recharge_fields = (
49
50     'recharge_amount'       => { 'name' => 'Cost of recharge for this package',
51                          'default' => '',
52                          'check' => sub { shift =~ /^\d*(\.\d{2})?$/ },
53                        },
54     'recharge_seconds'      => { 'name' => 'Recharge time for this package',
55                          'default' => '',
56                          'check' => sub { shift =~ /^\d*$/ },
57                        },
58     'recharge_upbytes'      => { 'name' => 'Recharge upload for this package',
59                          'default' => '',
60                          'check' => sub { shift =~ /^\d*$/ },
61                          'format' => \&FS::UI::bytecount::display_bytecount,
62                          'parse' => \&FS::UI::bytecount::parse_bytecount,
63                        },
64     'recharge_downbytes'    => { 'name' => 'Recharge download for this package',
65                          'default' => '',
66                          'check' => sub { shift =~ /^\d*$/ },
67                          'format' => \&FS::UI::bytecount::display_bytecount,
68                          'parse' => \&FS::UI::bytecount::parse_bytecount,
69                        },
70     'recharge_totalbytes'   => { 'name' => 'Recharge transfer for this package',
71                          'default' => '',
72                          'check' => sub { shift =~ /^\d*$/ },
73                          'format' => \&FS::UI::bytecount::display_bytecount,
74                          'parse' => \&FS::UI::bytecount::parse_bytecount,
75                        },
76     'usage_rollover' => { 'name' => 'Allow usage from previous period to roll '.
77                                     ' over into current period',
78                           'type' => 'checkbox',
79                         },
80     'recharge_reset' => { 'name' => 'Reset usage to these values on manual '.
81                                     'package recharge',
82                           'type' => 'checkbox',
83                         },
84 );
85
86 @usage_fieldorder = qw( seconds upbytes downbytes totalbytes );
87 @usage_recharge_fieldorder = qw(
88   recharge_amount recharge_seconds recharge_upbytes
89   recharge_downbytes recharge_totalbytes
90   usage_rollover recharge_reset
91 );
92
93 %info = (
94   'name' => 'Flat rate (anniversary billing)',
95   'shortname' => 'Anniversary',
96   'fields' => {
97     'setup_fee'     => { 'name' => 'Setup fee for this package',
98                          'default' => 0,
99                        },
100     'recur_fee'     => { 'name' => 'Recurring fee for this package',
101                          'default' => 0,
102                        },
103
104     #false laziness w/voip_cdr.pm
105     'recur_temporality' => { 'name' => 'Charge recurring fee for period',
106                              'type' => 'select',
107                              'select_options' => \%temporalities,
108                            },
109
110     %usage_fields,
111     %usage_recharge_fields,
112
113     'unused_credit' => { 'name' => 'Credit the customer for the unused portion'.
114                                    ' of service at cancellation',
115                          'type' => 'checkbox',
116                        },
117     'externalid' => { 'name'   => 'Optional External ID',
118                       'default' => '',
119                     },
120   },
121   'fieldorder' => [ qw( setup_fee recur_fee recur_temporality unused_credit ),
122                     @usage_fieldorder, @usage_recharge_fieldorder,
123                     qw( externalid ),
124                   ],
125   'weight' => 10,
126 );
127
128 sub calc_setup {
129   my($self, $cust_pkg, $sdate, $details ) = @_;
130
131   my $i = 0;
132   my $count = $self->option( 'additional_count', 'quiet' ) || 0;
133   while ($i < $count) {
134     push @$details, $self->option( 'additional_info' . $i++ );
135   }
136
137   my $quantity = $cust_pkg->quantity || 1;
138
139   sprintf("%.2f", $quantity * $self->unit_setup($cust_pkg, $sdate, $details) );
140 }
141
142 sub unit_setup {
143   my($self, $cust_pkg, $sdate, $details ) = @_;
144
145   $self->option('setup_fee');
146 }
147
148 sub calc_recur {
149   my $self = shift;
150   my($cust_pkg) = @_;
151
152   #my $last_bill = $cust_pkg->last_bill;
153   my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup
154
155   return 0
156     if $self->option('recur_temporality', 1) eq 'preceding' && $last_bill == 0;
157
158   my $br = $self->base_recur(@_);
159
160   my $discount = $self->calc_discount(@_);
161
162   sprintf('%.2f', $br - $discount);
163 }
164
165 sub calc_discount {
166   my $self = shift;
167   my($cust_pkg, $sdate, $details, $param ) = @_;
168
169   my $br = $self->base_recur(@_);
170
171   my $tot_discount = 0;
172   #UI enforces just 1 for now, will need ordering when they can be stacked
173   foreach my $cust_pkg_discount ( $cust_pkg->cust_pkg_discount_active ) {
174      my $discount = $cust_pkg_discount->discount;
175      #UI enforces one or the other (for now?  probably for good)
176      my $amount = 0;
177      $amount += $discount->amount;
178      $amount += sprintf('%.2f', $discount->percent * $br / 100 );
179
180      my $chg_months = $param->{'months'} || $cust_pkg->part_pkg->freq;
181      
182      my $months = $discount->months
183                     ? min( $chg_months,
184                            $discount->months - $cust_pkg->months_used )
185                     : $chg_months;
186
187      my $error = $cust_pkg_discount->increment_months_used($months);
188      die "error discounting: $error" if $error;
189
190      $amount *= $months;
191      $amount = sprintf('%.2f', $amount);
192
193      #add details on discount to invoice
194      my $conf = new FS::Conf;
195      my $money_char = $conf->config('money_char') || '$';  
196      $months = sprintf('%.2f', $months) if $months =~ /\./;
197
198      my $d = 'Includes ';
199      $d .= $discount->name. ' ' if $discount->name;
200      $d .= 'discount of '. $discount->description_short;
201      $d .= " for $months month". ( $months!=1 ? 's' : '' );
202      $d .= ": $money_char$amount" if $months != 1 || $discount->percent;
203      push @$details, $d;
204
205      $tot_discount += $amount;
206   }
207
208   sprintf('%.2f', $tot_discount);
209 }
210
211 sub base_recur {
212   my($self, $cust_pkg) = @_;
213   $self->option('recur_fee', 1) || 0;
214 }
215
216 sub base_recur_permonth {
217   my($self, $cust_pkg) = @_;
218
219   return 0 unless $self->freq =~ /^\d+$/ && $self->freq > 0;
220
221   sprintf('%.2f', $self->base_recur($cust_pkg) / $self->freq );
222 }
223
224 sub calc_remain {
225   my ($self, $cust_pkg, %options) = @_;
226
227   my $time;
228   if ($options{'time'}) {
229     $time = $options{'time'};
230   } else {
231     $time = time;
232   }
233
234   my $next_bill = $cust_pkg->getfield('bill') || 0;
235
236   #my $last_bill = $cust_pkg->last_bill || 0;
237   my $last_bill = $cust_pkg->get('last_bill') || 0; #->last_bill falls back to setup
238
239   return 0 if    ! $self->base_recur($cust_pkg)
240               || ! $self->option('unused_credit', 1)
241               || ! $last_bill
242               || ! $next_bill
243               || $next_bill < $time;
244
245   my %sec = (
246     'h' =>    3600, # 60 * 60
247     'd' =>   86400, # 60 * 60 * 24
248     'w' =>  604800, # 60 * 60 * 24 * 7
249     'm' => 2629744, # 60 * 60 * 24 * 365.2422 / 12 
250   );
251
252   $self->freq =~ /^(\d+)([hdwm]?)$/
253     or die 'unparsable frequency: '. $self->freq;
254   my $freq_sec = $1 * $sec{$2||'m'};
255   return 0 unless $freq_sec;
256
257   sprintf("%.2f", $self->base_recur($cust_pkg) * ( $next_bill - $time ) / $freq_sec );
258
259 }
260
261 sub is_free_options {
262   qw( setup_fee recur_fee );
263 }
264
265 sub is_prepaid { 0; } #no, we're postpaid
266
267 #XXX discounts only on recurring fees for now (no setup/one-time or usage)
268 sub can_discount {
269   my $self = shift;
270   $self->freq =~ /^\d+$/ && $self->freq > 0;
271 }
272
273 sub usage_valuehash {
274   my $self = shift;
275   map { $_, $self->option($_) }
276     grep { $self->option($_, 'hush') } 
277     qw(seconds upbytes downbytes totalbytes);
278 }
279
280 sub reset_usage {
281   my($self, $cust_pkg, %opt) = @_;
282   warn "   resetting usage counters" if defined($opt{debug}) && $opt{debug} > 1;
283   my %values = $self->usage_valuehash;
284   if ($self->option('usage_rollover', 1)) {
285     $cust_pkg->recharge(\%values);
286   }else{
287     $cust_pkg->set_usage(\%values, %opt);
288   }
289 }
290
291 1;