+
+ my $regionnum = $rate_detail->dest_regionnum;
+ my $rate_region = $rate_detail->dest_region;
+ 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 { $_->rate_time->intervals }
+ qsearch({ 'table' => 'rate_detail',
+ 'hashref' => { 'ratenum' => $rate->ratenum },
+ 'extra_sql' => 'AND ratetimenum IS NOT NULL',
+ })
+ );
+ $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 $rated_seconds = $part_pkg->option_cacheable('use_duration')
+ ? $self->duration
+ : $self->billsec;
+ my $seconds_left = $rated_seconds;
+
+ #no, do this later so it respects (group) included minutes
+ # # 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 $seconds = 0;
+ 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
+
+ # 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 $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;
+
+ if ( $rate_detail->min_included ) {
+ # the old, kind of deprecated way to do this:
+ #
+ # The rate detail itself has included minutes. We MUST have a place
+ # to track them.
+ my $included_min = $opt{'detail_included_min_hashref'}
+ or return "unable to rate CDR: rate detail has included minutes, but ".
+ "no detail_included_min_hashref provided.\n";
+
+ # by default, set the included minutes for this region/time to
+ # what's in the rate_detail
+ $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included
+ unless exists $included_min->{$regionnum}{$ratetimenum};
+
+ if ( $included_min->{$regionnum}{$ratetimenum} >= $minutes ) {
+ $charge_sec = 0;
+ $included_min->{$regionnum}{$ratetimenum} -= $minutes;
+ } else {
+ $charge_sec -= ($included_min->{$regionnum}{$ratetimenum} * 60);
+ $included_min->{$regionnum}{$ratetimenum} = 0;
+ }
+ } elsif ( $opt{plan_included_min} && ${ $opt{plan_included_min} } > 0 ) {
+ # The package definition has included minutes, but only for in-group
+ # rate details. Decrement them if this is an in-group call.
+ if ( $rate_detail->region_group ) {
+ if ( ${ $opt{'plan_included_min'} } >= $minutes ) {
+ $charge_sec = 0;
+ ${ $opt{'plan_included_min'} } -= $minutes;
+ } else {
+ $charge_sec -= (${ $opt{'plan_included_min'} } * 60);
+ ${ $opt{'plan_included_min'} } = 0;
+ }
+ }
+ } else {
+ # the new way!
+ my $applied_min = $cust_pkg->apply_usage(
+ 'cdr' => $self,
+ 'rate_detail' => $rate_detail,
+ 'minutes' => $minutes
+ );
+ # for now, usage pools deal only in whole minutes
+ $charge_sec -= $applied_min * 60;
+ }
+
+ if ( $charge_sec > 0 ) {
+
+ #NOW do connection charges here... right?
+ #my $conn_seconds = min($seconds_left, $rate_detail->conn_sec);
+ my $conn_seconds = 0;
+ unless ( $connection_charged++ ) { #only one connection charge
+ $conn_seconds = min($charge_sec, $rate_detail->conn_sec);
+ $seconds_left -= $conn_seconds;
+ $weektime += $conn_seconds;
+ $charge += $rate_detail->conn_charge;
+ }
+
+ #should preserve (display?) this
+ if ( $granularity == 0 ) { # per call rate
+ $charge += $rate_detail->min_charge;
+ } else {
+ my $charge_min = ( $charge_sec - $conn_seconds ) / 60;
+ $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded
+ }
+
+ }
+
+ # 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;
+
+ # NOW round it.
+ my $rounding = $part_pkg->option_cacheable('rounding') || 2;
+ my $sprintformat = '%.'. $rounding. 'f';
+ my $roundup = 10**(-3-$rounding);
+ my $price = sprintf($sprintformat, $charge + $roundup);
+
+ $self->set_status_and_rated_price(
+ 'rated',
+ $price,
+ $opt{'svcnum'},
+ 'rated_pretty_dst' => $pretty_dst,
+ 'rated_regionname' => ($rate_region ? $rate_region->regionname : ''),
+ 'rated_seconds' => $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'},
+ 'rated_classnum' => $self->calltypenum,
+ 'rated_seconds' => $self->billsec,
+ # others? upstream_*_regionname => rated_regionname is possible
+ );
+}
+
+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{plan_included_min}} -= $minutes;
+ if ( ${$opt{plan_included_min}} > 0 ) {
+ $charge_min = 0;
+ } else {
+ $charge_min = 0 - ${$opt{plan_included_min}};
+ ${$opt{plan_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'},
+ 'rated_granularity' => $granularity,
+ 'rated_seconds' => $seconds,
+ );
+
+}
+
+=item rate_cost
+
+Rates an already-rated CDR according to the cost fields from the rate plan.
+
+Returns the amount.
+
+=cut
+
+sub rate_cost {
+ my $self = shift;
+
+ return 0 unless $self->rated_ratedetailnum;
+
+ my $rate_detail =
+ qsearchs('rate_detail', { 'ratedetailnum' => $self->rated_ratedetailnum } );
+
+ my $charge = 0;
+ $charge += ($self->upstream_price || 0) * ($rate_detail->upstream_mult_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 );
+
+}
+
+=item cdr_termination [ TERMPART ]
+
+=cut
+
+sub cdr_termination {
+ my $self = shift;
+
+ if ( scalar(@_) && $_[0] ) {
+ my $termpart = shift;
+
+ qsearchs('cdr_termination', { acctid => $self->acctid,
+ termpart => $termpart,
+ }
+ );
+
+ } else {
+
+ qsearch('cdr_termination', { acctid => $self->acctid, } );
+
+ }
+