fix dated one-time charges with freeside-daily -d, RT#29339
[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   map { $terms{$_->months} = 1 }
95     grep { $_->months && $_->months > 1 }
96     map { $_->discount }
97     map { $_->part_pkg->part_pkg_discount }
98     @discount_pkgs;
99
100   return sort { $a <=> $b } keys %terms;
101
102 }
103
104 =item discount_term_values MONTHS
105
106 Returns a list with credit, dollar amount saved, and total bill acheived
107 by prepaying the most recent invoice for MONTHS.
108
109 =cut
110
111 sub discount_term_values {
112   my $self = shift;
113   my $term = shift;
114
115   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
116
117   warn "$me discount_term_values called with $term\n" if $DEBUG;
118
119   my %result = ();
120
121   my @packages = $self->_discount_pkgs_and_bill;
122   my $cust_bill = shift(@packages);
123   @packages = _discountable_pkgs_at_term( $term, @packages );
124   return () unless scalar(@packages);
125
126   $_->bill($_->last_bill) foreach @packages;
127   my @final = map { new FS::cust_pkg { $_->hash } } @packages;
128
129   my %options = (
130                   'recurring_only' => 1,
131                   'no_usage_reset' => 1,
132                   'no_commit'      => 1,
133                 );
134
135   my %params =  (
136                   'return_bill'    => [],
137                   'pkg_list'       => \@packages,
138                   'time'           => $cust_bill->_date,
139                 );
140
141   my $error = $self->bill(%options, %params);
142   die $error if $error; # XXX think about this a bit more
143
144   my $credit = 0;
145   $credit += $_->charged foreach @{$params{return_bill}};
146   $credit = sprintf('%.2f', $credit);
147   warn "$me discount_term_values $term credit: $credit\n" if $DEBUG;
148
149   %params =  (
150                'return_bill'    => [],
151                'pkg_list'       => \@packages,
152                'time'           => $packages[0]->part_pkg->add_freq($cust_bill->_date)
153              );
154
155   $error = $self->bill(%options, %params);
156   die $error if $error; # XXX think about this a bit more
157
158   my $next = 0;
159   $next += $_->charged foreach @{$params{return_bill}};
160   warn "$me discount_term_values $term next: $next\n" if $DEBUG;
161   
162   %params =  ( 
163                'return_bill'    => [],
164                'pkg_list'       => \@final,
165                'time'           => $cust_bill->_date,
166                'freq_override'  => $term,
167              );
168
169   $error = $self->bill(%options, %params);
170   die $error if $error; # XXX think about this a bit more
171
172   my $final = $self->balance - $credit;
173   $final += $_->charged foreach @{$params{return_bill}};
174   $final = sprintf('%.2f', $final);
175   warn "$me discount_term_values $term final: $final\n" if $DEBUG;
176
177   my $savings = sprintf('%.2f', $self->balance + ($term - 1) * $next - $final);
178
179   ( $credit, $savings, $final );
180
181 }
182
183 sub discount_terms_hash {
184   my $self = shift;
185
186   my %result = ();
187   my @terms = $self->discount_terms;
188   foreach my $term (@terms) {
189     my @result = $self->discount_term_values($term);
190     $result{$term} = [ @result ] if scalar(@result);
191   }
192
193   return %result;
194
195 }
196
197 =back
198
199 =head1 BUGS
200
201 =head1 SEE ALSO
202
203 L<FS::cust_main>, L<FS::cust_main::Billing>
204
205 =cut
206
207 1;