Add action type *dynamicRate

This commit is contained in:
arberkatellari
2025-08-05 17:52:59 +02:00
committed by Dan Christian Bogos
parent 9065e58158
commit 2ffdb409a6
4 changed files with 412 additions and 9 deletions

View File

@@ -92,7 +92,7 @@ func (aL *actDynamicThreshold) execute(ctx *context.Context, data utils.MapStora
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
@@ -261,7 +261,7 @@ func (aL *actDynamicStats) execute(ctx *context.Context, data utils.MapStorage,
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
@@ -472,7 +472,7 @@ func (aL *actDynamicAttribute) execute(ctx *context.Context, data utils.MapStora
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
@@ -648,7 +648,7 @@ func (aL *actDynamicResource) execute(ctx *context.Context, data utils.MapStorag
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
@@ -811,7 +811,7 @@ func (aL *actDynamicTrend) execute(ctx *context.Context, data utils.MapStorage,
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
@@ -963,7 +963,7 @@ func (aL *actDynamicRanking) execute(ctx *context.Context, data utils.MapStorage
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
@@ -1090,7 +1090,7 @@ func (aL *actDynamicFilter) execute(ctx *context.Context, data utils.MapStorage,
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
@@ -1216,7 +1216,7 @@ func (aL *actDynamicRoute) execute(ctx *context.Context, data utils.MapStorage,
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
@@ -1477,3 +1477,226 @@ func (aL *actDynamicRoute) execute(ctx *context.Context, data utils.MapStorage,
}
return
}
// actDynamicRate processes the `ActionDiktatsOpts` field from the action to construct a RateProfile
//
// The ActionDiktatsOpts field format is expected as follows:
//
// 0 Tenant: string
// 1 ID: string
// 2 FilterIDs: strings separated by "&".
// 3 Weights: strings separated by "&".
// 4 MinCost: string
// 5 MaxCost: string
// 6 MaxCostStrategy: string
// 7 RateID: string
// 8 RateFilterIDs: strings separated by "&".
// 9 RateActivationStart: string
// 10 RateWeights: strings separated by "&".
// 11 RateBlocker: bool
// 12 RateIntervalStart: string
// 13 RateFixedFee: string
// 14 RateRecurrentFee: string
// 15 RateUnit: string
// 16 RateIncrement: string
// 17 APIOpts: set of key-value pairs (separated by "&").
//
// Parameters are separated by ";" and must be provided in the specified order.
type actDynamicRate struct {
config *config.CGRConfig
connMgr *engine.ConnManager
fltrS *engine.FilterS
aCfg *utils.APAction
tnt string
cgrEv *utils.CGREvent
}
func (aL *actDynamicRate) id() string {
return aL.aCfg.ID
}
func (aL *actDynamicRate) cfg() *utils.APAction {
return aL.aCfg
}
// execute implements actioner interface
func (aL *actDynamicRate) execute(ctx *context.Context, data utils.MapStorage, trgID string) (err error) {
if len(aL.config.ActionSCfg().AdminSConns) == 0 {
return fmt.Errorf("no connection with AdminS")
}
data[utils.MetaNow] = time.Now()
data[utils.MetaTenant] = utils.FirstNonEmpty(aL.cgrEv.Tenant, aL.tnt,
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were specified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
for _, diktat := range aL.aCfg.Diktats {
if pass, err := aL.fltrS.Pass(ctx, aL.tnt, diktat.FilterIDs, data); err != nil {
return err
} else if !pass {
continue
}
weight, err := engine.WeightFromDynamics(ctx, diktat.Weights, aL.fltrS, aL.tnt, data)
if err != nil {
return err
}
weights[diktat.ID] = weight
diktats = append(diktats, diktat)
}
// Sort by weight (higher values first).
slices.SortFunc(diktats, func(a, b *utils.APDiktat) int {
return cmp.Compare(weights[b.ID], weights[a.ID])
})
for _, diktat := range diktats {
params := strings.Split(utils.IfaceAsString(diktat.Opts[utils.MetaTemplate]),
utils.InfieldSep)
if len(params) != 18 {
return fmt.Errorf("invalid number of parameters <%d> expected 18", len(params))
}
// parse dynamic parameters
for i := range params {
if params[i], err = utils.ParseParamForDataProvider(params[i], data, false); err != nil {
return err
}
}
// Prepare request arguments based on provided parameters.
args := &utils.APIRateProfile{
RateProfile: &utils.RateProfile{
Tenant: params[0],
ID: params[1],
MaxCostStrategy: params[6],
Rates: map[string]*utils.Rate{
params[7]: {
ID: params[7],
ActivationTimes: params[9],
IntervalRates: []*utils.IntervalRate{{}},
},
},
},
APIOpts: make(map[string]any),
}
// populate RateProfile's FilterIDs
if params[2] != utils.EmptyString {
args.FilterIDs = strings.Split(params[2], utils.ANDSep)
}
// populate RateProfile's Weights
if params[3] != utils.EmptyString {
args.Weights = utils.DynamicWeights{&utils.DynamicWeight{}}
wghtSplit := strings.Split(params[3], utils.ANDSep)
if len(wghtSplit) > 2 {
return utils.ErrUnsupportedFormat
}
if wghtSplit[0] != utils.EmptyString {
args.Weights[0].FilterIDs = []string{wghtSplit[0]}
}
if wghtSplit[1] != utils.EmptyString {
args.Weights[0].Weight, err = strconv.ParseFloat(wghtSplit[1], 64)
if err != nil {
return err
}
}
}
// populate RateProfile's MinCost
if params[4] != utils.EmptyString {
args.MinCost, err = utils.NewDecimalFromString(params[4])
if err != nil {
return err
}
}
// populate RateProfile's MaxCost
if params[5] != utils.EmptyString {
args.MaxCost, err = utils.NewDecimalFromString(params[5])
if err != nil {
return err
}
}
// populate RateProfile's Rate
if params[7] != utils.EmptyString {
// populate Rate's FilterIDs
if params[8] != utils.EmptyString {
args.Rates[params[7]].FilterIDs = strings.Split(params[8], utils.ANDSep)
}
// populate Rate's Weights
if params[10] != utils.EmptyString {
args.Rates[params[7]].Weights = utils.DynamicWeights{&utils.DynamicWeight{}}
wghtSplit := strings.Split(params[10], utils.ANDSep)
if len(wghtSplit) > 2 {
return utils.ErrUnsupportedFormat
}
if wghtSplit[0] != utils.EmptyString {
args.Rates[params[7]].Weights[0].FilterIDs = []string{wghtSplit[0]}
}
if wghtSplit[1] != utils.EmptyString {
args.Rates[params[7]].Weights[0].Weight, err = strconv.ParseFloat(wghtSplit[1], 64)
if err != nil {
return err
}
}
}
// populate Rate's Blocker
if params[11] != utils.EmptyString {
args.Rates[params[7]].Blocker, err = strconv.ParseBool(params[11])
if err != nil {
return err
}
}
// populate Rate's IntervalStart
if params[12] != utils.EmptyString {
args.Rates[params[7]].IntervalRates[0].IntervalStart, err = utils.NewDecimalFromString(params[12])
if err != nil {
return err
}
}
// populate Rate's FixedFee
if params[13] != utils.EmptyString {
args.Rates[params[7]].IntervalRates[0].FixedFee, err = utils.NewDecimalFromString(params[13])
if err != nil {
return err
}
}
// populate Rate's RecurrentFee
if params[14] != utils.EmptyString {
args.Rates[params[7]].IntervalRates[0].RecurrentFee, err = utils.NewDecimalFromString(params[14])
if err != nil {
return err
}
}
// populate Rate's Unit
if params[15] != utils.EmptyString {
args.Rates[params[7]].IntervalRates[0].Unit, err = utils.NewDecimalFromString(params[15])
if err != nil {
return err
}
}
// populate Rate's Increment
if params[16] != utils.EmptyString {
args.Rates[params[7]].IntervalRates[0].Increment, err = utils.NewDecimalFromString(params[16])
if err != nil {
return err
}
}
}
// populate RateProfile's APIOpts
if params[17] != utils.EmptyString {
if err := parseParamStringToMap(params[17], args.APIOpts); err != nil {
return err
}
}
// create the RateProfile based on the populated parameters
var rply string
if err = aL.connMgr.Call(ctx, aL.config.ActionSCfg().AdminSConns,
utils.AdminSv1SetRateProfile, args, &rply); err != nil {
return err
}
if blocker, err := engine.BlockerFromDynamics(ctx, diktat.Blockers, aL.fltrS, aL.tnt, data); err != nil {
return err
} else if blocker {
break
}
}
return
}

View File

@@ -49,6 +49,8 @@ func actionTarget(act string) string {
return utils.MetaFilters
case utils.MetaDynamicRoute:
return utils.MetaRoutes
case utils.MetaDynamicRate:
return utils.MetaRates
default:
return utils.MetaNone
}
@@ -161,6 +163,8 @@ func newActioner(ctx *context.Context, cgrEv *utils.CGREvent, cfg *config.CGRCon
return &actDynamicFilter{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil
case utils.MetaDynamicRoute:
return &actDynamicRoute{cfg, connMgr, dm, fltrS, aCfg, tnt, cgrEv}, nil
case utils.MetaDynamicRate:
return &actDynamicRate{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil
default:
return nil, fmt.Errorf("unsupported action type: <%s>", aCfg.Type)

View File

@@ -262,6 +262,7 @@ func TestDynThdIT(t *testing.T) {
utils.MetaRankings: {"someID": {}},
utils.MetaFilters: {"someID": {}},
utils.MetaRoutes: {"someID": {}},
utils.MetaRates: {"someID": {}},
},
Actions: []*utils.APAction{
{
@@ -720,6 +721,63 @@ func TestDynThdIT(t *testing.T) {
},
},
},
{
ID: "Dynamic_Rate_ID",
Type: utils.MetaDynamicRate,
Diktats: []*utils.APDiktat{
{
ID: "CreateDynamicRate1002",
FilterIDs: []string{"*string:~*req.Account:1002"},
Opts: map[string]any{
"*template": "*tenant;DYNAMICLY_RATE_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;*string:~*req.Account:1002&10;5;10;*free;RT_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;* * * * *;*string:~*req.Account:1002&20;true;0s;5;0.01;1m;1s;~*opts",
},
Weights: utils.DynamicWeights{
{
Weight: 50,
},
},
},
{
ID: "CreateDynamicRate1002NotFoundFilter",
FilterIDs: []string{"*string:~*req.Account:1003"},
Opts: map[string]any{
"*template": "*tenant;DYNAMICLY_RATE_2_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;*string:~*req.Account:1002&10;5;10;*free;RT_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;* * * * *;*string:~*req.Account:1002&20;true;0s;5;0.01;1m;1s;~*opts",
},
Weights: utils.DynamicWeights{
{
Weight: 90,
},
},
},
{
ID: "CreateDynamicRate1002Blocker",
Opts: map[string]any{
"*template": "*tenant;DYNAMICLY_RATE_3_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;*string:~*req.Account:1002&10;5;10;*free;RT_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;* * * * *;*string:~*req.Account:1002&20;true;0s;5;0.01;1m;1s;~*opts",
},
Weights: utils.DynamicWeights{
{
Weight: 20,
},
},
Blockers: utils.DynamicBlockers{
{
Blocker: true,
},
},
},
{
ID: "CreateDynamicRate1002Blocked",
Opts: map[string]any{
"*template": "*tenant;DYNAMICLY_RATE_4_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;*string:~*req.Account:1002&10;5;10;*free;RT_<~*req.Account>;*string:~*req.Account:1002&*string:~*req.Account:1003;* * * * *;*string:~*req.Account:1002&20;true;0s;5;0.01;1m;1s;~*opts",
},
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
},
},
},
},
},
}
@@ -818,7 +876,7 @@ func TestDynThdIT(t *testing.T) {
} else if !reflect.DeepEqual(ids, []string{"THD_ACNT_1002"}) {
t.Error("Unexpected reply returned", ids)
}
time.Sleep(100 * time.Millisecond) //wait for async
time.Sleep(1000 * time.Millisecond) //wait for async
})
t.Run("GetDynamicThresholdProfile", func(t *testing.T) {
var thrsholds []*engine.ThresholdProfile
@@ -1435,4 +1493,121 @@ func TestDynThdIT(t *testing.T) {
t.Errorf("Expected <%v>\nReceived <%v>", utils.ToJSON(exp), utils.ToJSON(rcv))
}
})
t.Run("GetDynamicRateProfile", func(t *testing.T) {
var rcv []*utils.RateProfile
if err := client.Call(context.Background(), utils.AdminSv1GetRateProfiles,
&utils.ArgsItemIDs{
Tenant: utils.CGRateSorg,
}, &rcv); err != nil {
t.Errorf("AdminSv1GetRateProfiles failed unexpectedly: %v", err)
}
if len(rcv) != 2 {
t.Fatalf("AdminSv1GetRateProfiles len(rcv)=%v, want 2", len(rcv))
}
sort.Slice(rcv, func(i, j int) bool {
return rcv[i].ID > rcv[j].ID
})
exp := []*utils.RateProfile{
{
Tenant: "cgrates.org",
ID: "DYNAMICLY_RATE_3_1002",
FilterIDs: []string{
"*string:~*req.Account:1002",
"*string:~*req.Account:1003",
},
Weights: utils.DynamicWeights{
{
FilterIDs: []string{
"*string:~*req.Account:1002",
},
Weight: 10,
},
},
MinCost: utils.NewDecimalFromFloat64(5),
MaxCost: utils.NewDecimalFromFloat64(10),
MaxCostStrategy: utils.MetaMaxCostFree,
Rates: map[string]*utils.Rate{
"RT_1002": {
ID: "RT_1002",
FilterIDs: []string{
"*string:~*req.Account:1002",
"*string:~*req.Account:1003",
},
ActivationTimes: "* * * * *",
Weights: utils.DynamicWeights{
{
FilterIDs: []string{
"*string:~*req.Account:1002",
},
Weight: 20,
},
},
Blocker: true,
IntervalRates: []*utils.IntervalRate{
{
IntervalStart: utils.NewDecimalFromFloat64(0),
FixedFee: utils.NewDecimalFromFloat64(5),
RecurrentFee: utils.NewDecimalFromFloat64(0.01),
Unit: utils.NewDecimalFromFloat64(60000000000),
Increment: utils.NewDecimalFromFloat64(1000000000),
},
},
},
},
},
{
Tenant: "cgrates.org",
ID: "DYNAMICLY_RATE_1002",
FilterIDs: []string{
"*string:~*req.Account:1002",
"*string:~*req.Account:1003",
},
Weights: utils.DynamicWeights{
{
FilterIDs: []string{
"*string:~*req.Account:1002",
},
Weight: 10,
},
},
MinCost: utils.NewDecimalFromFloat64(5),
MaxCost: utils.NewDecimalFromFloat64(10),
MaxCostStrategy: utils.MetaMaxCostFree,
Rates: map[string]*utils.Rate{
"RT_1002": {
ID: "RT_1002",
FilterIDs: []string{
"*string:~*req.Account:1002",
"*string:~*req.Account:1003",
},
ActivationTimes: "* * * * *",
Weights: utils.DynamicWeights{
{
FilterIDs: []string{
"*string:~*req.Account:1002",
},
Weight: 20,
},
},
Blocker: true,
IntervalRates: []*utils.IntervalRate{
{
IntervalStart: utils.NewDecimalFromFloat64(0),
FixedFee: utils.NewDecimalFromFloat64(5),
RecurrentFee: utils.NewDecimalFromFloat64(0.01),
Unit: utils.NewDecimalFromFloat64(60000000000),
Increment: utils.NewDecimalFromFloat64(1000000000),
},
},
},
},
},
}
if !reflect.DeepEqual(exp, rcv) {
t.Errorf("Expected <%v>\nReceived <%v>", utils.ToJSON(exp), utils.ToJSON(rcv))
}
})
}

View File

@@ -1155,6 +1155,7 @@ const (
MetaDynamicRanking = "*dynamicRanking"
MetaDynamicFilter = "*dynamicFilter"
MetaDynamicRoute = "*dynamicRoute"
MetaDynamicRate = "*dynamicRate"
// Diktats Opts Fields
MetaBalancePath = "*balancePath"