diff --git a/apier/v1/apier_it_test.go b/apier/v1/apier_it_test.go index ff3570b07..c9a6b2beb 100644 --- a/apier/v1/apier_it_test.go +++ b/apier/v1/apier_it_test.go @@ -1061,7 +1061,7 @@ func testApierGetActionTrigger(t *testing.T) { //set an ActionTrigger in database var reply string if err := rater.Call(context.Background(), utils.APIerSv1SetActionTrigger, - AttrSetActionTrigger{ + engine.AttrSetActionTrigger{ GroupID: "TEST_ID1", UniqueID: "TEST_ID2", }, &reply); err != nil { @@ -1394,7 +1394,7 @@ func testApierSetAccountActionTriggers(t *testing.T) { setReq := AttrSetAccountActionTriggers{ Tenant: "cgrates.org", Account: "dan2", - AttrSetActionTrigger: AttrSetActionTrigger{ + AttrSetActionTrigger: engine.AttrSetActionTrigger{ UniqueID: reply[0].UniqueID, ActionTrigger: map[string]any{ utils.ActivationDate: "2016-02-05T18:00:00Z", diff --git a/apier/v1/replicate_it_test.go b/apier/v1/replicate_it_test.go index 702e19e13..f6610b135 100644 --- a/apier/v1/replicate_it_test.go +++ b/apier/v1/replicate_it_test.go @@ -1131,7 +1131,7 @@ func testInternalReplicateITActionTrigger(t *testing.T) { } // set var reply string - attrSet := AttrSetActionTrigger{ + attrSet := engine.AttrSetActionTrigger{ GroupID: "TestATR", UniqueID: "UniqueID", ActionTrigger: map[string]any{ diff --git a/apier/v1/triggers.go b/apier/v1/triggers.go index bf9e61dcc..2783673fa 100644 --- a/apier/v1/triggers.go +++ b/apier/v1/triggers.go @@ -19,7 +19,6 @@ along with this program. If not, see package v1 import ( - "errors" "strings" "time" @@ -204,151 +203,7 @@ func (apierSv1 *APIerSv1) ResetAccountActionTriggers(ctx *context.Context, attr type AttrSetAccountActionTriggers struct { Tenant string Account string - AttrSetActionTrigger -} -type AttrSetActionTrigger struct { - GroupID string - UniqueID string - ActionTrigger map[string]any -} - -// UpdateActionTrigger updates the ActionTrigger if is matching -func (attr *AttrSetActionTrigger) UpdateActionTrigger(at *engine.ActionTrigger, timezone string) (updated bool, err error) { - if at == nil { - return false, errors.New("Empty ActionTrigger") - } - if at.ID == utils.EmptyString { // New AT, update it's data - if attr.GroupID == utils.EmptyString { - return false, utils.NewErrMandatoryIeMissing(utils.GroupID) - } - if missing := utils.MissingMapFields(attr.ActionTrigger, []string{"ThresholdType", "ThresholdValue"}); len(missing) != 0 { - return false, utils.NewErrMandatoryIeMissing(missing...) - } - at.ID = attr.GroupID - if attr.UniqueID != utils.EmptyString { - at.UniqueID = attr.UniqueID - } - } - if attr.GroupID != utils.EmptyString && attr.GroupID != at.ID { - return - } - if attr.UniqueID != utils.EmptyString && attr.UniqueID != at.UniqueID { - return - } - // at matches - updated = true - if thr, has := attr.ActionTrigger[utils.ThresholdType]; has { - at.ThresholdType = utils.IfaceAsString(thr) - } - if thr, has := attr.ActionTrigger[utils.ThresholdValue]; has { - if at.ThresholdValue, err = utils.IfaceAsFloat64(thr); err != nil { - return - } - } - if rec, has := attr.ActionTrigger[utils.Recurrent]; has { - if at.Recurrent, err = utils.IfaceAsBool(rec); err != nil { - return - } - } - if exec, has := attr.ActionTrigger[utils.Executed]; has { - if at.Executed, err = utils.IfaceAsBool(exec); err != nil { - return - } - } - if minS, has := attr.ActionTrigger[utils.MinSleep]; has { - if at.MinSleep, err = utils.IfaceAsDuration(minS); err != nil { - return - } - } - if exp, has := attr.ActionTrigger[utils.ExpirationDate]; has { - if at.ExpirationDate, err = utils.IfaceAsTime(exp, timezone); err != nil { - return - } - } - if act, has := attr.ActionTrigger[utils.ActivationDate]; has { - if at.ActivationDate, err = utils.IfaceAsTime(act, timezone); err != nil { - return - } - } - if at.Balance == nil { - at.Balance = &engine.BalanceFilter{} - } - if bid, has := attr.ActionTrigger[utils.BalanceID]; has { - at.Balance.ID = utils.StringPointer(utils.IfaceAsString(bid)) - } - if btype, has := attr.ActionTrigger[utils.BalanceType]; has { - at.Balance.Type = utils.StringPointer(utils.IfaceAsString(btype)) - } - if bdest, has := attr.ActionTrigger[utils.BalanceDestinationIds]; has { - var bdIds []string - if bdIds, err = utils.IfaceAsSliceString(bdest); err != nil { - return - } - at.Balance.DestinationIDs = utils.StringMapPointer(utils.NewStringMap(bdIds...)) - } - if bweight, has := attr.ActionTrigger[utils.BalanceWeight]; has { - var bw float64 - if bw, err = utils.IfaceAsFloat64(bweight); err != nil { - return - } - at.Balance.Weight = utils.Float64Pointer(bw) - } - if exp, has := attr.ActionTrigger[utils.BalanceExpirationDate]; has { - var balanceExpTime time.Time - if balanceExpTime, err = utils.IfaceAsTime(exp, timezone); err != nil { - return - } - at.Balance.ExpirationDate = utils.TimePointer(balanceExpTime) - } - if bTimeTag, has := attr.ActionTrigger[utils.BalanceTimingTags]; has { - var timeTag []string - if timeTag, err = utils.IfaceAsSliceString(bTimeTag); err != nil { - return - } - at.Balance.TimingIDs = utils.StringMapPointer(utils.NewStringMap(timeTag...)) - } - if brs, has := attr.ActionTrigger[utils.BalanceRatingSubject]; has { - at.Balance.RatingSubject = utils.StringPointer(utils.IfaceAsString(brs)) - } - if bcat, has := attr.ActionTrigger[utils.BalanceCategories]; has { - var cat []string - if cat, err = utils.IfaceAsSliceString(bcat); err != nil { - return - } - at.Balance.Categories = utils.StringMapPointer(utils.NewStringMap(cat...)) - } - if bsg, has := attr.ActionTrigger[utils.BalanceSharedGroups]; has { - var shrgrps []string - if shrgrps, err = utils.IfaceAsSliceString(bsg); err != nil { - return - } - at.Balance.SharedGroups = utils.StringMapPointer(utils.NewStringMap(shrgrps...)) - } - if bb, has := attr.ActionTrigger[utils.BalanceBlocker]; has { - var bBlocker bool - if bBlocker, err = utils.IfaceAsBool(bb); err != nil { - return - } - at.Balance.Blocker = utils.BoolPointer(bBlocker) - } - if bd, has := attr.ActionTrigger[utils.BalanceDisabled]; has { - var bDis bool - if bDis, err = utils.IfaceAsBool(bd); err != nil { - return - } - at.Balance.Disabled = utils.BoolPointer(bDis) - } - if minQ, has := attr.ActionTrigger[utils.MinQueuedItems]; has { - var mQ int64 - if mQ, err = utils.IfaceAsTInt64(minQ); err != nil { - return - } - at.MinQueuedItems = int(mQ) - } - if accID, has := attr.ActionTrigger[utils.ActionsID]; has { - at.ActionsID = utils.IfaceAsString(accID) - } - return + engine.AttrSetActionTrigger } // SetAccountActionTriggers updates or creates if not present the ActionTrigger for an Account @@ -444,7 +299,7 @@ func (apierSv1 *APIerSv1) RemoveActionTrigger(ctx *context.Context, attr *AttrRe } // SetActionTrigger updates a ActionTrigger -func (apierSv1 *APIerSv1) SetActionTrigger(ctx *context.Context, attr *AttrSetActionTrigger, reply *string) (err error) { +func (apierSv1 *APIerSv1) SetActionTrigger(ctx *context.Context, attr *engine.AttrSetActionTrigger, reply *string) (err error) { if missing := utils.MissingStructFields(attr, []string{"GroupID"}); len(missing) != 0 { return utils.NewErrMandatoryIeMissing(missing...) } diff --git a/apier/v1/triggers_test.go b/apier/v1/triggers_test.go index 02db79102..be9dc09b1 100644 --- a/apier/v1/triggers_test.go +++ b/apier/v1/triggers_test.go @@ -28,7 +28,7 @@ import ( ) func TestAttrSetActionTriggerUpdateActionTrigger(t *testing.T) { - ast := AttrSetActionTrigger{} + ast := engine.AttrSetActionTrigger{} if _, err := ast.UpdateActionTrigger(nil, ""); err == nil || err.Error() != "Empty ActionTrigger" { t.Errorf("Expected error \"Empty ActionTrigger\", received: %v", err) } @@ -43,7 +43,7 @@ func TestAttrSetActionTriggerUpdateActionTrigger(t *testing.T) { t.Errorf("Expected error %s , received: %v", expErr, err) } tNow := time.Now() - ast = AttrSetActionTrigger{ + ast = engine.AttrSetActionTrigger{ GroupID: "GroupID", UniqueID: "ID", ActionTrigger: map[string]any{ @@ -182,7 +182,7 @@ func TestAttrSetActionTriggerUpdateActionTrigger(t *testing.T) { ID: "GroupID2", UniqueID: "ID2", } - ast = AttrSetActionTrigger{ + ast = engine.AttrSetActionTrigger{ GroupID: "GroupID", UniqueID: "ID", } @@ -195,7 +195,7 @@ func TestAttrSetActionTriggerUpdateActionTrigger(t *testing.T) { ID: "GroupID", UniqueID: "ID2", } - ast = AttrSetActionTrigger{ + ast = engine.AttrSetActionTrigger{ GroupID: "GroupID", UniqueID: "ID", } diff --git a/apier/v2/apierv2_it_test.go b/apier/v2/apierv2_it_test.go index 90fedd320..f61c881cb 100644 --- a/apier/v2/apierv2_it_test.go +++ b/apier/v2/apierv2_it_test.go @@ -174,7 +174,7 @@ func testAPIerSv2itSetAccountActionTriggers(t *testing.T) { attrs := v1.AttrSetAccountActionTriggers{ Tenant: "cgrates.org", Account: "dan", - AttrSetActionTrigger: v1.AttrSetActionTrigger{ + AttrSetActionTrigger: engine.AttrSetActionTrigger{ GroupID: "MONITOR_MAX_BALANCE", ActionTrigger: map[string]any{ utils.ThresholdType: utils.TriggerMaxBalance, diff --git a/console/trigger_set.go b/console/trigger_set.go index d99e7f5c0..b2dcc8c64 100644 --- a/console/trigger_set.go +++ b/console/trigger_set.go @@ -19,7 +19,7 @@ along with this program. If not, see package console import ( - v1 "github.com/cgrates/cgrates/apier/v1" + "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" ) @@ -27,7 +27,7 @@ func init() { c := &CmdSetTriggers{ name: "triggers_set", rpcMethod: utils.APIerSv1SetActionTrigger, - rpcParams: &v1.AttrSetActionTrigger{}, + rpcParams: &engine.AttrSetActionTrigger{}, } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} @@ -37,7 +37,7 @@ func init() { type CmdSetTriggers struct { name string rpcMethod string - rpcParams *v1.AttrSetActionTrigger + rpcParams *engine.AttrSetActionTrigger *CommandExecuter } @@ -51,7 +51,7 @@ func (self *CmdSetTriggers) RpcMethod() string { func (self *CmdSetTriggers) RpcParams(reset bool) any { if reset || self.rpcParams == nil { - self.rpcParams = &v1.AttrSetActionTrigger{} + self.rpcParams = &engine.AttrSetActionTrigger{} } return self.rpcParams } diff --git a/engine/action.go b/engine/action.go index b8bd618ba..2372d0a97 100644 --- a/engine/action.go +++ b/engine/action.go @@ -131,7 +131,7 @@ func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCf utils.MetaDynamicActionPlanAccounts, utils.MetaDynamicAction, utils.MetaDynamicDestination, utils.MetaDynamicFilter, utils.MetaDynamicRoute, utils.MetaDynamicRatingProfile, - utils.MetaDynamicResource, + utils.MetaDynamicResource, utils.MetaDynamicActionTrigger, } act := ActionConnCfg{} switch source { @@ -204,6 +204,7 @@ func init() { actionFuncMap[utils.MetaDynamicRatingProfile] = dynamicRatingProfile actionFuncMap[utils.MetaDynamicRanking] = dynamicTrend actionFuncMap[utils.MetaDynamicResource] = dynamicResource + actionFuncMap[utils.MetaDynamicActionTrigger] = dynamicActionTrigger } func getActionFunc(typ string) (f actionTypeFunc, exists bool) { @@ -2781,3 +2782,172 @@ func dynamicResource(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetResourceProfile, rsc, &reply) } + +// dynamicActionTrigger processes the `ExtraParameters` field from the action to +// construct a ActionTrigger +// +// The ExtraParameters field format is expected as follows: +// +// 0 Tag: string +// 1 UniqueId: string +// 2 ThresholdType: string +// 3 ThresholdValue: float +// 4 Recurrent: bool +// 5 MinSleep: duration +// 6 ExpiryTime: time +// 7 ActivationTime: time +// 8 BalanceTag: string +// 9 BalanceType: string +// 10 BalanceCategories: strings separated by "&". +// 11 BalanceDestinationIds: strings separated by "&". +// 12 BalanceRatingSubject: string +// 13 BalanceSharedGroup: strings separated by "&". +// 14 BalanceExpiryTime: time +// 15 BalanceTimingIds: strings separated by "&". +// 16 BalanceWeight: float +// 17 BalanceBlocker: bool +// 18 BalanceDisabled: bool +// 19 ActionsId: string +// 20 Weight: float +// +// Parameters are separated by ";" and must be provided in the specified order. +func dynamicActionTrigger(_ *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) != 21 { + return fmt.Errorf("invalid number of parameters <%d> expected 21", 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. + at := &AttrSetActionTrigger{ + GroupID: params[0], + UniqueID: utils.FirstNonEmpty(params[1], utils.GenUUID()), + ActionTrigger: make(map[string]any), + } + // populate ActionTrigger's ThresholdType + if params[2] != utils.EmptyString { + at.ActionTrigger[utils.ThresholdType] = params[2] + } + // populate ActionTrigger's ThresholdValue + if params[3] != utils.EmptyString { + at.ActionTrigger[utils.ThresholdValue], err = strconv.ParseFloat(params[3], 64) + if err != nil { + return err + } + } + // populate ActionTrigger's Recurrent + if params[4] != utils.EmptyString { + at.ActionTrigger[utils.Recurrent], err = strconv.ParseBool(params[4]) + if err != nil { + return err + } + } + // populate ActionTrigger's MinSleep + if params[5] != utils.EmptyString { + at.ActionTrigger[utils.MinSleep], err = utils.ParseDurationWithNanosecs(params[5]) + if err != nil { + return err + } + } + // populate ActionTrigger's ExpirationDate + if params[6] != utils.EmptyString { + at.ActionTrigger[utils.ExpirationDate], err = utils.ParseTimeDetectLayout(params[6], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + return err + } + } + // populate ActionTrigger's ActivationDate + if params[7] != utils.EmptyString { + at.ActionTrigger[utils.ActivationDate], err = utils.ParseTimeDetectLayout(params[7], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + return err + } + } + // populate ActionTrigger's BalanceID + if params[8] != utils.EmptyString { + at.ActionTrigger[utils.BalanceID] = params[8] + } + // populate ActionTrigger's BalanceType + if params[9] != utils.EmptyString { + at.ActionTrigger[utils.BalanceType] = params[9] + } + // populate ActionTrigger's BalanceCategories + if params[10] != utils.EmptyString { + at.ActionTrigger[utils.BalanceCategories] = strings.Split(params[10], utils.ANDSep) + } + // populate ActionTrigger's BalanceDestinationIds + if params[11] != utils.EmptyString { + at.ActionTrigger[utils.BalanceDestinationIds] = strings.Split(params[11], utils.ANDSep) + } + // populate ActionTrigger's BalanceRatingSubject + if params[12] != utils.EmptyString { + at.ActionTrigger[utils.BalanceRatingSubject] = params[12] + } + // populate ActionTrigger's BalanceSharedGroups + if params[13] != utils.EmptyString { + at.ActionTrigger[utils.BalanceSharedGroups] = strings.Split(params[13], utils.ANDSep) + } + // populate ActionTrigger's BalanceExpirationDate + if params[14] != utils.EmptyString { + at.ActionTrigger[utils.BalanceExpirationDate], err = utils.ParseTimeDetectLayout(params[14], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + return err + } + } + // populate ActionTrigger's BalanceTimingTags + if params[15] != utils.EmptyString { + at.ActionTrigger[utils.BalanceTimingTags] = strings.Split(params[15], utils.ANDSep) + } + // populate ActionTrigger's BalanceWeight + if params[16] != utils.EmptyString { + at.ActionTrigger[utils.BalanceWeight], err = strconv.ParseFloat(params[16], 64) + if err != nil { + return err + } + } + // populate ActionTrigger's BalanceBlocker + if params[17] != utils.EmptyString { + at.ActionTrigger[utils.BalanceBlocker], err = strconv.ParseBool(params[17]) + if err != nil { + return err + } + } + // populate ActionTrigger's BalanceDisabled + if params[18] != utils.EmptyString { + at.ActionTrigger[utils.BalanceDisabled], err = strconv.ParseBool(params[18]) + if err != nil { + return err + } + } + // populate ActionTrigger's ActionsID + if params[19] != utils.EmptyString { + at.ActionTrigger[utils.ActionsID] = params[19] + } + // populate ActionTrigger's Weight + if params[20] != utils.EmptyString { + at.ActionTrigger[utils.Weight], err = strconv.ParseFloat(params[20], 64) + if err != nil { + return err + } + } + + // create the ActionTrigger based on the populated parameters + var reply string + return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetActionTrigger, at, &reply) +} diff --git a/engine/action_trigger.go b/engine/action_trigger.go index d3c09532e..17b2c7c11 100644 --- a/engine/action_trigger.go +++ b/engine/action_trigger.go @@ -20,6 +20,7 @@ package engine import ( "encoding/json" + "errors" "fmt" "sort" "time" @@ -314,3 +315,154 @@ func (at *ActionTrigger) FieldAsString(fldPath []string) (val string, err error) } return utils.IfaceAsString(iface), nil } + +type AttrSetActionTrigger struct { + GroupID string + UniqueID string + ActionTrigger map[string]any +} + +// UpdateActionTrigger updates the ActionTrigger if is matching +func (attr *AttrSetActionTrigger) UpdateActionTrigger(at *ActionTrigger, timezone string) (updated bool, err error) { + if at == nil { + return false, errors.New("Empty ActionTrigger") + } + if at.ID == utils.EmptyString { // New AT, update it's data + if attr.GroupID == utils.EmptyString { + return false, utils.NewErrMandatoryIeMissing(utils.GroupID) + } + if missing := utils.MissingMapFields(attr.ActionTrigger, []string{"ThresholdType", "ThresholdValue"}); len(missing) != 0 { + return false, utils.NewErrMandatoryIeMissing(missing...) + } + at.ID = attr.GroupID + if attr.UniqueID != utils.EmptyString { + at.UniqueID = attr.UniqueID + } + } + if attr.GroupID != utils.EmptyString && attr.GroupID != at.ID { + return + } + if attr.UniqueID != utils.EmptyString && attr.UniqueID != at.UniqueID { + return + } + // at matches + updated = true + if thr, has := attr.ActionTrigger[utils.ThresholdType]; has { + at.ThresholdType = utils.IfaceAsString(thr) + } + if thr, has := attr.ActionTrigger[utils.ThresholdValue]; has { + if at.ThresholdValue, err = utils.IfaceAsFloat64(thr); err != nil { + return + } + } + if rec, has := attr.ActionTrigger[utils.Recurrent]; has { + if at.Recurrent, err = utils.IfaceAsBool(rec); err != nil { + return + } + } + if exec, has := attr.ActionTrigger[utils.Executed]; has { + if at.Executed, err = utils.IfaceAsBool(exec); err != nil { + return + } + } + if minS, has := attr.ActionTrigger[utils.MinSleep]; has { + if at.MinSleep, err = utils.IfaceAsDuration(minS); err != nil { + return + } + } + if exp, has := attr.ActionTrigger[utils.ExpirationDate]; has { + if at.ExpirationDate, err = utils.IfaceAsTime(exp, timezone); err != nil { + return + } + } + if act, has := attr.ActionTrigger[utils.ActivationDate]; has { + if at.ActivationDate, err = utils.IfaceAsTime(act, timezone); err != nil { + return + } + } + if at.Balance == nil { + at.Balance = &BalanceFilter{} + } + if bid, has := attr.ActionTrigger[utils.BalanceID]; has { + at.Balance.ID = utils.StringPointer(utils.IfaceAsString(bid)) + } + if btype, has := attr.ActionTrigger[utils.BalanceType]; has { + at.Balance.Type = utils.StringPointer(utils.IfaceAsString(btype)) + } + if bdest, has := attr.ActionTrigger[utils.BalanceDestinationIds]; has { + var bdIds []string + if bdIds, err = utils.IfaceAsSliceString(bdest); err != nil { + return + } + at.Balance.DestinationIDs = utils.StringMapPointer(utils.NewStringMap(bdIds...)) + } + if bweight, has := attr.ActionTrigger[utils.BalanceWeight]; has { + var bw float64 + if bw, err = utils.IfaceAsFloat64(bweight); err != nil { + return + } + at.Balance.Weight = utils.Float64Pointer(bw) + } + if exp, has := attr.ActionTrigger[utils.BalanceExpirationDate]; has { + var balanceExpTime time.Time + if balanceExpTime, err = utils.IfaceAsTime(exp, timezone); err != nil { + return + } + at.Balance.ExpirationDate = utils.TimePointer(balanceExpTime) + } + if bTimeTag, has := attr.ActionTrigger[utils.BalanceTimingTags]; has { + var timeTag []string + if timeTag, err = utils.IfaceAsSliceString(bTimeTag); err != nil { + return + } + at.Balance.TimingIDs = utils.StringMapPointer(utils.NewStringMap(timeTag...)) + } + if brs, has := attr.ActionTrigger[utils.BalanceRatingSubject]; has { + at.Balance.RatingSubject = utils.StringPointer(utils.IfaceAsString(brs)) + } + if bcat, has := attr.ActionTrigger[utils.BalanceCategories]; has { + var cat []string + if cat, err = utils.IfaceAsSliceString(bcat); err != nil { + return + } + at.Balance.Categories = utils.StringMapPointer(utils.NewStringMap(cat...)) + } + if bsg, has := attr.ActionTrigger[utils.BalanceSharedGroups]; has { + var shrgrps []string + if shrgrps, err = utils.IfaceAsSliceString(bsg); err != nil { + return + } + at.Balance.SharedGroups = utils.StringMapPointer(utils.NewStringMap(shrgrps...)) + } + if bb, has := attr.ActionTrigger[utils.BalanceBlocker]; has { + var bBlocker bool + if bBlocker, err = utils.IfaceAsBool(bb); err != nil { + return + } + at.Balance.Blocker = utils.BoolPointer(bBlocker) + } + if bd, has := attr.ActionTrigger[utils.BalanceDisabled]; has { + var bDis bool + if bDis, err = utils.IfaceAsBool(bd); err != nil { + return + } + at.Balance.Disabled = utils.BoolPointer(bDis) + } + if minQ, has := attr.ActionTrigger[utils.MinQueuedItems]; has { + var mQ int64 + if mQ, err = utils.IfaceAsTInt64(minQ); err != nil { + return + } + at.MinQueuedItems = int(mQ) + } + if accID, has := attr.ActionTrigger[utils.ActionsID]; has { + at.ActionsID = utils.IfaceAsString(accID) + } + + if weight, has := attr.ActionTrigger[utils.Weight]; has { + if at.Weight, err = utils.IfaceAsFloat64(weight); err != nil { + return + } + } + return +} diff --git a/engine/actions_test.go b/engine/actions_test.go index d85acad66..dbd4efba4 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -7334,3 +7334,264 @@ func TestDynamicResource(t *testing.T) { }) } } + +func TestDynamicActionTrigger(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 at *AttrSetActionTrigger + ccMock := &ccMock{ + calls: map[string]func(ctx *context.Context, args any, reply any) error{ + utils.APIerSv1SetActionTrigger: func(ctx *context.Context, args, reply any) error { + var canCast bool + if at, canCast = args.(*AttrSetActionTrigger); !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 + expAt *AttrSetActionTrigger + expectedErr string + }{ + { + name: "SuccessfulRequest", + connIDs: []string{connID}, + expAt: &AttrSetActionTrigger{ + GroupID: "STANDARD_TRIGGERS", + UniqueID: "uid", + ActionTrigger: map[string]any{ + utils.ThresholdType: "*max_balance", + utils.ThresholdValue: float64(20), + utils.Recurrent: true, + utils.MinSleep: time.Second, + utils.ActivationDate: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), + utils.BalanceID: "*default", + utils.BalanceType: "*monetary", + utils.BalanceCategories: []string{utils.Call, "data"}, + utils.BalanceDestinationIds: []string{"DST1", "DST2"}, + utils.BalanceRatingSubject: "SPECIAL_1002", + utils.BalanceSharedGroups: []string{"SHRGroup1", "SHRGroup2"}, + utils.BalanceExpirationDate: time.Date(2030, 7, 29, 15, 0, 0, 0, time.UTC), + utils.BalanceTimingTags: []string{"*asap"}, + utils.BalanceWeight: float64(10), + utils.BalanceBlocker: true, + utils.BalanceDisabled: true, + utils.ActionsID: "ACT_1001", + utils.Weight: float64(20), + }, + }, + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", + }, + { + name: "SuccessfulRequestWithDynamicPaths", + connIDs: []string{connID}, + expAt: &AttrSetActionTrigger{ + GroupID: "STANDARD_TRIGGERS_1001", + UniqueID: "uid_1001", + ActionTrigger: map[string]any{ + utils.ThresholdType: "*max_balance", + utils.ThresholdValue: float64(20), + utils.Recurrent: true, + utils.MinSleep: time.Second, + utils.ExpirationDate: time.Now(), + utils.ActivationDate: time.Now(), + utils.BalanceID: "*default", + utils.BalanceType: "*monetary", + utils.BalanceCategories: []string{utils.Call, "data"}, + utils.BalanceDestinationIds: []string{"DST_1001", "DST2"}, + utils.BalanceRatingSubject: "SPECIAL_1001", + utils.BalanceSharedGroups: []string{"SHRGroup_1001", "SHRGroup2"}, + utils.BalanceExpirationDate: time.Now(), + utils.BalanceTimingTags: []string{"*asap"}, + utils.BalanceWeight: float64(10), + utils.BalanceBlocker: true, + utils.BalanceDisabled: true, + utils.ActionsID: "ACT_1001", + utils.Weight: float64(20), + }, + }, + extraParams: "STANDARD_TRIGGERS_<~*req.Account>;uid_<~*req.Account>;*max_balance;20;true;1s;*now;*now;*default;*monetary;call&data;DST_<~*req.Account>&DST2;SPECIAL_<~*req.Account>;SHRGroup_<~*req.Account>&SHRGroup2;*now;*asap;10;true;true;ACT_<~*req.Account>;20", + }, + { + name: "SuccessfulRequestEmptyFields", + connIDs: []string{connID}, + expAt: &AttrSetActionTrigger{ + GroupID: "STANDARD_TRIGGERS", + UniqueID: "uid", + ActionTrigger: map[string]any{ + utils.ThresholdType: "*max_balance", + utils.ThresholdValue: float64(20), + }, + }, + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;;;;;;;;;;;;;;;;;", + }, + { + name: "MissingConns", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", + expectedErr: "MANDATORY_IE_MISSING: [connIDs]", + }, + { + name: "WrongNumberOfParams", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20", + expectedErr: "invalid number of parameters <4> expected 21", + }, + { + name: "ThresholdValueFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;BadString;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", + expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, + }, + { + name: "RecurrentFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;BadString;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", + expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, + }, + { + name: "MinSleepFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;BadString;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", + expectedErr: `time: invalid duration "BadString"`, + }, + { + name: "ExpirationDateBadStringFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;bad String;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", + expectedErr: `Unsupported time format`, + }, + { + name: "ActivationDateBadStringFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;bad String;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", + expectedErr: `Unsupported time format`, + }, + { + name: "BalanceExpirationDateBadStringFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;bad String;*asap;10;true;true;ACT_1001;20", + expectedErr: `Unsupported time format`, + }, + { + name: "BalanceWeightFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;BadString;true;true;ACT_1001;20", + expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, + }, + { + name: "BalanceBlockerFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;BadString;true;ACT_1001;20", + expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, + }, + { + name: "BalanceDisabledFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;BadString;ACT_1001;20", + expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, + }, + { + name: "WeightFail", + extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;BadString", + expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, + }, + } + + 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", + }, + } + t.Cleanup(func() { + at = nil + }) + err := dynamicActionTrigger(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(at, tc.expAt) { + if i != 1 { + t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) + } else { + // Get the absolute difference between the times + rcvExpDate, err := utils.IfaceAsTime(at.ActionTrigger[utils.ExpirationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + t.Fatal(err) + } + expExpDate, err := utils.IfaceAsTime(tc.expAt.ActionTrigger[utils.ExpirationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + t.Fatal(err) + } + diff := rcvExpDate.Sub(expExpDate) + 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 { + t.Fatalf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) + } + tc.expAt.ActionTrigger[utils.ExpirationDate] = rcvExpDate + // Get the absolute difference between the times + rcvExpDate, err = utils.IfaceAsTime(at.ActionTrigger[utils.ActivationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + t.Fatal(err) + } + expExpDate, err = utils.IfaceAsTime(tc.expAt.ActionTrigger[utils.ActivationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + t.Fatal(err) + } + diff = rcvExpDate.Sub(expExpDate) + 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 { + t.Fatalf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) + } + tc.expAt.ActionTrigger[utils.ActivationDate] = rcvExpDate + // Get the absolute difference between the times + rcvExpDate, err = utils.IfaceAsTime(at.ActionTrigger[utils.BalanceExpirationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + t.Fatal(err) + } + expExpDate, err = utils.IfaceAsTime(tc.expAt.ActionTrigger[utils.BalanceExpirationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) + if err != nil { + t.Fatal(err) + } + diff = rcvExpDate.Sub(expExpDate) + 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 { + t.Fatalf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) + } + tc.expAt.ActionTrigger[utils.BalanceExpirationDate] = rcvExpDate + if !reflect.DeepEqual(at, tc.expAt) { + t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) + } + } + } + }) + } +} diff --git a/general_tests/offline_internal_it_test.go b/general_tests/offline_internal_it_test.go index c0da2be90..bd5e0c979 100644 --- a/general_tests/offline_internal_it_test.go +++ b/general_tests/offline_internal_it_test.go @@ -216,7 +216,7 @@ func TestOfflineInternal(t *testing.T) { // run with sudo t.Run("GetActionTriggers", func(t *testing.T) { var reply string - if err := client.Call(context.Background(), utils.APIerSv1SetActionTrigger, v1.AttrSetActionTrigger{ + if err := client.Call(context.Background(), utils.APIerSv1SetActionTrigger, engine.AttrSetActionTrigger{ GroupID: "GroupID", UniqueID: "ID", ActionTrigger: map[string]any{ diff --git a/general_tests/tp_it_test.go b/general_tests/tp_it_test.go index bf11f81ca..b1ab3bf48 100644 --- a/general_tests/tp_it_test.go +++ b/general_tests/tp_it_test.go @@ -162,7 +162,7 @@ func testTpActionTriggers(t *testing.T) { t.Errorf("Calling v1.GetActionTriggers got: %v", atrs) } var reply string - if err := tpRPC.Call(context.Background(), utils.APIerSv1SetActionTrigger, v1.AttrSetActionTrigger{ + if err := tpRPC.Call(context.Background(), utils.APIerSv1SetActionTrigger, engine.AttrSetActionTrigger{ GroupID: "TestATR", UniqueID: "Unique atr id", ActionTrigger: map[string]any{ diff --git a/utils/consts.go b/utils/consts.go index e30e2d9f4..aefcecba3 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1241,6 +1241,7 @@ const ( MetaDynamicRatingProfile = "*dynamic_rating_profile" MetaDynamicTrend = "*dynamic_trend" MetaDynamicResource = "*dynamic_resource" + MetaDynamicActionTrigger = "*dynamic_action_trigger" ActionID = "ActionID" ActionType = "ActionType" ActionValue = "ActionValue"