diff --git a/actions/dynamic.go b/actions/dynamic.go index 75f285877..ab2758155 100644 --- a/actions/dynamic.go +++ b/actions/dynamic.go @@ -92,7 +92,7 @@ func (aL *actDynamicThreshold) execute(ctx *context.Context, data utils.MapStora config.CgrConfig().GeneralCfg().DefaultTenant) // Parse action parameters based on the predefined format. if len(aL.aCfg.Diktats) == 0 { - return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) } weights := make(map[string]float64) // stores sorting weights by Diktat ID diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later @@ -261,7 +261,7 @@ func (aL *actDynamicStats) execute(ctx *context.Context, data utils.MapStorage, config.CgrConfig().GeneralCfg().DefaultTenant) // Parse action parameters based on the predefined format. if len(aL.aCfg.Diktats) == 0 { - return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) } weights := make(map[string]float64) // stores sorting weights by Diktat ID diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later @@ -472,7 +472,7 @@ func (aL *actDynamicAttribute) execute(ctx *context.Context, data utils.MapStora config.CgrConfig().GeneralCfg().DefaultTenant) // Parse action parameters based on the predefined format. if len(aL.aCfg.Diktats) == 0 { - return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) } weights := make(map[string]float64) // stores sorting weights by Diktat ID diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later @@ -648,7 +648,7 @@ func (aL *actDynamicResource) execute(ctx *context.Context, data utils.MapStorag config.CgrConfig().GeneralCfg().DefaultTenant) // Parse action parameters based on the predefined format. if len(aL.aCfg.Diktats) == 0 { - return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) } weights := make(map[string]float64) // stores sorting weights by Diktat ID diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later @@ -811,7 +811,7 @@ func (aL *actDynamicTrend) execute(ctx *context.Context, data utils.MapStorage, config.CgrConfig().GeneralCfg().DefaultTenant) // Parse action parameters based on the predefined format. if len(aL.aCfg.Diktats) == 0 { - return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) } weights := make(map[string]float64) // stores sorting weights by Diktat ID diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later @@ -963,7 +963,7 @@ func (aL *actDynamicRanking) execute(ctx *context.Context, data utils.MapStorage config.CgrConfig().GeneralCfg().DefaultTenant) // Parse action parameters based on the predefined format. if len(aL.aCfg.Diktats) == 0 { - return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) } weights := make(map[string]float64) // stores sorting weights by Diktat ID diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later @@ -1090,7 +1090,7 @@ func (aL *actDynamicFilter) execute(ctx *context.Context, data utils.MapStorage, config.CgrConfig().GeneralCfg().DefaultTenant) // Parse action parameters based on the predefined format. if len(aL.aCfg.Diktats) == 0 { - return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) } weights := make(map[string]float64) // stores sorting weights by Diktat ID diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later @@ -1216,7 +1216,7 @@ func (aL *actDynamicRoute) execute(ctx *context.Context, data utils.MapStorage, config.CgrConfig().GeneralCfg().DefaultTenant) // Parse action parameters based on the predefined format. if len(aL.aCfg.Diktats) == 0 { - return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) } weights := make(map[string]float64) // stores sorting weights by Diktat ID diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later @@ -1477,3 +1477,226 @@ func (aL *actDynamicRoute) execute(ctx *context.Context, data utils.MapStorage, } return } + +// actDynamicRate processes the `ActionDiktatsOpts` field from the action to construct a RateProfile +// +// The ActionDiktatsOpts field format is expected as follows: +// +// 0 Tenant: string +// 1 ID: string +// 2 FilterIDs: strings separated by "&". +// 3 Weights: strings separated by "&". +// 4 MinCost: string +// 5 MaxCost: string +// 6 MaxCostStrategy: string +// 7 RateID: string +// 8 RateFilterIDs: strings separated by "&". +// 9 RateActivationStart: string +// 10 RateWeights: strings separated by "&". +// 11 RateBlocker: bool +// 12 RateIntervalStart: string +// 13 RateFixedFee: string +// 14 RateRecurrentFee: string +// 15 RateUnit: string +// 16 RateIncrement: string +// 17 APIOpts: set of key-value pairs (separated by "&"). +// +// Parameters are separated by ";" and must be provided in the specified order. +type actDynamicRate struct { + config *config.CGRConfig + connMgr *engine.ConnManager + fltrS *engine.FilterS + aCfg *utils.APAction + tnt string + cgrEv *utils.CGREvent +} + +func (aL *actDynamicRate) id() string { + return aL.aCfg.ID +} + +func (aL *actDynamicRate) cfg() *utils.APAction { + return aL.aCfg +} + +// execute implements actioner interface +func (aL *actDynamicRate) execute(ctx *context.Context, data utils.MapStorage, trgID string) (err error) { + if len(aL.config.ActionSCfg().AdminSConns) == 0 { + return fmt.Errorf("no connection with AdminS") + } + data[utils.MetaNow] = time.Now() + data[utils.MetaTenant] = utils.FirstNonEmpty(aL.cgrEv.Tenant, aL.tnt, + config.CgrConfig().GeneralCfg().DefaultTenant) + // Parse action parameters based on the predefined format. + if len(aL.aCfg.Diktats) == 0 { + return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID) + } + weights := make(map[string]float64) // stores sorting weights by Diktat ID + diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later + for _, diktat := range aL.aCfg.Diktats { + if pass, err := aL.fltrS.Pass(ctx, aL.tnt, diktat.FilterIDs, data); err != nil { + return err + } else if !pass { + continue + } + weight, err := engine.WeightFromDynamics(ctx, diktat.Weights, aL.fltrS, aL.tnt, data) + if err != nil { + return err + } + weights[diktat.ID] = weight + diktats = append(diktats, diktat) + } + // Sort by weight (higher values first). + slices.SortFunc(diktats, func(a, b *utils.APDiktat) int { + return cmp.Compare(weights[b.ID], weights[a.ID]) + }) + for _, diktat := range diktats { + params := strings.Split(utils.IfaceAsString(diktat.Opts[utils.MetaTemplate]), + utils.InfieldSep) + if len(params) != 18 { + return fmt.Errorf("invalid number of parameters <%d> expected 18", len(params)) + } + // parse dynamic parameters + for i := range params { + if params[i], err = utils.ParseParamForDataProvider(params[i], data, false); err != nil { + return err + } + } + // Prepare request arguments based on provided parameters. + args := &utils.APIRateProfile{ + RateProfile: &utils.RateProfile{ + Tenant: params[0], + ID: params[1], + MaxCostStrategy: params[6], + Rates: map[string]*utils.Rate{ + params[7]: { + ID: params[7], + ActivationTimes: params[9], + IntervalRates: []*utils.IntervalRate{{}}, + }, + }, + }, + APIOpts: make(map[string]any), + } + // populate RateProfile's FilterIDs + if params[2] != utils.EmptyString { + args.FilterIDs = strings.Split(params[2], utils.ANDSep) + } + // populate RateProfile's Weights + if params[3] != utils.EmptyString { + args.Weights = utils.DynamicWeights{&utils.DynamicWeight{}} + wghtSplit := strings.Split(params[3], utils.ANDSep) + if len(wghtSplit) > 2 { + return utils.ErrUnsupportedFormat + } + if wghtSplit[0] != utils.EmptyString { + args.Weights[0].FilterIDs = []string{wghtSplit[0]} + } + if wghtSplit[1] != utils.EmptyString { + args.Weights[0].Weight, err = strconv.ParseFloat(wghtSplit[1], 64) + if err != nil { + return err + } + } + } + // populate RateProfile's MinCost + if params[4] != utils.EmptyString { + args.MinCost, err = utils.NewDecimalFromString(params[4]) + if err != nil { + return err + } + } + // populate RateProfile's MaxCost + if params[5] != utils.EmptyString { + args.MaxCost, err = utils.NewDecimalFromString(params[5]) + if err != nil { + return err + } + } + // populate RateProfile's Rate + if params[7] != utils.EmptyString { + // populate Rate's FilterIDs + if params[8] != utils.EmptyString { + args.Rates[params[7]].FilterIDs = strings.Split(params[8], utils.ANDSep) + } + // populate Rate's Weights + if params[10] != utils.EmptyString { + args.Rates[params[7]].Weights = utils.DynamicWeights{&utils.DynamicWeight{}} + wghtSplit := strings.Split(params[10], utils.ANDSep) + if len(wghtSplit) > 2 { + return utils.ErrUnsupportedFormat + } + if wghtSplit[0] != utils.EmptyString { + args.Rates[params[7]].Weights[0].FilterIDs = []string{wghtSplit[0]} + } + if wghtSplit[1] != utils.EmptyString { + args.Rates[params[7]].Weights[0].Weight, err = strconv.ParseFloat(wghtSplit[1], 64) + if err != nil { + return err + } + } + } + // populate Rate's Blocker + if params[11] != utils.EmptyString { + args.Rates[params[7]].Blocker, err = strconv.ParseBool(params[11]) + if err != nil { + return err + } + } + // populate Rate's IntervalStart + if params[12] != utils.EmptyString { + args.Rates[params[7]].IntervalRates[0].IntervalStart, err = utils.NewDecimalFromString(params[12]) + if err != nil { + return err + } + } + // populate Rate's FixedFee + if params[13] != utils.EmptyString { + args.Rates[params[7]].IntervalRates[0].FixedFee, err = utils.NewDecimalFromString(params[13]) + if err != nil { + return err + } + } + // populate Rate's RecurrentFee + if params[14] != utils.EmptyString { + args.Rates[params[7]].IntervalRates[0].RecurrentFee, err = utils.NewDecimalFromString(params[14]) + if err != nil { + return err + } + } + // populate Rate's Unit + if params[15] != utils.EmptyString { + args.Rates[params[7]].IntervalRates[0].Unit, err = utils.NewDecimalFromString(params[15]) + if err != nil { + return err + } + } + // populate Rate's Increment + if params[16] != utils.EmptyString { + args.Rates[params[7]].IntervalRates[0].Increment, err = utils.NewDecimalFromString(params[16]) + if err != nil { + return err + } + } + } + // populate RateProfile's APIOpts + if params[17] != utils.EmptyString { + if err := parseParamStringToMap(params[17], args.APIOpts); err != nil { + return err + } + } + + // create the RateProfile based on the populated parameters + var rply string + if err = aL.connMgr.Call(ctx, aL.config.ActionSCfg().AdminSConns, + utils.AdminSv1SetRateProfile, args, &rply); err != nil { + return err + } + if blocker, err := engine.BlockerFromDynamics(ctx, diktat.Blockers, aL.fltrS, aL.tnt, data); err != nil { + return err + } else if blocker { + break + } + } + return +} diff --git a/actions/libactions.go b/actions/libactions.go index 4728fab1c..9860868e7 100644 --- a/actions/libactions.go +++ b/actions/libactions.go @@ -49,6 +49,8 @@ func actionTarget(act string) string { return utils.MetaFilters case utils.MetaDynamicRoute: return utils.MetaRoutes + case utils.MetaDynamicRate: + return utils.MetaRates default: return utils.MetaNone } @@ -161,6 +163,8 @@ func newActioner(ctx *context.Context, cgrEv *utils.CGREvent, cfg *config.CGRCon return &actDynamicFilter{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil case utils.MetaDynamicRoute: return &actDynamicRoute{cfg, connMgr, dm, fltrS, aCfg, tnt, cgrEv}, nil + case utils.MetaDynamicRate: + return &actDynamicRate{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil default: return nil, fmt.Errorf("unsupported action type: <%s>", aCfg.Type) diff --git a/general_tests/dynamic_thresholds_it_test.go b/general_tests/dynamic_thresholds_it_test.go index 4311997ec..ef315941c 100644 --- a/general_tests/dynamic_thresholds_it_test.go +++ b/general_tests/dynamic_thresholds_it_test.go @@ -262,6 +262,7 @@ func TestDynThdIT(t *testing.T) { utils.MetaRankings: {"someID": {}}, utils.MetaFilters: {"someID": {}}, utils.MetaRoutes: {"someID": {}}, + utils.MetaRates: {"someID": {}}, }, Actions: []*utils.APAction{ { @@ -720,6 +721,63 @@ func TestDynThdIT(t *testing.T) { }, }, }, + { + ID: "Dynamic_Rate_ID", + Type: utils.MetaDynamicRate, + Diktats: []*utils.APDiktat{ + { + ID: "CreateDynamicRate1002", + FilterIDs: []string{"*string:~*req.Account:1002"}, + Opts: map[string]any{ + "*template": "*tenant;DYNAMICLY_RATE_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;*string:~*req.Account:1002&10;5;10;*free;RT_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;* * * * *;*string:~*req.Account:1002&20;true;0s;5;0.01;1m;1s;~*opts", + }, + Weights: utils.DynamicWeights{ + { + Weight: 50, + }, + }, + }, + { + ID: "CreateDynamicRate1002NotFoundFilter", + FilterIDs: []string{"*string:~*req.Account:1003"}, + Opts: map[string]any{ + "*template": "*tenant;DYNAMICLY_RATE_2_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;*string:~*req.Account:1002&10;5;10;*free;RT_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;* * * * *;*string:~*req.Account:1002&20;true;0s;5;0.01;1m;1s;~*opts", + }, + Weights: utils.DynamicWeights{ + { + Weight: 90, + }, + }, + }, + { + ID: "CreateDynamicRate1002Blocker", + Opts: map[string]any{ + "*template": "*tenant;DYNAMICLY_RATE_3_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;*string:~*req.Account:1002&10;5;10;*free;RT_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;* * * * *;*string:~*req.Account:1002&20;true;0s;5;0.01;1m;1s;~*opts", + }, + Weights: utils.DynamicWeights{ + { + Weight: 20, + }, + }, + Blockers: utils.DynamicBlockers{ + { + Blocker: true, + }, + }, + }, + { + ID: "CreateDynamicRate1002Blocked", + Opts: map[string]any{ + "*template": "*tenant;DYNAMICLY_RATE_4_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;*string:~*req.Account:1002&10;5;10;*free;RT_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;* * * * *;*string:~*req.Account:1002&20;true;0s;5;0.01;1m;1s;~*opts", + }, + Weights: utils.DynamicWeights{ + { + Weight: 10, + }, + }, + }, + }, + }, }, }, } @@ -818,7 +876,7 @@ func TestDynThdIT(t *testing.T) { } else if !reflect.DeepEqual(ids, []string{"THD_ACNT_1002"}) { t.Error("Unexpected reply returned", ids) } - time.Sleep(100 * time.Millisecond) //wait for async + time.Sleep(1000 * time.Millisecond) //wait for async }) t.Run("GetDynamicThresholdProfile", func(t *testing.T) { var thrsholds []*engine.ThresholdProfile @@ -1435,4 +1493,121 @@ func TestDynThdIT(t *testing.T) { t.Errorf("Expected <%v>\nReceived <%v>", utils.ToJSON(exp), utils.ToJSON(rcv)) } }) + + t.Run("GetDynamicRateProfile", func(t *testing.T) { + var rcv []*utils.RateProfile + if err := client.Call(context.Background(), utils.AdminSv1GetRateProfiles, + &utils.ArgsItemIDs{ + Tenant: utils.CGRateSorg, + }, &rcv); err != nil { + t.Errorf("AdminSv1GetRateProfiles failed unexpectedly: %v", err) + } + if len(rcv) != 2 { + t.Fatalf("AdminSv1GetRateProfiles len(rcv)=%v, want 2", len(rcv)) + } + sort.Slice(rcv, func(i, j int) bool { + return rcv[i].ID > rcv[j].ID + }) + exp := []*utils.RateProfile{ + { + Tenant: "cgrates.org", + ID: "DYNAMICLY_RATE_3_1002", + FilterIDs: []string{ + "*string:~*req.Account:1002", + "*string:~*req.Account:1003", + }, + Weights: utils.DynamicWeights{ + { + FilterIDs: []string{ + "*string:~*req.Account:1002", + }, + Weight: 10, + }, + }, + MinCost: utils.NewDecimalFromFloat64(5), + MaxCost: utils.NewDecimalFromFloat64(10), + MaxCostStrategy: utils.MetaMaxCostFree, + Rates: map[string]*utils.Rate{ + "RT_1002": { + ID: "RT_1002", + FilterIDs: []string{ + "*string:~*req.Account:1002", + "*string:~*req.Account:1003", + }, + ActivationTimes: "* * * * *", + Weights: utils.DynamicWeights{ + { + FilterIDs: []string{ + "*string:~*req.Account:1002", + }, + Weight: 20, + }, + }, + Blocker: true, + IntervalRates: []*utils.IntervalRate{ + { + IntervalStart: utils.NewDecimalFromFloat64(0), + FixedFee: utils.NewDecimalFromFloat64(5), + RecurrentFee: utils.NewDecimalFromFloat64(0.01), + Unit: utils.NewDecimalFromFloat64(60000000000), + Increment: utils.NewDecimalFromFloat64(1000000000), + }, + }, + }, + }, + }, + { + + Tenant: "cgrates.org", + ID: "DYNAMICLY_RATE_1002", + FilterIDs: []string{ + "*string:~*req.Account:1002", + "*string:~*req.Account:1003", + }, + Weights: utils.DynamicWeights{ + { + FilterIDs: []string{ + "*string:~*req.Account:1002", + }, + Weight: 10, + }, + }, + MinCost: utils.NewDecimalFromFloat64(5), + MaxCost: utils.NewDecimalFromFloat64(10), + MaxCostStrategy: utils.MetaMaxCostFree, + Rates: map[string]*utils.Rate{ + "RT_1002": { + ID: "RT_1002", + FilterIDs: []string{ + "*string:~*req.Account:1002", + "*string:~*req.Account:1003", + }, + ActivationTimes: "* * * * *", + Weights: utils.DynamicWeights{ + { + FilterIDs: []string{ + "*string:~*req.Account:1002", + }, + Weight: 20, + }, + }, + Blocker: true, + IntervalRates: []*utils.IntervalRate{ + { + IntervalStart: utils.NewDecimalFromFloat64(0), + FixedFee: utils.NewDecimalFromFloat64(5), + RecurrentFee: utils.NewDecimalFromFloat64(0.01), + Unit: utils.NewDecimalFromFloat64(60000000000), + Increment: utils.NewDecimalFromFloat64(1000000000), + }, + }, + }, + }, + }, + } + + if !reflect.DeepEqual(exp, rcv) { + t.Errorf("Expected <%v>\nReceived <%v>", utils.ToJSON(exp), utils.ToJSON(rcv)) + } + }) } diff --git a/utils/consts.go b/utils/consts.go index d148d625e..2c49bca0e 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1155,6 +1155,7 @@ const ( MetaDynamicRanking = "*dynamicRanking" MetaDynamicFilter = "*dynamicFilter" MetaDynamicRoute = "*dynamicRoute" + MetaDynamicRate = "*dynamicRate" // Diktats Opts Fields MetaBalancePath = "*balancePath"