From 959a9f38d23f77032685bfa4cf764781dba3df56 Mon Sep 17 00:00:00 2001 From: arberkatellari Date: Mon, 9 Jun 2025 16:42:20 +0200 Subject: [PATCH] Add action type *dynamic_rating_profile --- engine/action.go | 69 ++++++++++- engine/actions_test.go | 124 ++++++++++++++++++++ general_tests/dynamic_thresholds_it_test.go | 100 ++++++++++++++++ utils/consts.go | 1 + 4 files changed, 293 insertions(+), 1 deletion(-) diff --git a/engine/action.go b/engine/action.go index 31eec3884..dab1b2ec1 100644 --- a/engine/action.go +++ b/engine/action.go @@ -130,7 +130,7 @@ func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCf utils.MetaDynamicAttribute, utils.MetaDynamicActionPlan, utils.MetaDynamicActionPlanAccounts, utils.MetaDynamicAction, utils.MetaDynamicDestination, utils.MetaDynamicFilter, - utils.MetaDynamicRoute, + utils.MetaDynamicRoute, utils.MetaDynamicRatingProfile, } act := ActionConnCfg{} switch source { @@ -200,6 +200,7 @@ func init() { actionFuncMap[utils.MetaDynamicFilter] = dynamicFilter actionFuncMap[utils.MetaDynamicRoute] = dynamicRoute actionFuncMap[utils.MetaDynamicRanking] = dynamicRanking + actionFuncMap[utils.MetaDynamicRatingProfile] = dynamicRatingProfile } func getActionFunc(typ string) (f actionTypeFunc, exists bool) { @@ -2482,3 +2483,69 @@ func dynamicRanking(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetRankingProfile, ranking, &reply) } + +// dynamicRatingProfile processes the `ExtraParameters` field from the action to +// construct a RatingProfile +// +// The ExtraParameters field format is expected as follows: +// +// 0 Tenant: string +// 1 Category: string +// 2 Subject: string +// 3 ActivationTime: string +// 4 RatingPlanId: string +// 5 RatesFallbackSubject: string +// 6 APIOpts: set of key-value pairs (separated by "&"). +// +// Parameters are separated by ";" and must be provided in the specified order. +func dynamicRatingProfile(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, + _ SharedActionsData, connCfg ActionConnCfg) (err error) { + cgrEv, canCast := ev.(*utils.CGREvent) + if !canCast { + return errors.New("Couldn't cast event to CGREvent") + } + dP := utils.MapStorage{ // create DataProvider from event + utils.MetaReq: cgrEv.Event, + utils.MetaTenant: cgrEv.Tenant, + utils.MetaNow: time.Now(), + utils.MetaOpts: cgrEv.APIOpts, + } + // Parse action parameters based on the predefined format. + params := strings.Split(act.ExtraParameters, utils.InfieldSep) + if len(params) != 7 { + return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 7", len(params))) + } + // parse dynamic parameters + for i := range params { + var onlyEncapsulatead bool + if i == 3 { // dont parse "*now" string for ActivationTime + onlyEncapsulatead = true + } + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, onlyEncapsulatead); err != nil { + return err + } + } + // Prepare request arguments based on provided parameters. + ratingProf := &utils.AttrSetRatingProfile{ + Tenant: params[0], + Category: params[1], + Subject: params[2], + RatingPlanActivations: []*utils.TPRatingActivation{ + { + ActivationTime: params[3], + RatingPlanId: params[4], + FallbackSubjects: params[5], + }, + }, + APIOpts: make(map[string]any), + } + // populate RatingProfiles's APIOpts + if params[6] != utils.EmptyString { + if err := parseParamStringToMap(params[6], ratingProf.APIOpts); err != nil { + return err + } + } + // create the RatingProfile based on the populated parameters + var reply string + return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetRatingProfile, ratingProf, &reply) +} diff --git a/engine/actions_test.go b/engine/actions_test.go index 8dfe98e79..df5927db6 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -6818,3 +6818,127 @@ func TestDynamicRanking(t *testing.T) { }) } } + +func TestDynamicRatingProfile(t *testing.T) { + tempConn := connMgr + tmpDm := dm + tmpCache := Cache + defer func() { + config.SetCgrConfig(config.NewDefaultCGRConfig()) + SetConnManager(tempConn) + dm = tmpDm + Cache = tmpCache + }() + Cache.Clear(nil) + var rpwo *utils.AttrSetRatingProfile + ccMock := &ccMock{ + calls: map[string]func(ctx *context.Context, args any, reply any) error{ + utils.APIerSv1SetRatingProfile: func(ctx *context.Context, args, reply any) error { + var canCast bool + if rpwo, canCast = args.(*utils.AttrSetRatingProfile); !canCast { + return fmt.Errorf("couldnt cast") + } + return nil + }, + }, + } + connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) + clientconn := make(chan birpc.ClientConnector, 1) + clientconn <- ccMock + NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ + connID: clientconn, + }) + testcases := []struct { + name string + extraParams string + connIDs []string + expRpwo *utils.AttrSetRatingProfile + expectedErr string + }{ + { + name: "SuccessfulRequest", + connIDs: []string{connID}, + expRpwo: &utils.AttrSetRatingProfile{ + Tenant: "cgrates.org", + Category: "call", + Subject: utils.MetaAny, + RatingPlanActivations: []*utils.TPRatingActivation{ + { + ActivationTime: "2014-07-29T15:00:00Z", + RatingPlanId: "RP_TESTIT1", + FallbackSubjects: "RP_TEST", + }, + }, + APIOpts: map[string]any{ + "key": "value", + }, + }, + extraParams: "cgrates.org;call;*any;2014-07-29T15:00:00Z;RP_TESTIT1;RP_TEST;key:value", + }, + { + name: "SuccessfulRequestWithDynamicPaths", + connIDs: []string{connID}, + expRpwo: &utils.AttrSetRatingProfile{ + Tenant: "cgrates.org", + Category: "call", + Subject: "1001", + RatingPlanActivations: []*utils.TPRatingActivation{ + { + ActivationTime: "*now", + RatingPlanId: "RP_TEST1001", + FallbackSubjects: "RP_TEST", + }, + }, + APIOpts: map[string]any{ + "key": "value", + }, + }, + extraParams: "*tenant;call;~*req.Account;*now;RP_TEST<~*req.Account>;RP_TEST;key:value", + }, + { + name: "MissingConns", + extraParams: "cgrates.org;call;*any;2014-07-29T15:00:00Z;RP_TESTIT1;RP_TEST;key:value", + expectedErr: "MANDATORY_IE_MISSING: [connIDs]", + }, + { + name: "WrongNumberOfParams", + extraParams: "cgrates.org;;;", + expectedErr: "invalid number of parameters <4> expected 7", + }, + { + name: "InvalidOptsMap", + extraParams: "cgrates.org;call;*any;2014-07-29T15:00:00Z;RP_TESTIT1;RP_TEST;opt", + expectedErr: "invalid key-value pair: opt", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + action := &Action{ExtraParameters: tc.extraParams} + ev := &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "evID", + Time: &time.Time{}, + Event: map[string]any{ + utils.AccountField: "1001", + }, + } + t.Cleanup(func() { + rpwo = nil + }) + err := dynamicRatingProfile(nil, action, nil, nil, ev, + SharedActionsData{}, ActionConnCfg{ + ConnIDs: tc.connIDs, + }) + if tc.expectedErr != "" { + if err == nil || err.Error() != tc.expectedErr { + t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) + } + } else if err != nil { + t.Error(err) + } else if !reflect.DeepEqual(rpwo, tc.expRpwo) { + t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo)) + } + }) + } +} diff --git a/general_tests/dynamic_thresholds_it_test.go b/general_tests/dynamic_thresholds_it_test.go index 74fa922af..6b424e4bd 100644 --- a/general_tests/dynamic_thresholds_it_test.go +++ b/general_tests/dynamic_thresholds_it_test.go @@ -56,6 +56,8 @@ var ( testDynThdCheckForActionPlan, testDynThdCheckForAction, testDynThdCheckForDestination, + testDynThdCheckForFilter, + testDynThdCheckForRoute, testDynThdSetLogAction, testDynThdSetAction, testDynThdSetThresholdProfile, @@ -73,6 +75,8 @@ var ( testDynThdCheckForDynCreatedActionPlan, testDynThdCheckForDynCreatedAction, testDynThdCheckForDynCreatedDestination, + testDynThdCheckForDynCreatedFilter, + testDynThdCheckForDynCreatedRoute, testDynThdStopEngine, } ) @@ -184,6 +188,20 @@ func testDynThdCheckForDestination(t *testing.T) { } } +func testDynThdCheckForFilter(t *testing.T) { + var rply *engine.Filter + if err := dynThdRpc.Call(context.Background(), utils.APIerSv1GetFilter, &utils.TenantID{Tenant: "cgrates.org", ID: "DYNAMICLY_FLT_1002"}, &rply); err == nil || err.Error() != utils.ErrNotFound.Error() { + t.Error(err) + } +} + +func testDynThdCheckForRoute(t *testing.T) { + var rply *engine.RouteProfile + if err := dynThdRpc.Call(context.Background(), utils.APIerSv1GetRouteProfile, &utils.TenantID{Tenant: "cgrates.org", ID: "DYNAMICLY_ROUTE_1002"}, &rply); err == nil || err.Error() != utils.ErrNotFound.Error() { + t.Error(err) + } +} + func testDynThdSetLogAction(t *testing.T) { var reply string @@ -235,6 +253,14 @@ func testDynThdSetAction(t *testing.T) { Identifier: utils.MetaDynamicDestination, ExtraParameters: "DYNAMICLY_DST_1005;1005", }, + { + Identifier: utils.MetaDynamicFilter, + ExtraParameters: "*tenant;DYNAMICLY_FLT_<~*req.ID>;*string;~*req.Account;<~*req.ID>;*now;", + }, + { + Identifier: utils.MetaDynamicRoute, + ExtraParameters: "*tenant;DYNAMICLY_ROUTE_<~*req.ID>;*string:~*req.Account:<~*req.ID>&*string:~*req.Destination:1003;*now;*weight;*acd&*tcc;route1;*string:~*req.Account:<~*req.ID>&*string:~*req.Destination:1003;<~*req.ID>;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;key:value", + }, }} if err := dynThdRpc.Call(context.Background(), utils.APIerSv2SetActions, act, &reply); err != nil { @@ -611,6 +637,80 @@ func testDynThdCheckForDynCreatedDestination(t *testing.T) { } } +func testDynThdCheckForDynCreatedFilter(t *testing.T) { + time.Sleep(50 * time.Millisecond) + var result1 *engine.Filter + if err := dynThdRpc.Call(context.Background(), utils.APIerSv1GetFilter, &utils.TenantID{Tenant: "cgrates.org", ID: "DYNAMICLY_FLT_1002"}, &result1); err != nil { + t.Fatal(err) + } + exp := &engine.Filter{ + Tenant: "cgrates.org", + ID: "DYNAMICLY_FLT_1002", + Rules: []*engine.FilterRule{ + { + Type: utils.MetaString, + Element: "~*req.Account", + Values: []string{"1002"}, + }, + }, + ActivationInterval: result1.ActivationInterval, + } + if !reflect.DeepEqual(exp, result1) { + t.Errorf("\nexpected: <%+v>, \nreceived: <%+v>", utils.ToJSON(exp), utils.ToJSON(result1)) + } +} + +func testDynThdCheckForDynCreatedRoute(t *testing.T) { + time.Sleep(50 * time.Millisecond) + var result1 *engine.RouteProfile + if err := dynThdRpc.Call(context.Background(), utils.APIerSv1GetRouteProfile, &utils.TenantID{Tenant: "cgrates.org", ID: "DYNAMICLY_ROUTE_1002"}, &result1); err != nil { + t.Fatal(err) + } + exp := &engine.RouteProfile{ + Tenant: "cgrates.org", + ID: "DYNAMICLY_ROUTE_1002", + FilterIDs: []string{ + "*string:~*req.Account:1002", + "*string:~*req.Destination:1003", + }, + ActivationInterval: &utils.ActivationInterval{ + ActivationTime: result1.ActivationInterval.ActivationTime, + }, + Sorting: utils.MetaWeight, + SortingParameters: []string{utils.MetaACD, utils.MetaTCC}, + Routes: []*engine.Route{ + { + ID: "route1", + FilterIDs: []string{ + "*string:~*req.Account:1002", + "*string:~*req.Destination:1003", + }, + AccountIDs: []string{"1002"}, + RatingPlanIDs: []string{ + "RP1", + "RP2", + }, + ResourceIDs: []string{ + "RS1", + "RS2", + }, + StatIDs: []string{ + "Stat_1", + "Stat_1_1", + }, + Weight: 10, + Blocker: true, + RouteParameters: "param", + }, + }, + Weight: 10, + } + + if !reflect.DeepEqual(exp, result1) { + t.Errorf("\nexpected: <%+v>, \nreceived: <%+v>", utils.ToJSON(exp), utils.ToJSON(result1)) + } +} + func testDynThdStopEngine(t *testing.T) { if err := engine.KillEngine(dynThdDelay); err != nil { t.Error(err) diff --git a/utils/consts.go b/utils/consts.go index 25d4ed5d9..b2b42eaed 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1238,6 +1238,7 @@ const ( MetaDynamicFilter = "*dynamic_filter" MetaDynamicRoute = "*dynamic_route" MetaDynamicRanking = "*dynamic_ranking" + MetaDynamicRatingProfile = "*dynamic_rating_profile" ActionID = "ActionID" ActionType = "ActionType" ActionValue = "ActionValue"