4.x+ self-service API: list and remove cards on file, RT#38919
[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, "EXISTS( SELECT 1 FROM part_pkg_discount
46                           WHERE part_pkg.pkgpart = part_pkg_discount.pkgpart )";
47   push @where,
48     "NOT EXISTS (
49        SELECT 1 FROM cust_bill_pkg_discount
50          WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_discount.billpkgnum
51     )";
52
53   my $extra_sql = 'WHERE '. join(' AND ', @where);
54
55   my @cust_pkg = 
56     qsearch({
57       'table' => 'cust_pkg',
58       'select' => "DISTINCT cust_pkg.*",
59       'addl_from' => 'JOIN cust_bill_pkg USING(pkgnum) '.
60                      'JOIN part_pkg USING(pkgpart)',
61       'hashref' => {},
62       'extra_sql' => $extra_sql,
63     }); 
64
65   ($cust_bill, @cust_pkg);
66 }
67
68 =item _discountable_pkgs_at_term
69
70 =cut
71
72 #this isn't even a method
73 sub _discountable_pkgs_at_term {
74   my ($term, @pkgs) = @_;
75   my $part_pkg = new FS::part_pkg { freq => $term - 1 };
76   grep { ( !$_->adjourn || $_->adjourn > $part_pkg->add_freq($_->bill) ) && 
77          ( !$_->expire  || $_->expire  > $part_pkg->add_freq($_->bill) )
78        }
79     @pkgs;
80 }
81
82 =item discount_terms
83
84 Returns a list of lengths for term discounts
85
86 =cut
87
88 sub discount_terms {
89   my $self = shift;
90
91   my %terms = ();
92
93   my @discount_pkgs = $self->_discount_pkgs_and_bill;
94   shift @discount_pkgs; #discard bill;
95   
96   map { $terms{$_->months} = 1 }
97     grep { $_->months && $_->months > 1 }
98     map { $_->discount }
99     map { $_->part_pkg->part_pkg_discount }
100     @discount_pkgs;
101
102   return sort { $a <=> $b } keys %terms;
103
104 }
105
106 =item discount_term_values MONTHS
107
108 Returns a list with credit, dollar amount saved, and total bill acheived
109 by prepaying the most recent invoice for MONTHS.
110
111 =cut
112
113 # XXX this should work by creating a quotation; then we can finally retire
114 # the "no_commit" option, which doesn't work with modern tax calculation
115
116 sub discount_term_values {
117   my $self = shift;
118   my $term = shift;
119
120   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
121
122   warn "$me discount_term_values called with $term\n" if $DEBUG;
123
124   my %result = ();
125
126   my @packages = $self->_discount_pkgs_and_bill;
127   my $cust_bill = shift(@packages);
128   @packages = _discountable_pkgs_at_term( $term, @packages );
129   return () unless scalar(@packages);
130
131   $_->bill($_->last_bill) foreach @packages;
132   my @final = map { new FS::cust_pkg { $_->hash } } @packages;
133
134   my %options = (
135                   'recurring_only' => 1,
136                   'no_usage_reset' => 1,
137                   'no_commit'      => 1,
138                 );
139
140   my %params =  (
141                   'return_bill'    => [],
142                   'pkg_list'       => \@packages,
143                   'time'           => $cust_bill->_date,
144                 );
145
146   my $error = $self->bill(%options, %params);
147   die $error if $error; # XXX think about this a bit more
148
149   my $credit = 0;
150   $credit += $_->charged foreach @{$params{return_bill}};
151   $credit = sprintf('%.2f', $credit);
152   warn "$me discount_term_values $term credit: $credit\n" if $DEBUG;
153
154   %params =  (
155                'return_bill'    => [],
156                'pkg_list'       => \@packages,
157                'time'           => $packages[0]->part_pkg->add_freq($cust_bill->_date)
158              );
159
160   $error = $self->bill(%options, %params);
161   die $error if $error; # XXX think about this a bit more
162
163   my $next = 0;
164   $next += $_->charged foreach @{$params{return_bill}};
165   warn "$me discount_term_values $term next: $next\n" if $DEBUG;
166   
167   %params =  ( 
168                'return_bill'    => [],
169                'pkg_list'       => \@final,
170                'time'           => $cust_bill->_date,
171                'freq_override'  => $term,
172              );
173
174   $error = $self->bill(%options, %params);
175   die $error if $error; # XXX think about this a bit more
176
177   my $final = $self->balance - $credit;
178   $final += $_->charged foreach @{$params{return_bill}};
179   $final = sprintf('%.2f', $final);
180   warn "$me discount_term_values $term final: $final\n" if $DEBUG;
181
182   my $savings = sprintf('%.2f', $self->balance + ($term - 1) * $next - $final);
183
184   ( $credit, $savings, $final );
185
186 }
187
188 sub discount_terms_hash {
189   my $self = shift;
190
191   my %result = ();
192   my @terms = $self->discount_terms;
193   foreach my $term (@terms) {
194     my @result = $self->discount_term_values($term);
195     $result{$term} = [ @result ] if scalar(@result);
196   }
197
198   return %result;
199
200 }
201
202 =back
203
204 =head1 BUGS
205
206 =head1 SEE ALSO
207
208 L<FS::cust_main>, L<FS::cust_main::Billing>
209
210 =cut
211
212 1;