From da41db3f560e0c8953d2d47e02bd85498e32471d Mon Sep 17 00:00:00 2001 From: arberkatellari Date: Fri, 14 Nov 2025 11:39:07 +0200 Subject: [PATCH] Make RateProfiles storable in MySQL and Postgres --- apis/rates_it_test.go | 52 ++++- config/config_defaults.go | 2 +- config/configsanity.go | 6 +- .../cgrates.json | 10 +- data/conf/samples/rates_mysql/cgrates.json | 3 +- data/conf/samples/rates_postgres/cgrates.json | 46 +++++ data/conf/samples/rates_redis/cgrates.json | 45 +++++ data/storage/mysql/create_db_tables.sql | 24 ++- data/storage/postgres/create_db_tables.sql | 21 ++ engine/models.go | 24 +++ engine/storage_sql.go | 190 ++++++++++++++++-- utils/consts.go | 2 + utils/librates.go | 117 +++++++++++ 13 files changed, 502 insertions(+), 40 deletions(-) create mode 100644 data/conf/samples/rates_postgres/cgrates.json create mode 100644 data/conf/samples/rates_redis/cgrates.json diff --git a/apis/rates_it_test.go b/apis/rates_it_test.go index 3f670d343..e0dfc99f0 100644 --- a/apis/rates_it_test.go +++ b/apis/rates_it_test.go @@ -94,11 +94,11 @@ func TestRateSIT(t *testing.T) { case utils.MetaMongo: ratePrfConfigDIR = "rates_mongo" case utils.MetaRedis: - t.SkipNow() + ratePrfConfigDIR = "rates_redis" case utils.MetaMySQL: ratePrfConfigDIR = "rates_mysql" case utils.MetaPostgres: - t.SkipNow() + ratePrfConfigDIR = "rates_postgres" default: t.Fatal("Unknown Database type") } @@ -1598,8 +1598,15 @@ func testRateProfileUpdateRates(t *testing.T) { }, }, &result2); err != nil { t.Error(err) - } else if !reflect.DeepEqual(result2, expectedRate) { - t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } else if *utils.DBType != utils.MetaMySQL && *utils.DBType != utils.MetaPostgres { + if !reflect.DeepEqual(result2, expectedRate) { + t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } + } else if *utils.DBType == utils.MetaMySQL || *utils.DBType == utils.MetaPostgres { + expectedRate.Rates["RT_THUESDAY"].IntervalRates[0].FixedFee = utils.NewDecimal(2, 1) + if !reflect.DeepEqual(result2, expectedRate) { + t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } } } @@ -1686,9 +1693,17 @@ func testRateProfileRemoveMultipleRates(t *testing.T) { }, }, &result2); err != nil { t.Error(err) - } else if !reflect.DeepEqual(result2, expectedRate) { - t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } else if *utils.DBType != utils.MetaMySQL && *utils.DBType != utils.MetaPostgres { + if !reflect.DeepEqual(result2, expectedRate) { + t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } + } else if *utils.DBType == utils.MetaMySQL || *utils.DBType == utils.MetaPostgres { + expectedRate.Rates["RT_THUESDAY"].IntervalRates[0].FixedFee = utils.NewDecimal(2, 1) + if !reflect.DeepEqual(result2, expectedRate) { + t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } } + } func testRateProfileSetMultipleRatesInProfile(t *testing.T) { @@ -1878,8 +1893,16 @@ func testRateProfileSetMultipleRatesInProfile(t *testing.T) { }, }, &result2); err != nil { t.Error(err) - } else if !reflect.DeepEqual(result2, expectedRate) { - t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } else if *utils.DBType != utils.MetaMySQL && *utils.DBType != utils.MetaPostgres { + if !reflect.DeepEqual(result2, expectedRate) { + t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } + } else if *utils.DBType == utils.MetaMySQL || *utils.DBType == utils.MetaPostgres { + expectedRate.Rates["RT_THUESDAY"].IntervalRates[0].FixedFee = utils.NewDecimal(2, 1) + expectedRate.Rates["RT_SUNDAY"].IntervalRates[1].IntervalStart = utils.NewDecimal(100000000, 0) + if !reflect.DeepEqual(result2, expectedRate) { + t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedRate), utils.ToJSON(result2)) + } } } @@ -1937,7 +1960,18 @@ func testRateProfileUpdateProfileRatesOverwrite(t *testing.T) { }, }, &result2); err != nil { t.Error(err) - } else if !reflect.DeepEqual(result2, ratePrf.RateProfile) { + } else if *utils.DBType != utils.MetaMySQL && *utils.DBType != utils.MetaPostgres { + if !reflect.DeepEqual(result2, ratePrf.RateProfile) { + t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(ratePrf.RateProfile), utils.ToJSON(result2)) + } + } else if *utils.DBType == utils.MetaMySQL || *utils.DBType == utils.MetaPostgres { + ratePrf.RateProfile.MaxCost = utils.NewDecimal(5, 1) + if !reflect.DeepEqual(result2, ratePrf.RateProfile) { + t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(ratePrf.RateProfile), utils.ToJSON(result2)) + } + } + + if !reflect.DeepEqual(result2, ratePrf.RateProfile) { t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(ratePrf.RateProfile), utils.ToJSON(result2)) } diff --git a/config/config_defaults.go b/config/config_defaults.go index d8b2f4c1d..33b769585 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -180,10 +180,10 @@ const CGRATES_CFG_JSON = ` "*thresholds": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*filters": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*route_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, + "*rate_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, // compatible db types: <*internal|*redis|*mongo> "*actions": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, - "*rate_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*load_ids": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*resource_filter_indexes" : {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false, "dbConn": "*default"}, "*ip_filter_indexes" : {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false, "dbConn": "*default"}, diff --git a/config/configsanity.go b/config/configsanity.go index 4f7f8d4dc..d7cc38468 100644 --- a/config/configsanity.go +++ b/config/configsanity.go @@ -1027,6 +1027,7 @@ func (cfg *CGRConfig) checkConfigSanity() error { // DataDB sanity checks hasOneInternalDB := false // used to reutrn error in case more then 1 internaldb is found allDBsItems := []string{ + utils.CacheVersions, utils.MetaAccounts, utils.MetaIPProfiles, utils.MetaIPAllocations, @@ -1041,6 +1042,7 @@ func (cfg *CGRConfig) checkConfigSanity() error { utils.MetaThresholds, utils.MetaFilters, utils.MetaRouteProfiles, + utils.MetaRateProfiles, } for _, dbcfg := range cfg.dbCfg.DBConns { if dbcfg.Type == utils.MetaInternal { @@ -1080,11 +1082,11 @@ func (cfg *CGRConfig) checkConfigSanity() error { if item == utils.MetaCDRs { if !slices.Contains(storDBTypes, cfg.dbCfg.DBConns[val.DBConn].Type) { return fmt.Errorf("<%s> db item can only be of types <%v>, got <%s>", item, - storDBTypes, cfg.dbCfg.DBConns[val.DBConn].Type) + storDBTypes[4:], cfg.dbCfg.DBConns[val.DBConn].Type) } } else { if !slices.Contains(dataDBTypes, cfg.dbCfg.DBConns[val.DBConn].Type) { - return fmt.Errorf("<%s> db item can only be of types <%v>, got <%s>", item, dataDBTypes, cfg.dbCfg.DBConns[val.DBConn].Type) + return fmt.Errorf("<%s> db item can only be of types <%v>, got <%s>", item, dataDBTypes[3:], cfg.dbCfg.DBConns[val.DBConn].Type) } } } diff --git a/data/conf/samples/offline_internal_ms_rewrite_ms_limit/cgrates.json b/data/conf/samples/offline_internal_ms_rewrite_ms_limit/cgrates.json index c01f750a8..03b50076c 100644 --- a/data/conf/samples/offline_internal_ms_rewrite_ms_limit/cgrates.json +++ b/data/conf/samples/offline_internal_ms_rewrite_ms_limit/cgrates.json @@ -10,11 +10,11 @@ "db_conns": { "*default": { "db_type": "*internal", - "opts":{ - "internalDBDumpInterval": "500ms", - "internalDBRewriteInterval": "500ms", - "internalDBFileSizeLimit": "4k" - } + "opts":{ + "internalDBDumpInterval": "500ms", + "internalDBRewriteInterval": "500ms", + "internalDBFileSizeLimit": "4k" + } } }, "items":{ diff --git a/data/conf/samples/rates_mysql/cgrates.json b/data/conf/samples/rates_mysql/cgrates.json index d471d6fc9..5d9a4b56d 100644 --- a/data/conf/samples/rates_mysql/cgrates.json +++ b/data/conf/samples/rates_mysql/cgrates.json @@ -27,7 +27,8 @@ } }, "items": { - "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*rate_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} } }, diff --git a/data/conf/samples/rates_postgres/cgrates.json b/data/conf/samples/rates_postgres/cgrates.json new file mode 100644 index 000000000..474e7f289 --- /dev/null +++ b/data/conf/samples/rates_postgres/cgrates.json @@ -0,0 +1,46 @@ +{ + +"general": { + "node_id": "id", +}, + +"logger": { + "level": 7 +}, + +"db": { + "db_conns": { + "*default": { + "db_type": "redis", + "db_host": "127.0.0.1", + "db_port": 6379, + "db_name": "10", + "db_user": "cgrates" + }, + "StorDB": { + "db_type": "postgres", + "db_host": "127.0.0.1", + "db_port": 5432, + "db_name": "cgrates", + "db_user": "cgrates", + "db_password": "CGRateS.org" + } + }, + "items": { + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*rate_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + } +}, + +"rates": { + "enabled": true, + "prefix_indexed_fields": ["*req.Destination"], + "exists_indexed_fields": ["*req.Destination"], + "rate_prefix_indexed_fields": ["*req.Destination"] +}, + +"admins": { + "enabled": true +} + +} \ No newline at end of file diff --git a/data/conf/samples/rates_redis/cgrates.json b/data/conf/samples/rates_redis/cgrates.json new file mode 100644 index 000000000..d471d6fc9 --- /dev/null +++ b/data/conf/samples/rates_redis/cgrates.json @@ -0,0 +1,45 @@ +{ + +"general": { + "node_id": "id", +}, + +"logger": { + "level": 7 +}, + +"db": { + "db_conns": { + "*default": { + "db_type": "redis", + "db_host": "127.0.0.1", + "db_port": 6379, + "db_name": "10", + "db_user": "cgrates" + }, + "StorDB": { + "db_type": "mysql", + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "cgrates", + "db_user": "cgrates", + "db_password": "CGRateS.org" + } + }, + "items": { + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + } +}, + +"rates": { + "enabled": true, + "prefix_indexed_fields": ["*req.Destination"], + "exists_indexed_fields": ["*req.Destination"], + "rate_prefix_indexed_fields": ["*req.Destination"] +}, + +"admins": { + "enabled": true +} + +} \ No newline at end of file diff --git a/data/storage/mysql/create_db_tables.sql b/data/storage/mysql/create_db_tables.sql index b0b1b67bb..903f6b216 100644 --- a/data/storage/mysql/create_db_tables.sql +++ b/data/storage/mysql/create_db_tables.sql @@ -154,4 +154,26 @@ CREATE TABLE route_profiles ( PRIMARY KEY (`pk`), UNIQUE KEY unique_tenant_id (`tenant`, `id`) ); -CREATE UNIQUE INDEX route_profiles_idx ON route_profiles (`id`); \ No newline at end of file +CREATE UNIQUE INDEX route_profiles_idx ON route_profiles (`id`); + +DROP TABLE IF EXISTS rates; +DROP TABLE IF EXISTS rate_profiles; +CREATE TABLE rate_profiles ( + `pk` int(11) NOT NULL AUTO_INCREMENT, + `tenant` VARCHAR(40) NOT NULL, + `id` VARCHAR(64) NOT NULL, + `rate_profile` JSON NOT NULL, + PRIMARY KEY (`pk`), + UNIQUE KEY unique_tenant_id (`tenant`, `id`) +); +CREATE UNIQUE INDEX rate_profiles_idx ON rate_profiles (`id`); +CREATE TABLE rates ( + `pk` int(11) NOT NULL AUTO_INCREMENT, + `tenant` VARCHAR(40) NOT NULL, + `id` VARCHAR(64) NOT NULL, + `rate` JSON NOT NULL, + `rate_profile_id` VARCHAR(64) NOT NULL, + PRIMARY KEY (`pk`), + UNIQUE KEY unique_tenant_id_rate_profile_id (`tenant`, `id`, `rate_profile_id`), + FOREIGN KEY (rate_profile_id) REFERENCES rate_profiles (id) +); \ No newline at end of file diff --git a/data/storage/postgres/create_db_tables.sql b/data/storage/postgres/create_db_tables.sql index a94ed2e1b..1f11c66b0 100644 --- a/data/storage/postgres/create_db_tables.sql +++ b/data/storage/postgres/create_db_tables.sql @@ -152,3 +152,24 @@ CREATE TABLE route_profiles ( UNIQUE (tenant, id) ); CREATE UNIQUE INDEX route_profiles_idx ON route_profiles ("id"); + + +DROP TABLE IF EXISTS rates; +DROP TABLE IF EXISTS rate_profiles; +CREATE TABLE rate_profiles ( + pk SERIAL PRIMARY KEY, + tenant VARCHAR(40) NOT NULL, + id VARCHAR(64) NOT NULL, + rate_profile JSONB NOT NULL, + UNIQUE (tenant, id) +); +CREATE UNIQUE INDEX rate_profiles_idx ON rate_profiles ("id"); +CREATE TABLE rates ( + pk SERIAL PRIMARY KEY, + tenant VARCHAR(40) NOT NULL, + id VARCHAR(64) NOT NULL, + rate JSONB NOT NULL, + rate_profile_id VARCHAR(64) NOT NULL, + UNIQUE (tenant, id, rate_profile_id), + FOREIGN KEY (rate_profile_id) REFERENCES rate_profiles (id) +); \ No newline at end of file diff --git a/engine/models.go b/engine/models.go index 72315038d..a1feaed20 100644 --- a/engine/models.go +++ b/engine/models.go @@ -536,3 +536,27 @@ type RouteProfileMdl struct { func (RouteProfileMdl) TableName() string { return utils.TBLRouteProfiles } + +// Doesnt include Rates in RateProfile json, Rates taken from Rate using foreign keys +type RateProfileJSONMdl struct { + PK uint `gorm:"primary_key"` + Tenant string `index:"0" re:".*"` + ID string `index:"1" re:".*"` + RateProfile utils.JSONB `gorm:"type:jsonb" index:"2" re:".*"` +} + +func (RateProfileJSONMdl) TableName() string { + return utils.TBLRateProfiles +} + +type RateMdl struct { + PK uint `gorm:"primary_key"` + Tenant string `index:"0" re:".*"` + ID string `index:"1" re:".*"` + Rate utils.JSONB `gorm:"type:jsonb" index:"2" re:".*"` + RateProfileID string `gorm:"foreign_key" index:"3" re:".*"` +} + +func (RateMdl) TableName() string { + return utils.TBLRates +} diff --git a/engine/storage_sql.go b/engine/storage_sql.go index cc9573917..904bd1feb 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -127,6 +127,8 @@ func (sqls *SQLStorage) GetKeysForPrefix(ctx *context.Context, prefix string) (k keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLFilters, tntID) case utils.RouteProfilePrefix: keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLRouteProfiles, tntID) + case utils.RateProfilePrefix: + keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLRateProfiles, tntID) default: err = fmt.Errorf("unsupported prefix in GetKeysForPrefix: %q", prefix) } @@ -1040,6 +1042,172 @@ func (sqls *SQLStorage) RemoveRouteProfileDrv(ctx *context.Context, tenant, id s return } +func (sqls *SQLStorage) SetRateProfileDrv(ctx *context.Context, rpp *utils.RateProfile, optOverwrite bool) (err error) { + tx := sqls.db.Begin() + rpMdl := &RateProfileJSONMdl{ + Tenant: rpp.Tenant, + ID: rpp.ID, + RateProfile: rpp.AsMapStringInterface(), + } + if optOverwrite { + if err = tx.Model(&RateMdl{}).Where(&RateMdl{Tenant: rpMdl.Tenant, RateProfileID: rpMdl.ID}). + Delete(&RateMdl{}).Error; err != nil { + tx.Rollback() + return err + } + if err = tx.Model(&RateProfileJSONMdl{}).Where( + RateProfileJSONMdl{Tenant: rpMdl.Tenant, ID: rpMdl.ID}).Delete( + RateProfileJSONMdl{}).Error; err != nil { + tx.Rollback() + return + } + } + var existingRP RateProfileJSONMdl + result := tx.Where(RateProfileJSONMdl{Tenant: rpMdl.Tenant, ID: rpMdl.ID}).First(&existingRP) + switch result.Error { + case nil: // Record exists, update it + rpMdl.PK = existingRP.PK + if err = tx.Save(rpMdl).Error; err != nil { + tx.Rollback() + return + } + case gorm.ErrRecordNotFound: // Record doesn't exist, create it + if err = tx.Create(rpMdl).Error; err != nil { + tx.Rollback() + return + } + default: + tx.Rollback() + return result.Error + } + for rID, rate := range rpp.Rates { + rMdl := &RateMdl{ + Tenant: rpp.Tenant, + ID: rID, + Rate: rate.AsMapStringInterface(), + RateProfileID: rpp.ID, + } + if optOverwrite { + if err = tx.Model(&RateMdl{}).Where( + RateMdl{Tenant: rMdl.Tenant, ID: rMdl.ID}).Delete( + RateMdl{}).Error; err != nil { + tx.Rollback() + return + } + } + var existingRT RateMdl + result := tx.Where(RateMdl{Tenant: rMdl.Tenant, ID: rMdl.ID, RateProfileID: rpMdl.ID}).First(&existingRT) + switch result.Error { + case nil: // Record exists, update it + rMdl.PK = existingRT.PK + if err = tx.Save(rMdl).Error; err != nil { + tx.Rollback() + return + } + case gorm.ErrRecordNotFound: // Record doesn't exist, create it + if err = tx.Create(rMdl).Error; err != nil { + tx.Rollback() + return + } + default: + tx.Rollback() + return result.Error + } + } + tx.Commit() + return +} + +func (sqls *SQLStorage) GetRateProfileDrv(ctx *context.Context, tenant, id string) (rpp *utils.RateProfile, err error) { + var rpResult []*RateProfileJSONMdl + if err = sqls.db.Model(&RateProfileJSONMdl{}).Where(&RateProfileJSONMdl{Tenant: tenant, + ID: id}).Find(&rpResult).Error; err != nil { + return nil, err + } + if len(rpResult) == 0 { + return nil, utils.ErrNotFound + } + + if rpp, err = utils.MapStringInterfaceToRateProfile(rpResult[0].RateProfile); err != nil { + return nil, err + } + + var rtResult []*RateMdl + if err = sqls.db.Model(&RateMdl{}).Where(&RateMdl{Tenant: tenant, + RateProfileID: id}).Find(&rtResult).Error; err != nil { // find all rates for that rating profile + return nil, err + } + if len(rtResult) == 0 { + return nil, utils.ErrNotFound + } + for _, rateMdl := range rtResult { + if rt, err := utils.MapStringInterfaceToRate(rateMdl.Rate); err != nil { + return nil, err + } else { + rpp.Rates[rt.ID] = rt + } + } + return +} + +// GetRateProfileRatesDrv will return back all the RateIDs and Rates from a RateProfile +func (sqls *SQLStorage) GetRateProfileRatesDrv(ctx *context.Context, tnt, profileID, rtPrfx string, needIDs bool) (rateIDs []string, rates []*utils.Rate, err error) { + tx := sqls.db.Model(&RateMdl{}).Where(&RateMdl{RateProfileID: profileID}) + if rtPrfx != utils.EmptyString { + tx = tx.Where("id LIKE ?", rtPrfx+"%") + } + var rtResult []*RateMdl + if err = tx.Find(&rtResult).Error; err != nil { + return nil, nil, err + } + if len(rtResult) == 0 { + return nil, nil, utils.ErrNotFound + } + for _, ratesMdl := range rtResult { + rateIDs = append(rateIDs, ratesMdl.ID) + } + if needIDs { + // Only return IDs + return rateIDs, nil, nil + } + for _, rateMdl := range rtResult { + if rt, err := utils.MapStringInterfaceToRate(rateMdl.Rate); err != nil { + return nil, nil, err + } else { + rates = append(rates, rt) + } + } + return +} + +func (sqls *SQLStorage) RemoveRateProfileDrv(ctx *context.Context, tenant, id string, rateIDs *[]string) (err error) { + tx := sqls.db.Begin() + if rateIDs != nil { + for _, rateID := range *rateIDs { + if err = tx.Model(&RateMdl{}).Where(&RateMdl{Tenant: tenant, ID: rateID, RateProfileID: id}). + Delete(&RateMdl{}).Error; err != nil { + tx.Rollback() + return err + } + } + tx.Commit() + return + } + + if err = tx.Model(&RateMdl{}).Where(&RateMdl{Tenant: tenant, RateProfileID: id}). + Delete(&RateMdl{}).Error; err != nil { + tx.Rollback() + return err + } + if err = tx.Model(&RateProfileJSONMdl{}).Where(&RateProfileJSONMdl{Tenant: tenant, ID: id}). + Delete(&RateProfileJSONMdl{}).Error; err != nil { + tx.Rollback() + return err + } + tx.Commit() + return +} + // Used to check if specific subject is stored using prefix key attached to entity func (sqls *SQLStorage) HasDataDrv(ctx *context.Context, category, subject, tenant string) (has bool, err error) { var categoryModelMap = map[string]any{ @@ -1057,9 +1225,9 @@ func (sqls *SQLStorage) HasDataDrv(ctx *context.Context, category, subject, tena utils.RouteProfilePrefix: &RouteProfileMdl{}, utils.AttributeProfilePrefix: &AttributeProfileMdl{}, utils.ChargerProfilePrefix: &ChargerProfileMdl{}, + utils.RateProfilePrefix: &RateProfileJSONMdl{}, // utils.TrendPrefix: &TrendJSONMdl{}, // utils.TrendProfilePrefix: &TrendProfileMdl{}, - // utils.RateProfilePrefix: &RateProfileJSONMdl{}, } model, ok := categoryModelMap[category] if !ok { @@ -1189,26 +1357,6 @@ func (sqls *SQLStorage) RemoveLoadIDsDrv() (err error) { return utils.ErrNotImplemented } -// DataDB method not implemented yet -func (sqls *SQLStorage) SetRateProfileDrv(ctx *context.Context, rpp *utils.RateProfile, optOverwrite bool) (err error) { - return utils.ErrNotImplemented -} - -// DataDB method not implemented yet -func (sqls *SQLStorage) GetRateProfileDrv(ctx *context.Context, tenant, id string) (rpp *utils.RateProfile, err error) { - return nil, utils.ErrNotImplemented -} - -// GetRateProfileRateIDsDrv DataDB method not implemented yet -func (sqls *SQLStorage) GetRateProfileRatesDrv(ctx *context.Context, tnt, profileID, rtPrfx string, needIDs bool) (rateIDs []string, rates []*utils.Rate, err error) { - return nil, nil, utils.ErrNotImplemented -} - -// DataDB method not implemented yet -func (sqls *SQLStorage) RemoveRateProfileDrv(ctx *context.Context, tenant, id string, rateIDs *[]string) (err error) { - return utils.ErrNotImplemented -} - // GetIndexesDrv DataDB method not implemented yet func (sqls *SQLStorage) GetIndexesDrv(ctx *context.Context, idxItmType, tntCtx, idxKey, transactionID string) (indexes map[string]utils.StringSet, err error) { return nil, utils.ErrNotImplemented diff --git a/utils/consts.go b/utils/consts.go index 434802b94..23a0bc874 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1926,6 +1926,8 @@ const ( TBLThresholds = "thresholds" TBLFilters = "filters" TBLRouteProfiles = "route_profiles" + TBLRateProfiles = "rate_profiles" + TBLRates = "rates" OldSMCosts = "sm_costs" TBLTPDispatchers = "tp_dispatcher_profiles" TBLTPDispatcherHosts = "tp_dispatcher_hosts" diff --git a/utils/librates.go b/utils/librates.go index 59f1ef6ea..b35b6b996 100644 --- a/utils/librates.go +++ b/utils/librates.go @@ -1026,6 +1026,42 @@ func (rt *Rate) FieldAsInterface(fldPath []string) (_ any, err error) { } } +// AsMapStringInterface converts Rate struct to map[string]any +func (rt *Rate) AsMapStringInterface() map[string]any { + if rt == nil { + return nil + } + return map[string]any{ + ID: rt.ID, + FilterIDs: rt.FilterIDs, + ActivationTimes: rt.ActivationTimes, + Weights: rt.Weights, + Blocker: rt.Blocker, + IntervalRates: rt.IntervalRates, + } +} + +// MapStringInterfaceToRate converts map[string]any to Rate struct +func MapStringInterfaceToRate(m map[string]any) (*Rate, error) { + rt := &Rate{} + if v, ok := m[ID].(string); ok { + rt.ID = v + } + rt.FilterIDs = InterfaceToStringSlice(m[FilterIDs]) + if v, ok := m[ActivationTimes].(string); ok { + rt.ActivationTimes = v + } + rt.Weights = InterfaceToDynamicWeights(m[Weights]) + if v, ok := m[Blocker].(bool); ok { + rt.Blocker = v + } + var err error + if rt.IntervalRates, err = InterfaceToIntervalRates(m[IntervalRates]); err != nil { + return nil, err + } + return rt, nil +} + func (iR *IntervalRate) String() string { return ToJSON(iR) } func (iR *IntervalRate) FieldAsString(fldPath []string) (_ string, err error) { var val any @@ -1054,6 +1090,42 @@ func (iR *IntervalRate) FieldAsInterface(fldPath []string) (_ any, err error) { } } +// InterfaceToIntervalRates converts any to []*IntervalRate +func InterfaceToIntervalRates(v any) (intervalRates []*IntervalRate, err error) { + if v == nil { + return + } + switch val := v.(type) { + case []*IntervalRate: + return val, nil + case []any: + result := make([]*IntervalRate, 0, len(val)) + for _, item := range val { + if irMap, ok := item.(map[string]any); ok { + ir := new(IntervalRate) + if ir.IntervalStart, err = NewDecimalFromInterface(irMap[IntervalStart]); err != nil { + return nil, err + } + if ir.FixedFee, err = NewDecimalFromInterface(irMap[FixedFee]); err != nil { + return nil, err + } + if ir.RecurrentFee, err = NewDecimalFromInterface(irMap[RecurrentFee]); err != nil { + return nil, err + } + if ir.Unit, err = NewDecimalFromInterface(irMap[Unit]); err != nil { + return nil, err + } + if ir.Increment, err = NewDecimalFromInterface(irMap[Increment]); err != nil { + return nil, err + } + result = append(result, ir) + } + } + return result, nil + } + return +} + // AsDataDBMap is used to is a convert method in order to properly set trough a hasmap in redis server our rate profile func (rp *RateProfile) AsDataDBMap(ms Marshaler) (mp map[string]any, err error) { mp = map[string]any{ @@ -1089,6 +1161,51 @@ func (rp *RateProfile) AsDataDBMap(ms Marshaler) (mp map[string]any, err error) return mp, nil } +// AsMapStringInterface converts RateProfile struct to map[string]any +// +// ! Rates not included ! +func (rp *RateProfile) AsMapStringInterface() map[string]any { + if rp == nil { + return nil + } + return map[string]any{ + Tenant: rp.Tenant, + ID: rp.ID, + FilterIDs: rp.FilterIDs, + Weights: rp.Weights, + MinCost: rp.MinCost, + MaxCost: rp.MaxCost, + MaxCostStrategy: rp.MaxCostStrategy, + } +} + +// MapStringInterfaceToRateProfile converts map[string]any to RateProfile struct +// +// ! Rates not included ! +func MapStringInterfaceToRateProfile(m map[string]any) (*RateProfile, error) { + rp := &RateProfile{} + if v, ok := m[Tenant].(string); ok { + rp.Tenant = v + } + if v, ok := m[ID].(string); ok { + rp.ID = v + } + rp.FilterIDs = InterfaceToStringSlice(m[FilterIDs]) + rp.Weights = InterfaceToDynamicWeights(m[Weights]) + var err error + if rp.MinCost, err = NewDecimalFromInterface(m[MinCost]); err != nil { + return nil, err + } + if rp.MaxCost, err = NewDecimalFromInterface(m[MaxCost]); err != nil { + return nil, err + } + if v, ok := m[MaxCostStrategy].(string); ok { + rp.MaxCostStrategy = v + } + rp.Rates = make(map[string]*Rate) + return rp, nil +} + // NewRateProfileFromMapDataDBMap will convert a RateProfile map into a RatePRofile struct. This is used when we get the map from redis database func NewRateProfileFromMapDataDBMap(tnt, id string, mapRP map[string]any, ms Marshaler) (rp *RateProfile, err error) { rp = &RateProfile{