upstream-markup call rating and global default rates, #30633
authorMark Wells <mark@freeside.biz>
Fri, 24 Oct 2014 01:04:13 +0000 (18:04 -0700)
committerMark Wells <mark@freeside.biz>
Fri, 24 Oct 2014 01:04:13 +0000 (18:04 -0700)
FS/FS/Schema.pm
FS/FS/cdr.pm
FS/FS/part_pkg/agent_cdr.pm
FS/FS/rate.pm
FS/FS/rate_detail.pm
httemplate/edit/elements/rate_detail.html
httemplate/edit/process/rate_detail.html
httemplate/edit/rate_detail.html

index 54f84e0..9b1fa94 100644 (file)
@@ -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' ] ],
index 9859dfa..7a5668d 100644 (file)
@@ -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 );
 
index 55792f2..a638b5b 100644 (file)
@@ -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
 
index b2d121c..9505cea 100644 (file)
@@ -47,6 +47,16 @@ Rate name
 
 Optional agent (see L<FS::agent>) 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<FS::rate_detail>) 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<FS::rate_detail>), 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
index 389f439..f777965 100644 (file)
@@ -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<FS::rate_region>) 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
index 14b5211..7b5ec31 100644 (file)
@@ -47,34 +47,85 @@ with row headers showing the region name and prefixes.
 %   }
 %   foreach my $rate_time (@rate_time, '') {
   <TD>
-%     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++;
+  </TD>
+%   } # foreach @rate_time
+</TR>
+%   $row++;
+% }# foreach @rate_region
+% if ( !$opt{regionnum} ) {
+%# global default
+<TR>
+  <TD COLSPAN=2 STYLE="padding-top: 10px">
+    <B>Global default</B> (for calls not matching any prefix)
+  </TD>
+  <TD STYLE="padding-top: 10px">
+%   # default rate: set a null region
+    <B>
+    <& .detail_box,
+      detail      => $rate->default_detail,
+      ratetimenum => '',
+      cdrtypenum  => '',
+      regionnum   => '',
+      ratenum     => $rate->ratenum
+    &>
+    </B>
+  </TD>
+% }
+</TABLE>
+<%def .detail_box>
+<%args>
+$detail => undef,
+$ratetimenum
+$cdrtypenum
+$regionnum
+$ratenum
+</%args>
+% if ($detail) {
       <TABLE CLASS="inv" STYLE="border:none">
-      <TR><TD><% edit_link($detail) %><% $money_char.$detail->min_charge %>
+      <TR><TD><% 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) {
+              <BR>+ 
+%     }
+%   }
+%   if ( $detail->upstream_mult_charge > 0 
+%        or $detail->upstream_mult_cost > 0) {
+              <% $detail->upstream_mult_charge %> &times; 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 %></A>
       </TD></TR>
       <% granularity_detail($detail) %>
       <% min_included_detail($detail) %>
       <% conn_charge_detail($detail) %>
-      <TR><TD><% ( $rate_time || $cdrtypenum ) ? delete_link($detail) : '' %>
+      <TR><TD><% ( $ratetimenum || $cdrtypenum ) ? delete_link($detail) : '' %>
       </TD></TR>
     </TABLE>
-%     }
-%     else { #!$detail
-    <% add_link($rate, $region, $rate_time, $cdrtypenum) %>
-%     }
-%     $col++;
-  </TD>
-%   } # foreach @rate_time
-</TR>
-%   $row++;
-% }# foreach @rate_region
-</TABLE>
-
+% } else {
+    <% add_link($ratenum, $regionnum, $ratetimenum, $cdrtypenum) %>
+% }
+</%def>
 <%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) = @_;
   '<A HREF="javascript:void(0);" onclick="'.
   include( '/elements/popup_link_onclick.html',
              'action'      => "${p}edit/rate_detail.html?ratenum=".
-                                  $rate->ratenum.
+                                  $ratenum.
                                ';dest_regionnum='.
-                                  $region->regionnum.
+                                  $regionnum.
                                ';ratetimenum='.
-                                 ($rate_time ? $rate_time->ratetimenum : '').
+                                 ($ratetimenum || '').
                                ";cdrtypenum=$cdrtypenum",
              'actionlabel' => 'Add rate',
-             'height'      => 460,
+             'width'       => 580,
+             'height'      => 550,
              ).'">'.small('(add)').'</A>'
 }
 
@@ -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) {
     '<TR><TD>'.
     small('in '.$granularity{$rate_detail->sec_granularity}.' increments').
     '</TD></TR>';
index 6200d61..0709d50 100644 (file)
@@ -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;
+  }
+};
+
 </%init>
index 0de6ecc..3e80072 100644 (file)
@@ -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', },
                      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