/* Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Copyright (C) ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see */ package engine import ( "encoding/json" "fmt" "sort" "strings" "time" "github.com/cgrates/cgrates/utils" ) type RatingProfile struct { Id string RatingPlanActivations RatingPlanActivations } // Clone returns a clone of RatingProfile func (rp *RatingProfile) Clone() *RatingProfile { if rp == nil { return nil } result := &RatingProfile{ Id: rp.Id, RatingPlanActivations: rp.RatingPlanActivations.Clone(), } return result } // CacheClone returns a clone of RatingProfile used by ltcache CacheCloner func (rp *RatingProfile) CacheClone() any { return rp.Clone() } // RatingProfileWithAPIOpts is used in replicatorV1 for dispatcher type RatingProfileWithAPIOpts struct { *RatingProfile Tenant string APIOpts map[string]any } type RatingPlanActivation struct { ActivationTime time.Time RatingPlanId string FallbackKeys []string } // Clone returns a clone of RatingPlanActivation func (rpa *RatingPlanActivation) Clone() *RatingPlanActivation { if rpa == nil { return nil } result := &RatingPlanActivation{ ActivationTime: rpa.ActivationTime, RatingPlanId: rpa.RatingPlanId, } if rpa.FallbackKeys != nil { result.FallbackKeys = make([]string, len(rpa.FallbackKeys)) copy(result.FallbackKeys, rpa.FallbackKeys) } return result } func (rpa *RatingPlanActivation) Equal(orpa *RatingPlanActivation) bool { return rpa.ActivationTime == orpa.ActivationTime && rpa.RatingPlanId == orpa.RatingPlanId } type RatingPlanActivations []*RatingPlanActivation // Clone returns a clone of RatingPlanActivations func (rpas RatingPlanActivations) Clone() RatingPlanActivations { if rpas == nil { return nil } result := make(RatingPlanActivations, len(rpas)) for i, rpa := range rpas { result[i] = rpa.Clone() } return result } func (rpas RatingPlanActivations) Len() int { return len(rpas) } func (rpas RatingPlanActivations) Swap(i, j int) { rpas[i], rpas[j] = rpas[j], rpas[i] } func (rpas RatingPlanActivations) Less(i, j int) bool { return rpas[i].ActivationTime.Before(rpas[j].ActivationTime) } 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 RatingPlanId string MatchedPrefix string MatchedDestId string ActivationTime time.Time RateIntervals RateIntervalList FallbackKeys []string } // SelectRatingIntevalsForTimespan orders rate intervals in time preserving only those which aply to the specified timestamp func (ri RatingInfo) SelectRatingIntevalsForTimespan(ts *TimeSpan) (result RateIntervalList) { sorter := &RateIntervalTimeSorter{referenceTime: ts.TimeStart, ris: ri.RateIntervals} rateIntervals := sorter.Sort() // get the rating interval closest to begining of timespan var delta time.Duration = -1 var bestRateIntervalIndex int var bestIntervalWeight float64 for index, rateInterval := range rateIntervals { if !rateInterval.Contains(ts.TimeStart, false) { continue } if rateInterval.Weight < bestIntervalWeight { break // don't consider lower weights' } startTime := rateInterval.Timing.getLeftMargin(ts.TimeStart) tmpDelta := ts.TimeStart.Sub(startTime) if (startTime.Before(ts.TimeStart) || startTime.Equal(ts.TimeStart)) && (delta == -1 || tmpDelta < delta) { bestRateIntervalIndex = index bestIntervalWeight = rateInterval.Weight delta = tmpDelta } } result = append(result, rateIntervals[bestRateIntervalIndex]) // check if later rating intervals influence this timespan //log.Print("RIS: ", utils.ToIJSON(rateIntervals)) for i := bestRateIntervalIndex + 1; i < len(rateIntervals); i++ { if rateIntervals[i].Weight < bestIntervalWeight { break // don't consider lower weights' } startTime := rateIntervals[i].Timing.getLeftMargin(ts.TimeStart) if startTime.Before(ts.TimeEnd) { result = append(result, rateIntervals[i]) } } return } 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) } func (ris RatingInfos) String() string { b, _ := json.MarshalIndent(ris, "", " ") return string(b) } func (rpf *RatingProfile) GetRatingPlansForPrefix(cd *CallDescriptor) (err error) { var ris RatingInfos for index, rpa := range rpf.RatingPlanActivations.GetActiveForCall(cd) { rpl, err := dm.GetRatingPlan(rpa.RatingPlanId, false, utils.NonTransactional) if err != nil || rpl == nil { utils.Logger.Err(fmt.Sprintf("Error checking destination: %v", err)) continue } prefix := "" destinationID := "" var rps RateIntervalList if cd.Destination == utils.MetaAny || cd.Destination == "" { cd.Destination = utils.MetaAny if _, ok := rpl.DestinationRates[utils.MetaAny]; ok { rps = rpl.RateIntervalList(utils.MetaAny) prefix = utils.MetaAny destinationID = utils.MetaAny } } else { for _, p := range utils.SplitPrefix(cd.Destination, MIN_PREFIX_MATCH) { if destIDs, err := dm.GetReverseDestination(p, true, true, utils.NonTransactional); err == nil { var bestWeight *float64 for _, dID := range destIDs { var timeChecker bool if _, ok := rpl.DestinationRates[dID]; ok { ril := rpl.RateIntervalList(dID) //check if RateInverval is active for call descriptor time for _, ri := range ril { if !ri.Timing.IsActiveAt(cd.TimeStart) { continue } else { timeChecker = true break } } currentWeight := ril.GetWeight() if timeChecker && (bestWeight == nil || currentWeight > *bestWeight) { bestWeight = utils.Float64Pointer(currentWeight) rps = ril prefix = p destinationID = dID } } } } if rps != nil { break } } if rps == nil { // fallback on *any destination if _, ok := rpl.DestinationRates[utils.MetaAny]; ok { rps = rpl.RateIntervalList(utils.MetaAny) prefix = utils.MetaAny destinationID = utils.MetaAny } } } // check if it's the first ri and add a blank one for the initial part not covered if index == 0 && cd.TimeStart.Before(rpa.ActivationTime) { ris = append(ris, &RatingInfo{ MatchedSubject: "", MatchedPrefix: "", MatchedDestId: "", ActivationTime: cd.TimeStart, RateIntervals: nil, FallbackKeys: []string{cd.GetKey(FALLBACK_SUBJECT)}}) } if len(prefix) > 0 { ris = append(ris, &RatingInfo{ MatchedSubject: rpf.Id, RatingPlanId: rpl.Id, MatchedPrefix: prefix, MatchedDestId: destinationID, ActivationTime: rpa.ActivationTime, RateIntervals: rps, FallbackKeys: rpa.FallbackKeys}) } else { // add for fallback information if len(rpa.FallbackKeys) > 0 { ris = append(ris, &RatingInfo{ MatchedSubject: "", MatchedPrefix: "", MatchedDestId: "", ActivationTime: rpa.ActivationTime, RateIntervals: nil, FallbackKeys: rpa.FallbackKeys, }) } } } if len(ris) > 0 { cd.addRatingInfos(ris) return } return utils.ErrNotFound } type TenantRatingSubject struct { Tenant, Subject string } func RatingProfileSubjectPrefixMatching(key string) (rp *RatingProfile, err error) { if !getRpSubjectPrefixMatching() || strings.HasSuffix(key, utils.MetaAny) { return dm.GetRatingProfile(key, false, utils.NonTransactional) } if rp, err = dm.GetRatingProfile(key, false, utils.NonTransactional); err == nil && rp != nil { // rp nil represents cached no-result return } lastIndex := strings.LastIndex(key, utils.ConcatenatedKeySep) baseKey := key[:lastIndex] subject := key[lastIndex:] lenSubject := len(subject) for i := 1; i < lenSubject-1; i++ { if rp, err = dm.GetRatingProfile(baseKey+subject[:lenSubject-i], false, utils.NonTransactional); err == nil && rp != nil { return } } return }