From 72a521a66d433c4cbd6f370369254ff7121e01b5 Mon Sep 17 00:00:00 2001 From: DanB Date: Fri, 12 Jun 2020 20:41:06 +0200 Subject: [PATCH] RateS.V1CostForEvent receiving profile selectors, orderRatesOnIntervals function with tests --- engine/rateprofile.go | 15 ++-- rates/librates.go | 44 +++++++++++ rates/librates_test.go | 162 +++++++++++++++++++++++++++++++++++++++++ rates/rates.go | 99 +++++++++++++++++++------ 4 files changed, 292 insertions(+), 28 deletions(-) create mode 100644 rates/librates.go create mode 100644 rates/librates_test.go diff --git a/engine/rateprofile.go b/engine/rateprofile.go index bef7dc8af..a90a34e43 100644 --- a/engine/rateprofile.go +++ b/engine/rateprofile.go @@ -50,13 +50,14 @@ func (rpp *RateProfile) TenantID() string { // Route defines rate related information used within a RateProfile type Rate struct { - ID string // RateID - FilterIDs []string // RateFilterIDs - Weight float64 // RateWeight - Value float64 // RateValue - Unit time.Duration // RateUnit - Increment time.Duration // RateIncrement - Blocker bool // RateBlocker will make this rate recurrent + ID string // RateID + FilterIDs []string // RateFilterIDs + IntervalStart time.Duration // Starting point when the Rate kicks in + Weight float64 // RateWeight will decide the winner per interval start + Value float64 // RateValue + Unit time.Duration // RateUnit + Increment time.Duration // RateIncrement + Blocker bool // RateBlocker will make this rate recurrent, deactivating further intervals val *utils.Decimal // cached version of the Decimal } diff --git a/rates/librates.go b/rates/librates.go new file mode 100644 index 000000000..f57818eb1 --- /dev/null +++ b/rates/librates.go @@ -0,0 +1,44 @@ +/* +Real-time Online/Offline Charging System (OerS) 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 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package rates + +import ( + "sort" + "time" + + "github.com/cgrates/cgrates/engine" +) + +// orderRatesOnIntervals will order the rates based on intervalStart +// there can be only one winning Rate for each interval, prioritized by the Weight +func orderRatesOnIntervals(allRts map[time.Duration][]*engine.Rate) (ordRts []*engine.Rate) { + var idxOrdRts int + ordRts = make([]*engine.Rate, len(allRts)) + for _, rts := range allRts { + sort.Slice(rts, func(i, j int) bool { + return rts[i].Weight > rts[j].Weight + }) + ordRts[idxOrdRts] = rts[0] + idxOrdRts++ + } + sort.Slice(ordRts, func(i, j int) bool { + return ordRts[i].IntervalStart < ordRts[j].IntervalStart + }) + return +} diff --git a/rates/librates_test.go b/rates/librates_test.go new file mode 100644 index 000000000..befe2334c --- /dev/null +++ b/rates/librates_test.go @@ -0,0 +1,162 @@ +/* +Real-time Online/Offline Charging System (OerS) 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 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package rates + +import ( + "reflect" + "testing" + "time" + + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + +func TestOrderRatesOnIntervals(t *testing.T) { + allRts := map[time.Duration][]*engine.Rate{ + time.Duration(0): { + &engine.Rate{ + ID: "RATE0", + IntervalStart: time.Duration(0), + }, + &engine.Rate{ + ID: "RATE100", + IntervalStart: time.Duration(0), + Weight: 100, + }, + &engine.Rate{ + ID: "RATE50", + IntervalStart: time.Duration(0), + Weight: 50, + }, + }, + } + expOrdered := []*engine.Rate{ + &engine.Rate{ + ID: "RATE100", + IntervalStart: time.Duration(0), + Weight: 100, + }, + } + if ordRts := orderRatesOnIntervals(allRts); !reflect.DeepEqual(expOrdered, ordRts) { + t.Errorf("expecting: %s\n, received: %s", + utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) + } + allRts = map[time.Duration][]*engine.Rate{ + time.Duration(1 * time.Minute): { + &engine.Rate{ + ID: "RATE30", + IntervalStart: time.Duration(1 * time.Minute), + Weight: 30, + }, + &engine.Rate{ + ID: "RATE70", + IntervalStart: time.Duration(1 * time.Minute), + Weight: 70, + }, + }, + time.Duration(0): { + &engine.Rate{ + ID: "RATE0", + IntervalStart: time.Duration(0), + }, + &engine.Rate{ + ID: "RATE100", + IntervalStart: time.Duration(0), + Weight: 100, + }, + &engine.Rate{ + ID: "RATE50", + IntervalStart: time.Duration(0), + Weight: 50, + }, + }, + } + expOrdered = []*engine.Rate{ + &engine.Rate{ + ID: "RATE100", + IntervalStart: time.Duration(0), + Weight: 100, + }, + &engine.Rate{ + ID: "RATE70", + IntervalStart: time.Duration(1 * time.Minute), + Weight: 70, + }, + } + if ordRts := orderRatesOnIntervals(allRts); !reflect.DeepEqual(expOrdered, ordRts) { + t.Errorf("expecting: %s\n, received: %s", + utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) + } + allRts = map[time.Duration][]*engine.Rate{ + time.Duration(1 * time.Minute): { + &engine.Rate{ + ID: "RATE30", + IntervalStart: time.Duration(1 * time.Minute), + Weight: 30, + }, + &engine.Rate{ + ID: "RATE70", + IntervalStart: time.Duration(1 * time.Minute), + Weight: 70, + }, + }, + time.Duration(2 * time.Minute): { + &engine.Rate{ + ID: "RATE0", + IntervalStart: time.Duration(2 * time.Minute), + }, + }, + time.Duration(0): { + &engine.Rate{ + ID: "RATE0", + IntervalStart: time.Duration(0), + }, + &engine.Rate{ + ID: "RATE100", + IntervalStart: time.Duration(0), + Weight: 100, + }, + &engine.Rate{ + ID: "RATE50", + IntervalStart: time.Duration(0), + Weight: 50, + }, + }, + } + expOrdered = []*engine.Rate{ + &engine.Rate{ + ID: "RATE100", + IntervalStart: time.Duration(0), + Weight: 100, + }, + &engine.Rate{ + ID: "RATE70", + IntervalStart: time.Duration(1 * time.Minute), + Weight: 70, + }, + &engine.Rate{ + ID: "RATE0", + IntervalStart: time.Duration(2 * time.Minute), + }, + } + if ordRts := orderRatesOnIntervals(allRts); !reflect.DeepEqual(expOrdered, ordRts) { + t.Errorf("expecting: %s\n, received: %s", + utils.ToIJSON(expOrdered), utils.ToIJSON(ordRts)) + } +} diff --git a/rates/rates.go b/rates/rates.go index dac66c21a..c354de4f2 100644 --- a/rates/rates.go +++ b/rates/rates.go @@ -21,6 +21,7 @@ package rates import ( "fmt" "sort" + "time" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" @@ -71,36 +72,41 @@ func (rS *RateS) Call(serviceMethod string, args interface{}, reply interface{}) } // matchingRateProfileForEvent returns the matched RateProfile for the given event -func (rS *RateS) matchingRateProfileForEvent(cgrEv *utils.CGREvent) (rtPfl *engine.RateProfile, err error) { - var rPfIDs utils.StringMap - if rPfIDs, err = engine.MatchingItemIDsForEvent( - cgrEv.Event, - rS.cfg.RateSCfg().StringIndexedFields, - rS.cfg.RateSCfg().PrefixIndexedFields, - rS.dm, utils.CacheRateProfilesFilterIndexes, - cgrEv.Tenant, - rS.cfg.RouteSCfg().IndexedSelects, - rS.cfg.RouteSCfg().NestedFields, - ); err != nil { - return +func (rS *RateS) matchingRateProfileForEvent(args *ArgsCostForEvent) (rtPfl *engine.RateProfile, err error) { + rPfIDs := args.RateProfileIDs + if len(rPfIDs) == 0 { + var rPfIDMp utils.StringMap + if rPfIDMp, err = engine.MatchingItemIDsForEvent( + args.CGREvent.Event, + rS.cfg.RateSCfg().StringIndexedFields, + rS.cfg.RateSCfg().PrefixIndexedFields, + rS.dm, + utils.CacheRateProfilesFilterIndexes, + args.CGREvent.Tenant, + rS.cfg.RouteSCfg().IndexedSelects, + rS.cfg.RouteSCfg().NestedFields, + ); err != nil { + return + } + rPfIDs = rPfIDMp.Slice() } - var matchingRPfs []*engine.RateProfile - evNm := utils.MapStorage{utils.MetaReq: cgrEv.Event} - for rPfID := range rPfIDs { + matchingRPfs := make([]*engine.RateProfile, 0, len(rPfIDs)) + evNm := utils.MapStorage{utils.MetaReq: args.CGREvent.Event} + for _, rPfID := range rPfIDs { var rPf *engine.RateProfile - if rPf, err = rS.dm.GetRateProfile(cgrEv.Tenant, rPfID, true, true, utils.NonTransactional); err != nil { + if rPf, err = rS.dm.GetRateProfile(args.CGREvent.Tenant, rPfID, true, true, utils.NonTransactional); err != nil { if err == utils.ErrNotFound { err = nil continue } return } - if rPf.ActivationInterval != nil && cgrEv.Time != nil && - !rPf.ActivationInterval.IsActiveAtTime(*cgrEv.Time) { // not active + if rPf.ActivationInterval != nil && args.CGREvent.Time != nil && + !rPf.ActivationInterval.IsActiveAtTime(*args.CGREvent.Time) { // not active continue } var pass bool - if pass, err = rS.filterS.Pass(cgrEv.Tenant, rPf.FilterIDs, evNm); err != nil { + if pass, err = rS.filterS.Pass(args.CGREvent.Tenant, rPf.FilterIDs, evNm); err != nil { return } else if !pass { continue @@ -108,11 +114,62 @@ func (rS *RateS) matchingRateProfileForEvent(cgrEv *utils.CGREvent) (rtPfl *engi matchingRPfs = append(matchingRPfs, rPf) } + if len(matchingRPfs) == 0 { + return nil, utils.ErrNotFound + } sort.Slice(matchingRPfs, func(i, j int) bool { return matchingRPfs[i].Weight > matchingRPfs[j].Weight }) + rtPfl = matchingRPfs[0] return } -// V1CostForEvent will be called to calculate the cost for an event -func (rS *RateS) V1CostForEvent(cgrEv *utils.CGREventWithOpts, cC *utils.ChargedCost) (err error) { +// matchingRateProfileForEvent returns the matched RateProfile for the given event +// indexed based on intervalStart, there will be one winner per interval start +// returned in order of intervalStart +func (rS *RateS) matchingRatesForEvent(rtPfl *engine.RateProfile, cgrEv *utils.CGREvent) (rts []*engine.Rate, err error) { + var rtIDs utils.StringMap + if rtIDs, err = engine.MatchingItemIDsForEvent( + cgrEv.Event, + rS.cfg.RateSCfg().StringIndexedFields, + rS.cfg.RateSCfg().PrefixIndexedFields, + rS.dm, + utils.CacheRateProfilesFilterIndexes, + cgrEv.Tenant, + rS.cfg.RouteSCfg().IndexedSelects, + rS.cfg.RouteSCfg().NestedFields, + ); err != nil { + return + } + rtsWrk := make(map[time.Duration][]*engine.Rate) + evNm := utils.MapStorage{utils.MetaReq: cgrEv.Event} + for rtID := range rtIDs { + var rt *engine.Rate + for _, rtInst := range rtPfl.Rates { + if rtInst.ID == rtID { + rt = rtInst + break + } + } + var pass bool + if pass, err = rS.filterS.Pass(cgrEv.Tenant, rt.FilterIDs, evNm); err != nil { + return + } else if !pass { + continue + } + rtsWrk[rt.IntervalStart] = append(rtsWrk[rt.IntervalStart], rt) + } + rts = orderRatesOnIntervals(rtsWrk) + return +} + +// AttrArgsProcessEvent arguments used for proccess event +type ArgsCostForEvent struct { + RateProfileIDs []string + Opts map[string]interface{} + *utils.CGREvent + *utils.ArgDispatcher +} + +// V1CostForEvent will be called to calculate the cost for an event +func (rS *RateS) V1CostForEvent(args *ArgsCostForEvent, cC *utils.ChargedCost) (err error) { return }