mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
started work on rating plan activation time handling
This commit is contained in:
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/cgrates/cgrates/history"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"log/syslog"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -107,7 +106,7 @@ type CallDescriptor struct {
|
||||
CallDuration time.Duration // the call duration so far (till TimeEnd)
|
||||
Amount float64
|
||||
FallbackSubject string // the subject to check for destination if not found on primary subject
|
||||
RatingInfos []*RatingInfo
|
||||
RatingInfos RatingInfos
|
||||
Increments Increments
|
||||
userBalance *UserBalance
|
||||
}
|
||||
@@ -137,55 +136,84 @@ func (cd *CallDescriptor) getUserBalance() (ub *UserBalance, err error) {
|
||||
/*
|
||||
Restores the activation periods for the specified prefix from storage.
|
||||
*/
|
||||
func (cd *CallDescriptor) LoadRatingPlans() (destPrefixes []string, matchedSubject string, err error) {
|
||||
matchedSubject = cd.GetKey()
|
||||
/*if val, err := cache2go.GetXCached(cd.GetKey() + cd.Destination); err == nil {
|
||||
xaps := val.(xCachedRatingPlans)
|
||||
cd.RatingPlans = xaps.aps
|
||||
return xaps.destPrefix, matchedSubject, nil
|
||||
}*/
|
||||
destPrefixes, matchedSubject, values, err := cd.getRatingPlansForPrefix(cd.GetKey(), 1)
|
||||
func (cd *CallDescriptor) LoadRatingPlans() (err error) {
|
||||
err = cd.getRatingPlansForPrefix(cd.GetKey(), 1)
|
||||
if err != nil {
|
||||
// try general fallback
|
||||
fallbackKey := fmt.Sprintf("%s:%s:%s:%s", cd.Direction, cd.Tenant, cd.TOR, FALLBACK_SUBJECT)
|
||||
// use the default subject
|
||||
destPrefixes, matchedSubject, values, err = cd.getRatingPlansForPrefix(fallbackKey, 1)
|
||||
err = cd.getRatingPlansForPrefix(fallbackKey, 1)
|
||||
}
|
||||
//load the rating plans
|
||||
if err == nil && len(values) > 0 {
|
||||
/*
|
||||
xaps := xCachedRatingPlans{destPrefix, values, new(cache2go.XEntry)}
|
||||
xaps.XCache(cd.GetKey()+cd.Destination, debitPeriod+5*time.Second, xaps)
|
||||
*/
|
||||
cd.RatingInfos = values
|
||||
if err != nil || !cd.continousRatingInfos() {
|
||||
err = errors.New("Could not determine rating plans for call")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cd *CallDescriptor) getRatingPlansForPrefix(key string, recursionDepth int) (foundPrefixes []string, matchedSubject string, ris []*RatingInfo, err error) {
|
||||
matchedSubject = key
|
||||
func (cd *CallDescriptor) getRatingPlansForPrefix(key string, recursionDepth int) (err error) {
|
||||
if recursionDepth > RECURSION_MAX_DEPTH {
|
||||
err = errors.New("Max fallback recursion depth reached!" + key)
|
||||
return
|
||||
}
|
||||
rp, err := storageGetter.GetRatingProfile(key)
|
||||
if err != nil || rp == nil {
|
||||
return nil, "", nil, err
|
||||
return err
|
||||
}
|
||||
foundPrefixes, ris, err = rp.GetRatingPlansForPrefix(cd)
|
||||
if err != nil {
|
||||
if rp.FallbackKey != "" {
|
||||
if err = rp.GetRatingPlansForPrefix(cd); err != nil {
|
||||
// try rating profile fallback
|
||||
if len(rp.FallbackKeys) > 0 {
|
||||
recursionDepth++
|
||||
for _, fbk := range strings.Split(rp.FallbackKey, FALLBACK_SEP) {
|
||||
if destPrefix, matchedSubject, values, err := cd.getRatingPlansForPrefix(fbk, recursionDepth); err == nil {
|
||||
return destPrefix, matchedSubject, values, err
|
||||
for _, fbk := range rp.FallbackKeys {
|
||||
if err := cd.getRatingPlansForPrefix(fbk, recursionDepth); err == nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// checks if there is rating info for the entire call duration
|
||||
func (cd *CallDescriptor) continousRatingInfos() bool {
|
||||
if len(cd.RatingInfos) == 0 || cd.RatingInfos[0].ActivationTime.After(cd.TimeStart) {
|
||||
return false
|
||||
}
|
||||
for _, ri := range cd.RatingInfos {
|
||||
if ri.RateIntervals == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// adds a rating infos only if that call period is not already covered
|
||||
// returns true if added
|
||||
func (cd *CallDescriptor) addRatingInfos(ris RatingInfos) bool {
|
||||
if len(cd.RatingInfos) == 0 {
|
||||
cd.RatingInfos = append(cd.RatingInfos, ris...)
|
||||
return true
|
||||
}
|
||||
cd.RatingInfos.Sort()
|
||||
// check if we dont have the start covered
|
||||
if cd.RatingInfos[0].ActivationTime.After(cd.TimeStart) {
|
||||
if ris[0].ActivationTime.Before(cd.RatingInfos[0].ActivationTime) {
|
||||
cd.RatingInfos = append(cd.RatingInfos, ris[0])
|
||||
cd.RatingInfos.Sort()
|
||||
}
|
||||
}
|
||||
for _, ri := range cd.RatingInfos {
|
||||
if ri.RateIntervals == nil {
|
||||
for i, new_ri := range ris {
|
||||
_ = i
|
||||
_ = new_ri
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
Constructs the key for the storage lookup.
|
||||
The prefixLen is limiting the length of the destination prefix.
|
||||
@@ -307,7 +335,7 @@ func (cd *CallDescriptor) roundTimeSpansToIncrement(timespans []*TimeSpan) []*Ti
|
||||
Creates a CallCost structure with the cost information calculated for the received CallDescriptor.
|
||||
*/
|
||||
func (cd *CallDescriptor) GetCost() (*CallCost, error) {
|
||||
destPrefix, matchedSubject, err := cd.LoadRatingPlans()
|
||||
err := cd.LoadRatingPlans()
|
||||
if err != nil {
|
||||
Logger.Err(fmt.Sprintf("error getting cost for key %v: %v", cd.GetUserBalanceKey(), err))
|
||||
return &CallCost{Cost: -1}, err
|
||||
@@ -325,17 +353,19 @@ func (cd *CallDescriptor) GetCost() (*CallCost, error) {
|
||||
}
|
||||
// global rounding
|
||||
cost = utils.Round(cost, roundingDecimals, roundingMethod)
|
||||
startIndex := len(fmt.Sprintf("%s:%s:%s:", cd.Direction, cd.Tenant, cd.TOR))
|
||||
//startIndex := len(fmt.Sprintf("%s:%s:%s:", cd.Direction, cd.Tenant, cd.TOR))
|
||||
cc := &CallCost{
|
||||
Direction: cd.Direction,
|
||||
TOR: cd.TOR,
|
||||
Tenant: cd.Tenant,
|
||||
Subject: matchedSubject[startIndex:],
|
||||
Account: cd.Account,
|
||||
Destination: strings.Join(destPrefix, ";"),
|
||||
Cost: cost,
|
||||
ConnectFee: connectionFee,
|
||||
Timespans: timespans}
|
||||
Direction: cd.Direction,
|
||||
TOR: cd.TOR,
|
||||
Tenant: cd.Tenant,
|
||||
// TODO, FIXME: find out where to put matched subject
|
||||
//Subject: matchedSubject[startIndex:],
|
||||
Account: cd.Account,
|
||||
// TODO, FIXME: find out where to put matched prfixes
|
||||
//Destination: strings.Join(destPrefix, ";"),
|
||||
Cost: cost,
|
||||
ConnectFee: connectionFee,
|
||||
Timespans: timespans}
|
||||
//Logger.Info(fmt.Sprintf("<Rater> Get Cost: %s => %v", cd.GetKey(), cc))
|
||||
return cc, err
|
||||
}
|
||||
@@ -350,7 +380,7 @@ func (cd *CallDescriptor) GetMaxSessionTime(startTime time.Time) (seconds float6
|
||||
if cd.CallDuration == 0 {
|
||||
cd.CallDuration = cd.TimeEnd.Sub(cd.TimeStart)
|
||||
}
|
||||
_, _, err = cd.LoadRatingPlans()
|
||||
err = cd.LoadRatingPlans()
|
||||
if err != nil {
|
||||
Logger.Err(fmt.Sprintf("error getting cost for key %v: %v", cd.GetUserBalanceKey(), err))
|
||||
return 0, err
|
||||
|
||||
@@ -349,17 +349,20 @@ func (csvr *CSVReader) LoadRatingProfiles() (err error) {
|
||||
if !exists {
|
||||
return errors.New(fmt.Sprintf("Could not load destination rate timings for tag: %v", record[5]))
|
||||
}
|
||||
rp.RatingPlanActivations = append(rp.RatingPlanActivations, &RatingPlanActivation{at, record[5]})
|
||||
rpa := &RatingPlanActivation{
|
||||
ActivationTime: at,
|
||||
RatingPlanId: record[5],
|
||||
}
|
||||
if fallbacksubject != "" {
|
||||
var sslice utils.StringSlice = rpa.FallbackKeys
|
||||
for _, fbs := range strings.Split(fallbacksubject, ";") {
|
||||
newKey := fmt.Sprintf("%s:%s:%s:%s", direction, tenant, tor, fbs)
|
||||
var sslice utils.StringSlice = strings.Split(rp.FallbackKey, ";")
|
||||
if !sslice.Contains(newKey) {
|
||||
rp.FallbackKey += newKey + ";"
|
||||
rpa.FallbackKeys = append(rpa.FallbackKeys, newKey)
|
||||
}
|
||||
}
|
||||
rp.FallbackKey = strings.TrimRight(rp.FallbackKey, ";")
|
||||
}
|
||||
rp.RatingPlanActivations = append(rp.RatingPlanActivations, rpa)
|
||||
csvr.ratingProfiles[rp.Id] = rp
|
||||
}
|
||||
return
|
||||
|
||||
@@ -560,11 +560,11 @@ func TestLoadRatingProfiles(t *testing.T) {
|
||||
}
|
||||
rp := csvr.ratingProfiles["*out:test:0:trp"]
|
||||
expected := &RatingProfile{
|
||||
Id: "*out:test:0:trp",
|
||||
FallbackKey: "*out:test:0:rif;*out:test:0:danb",
|
||||
Id: "*out:test:0:trp",
|
||||
RatingPlanActivations: RatingPlanActivations{&RatingPlanActivation{
|
||||
ActivationTime: time.Date(2013, 10, 1, 0, 0, 0, 0, time.UTC),
|
||||
RatingPlanId: "TDRT",
|
||||
FallbackKeys: []string{"*out:test:0:rif", "*out:test:0:danb"},
|
||||
}},
|
||||
}
|
||||
if !reflect.DeepEqual(rp, expected) {
|
||||
|
||||
@@ -35,8 +35,8 @@ func TestApRestoreFromStorage(t *testing.T) {
|
||||
Subject: "rif:from:tm",
|
||||
Destination: "49"}
|
||||
cd.LoadRatingPlans()
|
||||
if len(cd.RatingInfos) != 2 {
|
||||
t.Error("Error restoring activation periods: ", cd.RatingInfos)
|
||||
if len(cd.RatingInfos) != 1 {
|
||||
t.Errorf("Error restoring activation periods: %+v", cd.RatingInfos[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func TestFallbackMultiple(t *testing.T) {
|
||||
Subject: "fall",
|
||||
Destination: "0723045"}
|
||||
cd.LoadRatingPlans()
|
||||
if len(cd.RatingInfos) != 2 {
|
||||
if len(cd.RatingInfos) != 1 {
|
||||
t.Errorf("Error restoring rating plans: %+v", cd.RatingInfos)
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,22 @@ func TestApAddRateIntervalGroups(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetActiveForCall(t *testing.T) {
|
||||
rpas := RatingPlanActivations{
|
||||
&RatingPlanActivation{ActivationTime: time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
&RatingPlanActivation{ActivationTime: time.Date(2013, 11, 12, 11, 40, 0, 0, time.UTC)},
|
||||
&RatingPlanActivation{ActivationTime: time.Date(2013, 11, 13, 0, 0, 0, 0, time.UTC)},
|
||||
}
|
||||
cd := &CallDescriptor{
|
||||
TimeStart: time.Date(2013, 11, 12, 11, 39, 0, 0, time.UTC),
|
||||
TimeEnd: time.Date(2013, 11, 12, 11, 45, 0, 0, time.UTC),
|
||||
}
|
||||
active := rpas.GetActiveForCall(cd)
|
||||
if len(active) != 2 {
|
||||
t.Errorf("Error getting active rating plans: %+v", active)
|
||||
}
|
||||
}
|
||||
|
||||
/**************************** Benchmarks *************************************/
|
||||
|
||||
func BenchmarkRatingPlanMarshalJson(b *testing.B) {
|
||||
|
||||
@@ -27,15 +27,16 @@ import (
|
||||
|
||||
type RatingProfile struct {
|
||||
Id string
|
||||
FallbackKey string // FallbackKey is used as complete combination of Tenant:TOR:Direction:Subject
|
||||
RatingPlanActivations RatingPlanActivations
|
||||
Tag, Tenant, TOR, Direction, Subject string // used only for loading
|
||||
DestRatesTimingTag, RatesFallbackSubject, ActivationTime string // used only for loading
|
||||
Tag, Tenant, TOR, Direction, Subject string // used only for loading
|
||||
DestRatesTimingTag, RatesFallbackSubject, ActivationTime string // used only for loading
|
||||
FallbackKeys []string // used only for loading
|
||||
}
|
||||
|
||||
type RatingPlanActivation struct {
|
||||
ActivationTime time.Time
|
||||
RatingPlanId string
|
||||
FallbackKeys []string
|
||||
}
|
||||
|
||||
func (rpa *RatingPlanActivation) Equal(orpa *RatingPlanActivation) bool {
|
||||
@@ -60,45 +61,84 @@ func (rpas RatingPlanActivations) Sort() {
|
||||
sort.Sort(rpas)
|
||||
}
|
||||
|
||||
func (rpas RatingPlanActivations) GetActiveForCall(cd *CallDescriptor) RatingPlanActivations {
|
||||
rpas.Sort()
|
||||
lastBeforeCallStart := 0
|
||||
firstAfterCallEnd := len(rpas)
|
||||
for index, rpa := range rpas {
|
||||
if rpa.ActivationTime.Before(cd.TimeStart) || rpa.ActivationTime.Equal(cd.TimeStart) {
|
||||
lastBeforeCallStart = index
|
||||
}
|
||||
if rpa.ActivationTime.After(cd.TimeEnd) {
|
||||
firstAfterCallEnd = index
|
||||
break
|
||||
}
|
||||
}
|
||||
return rpas[lastBeforeCallStart:firstAfterCallEnd]
|
||||
}
|
||||
|
||||
type RatingInfo struct {
|
||||
MatchedSubject string
|
||||
MatchedPrefix string
|
||||
ActivationTime time.Time
|
||||
RateIntervals RateIntervalList
|
||||
}
|
||||
|
||||
type RatingInfos []*RatingInfo
|
||||
|
||||
func (ris RatingInfos) Len() int {
|
||||
return len(ris)
|
||||
}
|
||||
|
||||
func (ris RatingInfos) Swap(i, j int) {
|
||||
ris[i], ris[j] = ris[j], ris[i]
|
||||
}
|
||||
|
||||
func (ris RatingInfos) Less(i, j int) bool {
|
||||
return ris[i].ActivationTime.Before(ris[j].ActivationTime)
|
||||
}
|
||||
|
||||
func (ris RatingInfos) Sort() {
|
||||
sort.Sort(ris)
|
||||
}
|
||||
|
||||
// TODO: what happens if there is no match for part of the call
|
||||
func (rp *RatingProfile) GetRatingPlansForPrefix(cd *CallDescriptor) (foundPrefixes []string, ris []*RatingInfo, err error) {
|
||||
rp.RatingPlanActivations.Sort()
|
||||
for _, rpa := range rp.RatingPlanActivations {
|
||||
if rpa.ActivationTime.Before(cd.TimeEnd) {
|
||||
rpl, err := storageGetter.GetRatingPlan(rpa.RatingPlanId)
|
||||
if err != nil || rpl == nil {
|
||||
func (rp *RatingProfile) GetRatingPlansForPrefix(cd *CallDescriptor) (err error) {
|
||||
var ris RatingInfos
|
||||
for _, rpa := range rp.RatingPlanActivations.GetActiveForCall(cd) {
|
||||
rpl, err := storageGetter.GetRatingPlan(rpa.RatingPlanId)
|
||||
if err != nil || rpl == nil {
|
||||
Logger.Err(fmt.Sprintf("Error checking destination: %v", err))
|
||||
continue
|
||||
}
|
||||
bestPrecision := 0
|
||||
var rps RateIntervalList
|
||||
for dId, _ := range rpl.DestinationRates {
|
||||
//precision, err := storageGetter.DestinationContainsPrefix(dId, cd.Destination)
|
||||
d, err := storageGetter.GetDestination(dId)
|
||||
if err != nil {
|
||||
Logger.Err(fmt.Sprintf("Error checking destination: %v", err))
|
||||
continue
|
||||
}
|
||||
bestPrecision := 0
|
||||
var rps RateIntervalList
|
||||
for dId, _ := range rpl.DestinationRates {
|
||||
//precision, err := storageGetter.DestinationContainsPrefix(dId, cd.Destination)
|
||||
d, err := storageGetter.GetDestination(dId)
|
||||
if err != nil {
|
||||
Logger.Err(fmt.Sprintf("Error checking destination: %v", err))
|
||||
continue
|
||||
}
|
||||
precision := d.containsPrefix(cd.Destination)
|
||||
if precision > bestPrecision {
|
||||
bestPrecision = precision
|
||||
rps = rpl.RateIntervalList(dId)
|
||||
}
|
||||
precision := d.containsPrefix(cd.Destination)
|
||||
if precision > bestPrecision {
|
||||
bestPrecision = precision
|
||||
rps = rpl.RateIntervalList(dId)
|
||||
}
|
||||
if bestPrecision > 0 {
|
||||
ris = append(ris, &RatingInfo{rpa.ActivationTime, rps})
|
||||
foundPrefixes = append(foundPrefixes, cd.Destination[:bestPrecision])
|
||||
}
|
||||
if bestPrecision > 0 {
|
||||
ris = append(ris, &RatingInfo{rp.Id, cd.Destination[:bestPrecision], rpa.ActivationTime, rps})
|
||||
} else {
|
||||
// mark the end of previous!
|
||||
if len(cd.RatingInfos) > 0 {
|
||||
ris = append(ris, &RatingInfo{"", "", rpa.ActivationTime, nil})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ris) > 0 {
|
||||
return foundPrefixes, ris, nil
|
||||
cd.addRatingInfos(ris)
|
||||
return
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("not found")
|
||||
return errors.New("not found")
|
||||
}
|
||||
|
||||
@@ -1104,12 +1104,11 @@ func (self *SQLStorage) GetTpRatingProfiles(tpid, tag string) (map[string]*Ratin
|
||||
if fallback_subject != "" {
|
||||
for _, fbs := range strings.Split(fallback_subject, ";") {
|
||||
newKey := fmt.Sprintf("%s:%s:%s:%s", direction, tenant, tor, fbs)
|
||||
var sslice utils.StringSlice = strings.Split(rp.FallbackKey, ";")
|
||||
var sslice utils.StringSlice = rp.FallbackKeys
|
||||
if !sslice.Contains(newKey) {
|
||||
rp.FallbackKey += newKey + ";"
|
||||
rp.FallbackKeys = append(rp.FallbackKeys, newKey)
|
||||
}
|
||||
}
|
||||
rp.FallbackKey = strings.TrimRight(rp.FallbackKey, ";")
|
||||
}
|
||||
}
|
||||
return rpfs, nil
|
||||
|
||||
Reference in New Issue
Block a user