1 package FS::part_pkg::voip_tiered;
2 use base qw( FS::part_pkg::voip_cdr );
5 use vars qw( $DEBUG %info );
10 use FS::Record qw(qsearchs); # qsearch);
19 tie my %cdr_inout, 'Tie::IxHash',
20 'outbound' => 'Outbound',
21 'inbound' => 'Inbound',
22 'outbound_inbound' => 'Outbound and Inbound',
25 tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
28 'name' => 'VoIP tiered rate pricing of CDRs',
29 'shortname' => 'VoIP/telco CDR tiered rating',
30 'inherit_fields' => [ 'voip_cdr', 'prorate_Mixin', 'global_Mixin' ],
32 'tiernum' => { 'name' => 'Tier plan',
34 'select_table' => 'rate_tier',
35 'select_key' => 'tiernum',
36 'select_label' => 'tiername',
38 'cdr_inout' => { 'name'=> 'Call direction when using phone number matching',
40 'select_options' => \%cdr_inout,
42 'min_included' => { 'name' => 'Minutes included',
44 'sec_granularity' => { 'name' => 'Granularity',
46 'select_options' => \%granularity,
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 },
62 recur_method cutoff_day ),
63 FS::part_pkg::prorate_Mixin::fieldorder,
65 cdr_svc_method cdr_inout
74 my($cust_pkg, $sdate, $details, $param ) = @_;
76 #my $last_bill = $cust_pkg->last_bill;
77 my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup
80 if $self->recur_temporality eq 'preceding'
81 && ( $last_bill eq '' || $last_bill == 0 );
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)
88 my $use_duration = $self->option('use_duration');
89 my $granularity = length($self->option('sec_granularity'))
90 ? $self->option('sec_granularity')
93 #for check_chargable, so we don't keep looking up options inside the loop
96 my($svc_table, $svc_field) = split('\.', $cdr_svc_method);
99 'disable_src' => $self->option('disable_src'),
100 'default_prefix' => $self->option('default_prefix'),
101 'cdrtypenum' => $self->option('use_cdrtypenum'),
104 ); # $last_bill, $$sdate )
105 $options{'by_svcnum'} = 1 if $svc_field eq 'svcnum';
108 # pass one: find the total minutes/calls and store the CDRs
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);
117 @cust_svc = $cust_pkg->cust_svc;
119 @cust_svc = grep { $_->part_svc->svcdb eq $svc_table } @cust_svc;
121 foreach my $cust_svc (@cust_svc) {
124 if( $self->option('bill_inactive_svcs',1) ) {
125 $svc_x = $cust_svc->h_svc_x($$sdate, $last_bill);
128 $svc_x = $cust_svc->svc_x;
131 foreach my $pass (split('_', $cdr_inout)) {
133 $options{'inbound'} = ( $pass eq 'inbound' );
135 my $cdr_search = $svc_x->psearch_cdrs(%options);
136 $cdr_search->limit(1000);
137 $cdr_search->increment(0);
138 while ( my $cdr = $cdr_search->fetch ) {
141 warn "rating CDR $cdr\n".
142 join('', map { " $_ => ". $cdr->{$_}. "\n" } keys %$cdr );
148 $seconds = $use_duration ? $cdr->duration : $cdr->billsec;
150 $seconds += $granularity - ( $seconds % $granularity )
151 if $seconds # don't granular-ize 0 billsec calls (bills them)
152 && $granularity # 0 is per call
153 && $seconds % $granularity;
154 my $minutes = $granularity ? ($seconds / 60) : 1;
156 my $charge_min = $minutes;
158 $included_min -= $minutes;
159 if ( $included_min > 0 ) {
162 $charge_min = 0 - $included_min;
166 my $error = $cdr->set_status_and_rated_price(
170 'inbound' => ($pass eq 'inbound'),
171 'rated_minutes' => $charge_min,
172 'rated_seconds' => $seconds,
174 die $error if $error;
176 $total += $charge_min;
178 $cdr_search->adjust(1) if $cdr->freesidestatus eq '';
187 # pass two: find a tiered rate and do the rest
190 my $rate_tier = qsearchs('rate_tier', { tiernum=>$self->option('tiernum') } )
191 or die "unknown tiernum ". $self->option('tiernum');
192 my $rate_tier_detail = $rate_tier->rate_tier_detail( $total )
193 or die "no base rate for tier? ($total)";
194 my $min_charge = $rate_tier_detail->min_charge;
196 my $output_format = $self->option('output_format', 'Hush!') || 'default';
198 my $formatter = FS::detail_format->new($output_format, buffer => $details);
202 $options{'status'} = 'processing-tiered';
204 foreach my $cust_svc (@cust_svc) {
207 if( $self->option('bill_inactive_svcs',1) ) {
208 $svc_x = $cust_svc->h_svc_x($$sdate, $last_bill);
211 $svc_x = $cust_svc->svc_x;
214 foreach my $pass (split('_', $cdr_inout)) {
216 $options{'inbound'} = ( $pass eq 'inbound' );
217 # tell the formatter what we're sending it
218 $formatter->inbound($options{'inbound'});
220 my $cdr_search = $svc_x->psearch_cdrs(%options);
221 $cdr_search->limit(1000);
222 $cdr_search->increment(0);
223 while ( my $cdr = $cdr_search->fetch ) {
225 my $object = $options{'inbound'}
226 ? $cdr->cdr_termination( 1 ) #1: inbound
229 my $charge_min = $object->rated_minutes;
231 my $charge = sprintf('%.4f', ( $min_charge * $charge_min )
232 + 0.0000000001 ); #so 1.00005 rounds to 1.0001
238 my $error = $cdr->set_status_and_rated_price(
242 'inbound' => $options{'inbound'},
243 'rated_minutes' => $charge_min,
244 'rated_seconds' => $object->rated_seconds,
246 die $error if $error;
248 $formatter->append($cdr);
250 $cdr_search->adjust(1) if $cdr->freesidestatus eq 'processing-tiered';
259 unshift @$details, $formatter->header if @$details;