diff --git a/engine/calldesc.go b/engine/calldesc.go index fb3d6e759..2b21e40b5 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -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(" 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 diff --git a/engine/loader_csv.go b/engine/loader_csv.go index 39fede695..1b99a5c91 100644 --- a/engine/loader_csv.go +++ b/engine/loader_csv.go @@ -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 diff --git a/engine/loader_csv_test.go b/engine/loader_csv_test.go index b7e2a2a08..1e1ed0124 100644 --- a/engine/loader_csv_test.go +++ b/engine/loader_csv_test.go @@ -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) { diff --git a/engine/ratingplan_test.go b/engine/ratingplan_test.go index 7bfffcde7..9e492d717 100644 --- a/engine/ratingplan_test.go +++ b/engine/ratingplan_test.go @@ -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) { diff --git a/engine/ratingprofile.go b/engine/ratingprofile.go index a1cb9d9db..ab7813932 100644 --- a/engine/ratingprofile.go +++ b/engine/ratingprofile.go @@ -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") } diff --git a/engine/storage_sql.go b/engine/storage_sql.go index c4499ce20..4fe086aae 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -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