[freeside-commits] freeside/FS/FS Conf.pm, 1.497, 1.498 Schema.pm, 1.346, 1.347 option_Common.pm, 1.11, 1.12 cdr.pm, 1.79, 1.80

Ivan,,, ivan at wavetail.420.am
Fri Dec 30 02:16:41 PST 2011


Update of /home/cvs/cvsroot/freeside/FS/FS
In directory wavetail.420.am:/tmp/cvs-serv16160/FS/FS

Modified Files:
	Conf.pm Schema.pm option_Common.pm cdr.pm 
Log Message:
Rate CDRs immediately, RT#15839

Index: option_Common.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/option_Common.pm,v
retrieving revision 1.11
retrieving revision 1.12
diff -u -w -d -r1.11 -r1.12
--- option_Common.pm	22 Sep 2011 21:41:46 -0000	1.11
+++ option_Common.pm	30 Dec 2011 10:16:39 -0000	1.12
@@ -323,6 +323,19 @@
   $obj ? $obj->$valuecol() : '';
 }
 
+=item option_cacheable OPTIONNAME
+
+Same as the option method, but may cache and return a cached value.
+Good for use within loops; otherwise, probably avoid.
+
+=cut
+
+sub option_cacheable {
+  my( $self, $name ) = @_;
+  return $self->{option_cache}{$name} if exists $self->{option_cache}{$name};
+  $self->{option_cache}{$name} = $self->option($name,1);
+}
+
 
 sub option_table {
   my $self = shift;

Index: Schema.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/Schema.pm,v
retrieving revision 1.346
retrieving revision 1.347
diff -u -w -d -r1.346 -r1.347
--- Schema.pm	28 Dec 2011 23:55:31 -0000	1.346
+++ Schema.pm	30 Dec 2011 10:16:39 -0000	1.347
@@ -1806,6 +1806,7 @@
         'svcdb',      'varchar',   '',   $char_d, '', '', 
         'disabled',   'char',  'NULL',   1, '', '', 
         'preserve',   'char', 'NULL',  1, '', '',
+        #'no_selfservice', 'char', 'NULL',         1, '', '',
       ],
       'primary_key' => 'svcpart',
       'unique' => [],
@@ -2984,9 +2985,15 @@
         'charged_party',       'varchar', 'NULL', $char_d, '', '',
 
         # how it was rated internally...
+        'rated_pretty_dst',       'varchar', 'NULL', $char_d, '', '',
+        'rated_regionname',       'varchar', 'NULL', $char_d, '', '',
         'rated_price',         'decimal', 'NULL',  '10,4', '', '',
         'rated_seconds',           'int', 'NULL',      '', '', '',
         'rated_minutes', 'double precision', 'NULL',   '', '', '',
+        'rated_granularity',          'int', 'NULL',      '', '', '',
+        'rated_ratedetailnum',        'int', 'NULL',      '', '', '',
+        'rated_classnum',             'int', 'NULL',      '', '', '', 
+        'rated_ratename',         'varchar', 'NULL', $char_d, '', '', 
 
         'carrierid',               'int', 'NULL',      '', '', '',
 

Index: cdr.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/cdr.pm,v
retrieving revision 1.79
retrieving revision 1.80
diff -u -w -d -r1.79 -r1.80
--- cdr.pm	21 Dec 2011 02:56:56 -0000	1.79
+++ cdr.pm	30 Dec 2011 10:16:39 -0000	1.80
@@ -15,6 +15,9 @@
 use FS::cdr_carrier;
 use FS::cdr_batch;
 use FS::cdr_termination;
+use FS::rate;
+use FS::rate_prefix;
+use FS::rate_detail;
 
 @ISA = qw(FS::Record);
 @EXPORT_OK = qw( _cdr_date_parser_maker _cdr_min_parser_maker );
@@ -130,7 +133,7 @@
 
 =item svcnum - Link to customer service (see L<FS::cust_svc>)
 
-=item freesidestatus - NULL, processing-tiered, done
+=item freesidestatus - NULL, processing-tiered, rated, done
 
 =item freesiderewritestatus - NULL, done, skipped
 
@@ -392,13 +395,29 @@
 
 }
 
-=item set_status_and_rated_price STATUS [ RATED_PRICE [ SVCNUM ] ]
+=item set_status STATUS
 
 Sets the status to the provided string.  If there is an error, returns the
 error, otherwise returns false.
 
 =cut
 
+sub set_status {
+  my($self, $status) = @_;
+  $self->freesidestatus($status);
+  $self->replace;
+}
+
+=item set_status_and_rated_price STATUS RATED_PRICE [ SVCNUM [ OPTION => VALUE ... ] ]
+
+Sets the status and rated price.
+
+Available options are: inbound, rated_seconds, rated_minutes, rated_classnum, rated_ratename
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
 sub set_status_and_rated_price {
   my($self, $status, $rated_price, $svcnum, %opt) = @_;
 
@@ -426,14 +445,428 @@
 
     $self->freesidestatus($status);
     $self->rated_price($rated_price);
-    $self->rated_seconds($opt{rated_seconds}) if exists($opt{rated_seconds});
-    $self->rated_minutes($opt{rated_minutes}) if exists($opt{rated_minutes});
+    $self->$_($opt{$_})
+      foreach grep exists($opt{$_}), map "rated_$_",
+        qw( pretty_dst regionname seconds minutes granularity
+            ratedetailnum classnum ratename );
     $self->svcnum($svcnum) if $svcnum;
     return $self->replace();
 
   }
 }
 
+=item rate [ OPTION => VALUE ... ]
+
+Rates this CDR according and sets the status to 'rated'.
+
+Available options are: part_pkg, svcnum, single_price_included_minutes, region_group, region_group_included_minutes.
+
+part_pkg is required.
+
+If svcnum is specified, will also associate this CDR with the specified svcnum.
+
+single_price_included_minutes is requried for single_price price plans
+(otherwise unused/ignored).  It should be set to a scalar reference of the
+number of included minutes and will be decremented by the rated minutes of this
+CDR.
+
+region_group_included_minutes is required for prefix price plans which have
+included minutes (otherwise unused/ignored).  It should be set to a scalar
+reference of the number of included minutes and will be decremented by the
+rated minutes of this CDR.
+
+region_group_included_minutes_hashref is required for prefix price plans which
+have included minues (otehrwise unused/ignored).  It should be set to an empty
+hashref at the start of a month's rating and then preserved across CDRs.
+
+=cut
+
+sub rate {
+  my( $self, %opt ) = @_;
+  my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
+
+  if ( $DEBUG > 1 ) {
+    warn "rating CDR $self\n".
+         join('', map { "  $_ => ". $self->{$_}. "\n" } keys %$self );
+  }
+
+  my $rating_method = $part_pkg->option_cacheable('rating_method') || 'prefix';
+  my $method = "rate_$rating_method";
+  $self->$method(%opt);
+}
+
+#here?
+our %interval_cache = (); # for timed rates
+
+sub rate_prefix {
+  my( $self, %opt ) = @_;
+  my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
+
+  my $da_rewrote = 0;
+  # this will result in those CDRs being marked as done... is that 
+  # what we want?
+  my @dirass = ();
+  if ( $part_pkg->option_cacheable('411_rewrite') ) {
+    my $dirass = $part_pkg->option_cacheable('411_rewrite');
+    $dirass =~ s/\s//g;
+    @dirass = split(',', $dirass);
+  }
+
+  if ( length($self->dst) && grep { $self->dst eq $_ } @dirass ) {
+    $self->dst('411');
+    $da_rewrote = 1;
+  }
+
+  my $reason = $part_pkg->check_chargable( $self,
+                                           'da_rewrote'   => $da_rewrote,
+                                         );
+  if ( $reason ) {
+    warn "not charging for CDR ($reason)\n" if $DEBUG;
+    return $self->set_status_and_rated_price( 'rated',
+                                              0,
+                                              $opt{'svcnum'},
+                                            );
+  }
+
+    
+  ###
+  # look up rate details based on called station id
+  # (or calling station id for toll free calls)
+  ###
+
+  my( $to_or_from, $number );
+  if ( $self->is_tollfree && ! $part_pkg->option_cacheable('disable_tollfree') )
+  { #tollfree call
+    $to_or_from = 'from';
+    $number = $self->src;
+  } else { #regular call
+    $to_or_from = 'to';
+    $number = $self->dst;
+  }
+
+  warn "parsing call $to_or_from $number\n" if $DEBUG;
+
+  #remove non-phone# stuff and whitespace
+  $number =~ s/\s//g;
+#          my $proto = '';
+#          $dest =~ s/^(\w+):// and $proto = $1; #sip:
+#          my $siphost = '';
+#          $dest =~ s/\@(.*)$// and $siphost = $1; # @10.54.32.1, @sip.example.com
+
+  #determine the country code
+  my $intl = $part_pkg->option_cacheable('international_prefix') || '011';
+  my $countrycode = '';
+  if (    $number =~ /^$intl(((\d)(\d))(\d))(\d+)$/
+       || $number =~ /^\+(((\d)(\d))(\d))(\d+)$/
+     )
+  {
+
+    my( $three, $two, $one, $u1, $u2, $rest ) = ( $1,$2,$3,$4,$5,$6 );
+    #first look for 1 digit country code
+    if ( qsearch('rate_prefix', { 'countrycode' => $one } ) ) {
+      $countrycode = $one;
+      $number = $u1.$u2.$rest;
+    } elsif ( qsearch('rate_prefix', { 'countrycode' => $two } ) ) { #or 2
+      $countrycode = $two;
+      $number = $u2.$rest;
+    } else { #3 digit country code
+      $countrycode = $three;
+      $number = $rest;
+    }
+
+  } else {
+    my $domestic_prefix = $part_pkg->option_cacheable('domestic_prefix');
+    $countrycode = length($domestic_prefix) ? $domestic_prefix : '1';
+    $number =~ s/^$countrycode//;# if length($number) > 10;
+  }
+
+  warn "rating call $to_or_from +$countrycode $number\n" if $DEBUG;
+  my $pretty_dst = "+$countrycode $number";
+  #asterisks here causes inserting the detail to barf, so:
+  $pretty_dst =~ s/\*//g;
+
+  my $eff_ratenum = $self->is_tollfree('accountcode')
+    ? $part_pkg->option_cacheable('accountcode_tollfree_ratenum')
+    : '';
+
+  my $ratename = '';
+  my $intrastate_ratenum = $part_pkg->option_cacheable('intrastate_ratenum');
+  if ( $intrastate_ratenum && !$self->is_tollfree ) {
+    $ratename = 'Interstate'; #until proven otherwise
+    # this is relatively easy only because:
+    # -assume all numbers are valid NANP numbers NOT in a fully-qualified format
+    # -disregard toll-free
+    # -disregard private or unknown numbers
+    # -there is exactly one record in rate_prefix for a given NPANXX
+    # -default to interstate if we can't find one or both of the prefixes
+    my $dstprefix = $self->dst;
+    $dstprefix =~ /^(\d{6})/;
+    $dstprefix = qsearchs('rate_prefix', {   'countrycode' => '1', 
+                                                'npa' => $1, 
+                                         }) || '';
+    my $srcprefix = $self->src;
+    $srcprefix =~ /^(\d{6})/;
+    $srcprefix = qsearchs('rate_prefix', {   'countrycode' => '1',
+                                             'npa' => $1, 
+                                         }) || '';
+    if ($srcprefix && $dstprefix
+        && $srcprefix->state && $dstprefix->state
+        && $srcprefix->state eq $dstprefix->state) {
+      $eff_ratenum = $intrastate_ratenum;
+      $ratename = 'Intrastate'; # XXX possibly just use the ratename?
+    }
+  }
+
+  $eff_ratenum ||= $part_pkg->option_cacheable('ratenum');
+  my $rate = qsearchs('rate', { 'ratenum' => $eff_ratenum })
+    or die "ratenum $eff_ratenum not found!";
+
+  my @ltime = localtime($self->startdate);
+  my $weektime = $ltime[0] + 
+                 $ltime[1]*60 +   #minutes
+                 $ltime[2]*3600 + #hours
+                 $ltime[6]*86400; #days since sunday
+  # if there's no timed rate_detail for this time/region combination,
+  # dest_detail returns the default.  There may still be a timed rate 
+  # that applies after the starttime of the call, so be careful...
+  my $rate_detail = $rate->dest_detail({ 'countrycode' => $countrycode,
+                                         'phonenum'    => $number,
+                                         'weektime'    => $weektime,
+                                         'cdrtypenum'  => $self->cdrtypenum,
+                                      });
+
+  unless ( $rate_detail ) {
+
+    if ( $part_pkg->option_cacheable('ignore_unrateable') ) {
+
+      if ( $part_pkg->option_cacheable('ignore_unrateable') == 2 ) {
+        # mark the CDR as unrateable
+        return $self->set_status_and_rated_price(
+          'failed',
+          '',
+          $opt{'svcnum'},
+        );
+      } elsif ( $part_pkg->option_cacheable('ignore_unrateable') == 1 ) {
+        # warn and continue
+        warn "no rate_detail found for CDR.acctid: ". $self->acctid.
+             "; skipping\n";
+        return '';
+
+      } else {
+        die "unknown ignore_unrateable, pkgpart ". $part_pkg->pkgpart;
+      }
+
+    } else {
+
+      die "FATAL: no rate_detail found in ".
+          $rate->ratenum. ":". $rate->ratename. " rate plan ".
+          "for +$countrycode $number (CDR acctid ". $self->acctid. "); ".
+          "add a rate or set ignore_unrateable flag on the package def\n";
+    }
+
+  }
+
+  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;
+
+  if ( !exists($interval_cache{$regionnum}) ) {
+    my @intervals = (
+      sort { $a->stime <=> $b->stime }
+      map { my $r = $_->rate_time; $r ? $r->intervals : () }
+      $rate->rate_detail
+    );
+    $interval_cache{$regionnum} = \@intervals;
+    warn "  cached ".scalar(@intervals)." interval(s)\n"
+      if $DEBUG;
+  }
+
+  ###
+  # find the price and add detail to the invoice
+  ###
+
+  # About this section:
+  # We don't round _anything_ (except granularizing) 
+  # until the final $charge = sprintf("%.2f"...).
+
+  my $seconds_left = $part_pkg->option_cacheable('use_duration')
+                       ? $self->duration
+                       : $self->billsec;
+  # charge for the first (conn_sec) seconds
+  my $seconds = min($seconds_left, $rate_detail->conn_sec);
+  $seconds_left -= $seconds; 
+  $weektime     += $seconds;
+  my $charge = $rate_detail->conn_charge; 
+
+  my $etime;
+  while($seconds_left) {
+    my $ratetimenum = $rate_detail->ratetimenum; # may be empty
+
+    # find the end of the current rate interval
+    if(@{ $interval_cache{$regionnum} } == 0) {
+      # There are no timed rates in this group, so just stay 
+      # in the default rate_detail for the entire duration.
+      # Set an "end" of 1 past the end of the current call.
+      $etime = $weektime + $seconds_left + 1;
+    } 
+    elsif($ratetimenum) {
+      # This is a timed rate, so go to the etime of this interval.
+      # If it's followed by another timed rate, the stime of that 
+      # interval should match the etime of this one.
+      my $interval = $rate_detail->rate_time->contains($weektime);
+      $etime = $interval->etime;
+    }
+    else {
+      # This is a default rate, so use the stime of the next 
+      # interval in the sequence.
+      my $next_int = first { $_->stime > $weektime } 
+                      @{ $interval_cache{$regionnum} };
+      if ($next_int) {
+        $etime = $next_int->stime;
+      }
+      else {
+        # weektime is near the end of the week, so decrement 
+        # it by a full week and use the stime of the first 
+        # interval.
+        $weektime -= (3600*24*7);
+        $etime = $interval_cache{$regionnum}->[0]->stime;
+      }
+    }
+
+    my $charge_sec = min($seconds_left, $etime - $weektime);
+
+    $seconds_left -= $charge_sec;
+
+    my $included_min = $opt{'region_group_included_min_hashref'} || {};
+
+    $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included
+      unless exists $included_min->{$regionnum}{$ratetimenum};
+
+    my $granularity = $rate_detail->sec_granularity;
+
+    my $minutes;
+    if ( $granularity ) { # charge per minute
+      # Round up to the nearest $granularity
+      if ( $charge_sec and $charge_sec % $granularity ) {
+        $charge_sec += $granularity - ($charge_sec % $granularity);
+      }
+      $minutes = $charge_sec / 60; #don't round this
+    }
+    else { # per call
+      $minutes = 1;
+      $seconds_left = 0;
+    }
+
+    $seconds += $charge_sec;
+
+    my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0;
+
+    ${$opt{region_group_included_min}} -= $minutes 
+        if $region_group && $rate_detail->region_group;
+
+    $included_min->{$regionnum}{$ratetimenum} -= $minutes;
+    if (
+         $included_min->{$regionnum}{$ratetimenum} <= 0
+         && ( ${$opt{region_group_included_min}} <= 0
+              || ! $rate_detail->region_group
+            )
+       )
+    {
+                           #should preserve (display?) this
+      my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum};
+      $included_min->{$regionnum}{$ratetimenum} = 0;
+      $charge += ($rate_detail->min_charge * $charge_min); #still not rounded
+
+    } elsif ( ${$opt{region_group_included_min}} > 0
+              && $region_group
+              && $rate_detail->region_group 
+           )
+    {
+        $included_min->{$regionnum}{$ratetimenum} = 0 
+    }
+
+    # choose next rate_detail
+    $rate_detail = $rate->dest_detail({ 'countrycode' => $countrycode,
+                                        'phonenum'    => $number,
+                                        'weektime'    => $etime,
+                                        'cdrtypenum'  => $self->cdrtypenum })
+            if($seconds_left);
+    # we have now moved forward to $etime
+    $weektime = $etime;
+
+  } #while $seconds_left
+
+  # this is why we need regionnum/rate_region....
+  warn "  (rate region $rate_region)\n" if $DEBUG;
+
+  $self->set_status_and_rated_price(
+    'rated',
+    sprintf('%.2f', $charge + 0.000001), # NOW round it.
+    $opt{'svcnum'},
+    'rated_pretty_dst'    => $pretty_dst,
+    'rated_regionname'    => $rate_region->regionname,
+    'rated_seconds'       => $seconds,
+    'rated_granularity'   => $rate_detail->sec_granularity, #$granularity
+    'rated_ratedetailnum' => $rate_detail->ratedetailnum,
+    'rated_classnum'      => $rate_detail->classnum, #rated_ratedetailnum?
+    'rated_ratename'      => $ratename, #not rate_detail - Intrastate/Interstate
+  );
+
+}
+
+sub rate_upstream_simple {
+  my( $self, %opt ) = @_;
+
+  $self->set_status_and_rated_price( 'rated',
+                                     sprintf('%.3f', $self->upstream_price),
+                                     $opt{'svcnum'},
+                                   );
+}
+
+sub rate_single_price {
+  my( $self, %opt ) = @_;
+  my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
+
+  # a little false laziness w/abov
+  # $rate_detail = new FS::rate_detail({sec_granularity => ... }) ?
+
+  my $granularity = length($part_pkg->option_cacheable('sec_granularity'))
+                      ? $part_pkg->option_cacheable('sec_granularity')
+                      : 60;
+
+  my $seconds = $part_pkg->option_cacheable('use_duration')
+                  ? $self->duration
+                  : $self->billsec;
+
+  $seconds += $granularity - ( $seconds % $granularity )
+    if $seconds      # don't granular-ize 0 billsec calls (bills them)
+    && $granularity  # 0 is per call
+    && $seconds % $granularity;
+  my $minutes = $granularity ? ($seconds / 60) : 1;
+
+  my $charge_min = $minutes;
+
+  ${$opt{single_price_included_min}} -= $minutes;
+  if ( ${$opt{single_price_included_min}} > 0 ) {
+    $charge_min = 0;
+  } else {
+     $charge_min = 0 - ${$opt{single_price_included_min}};
+     ${$opt{single_price_included_min}} = 0;
+  }
+
+  my $charge =
+    sprintf('%.4f', ( $part_pkg->option_cacheable('min_charge') * $charge_min )
+                    + 0.0000000001 ); #so 1.00005 rounds to 1.0001
+
+  $self->set_status_and_rated_price( 'rated',
+                                     $charge,
+                                     $opt{'svcnum'},
+                                   );
+
+}
+
 =item cdr_termination [ TERMPART ]
 
 =cut

Index: Conf.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/Conf.pm,v
retrieving revision 1.497
retrieving revision 1.498
diff -u -w -d -r1.497 -r1.498
--- Conf.pm	19 Dec 2011 21:27:43 -0000	1.497
+++ Conf.pm	30 Dec 2011 10:16:39 -0000	1.498
@@ -4755,6 +4755,35 @@
     'type'        => 'checkbox',
   },
 
+  {
+    'key'         => 'cdr-prerate',
+    'section'     => 'telephony',
+    'description' => 'Experimental feature to rate CDRs immediately, rather than waiting until invoice generation time.  Can reduce invoice generation time when processing lots of CDRs.  Currently works with "VoIP/telco CDR rating (standard)" price plans using "Phone numbers (svc_phone.phonenum)" CDR service matching, without any included minutes.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'cdr-prerate-cdrtypenums',
+    'section'     => 'telephony',
+    'description' => 'When using cdr-prerate to rate CDRs immediately, limit processing to these CDR types.',
+    'type'        => 'select-sub',
+    'multiple'    => 1,
+    'options_sub' => sub { require FS::Record;
+                           require FS::cdr_type;
+                           map { $_->cdrtypenum => $_->cdrtypename }
+                               FS::Record::qsearch( 'cdr_type', 
+			                            {} #{ 'disabled' => '' }
+						  );
+			 },
+    'option_sub'  => sub { require FS::Record;
+                           require FS::cdr_type;
+                           my $cdr_type = FS::Record::qsearchs(
+			     'cdr_type', { 'cdrtypenum'=>shift } );
+                           $cdr_type ? $cdr_type->cdrtypename : '';
+			 },
+  },
+  
+  
   { key => "apacheroot", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
   { key => "apachemachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
   { key => "apachemachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },



More information about the freeside-commits mailing list