separate CDR summary by rate table, #15535
[freeside.git] / FS / FS / part_pkg / voip_tiered.pm
1 package FS::part_pkg::voip_tiered;
2 use base qw( FS::part_pkg::voip_cdr );
3
4 use strict;
5 use vars qw( $DEBUG %info );
6 use Tie::IxHash;
7 use Date::Format;
8 use Text::CSV_XS;
9 use FS::Conf;
10 use FS::Record qw(qsearchs); # qsearch);
11 use FS::cdr;
12 use FS::rate_tier;
13 use FS::rate_detail;
14
15 use Data::Dumper;
16
17 $DEBUG = 0;
18
19 tie my %cdr_inout, 'Tie::IxHash',
20   'outbound'         => 'Outbound',
21   'inbound'          => 'Inbound',
22   'outbound_inbound' => 'Outbound and Inbound',
23 ;
24
25 tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
26
27 %info = (
28   'name' => 'VoIP tiered rate pricing of CDRs',
29   'shortname' => 'VoIP/telco CDR tiered rating',
30   'inherit_fields' => [ 'voip_cdr', 'prorate_Mixin', 'global_Mixin' ],
31   'fields' => {
32     'tiernum' => { 'name' => 'Tier plan',
33                    'type' => 'select',
34                    'select_table' => 'rate_tier',
35                    'select_key'   => 'tiernum',
36                    'select_label' => 'tiername',
37                  },
38     'cdr_inout' => { 'name'=> 'Call direction when using phone number matching',
39                      'type'=> 'select',
40                      'select_options' => \%cdr_inout,
41                    },
42     'min_included' => { 'name' => 'Minutes included',
43                     },
44     'sec_granularity' => { 'name' => 'Granularity',
45                            'type' => 'select',
46                            'select_options' => \%granularity,
47                          },
48     'rating_method'                          => { 'disabled' => 1 },
49     'ratenum'                                => { 'disabled' => 1 },
50     'intrastate_ratenum'                     => { 'disabled' => 1 },
51     'min_charge'                             => { 'disabled' => 1 },
52     'ignore_unrateable'                      => { 'disabled' => 1 },
53     'domestic_prefix'                        => { 'disabled' => 1 },
54     'international_prefix'                   => { 'disabled' => 1 },
55     'disable_tollfree'                       => { 'disabled' => 1 },
56     'noskip_src_length_accountcode_tollfree' => { 'disabled' => 1 },
57     'accountcode_tollfree_ratenum'           => { 'disabled' => 1 },
58     'noskip_dst_length_accountcode_tollfree' => { 'disabled' => 1 },
59   },
60   'fieldorder' => [qw(
61                        recur_temporality
62                        recur_method cutoff_day ),
63                        FS::part_pkg::prorate_Mixin::fieldorder,
64                    qw(
65                        cdr_svc_method cdr_inout
66                        tiernum
67                      )
68                   ],
69   'weight' => 44,
70 );
71
72 sub calc_usage {
73   my $self = shift;
74   my($cust_pkg, $sdate, $details, $param ) = @_;
75
76   #my $last_bill = $cust_pkg->last_bill;
77   my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup
78
79   return 0
80     if $self->recur_temporality eq 'preceding'
81     && ( $last_bill eq '' || $last_bill == 0 );
82
83   my $included_min    = $self->option('min_included', 1) || 0;
84   my $cdr_svc_method  = $self->option('cdr_svc_method',1)||'svc_phone.phonenum';
85   my $cdr_inout       = ($cdr_svc_method eq 'svc_phone.phonenum')
86                           && $self->option('cdr_inout',1)
87                           || 'outbound';
88   my $use_duration    = $self->option('use_duration');
89   my $granularity     = length($self->option('sec_granularity'))
90                           ? $self->option('sec_granularity')
91                           : 60;
92
93   #for check_chargable, so we don't keep looking up options inside the loop
94   my %opt_cache = ();
95
96   my($svc_table, $svc_field) = split('\.', $cdr_svc_method);
97
98   my %options = (
99     'disable_src'    => $self->option('disable_src'),
100     'default_prefix' => $self->option('default_prefix'),
101     'cdrtypenum'     => $self->option('use_cdrtypenum'),
102     'status'         => '',
103     'for_update'     => 1,
104   );  # $last_bill, $$sdate )
105   $options{'by_svcnum'} = 1 if $svc_field eq 'svcnum';
106
107   ###
108   # pass one: find the total minutes/calls and store the CDRs
109   ###
110   my $total = 0;
111
112   my @cust_svc;
113   if( $self->option('bill_inactive_svcs',1) ) {
114     #XXX in this mode do we need to restrict the set of CDRs by date also?
115     @cust_svc = $cust_pkg->h_cust_svc($$sdate, $last_bill);
116   } else {
117     @cust_svc = $cust_pkg->cust_svc;
118   }
119   @cust_svc = grep { $_->part_svc->svcdb eq $svc_table } @cust_svc;
120
121   foreach my $cust_svc (@cust_svc) {
122
123     my $svc_x;
124     if( $self->option('bill_inactive_svcs',1) ) {
125       $svc_x = $cust_svc->h_svc_x($$sdate, $last_bill);
126     }
127     else {
128       $svc_x = $cust_svc->svc_x;
129     }
130
131     foreach my $pass (split('_', $cdr_inout)) {
132
133       $options{'inbound'} = ( $pass eq 'inbound' );
134
135       foreach my $cdr (
136         $svc_x->get_cdrs( %options )
137       ) {
138         if ( $DEBUG > 1 ) {
139           warn "rating CDR $cdr\n".
140                join('', map { "  $_ => ". $cdr->{$_}. "\n" } keys %$cdr );
141         }
142
143         my $charge = '';
144         my $seconds = '';
145
146         $seconds = $use_duration ? $cdr->duration : $cdr->billsec;
147
148         $seconds += $granularity - ( $seconds % $granularity )
149           if $seconds      # don't granular-ize 0 billsec calls (bills them)
150           && $granularity  # 0 is per call
151           && $seconds % $granularity;
152         my $minutes = $granularity ? ($seconds / 60) : 1;
153
154         my $charge_min = $minutes;
155
156         $included_min -= $minutes;
157         if ( $included_min > 0 ) {
158           $charge_min = 0;
159         } else {
160            $charge_min = 0 - $included_min;
161            $included_min = 0;
162         }
163
164         my $error = $cdr->set_status_and_rated_price(
165           'processing-tiered',
166           '', #charge,
167           $cust_svc->svcnum,
168           'inbound'       => ($pass eq 'inbound'),
169           'rated_minutes' => $charge_min,
170           'rated_seconds' => $seconds,
171         );
172         die $error if $error;
173
174         $total += $charge_min;
175
176       } # $cdr
177
178     } # $pass
179  
180   } # $cust_svc
181
182   ###
183   # pass two: find a tiered rate and do the rest
184   ###
185
186   my $rate_tier = qsearchs('rate_tier', { tiernum=>$self->option('tiernum') } )
187     or die "unknown tiernum ". $self->option('tiernum');
188   my $rate_tier_detail = $rate_tier->rate_tier_detail( $total )
189     or die "no base rate for tier? ($total)";
190   my $min_charge = $rate_tier_detail->min_charge;
191
192   my $output_format = $self->option('output_format', 'Hush!') || 'default';
193
194   my $csv = new Text::CSV_XS;
195
196   my $charges = 0;
197   my @invoice_details_sort;
198
199   $options{'status'} = 'processing-tiered';
200
201   foreach my $cust_svc (@cust_svc) {
202
203     my $svc_x;
204     if( $self->option('bill_inactive_svcs',1) ) {
205       $svc_x = $cust_svc->h_svc_x($$sdate, $last_bill);
206     }
207     else {
208       $svc_x = $cust_svc->svc_x;
209     }
210
211     foreach my $pass (split('_', $cdr_inout)) {
212
213       $options{'inbound'} = ( $pass eq 'inbound' );
214
215       foreach my $cdr (
216         $svc_x->get_cdrs( %options )
217       ) {
218
219         my $object = $options{'inbound'}
220                        ? $cdr->cdr_termination( 1 ) #1: inbound
221                        : $cdr;
222
223         my $charge_min = $object->rated_minutes;
224
225         my $charge = sprintf('%.4f', ( $min_charge * $charge_min )
226                                      + 0.0000000001 ); #so 1.00005 rounds to 1.0001
227
228         if ( $charge > 0 ) {
229           $charges += $charge;
230
231           my $detail = $self->sum_usage ? '' :
232             $cdr->downstream_csv( 'format'  => $output_format,
233                                   'charge'  => $charge,
234                                   'seconds' => ($use_duration ? 
235                                                   $cdr->duration : 
236                                                   $cdr->billsec),
237                                   'granularity' => $granularity,
238                                 );
239
240           my $call_details =
241             { format      => 'C',
242               detail      => $detail,
243               amount      => $charge,
244               #classnum    => $cdr->calltypenum, #classnum
245               #phonenum    => $phonenum, #XXX need this to sort on them
246               accountcode => $cdr->accountcode,
247               startdate   => $cdr->startdate,
248               duration    => $object->rated_seconds,
249             };
250
251            #warn "  adding details on charge to invoice: [ ".
252           #    join(', ', @{$call_details} ). " ]"
253           #  if ( $DEBUG && ref($call_details) );
254           push @invoice_details_sort, [ $call_details, $cdr->calldate_unix ];
255         }
256
257         my $error = $cdr->set_status_and_rated_price(
258           'done',
259           $charge,
260           $cust_svc->svcnum,
261           'inbound'       => $options{'inbound'},
262           'rated_minutes' => $charge_min,
263           'rated_seconds' => $object->rated_seconds,
264         );
265         die $error if $error;
266
267       } # $cdr
268
269     } # $pass
270
271     if ( $self->sum_usage ) {
272       # then summarize all accumulated details within this svc_x
273       # and then flush them
274       push @$details, $self->sum_detail($svc_x, \@invoice_details_sort);
275       @invoice_details_sort = ();
276     }
277
278   } # $cust_svc
279
280   if ( !$self->sum_usage ) {
281     #sort them
282     my @sorted_invoice_details = 
283       sort { ${$a}[1] <=> ${$b}[1] } @invoice_details_sort;
284     foreach my $sorted_call_detail ( @sorted_invoice_details ) {
285         push @$details, ${$sorted_call_detail}[0];
286     }
287   }
288
289   unshift @$details, { format => 'C',
290                        detail => FS::cdr::invoice_header($output_format),
291                      }
292     if @$details;
293
294   $charges;
295 }
296
297 1;
298