From 6f2ca4da179be987ed021d6ec9e40774f817ac6e Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 23 Oct 2014 18:04:13 -0700 Subject: [PATCH] upstream-markup call rating and global default rates, #30633 --- FS/FS/Schema.pm | 11 ++-- FS/FS/cdr.pm | 20 ++++-- FS/FS/part_pkg/agent_cdr.pm | 8 ++- FS/FS/rate.pm | 31 +++++++-- FS/FS/rate_detail.pm | 20 ++++-- httemplate/edit/elements/rate_detail.html | 102 +++++++++++++++++++++++------- httemplate/edit/process/rate_detail.html | 32 ++++++++-- httemplate/edit/rate_detail.html | 22 ++++--- 8 files changed, 191 insertions(+), 55 deletions(-) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 54f84e038..9b1fa9456 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -3348,9 +3348,10 @@ sub tables_hashref { 'rate' => { 'columns' => [ - 'ratenum', 'serial', '', '', '', '', - 'ratename', 'varchar', '', $char_d, '', '', - 'agentnum', 'int', 'NULL', '', '', '', + 'ratenum', 'serial', '', '', '', '', + 'ratename', 'varchar', '',$char_d, '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'default_detailnum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'ratenum', 'unique' => [], @@ -3362,7 +3363,7 @@ sub tables_hashref { 'ratedetailnum', 'serial', '', '', '', '', 'ratenum', 'int', '', '', '', '', 'orig_regionnum', 'int', 'NULL', '', '', '', - 'dest_regionnum', 'int', '', '', '', '', + 'dest_regionnum', 'int', 'NULL', '', '', '', 'min_included', 'int', '', '', '', '', 'conn_charge', 'decimal', '', '10,4', '0.0000', '', 'conn_cost', 'decimal', '', '10,4', '0.0000', '', @@ -3374,6 +3375,8 @@ sub tables_hashref { 'classnum', 'int', 'NULL', '', '', '', 'cdrtypenum', 'int', 'NULL', '', '', '', 'region_group', 'char', 'NULL', 1, '', '', + 'upstream_mult_charge', 'decimal', '', '10,4', '0.0000', '', + 'upstream_mult_cost', 'decimal', '', '10,4', '0.0000', '', ], 'primary_key' => 'ratedetailnum', 'unique' => [ [ 'ratenum', 'orig_regionnum', 'dest_regionnum' ] ], diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index 9859dfade..7a5668d52 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -799,8 +799,8 @@ sub rate_prefix { } + my $regionnum = $rate_detail->dest_regionnum; my $rate_region = $rate_detail->dest_region; - my $regionnum = $rate_region->regionnum; warn " found rate for regionnum $regionnum ". "and rate detail $rate_detail\n" if $DEBUG; @@ -842,6 +842,11 @@ sub rate_prefix { my $charge = 0; my $connection_charged = 0; + # before doing anything else, if there's an upstream multiplier and + # an upstream price, add that to the charge. (usually the rate detail + # will then have a minute charge of zero, but not necessarily.) + $charge += ($self->upstream_price || 0) * $rate_detail->upstream_mult_charge; + my $etime; while($seconds_left) { my $ratetimenum = $rate_detail->ratetimenum; # may be empty @@ -989,7 +994,7 @@ sub rate_prefix { $price, $opt{'svcnum'}, 'rated_pretty_dst' => $pretty_dst, - 'rated_regionname' => $rate_region->regionname, + 'rated_regionname' => ($rate_region ? $rate_region->regionname : ''), 'rated_seconds' => $rated_seconds, #$seconds, 'rated_granularity' => $rate_detail->sec_granularity, #$granularity 'rated_ratedetailnum' => $rate_detail->ratedetailnum, @@ -1073,10 +1078,15 @@ sub rate_cost { my $rate_detail = qsearchs('rate_detail', { 'ratedetailnum' => $self->rated_ratedetailnum } ); - return $rate_detail->min_cost if $self->rated_granularity == 0; + my $charge = 0; + $charge += ($self->upstream_price || 0) * ($rate_detail->upstream_mult_cost); - my $minutes = $self->rated_seconds / 60; - my $charge = $rate_detail->conn_cost + $minutes * $rate_detail->min_cost; + if ( $self->rated_granularity == 0 ) { + $charge += $rate_detail->min_cost; + } else { + my $minutes = $self->rated_seconds / 60; + $charge += $rate_detail->conn_cost + $minutes * $rate_detail->min_cost; + } sprintf('%.2f', $charge + .00001 ); diff --git a/FS/FS/part_pkg/agent_cdr.pm b/FS/FS/part_pkg/agent_cdr.pm index 55792f2d2..a638b5b5a 100644 --- a/FS/FS/part_pkg/agent_cdr.pm +++ b/FS/FS/part_pkg/agent_cdr.pm @@ -23,7 +23,7 @@ tie my %temporalities, 'Tie::IxHash', %info = ( 'name' => 'Wholesale CDR cost billing, for master customers of an agent.', - 'shortname' => 'Whilesale CDR cost billing for agent.', + 'shortname' => 'Wholesale CDR cost billing for agent', 'inherit_fields' => [ 'prorate_Mixin', 'global_Mixin' ], 'fields' => { #false laziness w/cdr_termination @@ -177,7 +177,11 @@ sub calc_recur { my $classnum = ''; #usage class? #option to turn off? or just use squelch_cdr for the customer probably - push @$details, [ 'C', $call_details, $cost, $classnum ]; + # XXX use detail_format for this at some point + push @$details, { 'format' => 'C', + 'detail' => $call_details, + 'amount' => $cost, + 'classnum' => $classnum }; #eofalse laziness w/cdr_termination diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm index b2d121c22..9505cea86 100644 --- a/FS/FS/rate.pm +++ b/FS/FS/rate.pm @@ -47,6 +47,16 @@ Rate name Optional agent (see L) for agent-virtualized rates. +=item default_detailnum + +Optional rate detail to apply when a call doesn't match any region in the +rate plan. If this is not set, the call will either be left unrated (though +it may still be processed under a different pricing addon package), or be +marked as 'skipped', or throw a fatal error, depending on the setting of +the 'ignore_unrateable' package option. + +=item + =back =head1 METHODS @@ -269,6 +279,7 @@ sub check { $self->ut_numbern('ratenum') || $self->ut_text('ratename') #|| $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum') + || $self->ut_numbern('default_detailnum') ; return $error if $error; @@ -278,8 +289,8 @@ sub check { =item dest_detail REGIONNUM | RATE_REGION_OBJECTD | HASHREF Returns the rate detail (see L) for this rate to the -specificed destination, or the empty string if no rate can be found for -the given destination. +specificed destination. If no rate can be found, returns the default +rate if there is one, and an empty string otherwise. Destination can be specified as an FS::rate_detail object or regionnum (see L), or as a hashref containing the following keys: @@ -380,8 +391,8 @@ sub dest_detail { foreach (@details) { return $_ if $_->ratetimenum eq ''; } - # found nothing - return; + # if still nothing, return the global default rate for this plan + return $self->default_detail; } =item rate_detail @@ -408,6 +419,18 @@ sub agent { =back +=item default_detail + +Returns the default rate detail, if there is one. + +=cut + +sub default_detail { + my $self = shift; + $self->default_detailnum ? + FS::rate_detail->by_key($self->default_detailnum) : '' +} + =head1 SUBROUTINES =over 4 diff --git a/FS/FS/rate_detail.pm b/FS/FS/rate_detail.pm index 389f4393b..f77796560 100644 --- a/FS/FS/rate_detail.pm +++ b/FS/FS/rate_detail.pm @@ -61,6 +61,13 @@ inherits from FS::Record. The following fields are currently supported: =item region_group - Group in region group for rate plan +=item upstream_mult_charge - the multiplier to apply to the upstream price. +Defaults to zero, and should stay zero unless this rate is intended to include +a markup on pre-rated CDRs. + +=item upstream_mult_cost - the multiplier to apply to the upstream price to +calculate the wholesale cost. + =back =head1 METHODS @@ -125,7 +132,7 @@ sub check { $self->ut_numbern('ratedetailnum') || $self->ut_foreign_key('ratenum', 'rate', 'ratenum') || $self->ut_foreign_keyn('orig_regionnum', 'rate_region', 'regionnum' ) - || $self->ut_foreign_key('dest_regionnum', 'rate_region', 'regionnum' ) + || $self->ut_foreign_keyn('dest_regionnum', 'rate_region', 'regionnum' ) || $self->ut_number('min_included') #|| $self->ut_money('min_charge') @@ -139,6 +146,9 @@ sub check { || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum' ) || $self->ut_enum('region_group', [ '', 'Y' ]) + + || $self->ut_floatn('upstream_mult_charge') + || $self->ut_floatn('upstream_mult_cost') ; return $error if $error; @@ -190,10 +200,11 @@ with this call plan rate. sub dest_regionname { my $self = shift; - $self->dest_region->regionname; + my $dest_region = $self->dest_region; + $dest_region ? $dest_region->regionname : 'Global default'; } -=item dest_regionname +=item dest_prefixes_short Returns a short list of the prefixes for the destination region (see L) associated with this call plan rate. @@ -202,7 +213,8 @@ Returns a short list of the prefixes for the destination region sub dest_prefixes_short { my $self = shift; - $self->dest_region->prefixes_short; + my $dest_region = $self->dest_region; + $dest_region ? $dest_region->prefixes_short : ''; } =item rate_time diff --git a/httemplate/edit/elements/rate_detail.html b/httemplate/edit/elements/rate_detail.html index 14b52110b..7b5ec314a 100644 --- a/httemplate/edit/elements/rate_detail.html +++ b/httemplate/edit/elements/rate_detail.html @@ -47,34 +47,85 @@ with row headers showing the region name and prefixes. % } % foreach my $rate_time (@rate_time, '') { -% my $detail = $details[$row][$col]; -% if($detail) { + <& .detail_box, + detail => $details[$row][$col], + ratetimenum => ($rate_time ? $rate_time->ratetimenum : ''), + cdrtypenum => $cdrtypenum, + regionnum => $region->regionnum, + ratenum => $rate->ratenum + &> +% $col++; + +% } # foreach @rate_time + +% $row++; +% }# foreach @rate_region +% if ( !$opt{regionnum} ) { +%# global default + + + Global default (for calls not matching any prefix) + + +% # default rate: set a null region + + <& .detail_box, + detail => $rate->default_detail, + ratetimenum => '', + cdrtypenum => '', + regionnum => '', + ratenum => $rate->ratenum + &> + + +% } + +<%def .detail_box> +<%args> +$detail => undef, +$ratetimenum +$cdrtypenum +$regionnum +$ratenum + +% if ($detail) { - <% granularity_detail($detail) %> <% min_included_detail($detail) %> <% conn_charge_detail($detail) %> -
<% edit_link($detail) %><% $money_char.$detail->min_charge %> +
<% edit_link($detail) %> +% if ( $detail->min_charge > 0 or $detail->conn_charge > 0) { + <% $money_char.$detail->min_charge %> <% $detail->sec_granularity ? ' / minute':' / call' %> % if ( $detail->min_cost ) { (<% $money_char.$detail->min_cost %> cost) % } +% if ( $detail->upstream_mult_charge > 0 +% or $detail->upstream_mult_cost > 0) { +
+ +% } +% } +% if ( $detail->upstream_mult_charge > 0 +% or $detail->upstream_mult_cost > 0) { + <% $detail->upstream_mult_charge %> × upstream price +% if ( $detail->upstream_mult_cost > 0 ) { + (<% $detail->upstream_mult_cost %> cost) +% } +% } +% if ( $detail->upstream_mult_charge == 0 +% and $detail->min_charge == 0 +% and $detail->conn_charge == 0 ) { + Free +% } <% $edit_hint %>
<% ( $rate_time || $cdrtypenum ) ? delete_link($detail) : '' %> +
<% ( $ratetimenum || $cdrtypenum ) ? delete_link($detail) : '' %>
-% } -% else { #!$detail - <% add_link($rate, $region, $rate_time, $cdrtypenum) %> -% } -% $col++; - -% } # foreach @rate_time - -% $row++; -% }# foreach @rate_region - - +% } else { + <% add_link($ratenum, $regionnum, $ratetimenum, $cdrtypenum) %> +% } + <%once> tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); @@ -95,25 +146,27 @@ sub edit_link { include( '/elements/popup_link_onclick.html', 'action' => "${p}edit/rate_detail.html?$ratedetailnum", 'actionlabel' => 'Edit rate', - 'height' => 460, + 'height' => 550, + 'width' => 580, #default# 'width' => 540, #default# 'color' => '#333399', ) . '">' } sub add_link { - my ($rate, $region, $rate_time, $cdrtypenum) = @_; + my ($ratenum, $regionnum, $ratetimenum, $cdrtypenum) = @_; ' 'Add rate', - 'height' => 460, + 'width' => 580, + 'height' => 550, ).'">'.small('(add)').'' } @@ -133,7 +186,10 @@ sub delete_link { sub granularity_detail { my $rate_detail = shift; - if($rate_detail->sec_granularity != 60 && $rate_detail->sec_granularity > 0) { + if( + $rate_detail->sec_granularity != 60 + && $rate_detail->sec_granularity > 0 + && $rate_detail->min_charge > 0) { ''. small('in '.$granularity{$rate_detail->sec_granularity}.' increments'). ''; diff --git a/httemplate/edit/process/rate_detail.html b/httemplate/edit/process/rate_detail.html index 6200d615f..0709d5079 100644 --- a/httemplate/edit/process/rate_detail.html +++ b/httemplate/edit/process/rate_detail.html @@ -1,13 +1,35 @@ -<% include( 'elements/process.html', - 'table' => 'rate_detail', - 'popup_reload' => 'Rate changed', #a popup "parent reload" for now +<& elements/process.html, + 'table' => 'rate_detail', + 'popup_reload' => 'Rate changed', #a popup "parent reload" for now #someday change the individual element and go away instead - ) -%> + 'noerror_callback' => $set_default_detail +&> <%init> my $conf = new FS::Conf; die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); +my $set_default_detail = sub { + my ($cgi, $rate_detail) = @_; +warn Dumper $rate_detail; + if (!$rate_detail->dest_regionnum) { + # then this is a global default rate + my $rate = $rate_detail->rate; + if ($rate->default_detailnum) { + if ($rate->default_detailnum == $rate_detail->ratedetailnum) { + return; + } else { + # there's somehow an existing default rate. remove it. + my $old_default = $rate->default_detail; + my $error = $old_default->delete; + die "$error (removing old default rate)\n" if $error; + } + } + $rate->set('default_detailnum' => $rate_detail->ratedetailnum); + my $error = $rate->replace; + die "$error (setting default rate)\n" if $error; + } +}; + diff --git a/httemplate/edit/rate_detail.html b/httemplate/edit/rate_detail.html index 0de6ecc1e..3e800726e 100644 --- a/httemplate/edit/rate_detail.html +++ b/httemplate/edit/rate_detail.html @@ -15,6 +15,8 @@ 'conn_cost' => 'Wholesale connection cost', 'min_cost' => 'Wholesale cost per minute/call', 'classnum' => 'Usage class', + 'upstream_mult_charge'=> 'Upstream multiplier (retail)', + 'upstream_mult_cost' => 'Upstream multiplier (cost)', }, 'fields' => [ { field=>'ratenum', type=>'hidden', }, @@ -46,13 +48,15 @@ labels => \%granularity, disable_empty => 1, }, - { field =>'classnum', - type =>'select-table', - table =>'usage_class', - name_col =>'classname', - empty_label =>'(default)', - hashref =>{ disabled => '' }, + { field => 'classnum', + type => 'select-table', + table => 'usage_class', + name_col => 'classname', + empty_label => '(default)', + hashref => { disabled => '' }, }, + { field => 'upstream_mult_charge', type => 'text', }, + { field => 'upstream_mult_cost', type => 'text', }, ], 'new_hashref_callback' => sub { @@ -62,6 +66,8 @@ cdrtypenum => scalar($cgi->param('cdrtypenum')), min_included => 0, conn_charge => 0, + upstream_mult_charge => 0, + upstream_mult_cost => 0, } }, ) @@ -85,8 +91,8 @@ if ( $keywords =~ /^(\d+)$/ || $cgi->param('ratedetailnum') =~ /^(\d+)$/ ) { my $rate_detail = qsearchs('rate_detail', { 'ratedetailnum' => $1 } ) or die "unknown ratedetailnum $1"; - $name = - $rate_detail->rate->ratename. ' rate for '. $rate_detail->dest_regionname; + $name = $rate_detail->rate->ratename. ' rate for '. + ($rate_detail->dest_regionname || 'global default'); } #sec_granularity should default to 60! for new rates when this gets used for em -- 2.11.0