diff --git a/apier/v1/costs.go b/apier/v1/costs.go index f88c98da7..0351b5b6e 100644 --- a/apier/v1/costs.go +++ b/apier/v1/costs.go @@ -115,3 +115,23 @@ func (apierSv1 *APIerSv1) GetDataCost(ctx *context.Context, attrs *AttrGetDataCo } return nil } + +// GetAccountCost returns a simulated cost of an account without debiting from it (dryrun) +func (apierSv1 *APIerSv1) GetAccountCost(ctx *context.Context, args *utils.CGREvent, ec *engine.EventCost) (err error) { + cd, err := engine.NewCallDescriptorFromCGREvent(args, apierSv1.Config.GeneralCfg().DefaultTimezone) + if err != nil { + return + } + args.APIOpts[utils.MetaRALsDryRun] = true + var cc engine.CallCost + if err := apierSv1.Responder.Debit(context.Background(), + &engine.CallDescriptorWithAPIOpts{ + CallDescriptor: cd, + APIOpts: args.APIOpts, + }, &cc); err != nil { + return utils.NewErrServerError(err) + } + *ec = *engine.NewEventCostFromCallCost(&cc, cd.CgrID, cd.RunID) + ec.Compute() + return nil +} diff --git a/config/config_test.go b/config/config_test.go index d88247d26..e4d182b69 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2329,19 +2329,21 @@ func TestERSConfig(t *testing.T) { SessionSConns: []string{"*internal:*sessions"}, Readers: []*EventReaderCfg{ { - ID: utils.MetaDefault, - Type: utils.MetaNone, - RunDelay: 0, - ConcurrentReqs: 1024, - SourcePath: "/var/spool/cgrates/ers/in", - ProcessedPath: "/var/spool/cgrates/ers/out", - Tenant: nil, - Timezone: utils.EmptyString, - Filters: []string{}, - Flags: utils.FlagsWithParams{}, - Fields: nil, - CacheDumpFields: make([]*FCTemplate, 0), - PartialCommitFields: make([]*FCTemplate, 0), + ID: utils.MetaDefault, + Type: utils.MetaNone, + RunDelay: 0, + ConcurrentReqs: 1024, + SourcePath: "/var/spool/cgrates/ers/in", + ProcessedPath: "/var/spool/cgrates/ers/out", + Tenant: nil, + Timezone: utils.EmptyString, + Filters: []string{}, + Flags: utils.FlagsWithParams{}, + Fields: nil, + CacheDumpFields: make([]*FCTemplate, 0), + PartialCommitFields: make([]*FCTemplate, 0), + Reconnects: -1, + MaxReconnectInterval: 300000000000, Opts: &EventReaderOpts{ CSV: &CSVROpts{ FieldSeparator: utils.StringPointer(utils.FieldsSep), @@ -5356,16 +5358,18 @@ func TestCgrCdfEventReader(t *testing.T) { SessionSConns: []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaSessionS)}, Readers: []*EventReaderCfg{ { - ID: utils.MetaDefault, - Type: utils.MetaNone, - RunDelay: 0, - ConcurrentReqs: 1024, - SourcePath: "/var/spool/cgrates/ers/in", - ProcessedPath: "/var/spool/cgrates/ers/out", - Tenant: nil, - Timezone: utils.EmptyString, - Filters: []string{}, - Flags: utils.FlagsWithParams{}, + ID: utils.MetaDefault, + Type: utils.MetaNone, + RunDelay: 0, + ConcurrentReqs: 1024, + SourcePath: "/var/spool/cgrates/ers/in", + ProcessedPath: "/var/spool/cgrates/ers/out", + Tenant: nil, + Timezone: utils.EmptyString, + Filters: []string{}, + Flags: utils.FlagsWithParams{}, + Reconnects: -1, + MaxReconnectInterval: 300000000000, Fields: []*FCTemplate{ {Tag: utils.ToR, Path: utils.MetaCgreq + utils.NestingSep + utils.ToR, Type: utils.MetaVariable, Value: NewRSRParsersMustCompile("~*req.2", utils.InfieldSep), Mandatory: true, Layout: time.RFC3339}, @@ -5467,16 +5471,18 @@ func TestCgrCdfEventExporter(t *testing.T) { func TestCgrCfgEventReaderDefault(t *testing.T) { eCfg := &EventReaderCfg{ - ID: utils.MetaDefault, - Type: utils.MetaNone, - RunDelay: 0, - ConcurrentReqs: 1024, - SourcePath: "/var/spool/cgrates/ers/in", - ProcessedPath: "/var/spool/cgrates/ers/out", - Tenant: nil, - Timezone: utils.EmptyString, - Filters: []string{}, - Flags: utils.FlagsWithParams{}, + ID: utils.MetaDefault, + Type: utils.MetaNone, + RunDelay: 0, + ConcurrentReqs: 1024, + SourcePath: "/var/spool/cgrates/ers/in", + ProcessedPath: "/var/spool/cgrates/ers/out", + Tenant: nil, + Timezone: utils.EmptyString, + Filters: []string{}, + Flags: utils.FlagsWithParams{}, + Reconnects: -1, + MaxReconnectInterval: 300000000000, Fields: []*FCTemplate{ {Tag: utils.ToR, Path: utils.MetaCgreq + utils.NestingSep + utils.ToR, Type: utils.MetaVariable, Value: NewRSRParsersMustCompile("~*req.2", utils.InfieldSep), Mandatory: true, Layout: time.RFC3339}, diff --git a/general_tests/cdrs_processevent_dr_it_test.go b/general_tests/get_account_cost_it_test.go similarity index 79% rename from general_tests/cdrs_processevent_dr_it_test.go rename to general_tests/get_account_cost_it_test.go index c982b134d..429479696 100644 --- a/general_tests/cdrs_processevent_dr_it_test.go +++ b/general_tests/get_account_cost_it_test.go @@ -26,12 +26,15 @@ import ( "testing" "time" + "github.com/cgrates/birpc" "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" ) -func TestCDRsProcessEventDryrun(t *testing.T) { +var aSummaryBefore *engine.AccountSummary + +func TestGetAccountCost(t *testing.T) { cfgPath := path.Join(*dataDir, "conf", "samples", "rerate_cdrs_mysql") tpPath := path.Join(*dataDir, "tariffplans", "reratecdrs") client, _, shutdown, err := setupTest(t, "TestRerateCDRs", cfgPath, tpPath, utils.EmptyString, nil) @@ -42,8 +45,6 @@ func TestCDRsProcessEventDryrun(t *testing.T) { CGRID := utils.GenUUID() - var aSummaryBefore *engine.AccountSummary - t.Run("SetBalance", func(t *testing.T) { var reply string if err := client.Call(context.Background(), utils.APIerSv1SetBalances, @@ -129,6 +130,48 @@ func TestCDRsProcessEventDryrun(t *testing.T) { }) + t.Run("GetAccountCost", func(t *testing.T) { + var reply engine.EventCost + err := client.Call(context.Background(), utils.APIerSV1GetAccountCost, + &engine.ArgV1ProcessEvent{ + Flags: []string{utils.MetaRALs}, + CGREvent: utils.CGREvent{ + Tenant: "cgrates.org", + ID: "event1", + Event: map[string]any{ + utils.RunID: "run_1", + utils.CGRID: CGRID, + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.OriginID: "processCDR1", + utils.OriginHost: "OriginHost1", + utils.RequestType: utils.MetaPostpaid, + utils.AccountField: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2021, time.February, 2, 16, 14, 50, 0, time.UTC), + utils.AnswerTime: time.Date(2021, time.February, 2, 16, 15, 0, 0, time.UTC), + utils.Usage: 4 * time.Minute, + }, + APIOpts: map[string]any{ + utils.MetaRALsDryRun: true, + }, + }, + }, &reply) + if err != nil { + t.Fatal(err) + } + if *reply.Cost != 2.4 { + t.Errorf("expected cost to be <%+v>, received <%+v>", 2.4, *reply.Cost) + } + if *reply.Usage != 4*time.Minute { + t.Errorf("expected cost to be <%+v>, received <%+v>", 4*time.Minute, *reply.Usage) + } + + }) + + t.Run("CheckAccountBalancesAfterGetAccountCost", checkAccountBalances(client)) + t.Run("ProcessFirstCDR", func(t *testing.T) { var reply []*utils.EventWithFlags err := client.Call(context.Background(), utils.CDRsV2ProcessEvent, @@ -176,7 +219,13 @@ func TestCDRsProcessEventDryrun(t *testing.T) { } }) - t.Run("CheckAccountBalancesAfterFirstProcessCDR", func(t *testing.T) { + t.Run("CheckAccountBalancesAfterProcessCDR", checkAccountBalances(client)) + +} + +func checkAccountBalances(client *birpc.Client) func(t *testing.T) { + return func(t *testing.T) { + expAcnt := engine.Account{ ID: "cgrates.org:1001", BalanceMap: map[string]engine.Balances{ @@ -210,7 +259,5 @@ func TestCDRsProcessEventDryrun(t *testing.T) { if !reflect.DeepEqual(aSummaryBefore, aSummaryAfter) { t.Errorf("expected <%+v>, \nreceived <%+v>", utils.ToJSON(aSummaryBefore), utils.ToJSON(aSummaryAfter)) } - - }) - + } } diff --git a/go.mod b/go.mod index 5b1ccbbfa..53ad1b604 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/cgrates/fsock v0.0.0-20230123160954-12cae14030cc github.com/cgrates/kamevapi v0.0.0-20220525160402-5b8036487a6c github.com/cgrates/ltcache v0.0.0-20210405185848-da943e80c1ab - github.com/cgrates/radigo v0.0.0-20231010181109-b0e5b086459e + github.com/cgrates/radigo v0.0.0-20231107162145-dac6e5c377ea github.com/cgrates/rpcclient v0.0.0-20230605090759-8bb5188b73e5 github.com/cgrates/sipingo v1.0.1-0.20200514112313-699ebc1cdb8e github.com/cgrates/ugocodec v0.0.0-20201023092048-df93d0123f60 diff --git a/go.sum b/go.sum index 337ad4cf4..5c6397cef 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ github.com/cgrates/kamevapi v0.0.0-20220525160402-5b8036487a6c h1:ILTMiCcBw80hSe github.com/cgrates/kamevapi v0.0.0-20220525160402-5b8036487a6c/go.mod h1:R1iZadqJTrjkwWxhK8gVPcYhcWoE4d0A6HZ+y6ZHzys= github.com/cgrates/ltcache v0.0.0-20210405185848-da943e80c1ab h1:dKdAUwrij6vYwewe1WV1+pDSagqGI5JLqjTZZyN2ANo= github.com/cgrates/ltcache v0.0.0-20210405185848-da943e80c1ab/go.mod h1:9oSG/6gUoab/vKm/eQ3QcX6KeTR0wRw88N33iCnC/k4= -github.com/cgrates/radigo v0.0.0-20231010181109-b0e5b086459e h1:ZlZUnZwtho03GuD0pnr1yodrfCo7JfwTNEjtWcjv7tw= -github.com/cgrates/radigo v0.0.0-20231010181109-b0e5b086459e/go.mod h1:PizDxlLTjVQpyPU0ksWYfmM9UbYGu7q6at0nzuiZprI= +github.com/cgrates/radigo v0.0.0-20231107162145-dac6e5c377ea h1:P5yYTIwL3MXE70+bJECW720DMEOEK1RwK6eSxZNo0BY= +github.com/cgrates/radigo v0.0.0-20231107162145-dac6e5c377ea/go.mod h1:PizDxlLTjVQpyPU0ksWYfmM9UbYGu7q6at0nzuiZprI= github.com/cgrates/rpcclient v0.0.0-20230605090759-8bb5188b73e5 h1:GhA5qBUK7o0j+7fi1GACKnT454pv/LfCjoI52vFIz3E= github.com/cgrates/rpcclient v0.0.0-20230605090759-8bb5188b73e5/go.mod h1:tDqS6BieViKYpz696//gxseUN1b92hPHqk+w0CzY8AE= github.com/cgrates/sipingo v1.0.1-0.20200514112313-699ebc1cdb8e h1:izFjZB83/XRXInc+gMIssUxdbleGsGIuGCPj2u7RQo0= diff --git a/utils/consts.go b/utils/consts.go index af0d62c52..2ee7649da 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1455,6 +1455,7 @@ const ( APIerSv1GetTiming = "APIerSv1.GetTiming" APIerSv1SetTiming = "APIerSv1.SetTiming" APIerSv1RemoveTiming = "APIerSv1.RemoveTiming" + APIerSV1GetAccountCost = "APIerSv1.GetAccountCost" ) // APIerSv1 TP APIs