fix selfservice display when there are term discounts defined, looks like fallout...
[freeside.git] / FS / FS / cust_main / Billing_Discount.pm
1 package FS::cust_main::Billing_Discount;
2
3 use strict;
4 use vars qw( $DEBUG $me );
5 use FS::Record qw( qsearch ); #qsearchs );
6 use FS::cust_pkg;
7
8 # 1 is mostly method/subroutine entry and options
9 # 2 traces progress of some operations
10 # 3 is even more information including possibly sensitive data
11 $DEBUG = 0;
12 $me = '[FS::cust_main::Billing_Discount]';
13
14 =head1 NAME
15
16 FS::cust_main::Billing_Discount - Billing discount mixin for cust_main
17
18 =head1 SYNOPSIS
19
20 =head1 DESCRIPTION
21
22 These methods are available on FS::cust_main objects.
23
24 =head1 METHODS
25
26 =over 4
27
28 =item _discount_pkg_and_bill
29
30 =cut
31
32 sub _discount_pkgs_and_bill {
33   my $self = shift;
34
35   my @cust_bill = $self->cust_bill;
36   my $cust_bill = pop @cust_bill;
37   return () unless $cust_bill && $cust_bill->owed;
38
39   my @where = ();
40   push @where, "cust_bill_pkg.invnum = ". $cust_bill->invnum;
41   push @where, "cust_bill_pkg.pkgpart_override IS NULL";
42   push @where, "part_pkg.freq = '1'";
43   push @where, "(cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0)";
44   push @where, "(cust_pkg.susp   IS NULL OR cust_pkg.susp   = 0)";
45   push @where, "0<(SELECT count(*) FROM part_pkg_discount
46                   WHERE part_pkg.pkgpart = part_pkg_discount.pkgpart)";
47   push @where,
48     "0=(SELECT count(*) FROM cust_bill_pkg_discount
49          WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_discount.billpkgnum)";
50
51   my $extra_sql = 'WHERE '. join(' AND ', @where);
52
53   my @cust_pkg = 
54     qsearch({
55       'table' => 'cust_pkg',
56       'select' => "DISTINCT cust_pkg.*",
57       'addl_from' => 'JOIN cust_bill_pkg USING(pkgnum) '.
58                      'JOIN part_pkg USING(pkgpart)',
59       'hashref' => {},
60       'extra_sql' => $extra_sql,
61     }); 
62
63   ($cust_bill, @cust_pkg);
64 }
65
66 =item _discountable_pkgs_at_term
67
68 =cut
69
70 #this isn't even a method
71 sub _discountable_pkgs_at_term {
72   my ($term, @pkgs) = @_;
73   my $part_pkg = new FS::part_pkg { freq => $term - 1 };
74   grep { ( !$_->adjourn || $_->adjourn > $part_pkg->add_freq($_->bill) ) && 
75          ( !$_->expire  || $_->expire  > $part_pkg->add_freq($_->bill) )
76        }
77     @pkgs;
78 }
79
80 =item discount_terms
81
82 Returns a list of lengths for term discounts
83
84 =cut
85
86 sub discount_terms {
87   my $self = shift;
88
89   my %terms = ();
90
91   my @discount_pkgs = $self->_discount_pkgs_and_bill;
92   shift @discount_pkgs; #discard bill;
93
94   # convert @discount_pkgs (the list of packages that have available discounts)
95   # to a list of distinct term lengths in months, and strip any decimal places
96   # from the number of months, not that it should have any 
97   map { $terms{sprintf('%.0f', $_->months)} = 1 }
98     grep { $_->months && $_->months > 1 }
99     map { $_->discount }
100     map { $_->part_pkg->part_pkg_discount }
101     @discount_pkgs;
102
103   return sort { $a <=> $b } keys %terms;
104
105 }
106
107 =item discount_term_values MONTHS
108
109 Returns a list with credit, dollar amount saved, and total bill acheived
110 by prepaying the most recent invoice for MONTHS.
111
112 =cut
113
114 sub discount_term_values {
115   my $self = shift;
116   my $term = shift;
117
118   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
119
120   warn "$me discount_term_values called with $term\n" if $DEBUG;
121
122   my %result = ();
123
124   my @packages = $self->_discount_pkgs_and_bill;
125   my $cust_bill = shift(@packages);
126   @packages = _discountable_pkgs_at_term( $term, @packages );
127   return () unless scalar(@packages);
128
129   $_->bill($_->last_bill) foreach @packages;
130   my @final = map { new FS::cust_pkg { $_->hash } } @packages;
131
132   my %options = (
133                   'recurring_only' => 1,
134                   'no_usage_reset' => 1,
135                   'no_commit'      => 1,
136                 );
137
138   my %params =  (
139                   'return_bill'    => [],
140                   'pkg_list'       => \@packages,
141                   'time'           => $cust_bill->_date,
142                 );
143
144   my $error = $self->bill(%options, %params);
145   die $error if $error; # XXX think about this a bit more
146
147   my $credit = 0;
148   $credit += $_->charged foreach @{$params{return_bill}};
149   $credit = sprintf('%.2f', $credit);
150   warn "$me discount_term_values $term credit: $credit\n" if $DEBUG;
151
152   %params =  (
153                'return_bill'    => [],
154                'pkg_list'       => \@packages,
155                'time'           => $packages[0]->part_pkg->add_freq($cust_bill->_date)
156              );
157
158   $error = $self->bill(%options, %params);
159   die $error if $error; # XXX think about this a bit more
160
161   my $next = 0;
162   $next += $_->charged foreach @{$params{return_bill}};
163   warn "$me discount_term_values $term next: $next\n" if $DEBUG;
164   
165   %params =  ( 
166                'return_bill'    => [],
167                'pkg_list'       => \@final,
168                'time'           => $cust_bill->_date,
169                'freq_override'  => $term,
170              );
171
172   $error = $self->bill(%options, %params);
173   die $error if $error; # XXX think about this a bit more
174
175   my $final = $self->balance - $credit;
176   $final += $_->charged foreach @{$params{return_bill}};
177   $final = sprintf('%.2f', $final);
178   warn "$me discount_term_values $term final: $final\n" if $DEBUG;
179
180   my $savings = sprintf('%.2f', $self->balance + ($term - 1) * $next - $final);
181
182   ( $credit, $savings, $final );
183
184 }
185
186 sub discount_terms_hash {
187   my $self = shift;
188
189   my %result = ();
190   my @terms = $self->discount_terms;
191   foreach my $term (@terms) {
192     my @result = $self->discount_term_values($term);
193     $result{$term} = [ @result ] if scalar(@result);
194   }
195
196   return %result;
197
198 }
199
200 =back
201
202 =head1 BUGS
203
204 =head1 SEE ALSO
205
206 L<FS::cust_main>, L<FS::cust_main::Billing>
207
208 =cut
209
210 1;