diff --git a/apier/v1/apier.go b/apier/v1/apier.go index f523722ef..af2778192 100644 --- a/apier/v1/apier.go +++ b/apier/v1/apier.go @@ -815,6 +815,93 @@ func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *engine.Attr return nil } +func (apierSv1 *APIerSv1) SetActionPlanAccounts(ctx *context.Context, attrs *engine.AttrSetActionPlanAccounts, reply *string) (err error) { + if missing := utils.MissingStructFields(attrs, []string{"Id", "ActionPlan"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + for _, at := range attrs.ActionPlan { + requiredFields := []string{"ActionsId", "Time", "Weight"} + if missing := utils.MissingStructFields(at, requiredFields); len(missing) != 0 { + return fmt.Errorf("%s:Action:%s:%v", utils.ErrMandatoryIeMissing.Error(), at.ActionsId, missing) + } + } + err = guardian.Guardian.Guard(func() error { + var prevAccountIDs utils.StringMap + if prevAP, err := apierSv1.DataManager.GetActionPlan(attrs.Id, true, true, utils.NonTransactional); err != nil && err != utils.ErrNotFound { + return utils.NewErrServerError(err) + } else if err == nil && !attrs.Overwrite { + return utils.ErrExists + } else if prevAP != nil { + prevAccountIDs = prevAP.AccountIDs + } + ap := &engine.ActionPlan{ + Id: attrs.Id, + AccountIDs: utils.StringMapFromSlice(attrs.AccountIDs), + } + for _, apiAtm := range attrs.ActionPlan { + if exists, err := apierSv1.DataManager.HasData(utils.ActionPrefix, apiAtm.ActionsId, ""); err != nil { + return utils.NewErrServerError(err) + } else if !exists { + return fmt.Errorf("%s:%s", utils.ErrBrokenReference.Error(), apiAtm.ActionsId) + } + timing, err := apiAtm.GetRITiming(apierSv1.DataManager) + if err != nil { + return err + } + ap.ActionTimings = append(ap.ActionTimings, &engine.ActionTiming{ + Uuid: utils.GenUUID(), + Weight: apiAtm.Weight, + Timing: &engine.RateInterval{Timing: timing}, + ActionsID: apiAtm.ActionsId, + }) + } + if err := apierSv1.DataManager.SetActionPlan(ap.Id, ap, true, utils.NonTransactional); err != nil { + return utils.NewErrServerError(err) + } + if err := apierSv1.ConnMgr.Call(context.TODO(), apierSv1.Config.ApierCfg().CachesConns, + utils.CacheSv1ReloadCache, &utils.AttrReloadCacheWithAPIOpts{ + ActionPlanIDs: []string{ap.Id}, + }, reply); err != nil { + return err + } + for acntID := range prevAccountIDs { + if err := apierSv1.DataManager.RemAccountActionPlans(acntID, []string{attrs.Id}); err != nil { + if errors.Is(err, utils.ErrNotFound) { // needed for DynaPrepaid accounts + utils.Logger.Warning(fmt.Sprintf("<%s> error: <%s> when removing AccountActionPlans: accountID <%s> ActionPlanID <%s>", + utils.ApierS, err.Error(), acntID, attrs.Id)) + continue + } + return utils.NewErrServerError(err) + } + } + if len(prevAccountIDs) != 0 { + if err := apierSv1.ConnMgr.Call(context.TODO(), apierSv1.Config.ApierCfg().CachesConns, + utils.CacheSv1ReloadCache, &utils.AttrReloadCacheWithAPIOpts{ + AccountActionPlanIDs: prevAccountIDs.Slice(), + }, reply); err != nil { + return err + } + } + return nil + }, config.CgrConfig().GeneralCfg().LockingTimeout, utils.ActionPlanPrefix) + if err != nil { + return err + } + if attrs.ReloadScheduler { + sched := apierSv1.SchedulerService.GetScheduler() + if sched == nil { + return errors.New(utils.SchedulerNotRunningCaps) + } + sched.Reload() + } + //generate a loadID for CacheActionPlans and store it in database + if err := apierSv1.DataManager.SetLoadIDs(map[string]int64{utils.CacheActionPlans: time.Now().UnixNano()}); err != nil { + return utils.APIErrorHandler(err) + } + *reply = utils.OK + return nil +} + type AttrGetActionPlan struct { ID string } diff --git a/data/tariffplans/dynamic_tps/ActionPlanAccounts.csv b/data/tariffplans/dynamic_tps/ActionPlanAccounts.csv new file mode 100644 index 000000000..5e9f2b274 --- /dev/null +++ b/data/tariffplans/dynamic_tps/ActionPlanAccounts.csv @@ -0,0 +1,4 @@ +#Id[0];ActionsId[1];TimingId[2];Weight[3];Overwrite[4];Tenant:AccountIDs[5] +PACKAGE_1001;TOPUP_RST_MONETARY_10;*asap;10;false;cgrates.org:1001 +ACTION_PLAN_ENABLE_ACC_AFTER_5S;ACT_ENABLE_ACC;TM_AFTER_5S;10;true;<*tenant+:+~*opts.*accountID> +PACKAGE_<~*opts.*accountID>;TOPUP_RST_DATA_100;*asap;10;false;cgrates.org:<~*opts.*accountID>&cgrates.org:1002 diff --git a/data/tariffplans/dynamic_tps/ActionPlans.csv b/data/tariffplans/dynamic_tps/ActionPlans.csv new file mode 100644 index 000000000..c7dba06ad --- /dev/null +++ b/data/tariffplans/dynamic_tps/ActionPlans.csv @@ -0,0 +1,3 @@ +#Id[0];ActionsId[1];TimingId[2];Weight[3];Overwrite[4] +PACKAGE_1001;TOPUP_RST_MONETARY_10;*asap;10;false +ACTION_PLAN_LW5S;LOG_WARNING;TM_AFTER_5S;10;true \ No newline at end of file diff --git a/data/tariffplans/dynamic_tps/Actions.csv b/data/tariffplans/dynamic_tps/Actions.csv new file mode 100644 index 000000000..aa47a9b3a --- /dev/null +++ b/data/tariffplans/dynamic_tps/Actions.csv @@ -0,0 +1,17 @@ +#ActionsId[0];Action[1];ExtraParameters[2];Filter[3];BalanceId[4];BalanceType[5];Categories[6];DestinationIds[7];RatingSubject[8];SharedGroup[9];ExpiryTime[10];TimingIds[11];Units[12];BalanceWeight[13];BalanceBlocker[14];BalanceDisabled[15];Weight[16] +TOPUP_RST_10;*topup_reset;;;;*monetary;;*any;;;*unlimited;;10;10;false;false;10 + +TOPUP_RST_<~*req.Account>;*topup_reset;;;;*monetary;;*any;;;*unlimited;;5;20;false;false;10 +TOPUP_RST_<~*req.Account>;*topup_reset;;;;*voice;;DST_1002;SPECIAL_1002;;*unlimited;;90s;20;false;false;10 + +LOG_WARNING;*log;;;;;;;;;;;;;false;false;10 +ENABLE_AND_LOG;*log;;;;;;;;;;;;;false;false;10 +ENABLE_AND_LOG;*enable_account;;;;;;;;;;;;;false;false;10 + + +ACT_RAD_COA_ACNT_<~*req.Account>;*cgr_rpc;\f"{""Address"":""localhost:2012"";""Transport"":""*json"";""Method"":""SessionSv1.AlterSessions"";""Attempts"":1;""Async"":false;""Params"":{""Filters"":[""*string:~*req.Account:<~*req.Account>""];""Tenant"":""cgrates.org"";""APIOpts"":{""*radCoATemplate"":""mycoa""};""Event"":{""CustomFilter"":""custom_filter""}}}"\f;;;;;;;;;;;;;;20 + + +Alter_Session_<~*req.Account>;*alter_sessions;\fcgrates.org;*string:~*req.Account:<~*req.Account>;1;*radCoATemplate:mycoa;CustomFilter:mycustomvalue\f;*string:~*req.Account:<~*req.Account>&filter2;balID;*monetary;call&data;1002&1003;SPECIAL_1002;SHARED_A&SHARED_B;*unlimited;weekdays&offpeak;10;10;true;true;10 + +CDR_Log_<~*req.Account>;*cdrlog;\f{\"Account\":\"<~*req.Account>\",\"RequestType\":\"*pseudoprepaid\",\"Subject\":\"DifferentThanAccount\", \"ToR\":\"~ActionType:s/^\\*(.*)$/did_$1/\"}\f;*string:~*req.Account:<~*req.Account>&filter2;balID;*monetary;call&data;1002&1003;SPECIAL_1002;SHARED_A&SHARED_B;*unlimited;weekdays&offpeak;10;10;true;true;10 \ No newline at end of file diff --git a/data/tariffplans/dynamic_tps/Attributes.csv b/data/tariffplans/dynamic_tps/Attributes.csv new file mode 100644 index 000000000..d7fe202a7 --- /dev/null +++ b/data/tariffplans/dynamic_tps/Attributes.csv @@ -0,0 +1,5 @@ +#Tenant[0];ID[1];Contexts[2];FilterIDs[3];ActivationInterval[4];AttributeFilterIDs[5];Path[6];Type[7];Value[8];Blocker[9];Weight[10];APIOpts[11] + +cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1&SUPPLIER2;true;10;*accountID:<~*req.Account> + +*tenant;Attr_<~*req.Account>;*any;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;true;10; diff --git a/data/tariffplans/dynamic_tps/Destinations.csv b/data/tariffplans/dynamic_tps/Destinations.csv new file mode 100644 index 000000000..f11a4d416 --- /dev/null +++ b/data/tariffplans/dynamic_tps/Destinations.csv @@ -0,0 +1,2 @@ +#Id;Prefix +DST_<~*req.Destination>;<~*req.Destination> \ No newline at end of file diff --git a/data/tariffplans/dynamic_tps/Filters.csv b/data/tariffplans/dynamic_tps/Filters.csv new file mode 100644 index 000000000..14bafa02d --- /dev/null +++ b/data/tariffplans/dynamic_tps/Filters.csv @@ -0,0 +1,4 @@ +#Tenant[0];ID[1];Type[2];Path[3];Values[4];ActivationInterval[5];APIOpts[6] +cgrates.org,FLTR_1,*string,~*req.Account,<~*req.Account>;1002,*now; +cgrates.org;Fltr_2;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z; +*tenant;Fltr_<~*req.Account>;*string;~*req.<~*req.ExtraInfo>;<~*req.Account>&1002;*now&3000-07-29T15:00:00Z; \ No newline at end of file diff --git a/data/tariffplans/dynamic_tps/Rankings.csv b/data/tariffplans/dynamic_tps/Rankings.csv new file mode 100644 index 000000000..43f9f3a84 --- /dev/null +++ b/data/tariffplans/dynamic_tps/Rankings.csv @@ -0,0 +1,3 @@ +#Tenant[0];Id[1];Schedule[2];StatIDs[3];MetricIDs[4];Sorting[5];SortingParameters[6];Stored[7];ThresholdIDs[8];APIOpts[9] +cgrates.org;RANK1;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;;true;THD1&THD2 +*tenant;RANK_ACNT_<~*req.Account>;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;*accountID:<~*req.Account> \ No newline at end of file diff --git a/data/tariffplans/dynamic_tps/Routes.csv b/data/tariffplans/dynamic_tps/Routes.csv new file mode 100644 index 000000000..38463b21b --- /dev/null +++ b/data/tariffplans/dynamic_tps/Routes.csv @@ -0,0 +1,4 @@ +#Tenant[0];ID[1];FilterIDs[2];ActivationInterval[3];Sorting[4];SortingParameters[5];RouteID[6];RouteFilterIDs[7];RouteAccountIDs[8];RouteRatingPlanIDs[9];RouteResourceIDs[10];RouteStatIDs[11];RouteWeight[12];RouteBlocker[13];RouteParameters[14];Weight[15];APIOpts[16] +cgrates.org;ROUTE_WEIGHT_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10; + +*tenant;ROUTE_WEIGHT_ACNT_<~*req.Account>;*string:~*req.Account:<~*req.Account>&*string:~*req.Account:1002;*now&3000-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:<~*req.Account>&*string:~*req.Account:1002;<~*req.Account>&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;*accountID:<~*req.Account> \ No newline at end of file diff --git a/data/tariffplans/dynamic_tps/Stats.csv b/data/tariffplans/dynamic_tps/Stats.csv new file mode 100644 index 000000000..d73305b7d --- /dev/null +++ b/data/tariffplans/dynamic_tps/Stats.csv @@ -0,0 +1,6 @@ +#Tenant[0];Id[1];FilterIDs[2];ActivationInterval[3];QueueLength[4];TTL[5];MinItems[6];Metrics[7];MetricFilterIDs[8];Stored[9];Blocker[10];Weight[11];ThresholdIDs[12];APIOpts[13] +*tenant;Stat_<~*req.Account>;*string:~*req.Account:<~*req.Account>&*exists:~*opts.*accountID:;*now;;5s;;*sum#1;;true;false;10;THD_ACNT_<~*req.Account>&THD_BLOCKER_ACNT_<~*req.Account>; + +cgrates.org;Stat_1;FLTR_STS1;2014-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;*accountID:<~*req.Account> + +*tenant;Stat_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none; \ No newline at end of file diff --git a/data/tariffplans/dynamic_tps/Thresholds.csv b/data/tariffplans/dynamic_tps/Thresholds.csv new file mode 100644 index 000000000..aacca4617 --- /dev/null +++ b/data/tariffplans/dynamic_tps/Thresholds.csv @@ -0,0 +1,7 @@ +#Tenant[0];Id[1];FilterIDs[2];ActivationInterval[3];MaxHits[4];MinHits[5];MinSleep[6];Blocker[7];Weight[8];ActionIDs[9];Async[10];EeIDs[11];APIOpts[12] +*tenant;THD_ACNT_<~*req.Account>;*string:~*req.StatID:Stat_<~*req.Account>&*string:~*req.*sum#1:100;*now;-1;1;5s;true;4;ACT_BLOCK_ACC&ACT_DYN_ACT_PLAN_ACC_ENABLE;true;; + +*tenant;THD_BLOCKER_ACNT_<~*req.Account>;*string:~*opts.*accountID:<~*req.Account>;*now;-1;1;;true;3;;true;;*accountID:<~*req.Account> + +cgrates.org;THD_ACNT_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;1;1;1s;true;10;ACT_LOG_WARNING;true;eeID1&eeID2; + diff --git a/engine/action.go b/engine/action.go index e8546b942..31eec3884 100644 --- a/engine/action.go +++ b/engine/action.go @@ -128,8 +128,9 @@ func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCf dynamicActions := []string{ utils.MetaDynamicThreshold, utils.MetaDynamicStats, utils.MetaDynamicAttribute, utils.MetaDynamicActionPlan, - utils.MetaDynamicAction, utils.MetaDynamicDestination, - utils.MetaDynamicFilter, utils.MetaDynamicRoute, + utils.MetaDynamicActionPlanAccounts, utils.MetaDynamicAction, + utils.MetaDynamicDestination, utils.MetaDynamicFilter, + utils.MetaDynamicRoute, } act := ActionConnCfg{} switch source { @@ -193,6 +194,7 @@ func init() { actionFuncMap[utils.MetaDynamicStats] = dynamicStats actionFuncMap[utils.MetaDynamicAttribute] = dynamicAttribute actionFuncMap[utils.MetaDynamicActionPlan] = dynamicActionPlan + actionFuncMap[utils.MetaDynamicActionPlanAccounts] = dynamicActionPlanAccount actionFuncMap[utils.MetaDynamicAction] = dynamicAction actionFuncMap[utils.MetaDynamicDestination] = dynamicDestination actionFuncMap[utils.MetaDynamicFilter] = dynamicFilter @@ -1462,7 +1464,7 @@ func remoteSetAccount(ub *Account, a *Action, _ Actions, _ *FilterS, _ any, _ Sh // 9 ActionIDs: strings separated by "&". // 10 Async: bool // 11 EeIDs: strings separated by "&". -// 11 APIOpts: set of key-value pairs (separated by "&"). +// 12 APIOpts: set of key-value pairs (separated by "&"). // // Parameters are separated by ";" and must be provided in the specified order. func dynamicThreshold(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, @@ -1925,6 +1927,98 @@ func dynamicActionPlan(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetActionPlan, ap, &reply) } +// dynamicActionPlanAccount processes the `ExtraParameters` field from the action to construct an ActionPlan with account ids +// +// The ExtraParameters field format is expected as follows: +// +// 0 Id: string +// 1 ActionsId: string +// 2 TimingId: string +// 3 Weight: float +// 4 Overwrite: bool +// 5 Tenant:AccountIDs: strings separated by "&". +// +// Parameters are separated by ";" and must be provided in the specified order. +func dynamicActionPlanAccount(_ *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) != 6 { + return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 6", len(params))) + } + // parse dynamic parameters + for i := range params { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { + return err + } + } + // Prepare request arguments based on provided parameters. + ap := &AttrSetActionPlanAccounts{ + Id: params[0], + ReloadScheduler: true, + } + // populate ActionPlan's ActionsId + if params[1] == utils.EmptyString { + return fmt.Errorf("empty ActionsId for <%s> dynamic_action_plan", params[0]) + } + // Make sure ActionsId exists in DataDB + var actsRply []*utils.TPAction + if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetActions, utils.StringPointer(params[1]), &actsRply); err != nil { + return err + } + ap.ActionPlan = append(ap.ActionPlan, &AttrActionPlan{}) + ap.ActionPlan[0].ActionsId = params[1] + if params[2] != utils.EmptyString { + // Make sure TimingID exists in DataDB and use it for the action plan + var tpTiming utils.TPTiming + if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetTiming, &utils.ArgsGetTimingID{ID: params[2]}, &tpTiming); err != nil { + return err + } + ap.ActionPlan[0].TimingID = tpTiming.ID + ap.ActionPlan[0].Years = tpTiming.Years.Serialize(";") + ap.ActionPlan[0].Months = tpTiming.Months.Serialize(";") + ap.ActionPlan[0].MonthDays = tpTiming.MonthDays.Serialize(";") + ap.ActionPlan[0].WeekDays = tpTiming.WeekDays.Serialize(";") + if tpTiming.EndTime != utils.EmptyString { + ap.ActionPlan[0].Time = utils.InfieldJoin(tpTiming.StartTime, tpTiming.EndTime) + } else { + ap.ActionPlan[0].Time = tpTiming.StartTime + } + } + // populate ActionPlan's Weight + if params[3] != utils.EmptyString { + ap.ActionPlan[0].Weight, err = strconv.ParseFloat(params[3], 64) + if err != nil { + return err + } + } + // populate ActionPlan's Overwrite + if params[4] != utils.EmptyString { + ap.Overwrite, err = strconv.ParseBool(params[4]) + if err != nil { + return err + } + } + // populate ActionPlan's AccountIDs + if params[5] != utils.EmptyString { + ap.AccountIDs = strings.Split(params[5], utils.ANDSep) + } + + // create the ActionPlan based on the populated parameters + var reply string + return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetActionPlanAccounts, ap, &reply) +} + // dynamicAction processes the `ExtraParameters` field from the action to construct a new Action // // The ExtraParameters field format is expected as follows: diff --git a/engine/action_plan.go b/engine/action_plan.go index af03e22dc..acf28a6e9 100644 --- a/engine/action_plan.go +++ b/engine/action_plan.go @@ -390,6 +390,14 @@ func (atpl ActionTimingWeightOnlyPriorityList) Sort() { sort.Sort(atpl) } +type AttrSetActionPlanAccounts struct { + Id string // Profile id + ActionPlan []*AttrActionPlan // Set of actions this Actions profile will perform + AccountIDs []string // Set of accounts which the actions can be performed on + Overwrite bool // If previously defined, will be overwritten + ReloadScheduler bool // Enables automatic reload of the scheduler (eg: useful when adding a single action timing) +} + type AttrSetActionPlan struct { Id string // Profile id ActionPlan []*AttrActionPlan // Set of actions this Actions profile will perform diff --git a/engine/actions_test.go b/engine/actions_test.go index 174e4c736..8dfe98e79 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -5791,6 +5791,195 @@ func TestDynamicActionPlan(t *testing.T) { } } +func TestDynamicActionPlanAccount(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 ap *AttrSetActionPlanAccounts + ccMock := &ccMock{ + calls: map[string]func(ctx *context.Context, args any, reply any) error{ + utils.APIerSv1SetActionPlanAccounts: func(ctx *context.Context, args, reply any) error { + var canCast bool + if ap, canCast = args.(*AttrSetActionPlanAccounts); !canCast { + return fmt.Errorf("couldnt cast AttrSetActionPlanAccounts") + } + return nil + }, + utils.APIerSv1GetActions: func(ctx *context.Context, args2, reply any) error { + return nil + }, + utils.APIerSv1GetTiming: func(ctx *context.Context, args3, reply any) error { + var canCast bool + if args3, canCast = args3.(*utils.ArgsGetTimingID); !canCast { + return fmt.Errorf("couldnt cast ArgsGetTimingID") + } + var exp *utils.TPTiming + if utils.ToJSON(args3) == utils.ToJSON(&utils.ArgsGetTimingID{ID: "Timing_1"}) { + exp = &utils.TPTiming{ + ID: "Timing_1", + Years: utils.Years{2025}, + Months: utils.Months{2}, + MonthDays: utils.MonthDays{3}, + WeekDays: utils.WeekDays{1, 2, 3}, + StartTime: "00:12:12", + EndTime: "12:12:12", + } + } + if utils.ToJSON(args3) == utils.ToJSON(&utils.ArgsGetTimingID{ID: "*asap"}) { + exp = &utils.TPTiming{ + ID: "*asap", + Years: utils.Years{2025}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{}, + WeekDays: utils.WeekDays{}, + StartTime: "*asap", + } + } + if exp == nil { + return utils.ErrNotFound + } + *reply.(*utils.TPTiming) = *exp + 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 + expAp *AttrSetActionPlanAccounts + expectedErr string + }{ + { + name: "SuccessfulRequest", + connIDs: []string{connID}, + expAp: &AttrSetActionPlanAccounts{ + Id: "ActPl_1", + ActionPlan: []*AttrActionPlan{ + { + ActionsId: "Action_1", + TimingID: "Timing_1", + Years: "2025", + Months: "2", + MonthDays: "3", + WeekDays: "1;2;3", + Time: "00:12:12;12:12:12", + Weight: 10, + }, + }, + AccountIDs: []string{"cgrates.org:1001"}, + Overwrite: true, + ReloadScheduler: true, + }, + extraParams: "ActPl_1;Action_1;Timing_1;10;true;cgrates.org:1001", + }, + { + name: "SuccessfulRequestWithDynamicPaths", + connIDs: []string{connID}, + expAp: &AttrSetActionPlanAccounts{ + Id: "ActPl_1", + ActionPlan: []*AttrActionPlan{ + { + ActionsId: "Action_1001", + TimingID: "*asap", + Years: "2025", + Months: "*any", + MonthDays: "*any", + WeekDays: "*any", + Time: "*asap", + Weight: 10, + }, + }, + AccountIDs: []string{"cgrates.org:1001"}, + Overwrite: false, + ReloadScheduler: true, + }, + extraParams: "ActPl_1;Action_<~*req.Account>;*asap;10;;<*tenant+:+~*req.Account>", + }, + { + name: "SuccessfulRequestEmptyFields", + connIDs: []string{connID}, + expAp: &AttrSetActionPlanAccounts{ + Id: "ActPl_1", + ActionPlan: []*AttrActionPlan{ + { + ActionsId: "Action_1", + TimingID: "", + Weight: 0, + }, + }, + Overwrite: false, + ReloadScheduler: true, + }, + extraParams: "ActPl_1;Action_1;;;;", + }, + { + name: "MissingConns", + extraParams: "ActPl_1;Action_1;*asap;10;;", + expectedErr: "MANDATORY_IE_MISSING: [connIDs]", + }, + { + name: "WrongNumberOfParams", + extraParams: "ActPl_1;Action_1;*asap;10;;;", + expectedErr: "invalid number of parameters <7> expected 6", + }, + { + name: "ActionIdEmptyFail", + extraParams: "ActPl_1;;*asap;10;;", + expectedErr: `empty ActionsId for dynamic_action_plan`, + }, + { + name: "WeightFail", + connIDs: []string{connID}, + extraParams: "ActPl_1;Action_1;*asap;BadString;;", + expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, + }, + } + + 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() { + ap = nil + }) + err := dynamicActionPlanAccount(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 utils.ToJSON(ap) != utils.ToJSON(tc.expAp) { + t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAp), utils.ToJSON(ap)) + } + }) + } +} + func TestDynamicActionAll(t *testing.T) { tempConn := connMgr tmpDm := dm diff --git a/general_tests/dynamic_acc_sts_thld_it_test.go b/general_tests/dynamic_acc_sts_thld_it_test.go index 5b31c7f78..9abdb7306 100644 --- a/general_tests/dynamic_acc_sts_thld_it_test.go +++ b/general_tests/dynamic_acc_sts_thld_it_test.go @@ -79,7 +79,7 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) { attrs1 := &utils.AttrSetActions{ ActionsId: "ACT_BLOCK_ACC", Actions: []*utils.TPAction{ - { + { // Action that will block disable the account sent from stats event Identifier: utils.MetaDisableAccount, }, }, @@ -96,7 +96,7 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) { attrs1 := &utils.AttrSetActions{ ActionsId: "ACT_ENABLE_ACC", Actions: []*utils.TPAction{ - { + { // Action that will enable the account sent from actionPlans Identifier: utils.MetaEnableAccount, }, }, @@ -113,7 +113,7 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) { timing := &utils.TPTimingWithAPIOpts{ TPTiming: &utils.TPTiming{ ID: "TM_AFTER_5S", - StartTime: "+5s", + StartTime: "+5s", // timing which will start the moment the action plan is executed. After the duration in StartTime, the Action from the actionPlan will be executed. Action plans executed this way will be triggered only once right when timer finishes }, } var reply string @@ -129,15 +129,11 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) { ActionsId: "ACT_DYN_ACT_PLAN_ACC_ENABLE", Actions: []*utils.TPAction{ { - Identifier: utils.MetaDynamicActionPlan, - ExtraParameters: "ACT_PLAN_5S_ACC_ENABLE;ACT_ENABLE_ACC;TM_AFTER_5S;10;true", + // Dynamic Action Plan which will have in it the specified tenant:accountID. The *tenant and ~*opts.*accountID will be taken from the event which calles this action "ACT_DYN_ACT_PLAN_ACC_ENABLE". In this case its the event processed by "THD_ACNT_<~*req.Account>" threshold. When ACT_DYN_ACT_PLAN_ACC_ENABLE is executed, the action plan "ACT_PLAN_5S_ACC_ENABLE" will be created and the timer "TM_AFTER_5S" for the action "ACT_ENABLE_ACC" inside the action plan will start. Overwrite need to be true so that when the threshold triggers again in the future, the action plan will be renewed along with the timer + Identifier: utils.MetaDynamicActionPlanAccounts, + ExtraParameters: "ACT_PLAN_5S_ACC_ENABLE;ACT_ENABLE_ACC;TM_AFTER_5S;10;true;<*tenant+:+~*opts.*accountID>", Weight: 5, }, - // { - // Identifier: utils.MetaDynamicAccountAction, - // ExtraParameters: "cgrates.org;<~*opts.*accountID>;ACT_PLAN_5S_ACC_ENABLE;;;", - // Weight: 2, - // }, }, } var reply string @@ -153,19 +149,19 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) { ActionsId: "ACT_DYN_THRESHOLD_AND_STATS_CREATION", Actions: []*utils.TPAction{ { - // dynamic threshold for already created dynamic accounts, needed so we can ignore matching thresholds the events (which dont come from stats) where an account has already been dynamicaly created by the initial threhold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT. The threshold itself is only used for blocking + // dynamic threshold for already created dynamic accounts, needed so we can ignore matching thresholds for the events (which dont come from stats) where an account has already been dynamicaly created by the initial threhold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT. The threshold itself is only used for blocking Identifier: utils.MetaDynamicThreshold, - // get tenant and accountID from event, threshold triggers when the event account matches the already dynamicaly created account. If it matches the filter, it will block other thresholds from matching with the event. Make sure dynamic thresholds weight is higher than the initiative threshold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT + // get *tenant and *accountID from event, threshold triggers when the event account matches the already dynamicaly created account. If it matches the filter, it will block other thresholds from matching with the event. Make sure dynamic thresholds weight is higher than the initiative threshold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT ExtraParameters: "*tenant;THD_BLOCKER_ACNT_<~*req.Account>;*string:~*opts.*accountID:<~*req.Account>;*now;-1;1;;true;3;;true;;", }, { Identifier: utils.MetaDynamicThreshold, - // get tenant and accountID from event, threshold triggers when sum of statID hits 100, after triggers the action, the threshold will be disabled for 5 seconds, make sure dynamic thresholds weight is higher than the initiative threshold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT and blocker threshold THD_BLOCKER_ACNT_<~*req.Account> + // get *tenant and *accountID from event, threshold triggers when sum of statID hits 100, after it triggers the action, the threshold will be disabled for 5 seconds, make sure dynamic thresholds weight is higher than the initiative threshold THD_DYNAMIC_STATS_AND_THRESHOLD_INIT and blocker threshold THD_BLOCKER_ACNT_<~*req.Account> ExtraParameters: "*tenant;THD_ACNT_<~*req.Account>;*string:~*req.StatID:Stat_<~*req.Account>&*string:~*req.*sum#1:100;*now;-1;1;5s;true;4;ACT_BLOCK_ACC&ACT_DYN_ACT_PLAN_ACC_ENABLE;true;;", }, { Identifier: utils.MetaDynamicStats, - // get tenant and accountID from event, stat triggers when an event contains account with dynamicaly created accountID and also has a *accountID field in APIOpts, each encounter that matches the filters will raise the *sum number and call the thresholdIDs specified. when the ttl is reached, *sum will go down also + // get *tenant and *accountID from event, stat triggers when an event contains account with dynamicaly created accountID and also has a *accountID field in APIOpts, each encounter that matches the filters will raise the *sum number and call the thresholdIDs specified. when the ttl is reached, *sum will go down also ExtraParameters: "*tenant;Stat_<~*req.Account>;*string:~*req.Account:<~*req.Account>&*exists:~*opts.*accountID:;*now;;5s;;*sum#1;;true;false;10;THD_ACNT_<~*req.Account>&THD_BLOCKER_ACNT_<~*req.Account>;", }, }, @@ -340,6 +336,7 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) { }, }, }, + AccountIDs: utils.StringMap{"cgrates.org:CreatedAccount": true}, }, } if len(exp) != 1 || len(rcv) != 1 { @@ -350,36 +347,220 @@ func TestDynamicAccountWithStatsAndThreshold(t *testing.T) { } }) - // t.Run("CheckAccountReEnabled", func(t *testing.T) { - // time.Sleep(6 * time.Second) - // var acnt engine.Account - // if err := client.Call(context.Background(), utils.APIerSv2GetAccount, - // &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "CreatedAccount"}, &acnt); err != nil { - // t.Error(err) - // } - // expAcc := &engine.Account{ - // ID: "cgrates.org:CreatedAccount", - // BalanceMap: map[string]engine.Balances{ - // utils.MetaData: { - // &engine.Balance{ - // Uuid: acnt.BalanceMap[utils.MetaData][0].Uuid, - // ID: "", - // Categories: utils.StringMap{}, - // SharedGroups: utils.StringMap{}, - // TimingIDs: utils.StringMap{}, - // Value: 4096, - // ExpirationDate: acnt.BalanceMap[utils.MetaData][0].ExpirationDate, - // Weight: 10, - // DestinationIDs: utils.StringMap{}, - // }, - // }, - // }, - // UpdateTime: acnt.UpdateTime, - // Disabled: false, - // } - // if !reflect.DeepEqual(utils.ToJSON(expAcc), utils.ToJSON(acnt)) { - // t.Errorf("Expected <%v>, \nreceived <%v>", utils.ToJSON(expAcc), utils.ToJSON(acnt)) - // } - // }) + t.Run("CheckAccountReEnabled", func(t *testing.T) { + time.Sleep(6 * time.Second) + var acnt engine.Account + if err := client.Call(context.Background(), utils.APIerSv2GetAccount, + &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "CreatedAccount"}, &acnt); err != nil { + t.Error(err) + } + expAcc := &engine.Account{ + ID: "cgrates.org:CreatedAccount", + BalanceMap: map[string]engine.Balances{ + utils.MetaData: { + &engine.Balance{ + Uuid: acnt.BalanceMap[utils.MetaData][0].Uuid, + ID: "", + Categories: utils.StringMap{}, + SharedGroups: utils.StringMap{}, + TimingIDs: utils.StringMap{}, + Value: 4096, + ExpirationDate: acnt.BalanceMap[utils.MetaData][0].ExpirationDate, + Weight: 10, + DestinationIDs: utils.StringMap{}, + }, + }, + }, + UpdateTime: acnt.UpdateTime, + Disabled: false, + } + if !reflect.DeepEqual(utils.ToJSON(expAcc), utils.ToJSON(acnt)) { + t.Errorf("Expected <%v>, \nreceived <%v>", utils.ToJSON(expAcc), utils.ToJSON(acnt)) + } + }) + + t.Run("Make100AuthCalls2", func(t *testing.T) { + args1 := &sessions.V1AuthorizeArgs{ + GetMaxUsage: true, + ProcessThresholds: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]any{ + utils.OriginID: "sessDynaprepaid", + utils.OriginHost: "192.168.1.1", + utils.Source: "sessDynaprepaid", + utils.ToR: utils.MetaData, + utils.RequestType: utils.MetaDynaprepaid, + utils.AccountField: "CreatedAccount", + utils.Destination: "+1234567", + utils.AnswerTime: time.Date(2018, 8, 24, 16, 00, 26, 0, time.UTC), + utils.Usage: 1024, + }, + APIOpts: map[string]any{"*accountID": "CreatedAccount"}, // account has to be in apiopts for stats to push it to threhsoldsv1ProcessEvent so that it knows which account to disable + }, + } + var rply1 sessions.V1AuthorizeReply + if err := client.Call(context.Background(), utils.SessionSv1AuthorizeEvent, + args1, &rply1); err != nil { + t.Error(err) + return + } else if *rply1.MaxUsage != 1024*time.Nanosecond { + t.Errorf("Expected <%+v>, received <%+v>", 1024*time.Nanosecond, *rply1.MaxUsage) + } + for i := range 100 { + strI := strconv.Itoa(i) + args1 := &sessions.V1AuthorizeArgs{ + GetMaxUsage: true, + ProcessStats: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]any{ + utils.OriginID: "sessPrepaid" + strI, + utils.OriginHost: "192.168.1.1", + utils.Source: "sessPrepaid", + utils.ToR: utils.MetaData, + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "CreatedAccount", + utils.Destination: "+1234567", + utils.AnswerTime: time.Date(2018, 8, 24, 16, 00, 26, 0, time.UTC), + utils.Usage: 1024, + }, + APIOpts: map[string]any{"*accountID": "CreatedAccount"}, // account has to be in apiopts for stats to push it to threhsoldsv1ProcessEvent so that it knows which account to disable + }, + } + var rply1 sessions.V1AuthorizeReply + if err := client.Call(context.Background(), utils.SessionSv1AuthorizeEvent, + args1, &rply1); err != nil { + t.Error(err) + return + } else if *rply1.MaxUsage != 1024*time.Nanosecond { + t.Errorf("Expected <%+v>, received <%+v>", 1024*time.Nanosecond, *rply1.MaxUsage) + } + } + }) + + t.Run("CheckAccountBlocked2", func(t *testing.T) { + // wait for account to be disabled async + time.Sleep(10 * time.Millisecond) + var acnt engine.Account + if err := client.Call(context.Background(), utils.APIerSv2GetAccount, + &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "CreatedAccount"}, &acnt); err != nil { + t.Error(err) + } + expAcc := &engine.Account{ + ID: "cgrates.org:CreatedAccount", + BalanceMap: map[string]engine.Balances{ + utils.MetaData: { + &engine.Balance{ + Uuid: acnt.BalanceMap[utils.MetaData][0].Uuid, + ID: "", + Categories: utils.StringMap{}, + SharedGroups: utils.StringMap{}, + TimingIDs: utils.StringMap{}, + Value: 4096, + ExpirationDate: acnt.BalanceMap[utils.MetaData][0].ExpirationDate, + Weight: 10, + DestinationIDs: utils.StringMap{}, + }, + }, + }, + UpdateTime: acnt.UpdateTime, + Disabled: true, + } + if !reflect.DeepEqual(utils.ToJSON(expAcc), utils.ToJSON(acnt)) { + t.Errorf("Expected <%v>, \nreceived <%v>", utils.ToJSON(expAcc), utils.ToJSON(acnt)) + } + }) + + t.Run("CheckCreatedDynamicActionPlan2", func(t *testing.T) { + var reply []string + if err := client.Call(context.Background(), utils.APIerSv1GetActionPlanIDs, + &utils.PaginatorWithTenant{Tenant: "cgrates.org"}, + &reply); err != nil { + t.Error(err) + } else if len(reply) != 4 { + t.Errorf("Expected: 4 , received: <%+v>", reply) + } + slices.Sort(reply) + if reply[0] != "ACT_PLAN_5S_ACC_ENABLE" { + t.Errorf("Expected: ACT_PLAN_5S_ACC_ENABLE , received: <%v>", reply[0]) + } else if reply[1] != "DYNA_ACC" { + t.Errorf("Expected: DYNA_ACC , received: <%v>", reply[1]) + } else if reply[2] != "PACKAGE_1001" { + t.Errorf("Expected: PACKAGE_1001 , received: <%v>", reply[2]) + } else if reply[3] != "PACKAGE_1002" { + t.Errorf("Expected: PACKAGE_1002 , received: <%v>", reply[3]) + } + + var rcv []*engine.ActionPlan + if err := client.Call(context.Background(), utils.APIerSv1GetActionPlan, + &v1.AttrGetActionPlan{ID: "ACT_PLAN_5S_ACC_ENABLE"}, &rcv); err != nil { + t.Error(err) + } + exp := []*engine.ActionPlan{ + { + Id: "ACT_PLAN_5S_ACC_ENABLE", + ActionTimings: []*engine.ActionTiming{ + { + Uuid: rcv[0].ActionTimings[0].Uuid, + ActionsID: "ACT_ENABLE_ACC", + ExtraData: nil, + Weight: 10, + Timing: &engine.RateInterval{ + Timing: &engine.RITiming{ + ID: "TM_AFTER_5S", + Years: utils.Years{}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{}, + WeekDays: utils.WeekDays{}, + StartTime: "+5s", + }, + Rating: nil, + Weight: 0, + }, + }, + }, + AccountIDs: utils.StringMap{"cgrates.org:CreatedAccount": true}, + }, + } + if len(exp) != 1 || len(rcv) != 1 { + t.Fatalf("expected exp len 1, got <%v>, expected rcv len 1, got <%v>", len(exp), len(rcv)) + } + if !reflect.DeepEqual(exp, rcv) { + t.Errorf("expected <%v>, \nreceived <%v>", utils.ToJSON(exp), utils.ToJSON(rcv)) + } + }) + + t.Run("CheckAccountReEnabled2", func(t *testing.T) { + time.Sleep(6 * time.Second) + var acnt engine.Account + if err := client.Call(context.Background(), utils.APIerSv2GetAccount, + &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "CreatedAccount"}, &acnt); err != nil { + t.Error(err) + } + expAcc := &engine.Account{ + ID: "cgrates.org:CreatedAccount", + BalanceMap: map[string]engine.Balances{ + utils.MetaData: { + &engine.Balance{ + Uuid: acnt.BalanceMap[utils.MetaData][0].Uuid, + ID: "", + Categories: utils.StringMap{}, + SharedGroups: utils.StringMap{}, + TimingIDs: utils.StringMap{}, + Value: 4096, + ExpirationDate: acnt.BalanceMap[utils.MetaData][0].ExpirationDate, + Weight: 10, + DestinationIDs: utils.StringMap{}, + }, + }, + }, + UpdateTime: acnt.UpdateTime, + Disabled: false, + } + if !reflect.DeepEqual(utils.ToJSON(expAcc), utils.ToJSON(acnt)) { + t.Errorf("Expected <%v>, \nreceived <%v>", utils.ToJSON(expAcc), utils.ToJSON(acnt)) + } + }) } diff --git a/general_tests/dynamic_thresholds_it_test.go b/general_tests/dynamic_thresholds_it_test.go index 9b7615341..469cac1e2 100644 --- a/general_tests/dynamic_thresholds_it_test.go +++ b/general_tests/dynamic_thresholds_it_test.go @@ -179,7 +179,7 @@ func testDynThdCheckForAction(t *testing.T) { func testDynThdCheckForDestination(t *testing.T) { var rply *engine.Destination - if err := dynThdRpc.Call(context.Background(), utils.APIerSv1GetDestination, "1005", &rply); err == nil || err.Error() != utils.ErrNotFound.Error() { + if err := dynThdRpc.Call(context.Background(), utils.APIerSv1GetDestination, "DYNAMICLY_DST_1005", &rply); err == nil || err.Error() != utils.ErrNotFound.Error() { t.Error(err) } } @@ -233,7 +233,7 @@ func testDynThdSetAction(t *testing.T) { }, { Identifier: utils.MetaDynamicDestination, - ExtraParameters: "DST_1005;1005", + ExtraParameters: "DYNAMICLY_DST_1005;1005", }, }} if err := dynThdRpc.Call(context.Background(), utils.APIerSv2SetActions, @@ -591,50 +591,13 @@ func testDynThdCheckForDynCreatedAction(t *testing.T) { func testDynThdCheckForDynCreatedDestination(t *testing.T) { time.Sleep(50 * time.Millisecond) - args := &v2.AttrGetActions{ActionIDs: []string{"DYNAMICLY_DST_1005"}} - result1 := make(map[string]engine.Actions) - if err := dynThdRpc.Call(context.Background(), utils.APIerSv2GetActions, args, &result1); err != nil { - t.Fatal(err) + var result1 *engine.Destination + if err := dynThdRpc.Call(context.Background(), utils.APIerSv1GetDestination, "DYNAMICLY_DST_1005", &result1); err != nil { + t.Error(err) } - exp := map[string]engine.Actions{ - "DYNAMICLY_ACT_1002": { - { - Id: "DYNAMICLY_ACT_1002", - ActionType: utils.CDRLog, - ExtraParameters: "{\"Account\":\"1002\",\"RequestType\":\"*pseudoprepaid\",\"Subject\":\"DifferentThanAccount\", \"ToR\":\"~ActionType:s/^\\*(.*)$/did_$1/\"}", - Filters: []string{"*string:~*req.Account:1002", "filter2"}, - ExpirationString: utils.MetaUnlimited, - Weight: 10, - Balance: &engine.BalanceFilter{ - Uuid: result1["DYNAMICLY_ACT_1002"][0].Balance.Uuid, - ID: utils.StringPointer("balID"), - Type: utils.StringPointer(utils.MetaMonetary), - Value: &utils.ValueFormula{ - Method: utils.EmptyString, - Params: nil, - Static: 10, - }, - ExpirationDate: nil, - Weight: utils.Float64Pointer(10), - DestinationIDs: &utils.StringMap{"1002": true, "1003": true}, - RatingSubject: utils.StringPointer("SPECIAL_1002"), - Categories: &utils.StringMap{"call": true, "data": true}, - SharedGroups: &utils.StringMap{"SHARED_A": true, "SHARED_B": true}, - TimingIDs: &utils.StringMap{utils.MetaDaily: true}, - Timings: []*engine.RITiming{{ - ID: utils.MetaDaily, - Years: utils.Years{}, - Months: utils.Months{}, - MonthDays: utils.MonthDays{}, - WeekDays: utils.WeekDays{}, - StartTime: result1["DYNAMICLY_ACT_1002"][0].Balance.Timings[0].StartTime, //depends on time it ran - }}, - Disabled: utils.BoolPointer(false), - Factors: nil, - Blocker: utils.BoolPointer(true), - }, - }, - }, + exp := &engine.Destination{ + Id: "DYNAMICLY_DST_1005", + Prefixes: []string{"1005"}, } if !reflect.DeepEqual(exp, result1) { t.Errorf("\nexpected: <%+v>, \nreceived: <%+v>", utils.ToJSON(exp), utils.ToJSON(result1)) diff --git a/utils/consts.go b/utils/consts.go index f8d38e4fa..a0fb1abca 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1170,57 +1170,58 @@ const ( // Actions const ( - MetaLog = "*log" - MetaResetTriggers = "*reset_triggers" - MetaSetRecurrent = "*set_recurrent" - MetaUnsetRecurrent = "*unset_recurrent" - MetaAllowNegative = "*allow_negative" - MetaDenyNegative = "*deny_negative" - MetaResetAccount = "*reset_account" - MetaRemoveAccount = "*remove_account" - MetaRemoveBalance = "*remove_balance" - MetaTopUpReset = "*topup_reset" - MetaTopUp = "*topup" - MetaDebitReset = "*debit_reset" - MetaDebit = "*debit" - MetaTransferBalance = "*transfer_balance" - MetaResetCounters = "*reset_counters" - MetaEnableAccount = "*enable_account" - MetaDisableAccount = "*disable_account" - HttpPostAsync = "*http_post_async" - MetaMailAsync = "*mail_async" - MetaUnlimited = "*unlimited" - CDRLog = "*cdrlog" - MetaSetDDestinations = "*set_ddestinations" - MetaTransferMonetaryDefault = "*transfer_monetary_default" - MetaCgrRpc = "*cgr_rpc" - MetaAlterSessions = "*alter_sessions" - MetaForceDisconnectSessions = "*force_disconnect_sessions" - TopUpZeroNegative = "*topup_zero_negative" - SetExpiry = "*set_expiry" - MetaPublishAccount = "*publish_account" - MetaRemoveSessionCosts = "*remove_session_costs" - MetaRemoveExpired = "*remove_expired" - MetaPostEvent = "*post_event" - MetaCDRAccount = "*reset_account_cdr" - MetaResetThreshold = "*reset_threshold" - MetaResetStatQueue = "*reset_stat_queue" - MetaRemoteSetAccount = "*remote_set_account" - MetaDynamicThreshold = "*dynamic_threshold" - MetaDynamicStats = "*dynamic_stats" - MetaDynamicAttribute = "*dynamic_attribute" - MetaDynamicActionPlan = "*dynamic_action_plan" - MetaDynamicAction = "*dynamic_action" - MetaDynamicDestination = "*dynamic_destination" - MetaDynamicFilter = "*dynamic_filter" - MetaDynamicRoute = "*dynamic_route" - MetaDynamicRanking = "*dynamic_ranking" - ActionID = "ActionID" - ActionType = "ActionType" - ActionValue = "ActionValue" - BalanceValue = "BalanceValue" - BalanceUnits = "BalanceUnits" - ExtraParameters = "ExtraParameters" + MetaLog = "*log" + MetaResetTriggers = "*reset_triggers" + MetaSetRecurrent = "*set_recurrent" + MetaUnsetRecurrent = "*unset_recurrent" + MetaAllowNegative = "*allow_negative" + MetaDenyNegative = "*deny_negative" + MetaResetAccount = "*reset_account" + MetaRemoveAccount = "*remove_account" + MetaRemoveBalance = "*remove_balance" + MetaTopUpReset = "*topup_reset" + MetaTopUp = "*topup" + MetaDebitReset = "*debit_reset" + MetaDebit = "*debit" + MetaTransferBalance = "*transfer_balance" + MetaResetCounters = "*reset_counters" + MetaEnableAccount = "*enable_account" + MetaDisableAccount = "*disable_account" + HttpPostAsync = "*http_post_async" + MetaMailAsync = "*mail_async" + MetaUnlimited = "*unlimited" + CDRLog = "*cdrlog" + MetaSetDDestinations = "*set_ddestinations" + MetaTransferMonetaryDefault = "*transfer_monetary_default" + MetaCgrRpc = "*cgr_rpc" + MetaAlterSessions = "*alter_sessions" + MetaForceDisconnectSessions = "*force_disconnect_sessions" + TopUpZeroNegative = "*topup_zero_negative" + SetExpiry = "*set_expiry" + MetaPublishAccount = "*publish_account" + MetaRemoveSessionCosts = "*remove_session_costs" + MetaRemoveExpired = "*remove_expired" + MetaPostEvent = "*post_event" + MetaCDRAccount = "*reset_account_cdr" + MetaResetThreshold = "*reset_threshold" + MetaResetStatQueue = "*reset_stat_queue" + MetaRemoteSetAccount = "*remote_set_account" + MetaDynamicThreshold = "*dynamic_threshold" + MetaDynamicStats = "*dynamic_stats" + MetaDynamicAttribute = "*dynamic_attribute" + MetaDynamicActionPlan = "*dynamic_action_plan" + MetaDynamicActionPlanAccounts = "*dynamic_action_plan_accounts" + MetaDynamicAction = "*dynamic_action" + MetaDynamicDestination = "*dynamic_destination" + MetaDynamicFilter = "*dynamic_filter" + MetaDynamicRoute = "*dynamic_route" + MetaDynamicRanking = "*dynamic_ranking" + ActionID = "ActionID" + ActionType = "ActionType" + ActionValue = "ActionValue" + BalanceValue = "BalanceValue" + BalanceUnits = "BalanceUnits" + ExtraParameters = "ExtraParameters" MetaAddBalance = "*add_balance" MetaSetBalance = "*set_balance" @@ -1612,6 +1613,7 @@ const ( APIerSv1GetTPActionIds = "APIerSv1.GetTPActionIds" APIerSv1RemoveTPActions = "APIerSv1.RemoveTPActions" APIerSv1SetActionPlan = "APIerSv1.SetActionPlan" + APIerSv1SetActionPlanAccounts = "APIerSv1.SetActionPlanAccounts" APIerSv1ExecuteAction = "APIerSv1.ExecuteAction" APIerSv1SetTPRatingProfile = "APIerSv1.SetTPRatingProfile" APIerSv1GetTPRatingProfile = "APIerSv1.GetTPRatingProfile" diff --git a/utils/dynamicfieldpath.go b/utils/dynamicfieldpath.go index a169514de..2fb1ab2b5 100644 --- a/utils/dynamicfieldpath.go +++ b/utils/dynamicfieldpath.go @@ -46,9 +46,16 @@ func ProcessFieldPath(fldPath, sep string, dP DataProvider) (newPath string, err newPath = fldPath[:startIdx] for path := range strings.SplitSeq(fldPath[startIdx+1:endIdx], sep) { // proccess the found path var val string - if val, err = DPDynamicString(path, dP); err != nil { - newPath = EmptyString - return + if strings.HasPrefix(path, MetaNow) || strings.HasPrefix(path, MetaTenant) { + if val, err = dP.FieldAsString(SplitPath(path, NestingSep[0], -1)); err != nil { + newPath = EmptyString + return + } + } else { + if val, err = DPDynamicString(path, dP); err != nil { + newPath = EmptyString + return + } } newPath += val }