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
}