From ea5ed9eaad29ce10a24280c3ca3d0eb0f853c915 Mon Sep 17 00:00:00 2001 From: arberkatellari Date: Wed, 28 May 2025 16:40:06 +0200 Subject: [PATCH] Add *dynamic_filter action type and remove *dynamic_account_action --- .../dynamic_account_threshold/cgrates.json | 1 + engine/action.go | 106 +++++----- engine/actions_test.go | 186 ++++++++++++++++++ general_tests/dynamic_acc_sts_thld_it_test.go | 6 +- utils/consts.go | 2 +- utils/dynamicfieldpath.go | 16 +- 6 files changed, 259 insertions(+), 58 deletions(-) diff --git a/data/conf/samples/dynamic_account_threshold/cgrates.json b/data/conf/samples/dynamic_account_threshold/cgrates.json index 424f3103c..ee910fcbd 100644 --- a/data/conf/samples/dynamic_account_threshold/cgrates.json +++ b/data/conf/samples/dynamic_account_threshold/cgrates.json @@ -2,6 +2,7 @@ "general": { "log_level": 7, + "reply_timeout": "10s", }, "data_db": { diff --git a/engine/action.go b/engine/action.go index f58a367cd..7621eaf1c 100644 --- a/engine/action.go +++ b/engine/action.go @@ -129,7 +129,7 @@ func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCf utils.MetaDynamicThreshold, utils.MetaDynamicStats, utils.MetaDynamicAttribute, utils.MetaDynamicActionPlan, utils.MetaDynamicAction, utils.MetaDynamicDestination, - utils.MetaDynamicAccountAction, + utils.MetaDynamicFilter, } act := ActionConnCfg{} switch source { @@ -194,8 +194,8 @@ func init() { actionFuncMap[utils.MetaDynamicAttribute] = dynamicAttribute actionFuncMap[utils.MetaDynamicActionPlan] = dynamicActionPlan actionFuncMap[utils.MetaDynamicAction] = dynamicAction - actionFuncMap[utils.MetaDynamicAccountAction] = dynamicAccountAction actionFuncMap[utils.MetaDynamicDestination] = dynamicDestination + actionFuncMap[utils.MetaDynamicFilter] = dynamicFilter } func getActionFunc(typ string) (f actionTypeFunc, exists bool) { @@ -1481,7 +1481,7 @@ func dynamicThreshold(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, } // parse dynamic parameters for i := range params { - if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } @@ -1610,7 +1610,7 @@ func dynamicStats(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, } // parse dynamic parameters for i := range params { - if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } @@ -1752,7 +1752,7 @@ func dynamicAttribute(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, } // parse dynamic parameters for i := range params { - if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } @@ -1857,7 +1857,7 @@ func dynamicActionPlan(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, } // parse dynamic parameters for i := range params { - if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } @@ -1980,7 +1980,7 @@ func dynamicAction(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, params[11] = strings.ReplaceAll(params[11], utils.ANDSep, utils.InfieldSep) // parse dynamic parameters for i := range params { - if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } @@ -2053,7 +2053,7 @@ func dynamicDestination(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, } // parse dynamic parameters for i := range params { - if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { return err } } @@ -2071,20 +2071,21 @@ func dynamicDestination(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetDestination, dest, &reply) } -// dynamicAccountAction processes the `ExtraParameters` field from the action to -// populate action plans and action triggers for account +// dynamicFilter processes the `ExtraParameters` field from the action to +// construct a Filter // // The ExtraParameters field format is expected as follows: // -// 0 Tenant: string -// 1 Account: string -// 2 ActionPlanIds: strings separated by "&". -// 3 ActionTriggersIds: strings separated by "&". -// 4 AllowNegative: bool -// 5 Disabled: bool +// 0 Tenant: string +// 1 ID: string +// 2 Type: string +// 3 Path: string +// 4 Values: strings separated by "&". +// 5 ActivationInterval: strings separated by "&". +// 6 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. -func dynamicAccountAction(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, +func dynamicFilter(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, _ SharedActionsData, connCfg ActionConnCfg) (err error) { cgrEv, canCast := ev.(*utils.CGREvent) if !canCast { @@ -2093,51 +2094,64 @@ func dynamicAccountAction(_ *Account, act *Action, _ Actions, _ *FilterS, ev any dP := utils.MapStorage{ // create DataProvider from event utils.MetaReq: cgrEv.Event, utils.MetaTenant: cgrEv.Tenant, + utils.MetaNow: time.Now(), utils.MetaOpts: cgrEv.APIOpts, } - // Parse Account parameters based on the predefined format. + // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) - if len(params) != 6 { - return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 6", len(params))) + 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 { - if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil { + var onlyEncapsulatead bool + if i == 3 { // dont parse un-encapsulated "< >" string from Path + onlyEncapsulatead = true + } + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, onlyEncapsulatead); err != nil { return err } } - // populate Account's parameters - acc := &AttrSetAccount{ - Tenant: params[0], - Account: params[1], - ReloadScheduler: true, - ExtraOptions: make(map[string]bool), + // Prepare request arguments based on provided parameters. + fltr := &FilterWithAPIOpts{ + Filter: &Filter{ + Tenant: params[0], + ID: params[1], + Rules: []*FilterRule{{ + Type: params[2], + Element: params[3], + }}, + ActivationInterval: &utils.ActivationInterval{}, // avoid reaching inside a nil pointer + + }, + APIOpts: make(map[string]any), } - // populate Account's ActionPlanIDs - if params[2] != utils.EmptyString { - acc.ActionPlanIDs = strings.Split(params[2], utils.ANDSep) - } - // populate Account's ActionTriggerIDs - if params[3] != utils.EmptyString { - acc.ActionTriggerIDs = strings.Split(params[3], utils.ANDSep) - } - // populate Account's AllowNegative ExtraParams + // populate Filter's Values if params[4] != utils.EmptyString { - allNeg, err := strconv.ParseBool(params[4]) - if err != nil { + fltr.Filter.Rules[0].Values = strings.Split(params[4], utils.ANDSep) + } + // populate Filter's ActivationInterval + aISplit := strings.Split(params[5], utils.ANDSep) + if len(aISplit) > 2 { + return utils.ErrUnsupportedFormat + } + if len(aISplit) > 0 && aISplit[0] != utils.EmptyString { + if err := fltr.ActivationInterval.ActivationTime.UnmarshalText([]byte(aISplit[0])); err != nil { return err } - acc.ExtraOptions[utils.AllowNegative] = allNeg + if len(aISplit) == 2 { + if err := fltr.ActivationInterval.ExpiryTime.UnmarshalText([]byte(aISplit[1])); err != nil { + return err + } + } } - // populate Account's Disabled ExtraParams - if params[5] != utils.EmptyString { - disable, err := strconv.ParseBool(params[5]) - if err != nil { + // populate Filter's APIOpts + if params[6] != utils.EmptyString { + if err := parseParamStringToMap(params[6], fltr.APIOpts); err != nil { return err } - acc.ExtraOptions[utils.Disabled] = disable } - // create the Account based on the populated parameters + // create the Filter based on the populated parameters var reply string - return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv2SetAccount, acc, &reply) + return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetFilter, fltr, &reply) } diff --git a/engine/actions_test.go b/engine/actions_test.go index 12e47aaf3..5f09a3014 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -6060,3 +6060,189 @@ func TestDynamicDestination(t *testing.T) { }) } } + +func TestDynamicFilter(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 fwo *FilterWithAPIOpts + ccMock := &ccMock{ + calls: map[string]func(ctx *context.Context, args any, reply any) error{ + utils.APIerSv1SetFilter: func(ctx *context.Context, args, reply any) error { + var canCast bool + if fwo, canCast = args.(*FilterWithAPIOpts); !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 + expFwo *FilterWithAPIOpts + expectedErr string + }{ + { + name: "SuccessfulRequest", + connIDs: []string{connID}, + expFwo: &FilterWithAPIOpts{ + Filter: &Filter{ + Tenant: "cgrates.org", + ID: "Fltr_1", + Rules: []*FilterRule{ + { + Type: "*string", + Element: "~*req.Account", + Values: []string{"1001", "1002"}, + }, + }, + ActivationInterval: &utils.ActivationInterval{ + ActivationTime: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), + }, + }, + APIOpts: map[string]any{ + "key": "value", + }, + }, + extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z;key:value", + }, + { + name: "SuccessfulRequestWithDynamicPaths", + connIDs: []string{connID}, + expFwo: &FilterWithAPIOpts{ + Filter: &Filter{ + Tenant: "cgrates.org", + ID: "Fltr_1001", + Rules: []*FilterRule{ + { + Type: "*string", + Element: "~*req.Account", + Values: []string{"1001", "1002"}, + }, + }, + ActivationInterval: &utils.ActivationInterval{ + ActivationTime: time.Now(), + ExpiryTime: time.Date(3000, 7, 29, 15, 0, 0, 0, time.UTC), + }, + }, + APIOpts: map[string]any{ + "key": "value", + }, + }, + extraParams: "*tenant;Fltr_<~*req.Account>;*string;~*req.<~*req.ExtraInfo>;<~*req.Account>&1002;*now&3000-07-29T15:00:00Z;key:value", + }, + { + name: "SuccessfulRequestEmptyFields", + connIDs: []string{connID}, + expFwo: &FilterWithAPIOpts{ + Filter: &Filter{ + Tenant: "cgrates.org", + ID: "Fltr_1001", + Rules: []*FilterRule{ + { + Type: "", + Element: "", + Values: nil, + }, + }, + ActivationInterval: &utils.ActivationInterval{}, + }, + APIOpts: map[string]any{}, + }, + extraParams: "*tenant;Fltr_1001;;;;;", + }, + { + name: "MissingConns", + extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z;key:value", + expectedErr: "MANDATORY_IE_MISSING: [connIDs]", + }, + { + name: "WrongNumberOfParams", + extraParams: "tenant;;;", + expectedErr: "invalid number of parameters <4> expected 7", + }, + { + name: "ActivationIntervalLengthFail", + extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z&&;key:value", + expectedErr: utils.ErrUnsupportedFormat.Error(), + }, + { + name: "ActivationIntervalBadStringFail", + extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;bad String;key:value", + expectedErr: `parsing time "bad String" as "2006-01-02T15:04:05Z07:00": cannot parse "bad String" as "2006"`, + }, + { + name: "ActivationIntervalBadStringFail2", + extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z&bad String;key:value", + expectedErr: `parsing time "bad String" as "2006-01-02T15:04:05Z07:00": cannot parse "bad String" as "2006"`, + }, + { + name: "InvalidOptsMap", + extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z;opt", + expectedErr: "invalid key-value pair: opt", + }, + } + + for i, 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", + utils.ExtraInfo: utils.AccountField, + }, + } + t.Cleanup(func() { + fwo = nil + }) + err := dynamicFilter(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(fwo, tc.expFwo) { + if i != 1 { + t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expFwo), utils.ToJSON(fwo)) + } else { + // Get the absolute difference between the times + diff := fwo.ActivationInterval.ActivationTime.Sub(tc.expFwo.ActivationInterval.ActivationTime) + if diff < 0 { + diff = -diff // Make sure it's positive + } + // Check if difference is less than or equal to 1 second + if diff <= time.Second { + tc.expFwo.ActivationInterval.ActivationTime = fwo.ActivationInterval.ActivationTime + if !reflect.DeepEqual(fwo, tc.expFwo) { + t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expFwo), utils.ToJSON(fwo)) + } + } else { + t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expFwo), utils.ToJSON(fwo)) + } + } + } + }) + } +} diff --git a/general_tests/dynamic_acc_sts_thld_it_test.go b/general_tests/dynamic_acc_sts_thld_it_test.go index 968739502..9c670518c 100644 --- a/general_tests/dynamic_acc_sts_thld_it_test.go +++ b/general_tests/dynamic_acc_sts_thld_it_test.go @@ -21,8 +21,6 @@ along with this program. If not, see package general_tests import ( - "bytes" - "fmt" "path" "path/filepath" "reflect" @@ -50,9 +48,9 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) { ng := engine.TestEngine{ ConfigPath: filepath.Join(*utils.DataDir, "conf", "samples", "dynamic_account_threshold"), TpPath: path.Join(*utils.DataDir, "tariffplans", "testit"), - LogBuffer: &bytes.Buffer{}, + // LogBuffer: &bytes.Buffer{}, } - t.Cleanup(func() { fmt.Println(ng.LogBuffer) }) + // t.Cleanup(func() { fmt.Println(ng.LogBuffer) }) client, _ := ng.Run(t) t.Run("SetInitiativeThresholdProfile", func(t *testing.T) { diff --git a/utils/consts.go b/utils/consts.go index 339c0e651..da985eb7a 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1210,9 +1210,9 @@ const ( MetaDynamicStats = "*dynamic_stats" MetaDynamicAttribute = "*dynamic_attribute" MetaDynamicActionPlan = "*dynamic_action_plan" - MetaDynamicAccountAction = "*dynamic_account_action" MetaDynamicAction = "*dynamic_action" MetaDynamicDestination = "*dynamic_destination" + MetaDynamicFilter = "*dynamic_filter" ActionID = "ActionID" ActionType = "ActionType" ActionValue = "ActionValue" diff --git a/utils/dynamicfieldpath.go b/utils/dynamicfieldpath.go index 0cb21608f..a169514de 100644 --- a/utils/dynamicfieldpath.go +++ b/utils/dynamicfieldpath.go @@ -57,14 +57,14 @@ func ProcessFieldPath(fldPath, sep string, dP DataProvider) (newPath string, err } // ParseParamForDataProvider will parse a param string with prefix "~*req.", "~*otps.", "*tenant" or "*now"; or strings containing dynamic paths between "<" ">" signs. If none match, it returns the original string -func ParseParamForDataProvider(param string, dP DataProvider) (string, error) { +func ParseParamForDataProvider(param string, dP DataProvider, onlyEncapsulatead bool) (string, error) { // Check if the string contains any "&" characters if strings.Contains(param, ANDSep) { parts := strings.Split(param, ANDSep) var results []string // Process each part individually for _, part := range parts { - result, err := ParseParamForDataProvider(part, dP) + result, err := ParseParamForDataProvider(part, dP, onlyEncapsulatead) if err != nil { return EmptyString, err } @@ -73,11 +73,13 @@ func ParseParamForDataProvider(param string, dP DataProvider) (string, error) { // Join all processed parts with & return strings.Join(results, ANDSep), nil } - switch { - case strings.HasPrefix(param, MetaDynReq) || strings.HasPrefix(param, DynamicDataPrefix+MetaOpts): - return DPDynamicString(param, dP) - case strings.HasPrefix(param, MetaNow) || strings.HasPrefix(param, MetaTenant): - return dP.FieldAsString(SplitPath(param, NestingSep[0], -1)) + if !onlyEncapsulatead { + switch { + case strings.HasPrefix(param, MetaDynReq) || strings.HasPrefix(param, DynamicDataPrefix+MetaOpts): + return DPDynamicString(param, dP) + case strings.HasPrefix(param, MetaNow) || strings.HasPrefix(param, MetaTenant): + return dP.FieldAsString(SplitPath(param, NestingSep[0], -1)) + } } // look for dynamic paths in the param string, and parse it