[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