From 326939314157c1a664bbf1728ff43efbc2d06b9c Mon Sep 17 00:00:00 2001 From: ionutboangiu Date: Fri, 1 Mar 2024 12:24:47 -0500 Subject: [PATCH] Retrieve BalanceFactorID from Event It will be added to CallDescriptor in ExtraFields. Ensure CDR ExtraFields are passed to CallDescriptor before sending it to RALs. Ensure Clone function of CallDescriptor also clones the ExtraFields map. --- engine/account_test.go | 6 + engine/balances.go | 7 +- engine/calldesc.go | 10 +- engine/cdrs.go | 1 + general_tests/balance_it_test.go | 278 ++++++++++++++++++++++++++++--- utils/consts.go | 1 + 6 files changed, 278 insertions(+), 25 deletions(-) diff --git a/engine/account_test.go b/engine/account_test.go index 2fc418e7d..2dd31acd6 100644 --- a/engine/account_test.go +++ b/engine/account_test.go @@ -1729,6 +1729,9 @@ func TestDebitGenericBalance(t *testing.T) { ToR: cc.ToR, DurationIndex: cc.GetDuration(), testCallcost: cc, + ExtraFields: map[string]string{ + utils.BalanceFactorID: "call", + }, } rifsBalance := &Account{ ID: "other", BalanceMap: map[string]Balances{ @@ -1783,6 +1786,9 @@ func TestDebitGenericBalanceWithRatingSubject(t *testing.T) { ToR: cc.ToR, DurationIndex: cc.GetDuration(), testCallcost: cc, + ExtraFields: map[string]string{ + utils.BalanceFactorID: "call", + }, } rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ diff --git a/engine/balances.go b/engine/balances.go index 0ee06d8f4..1fd1b9e17 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -878,7 +878,8 @@ func (b *Balance) debit(cd *CallDescriptor, ub *Account, moneyBalances Balances, //log.Printf("INCREMENET: %+v", inc) amount := float64(inc.Duration) if b.Factor != nil { - amount = utils.Round(amount*b.Factor.GetValue(cd.Category), + amount = utils.Round( + amount*b.Factor.GetValue(cd.ExtraFields[utils.BalanceFactorID]), globalRoundingDecimals, utils.MetaRoundingUp) } if b.GetValue() >= amount { @@ -1022,7 +1023,9 @@ func (b *Balance) debit(cd *CallDescriptor, ub *Account, moneyBalances Balances, var moneyBal *Balance if isUnitBal { if b.Factor != nil { - amount = utils.Round(amount*b.Factor.GetValue(cd.Category), globalRoundingDecimals, utils.MetaRoundingUp) + amount = utils.Round( + amount*b.Factor.GetValue(cd.ExtraFields[utils.BalanceFactorID]), + globalRoundingDecimals, utils.MetaRoundingUp) } for _, mb := range moneyBalances { if mb.GetValue() >= cost { diff --git a/engine/calldesc.go b/engine/calldesc.go index 442ab75a5..b09997cd1 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -980,7 +980,7 @@ func (cd *CallDescriptor) CreateCallCost() *CallCost { } func (cd *CallDescriptor) Clone() *CallDescriptor { - return &CallDescriptor{ + cln := &CallDescriptor{ Category: cd.Category, Tenant: cd.Tenant, Subject: cd.Subject, @@ -1000,7 +1000,13 @@ func (cd *CallDescriptor) Clone() *CallDescriptor { CgrID: cd.CgrID, RunID: cd.RunID, } - + if cd.ExtraFields != nil { + cln.ExtraFields = make(map[string]string, len(cd.ExtraFields)) + for key, value := range cd.ExtraFields { + cln.ExtraFields[key] = value + } + } + return cln } // AccountSummary returns the AccountSummary for cached account diff --git a/engine/cdrs.go b/engine/cdrs.go index 02e2d00de..978923977 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -272,6 +272,7 @@ func (cdrS *CDRServer) getCostFromRater(cdr *CDRWithAPIOpts) (*CallCost, error) Subject: cdr.Subject, Account: cdr.Account, Destination: cdr.Destination, + ExtraFields: cdr.ExtraFields, TimeStart: timeStart, TimeEnd: timeStart.Add(cdr.Usage), DurationIndex: cdr.Usage, diff --git a/general_tests/balance_it_test.go b/general_tests/balance_it_test.go index 2a1d1e210..522341ad7 100644 --- a/general_tests/balance_it_test.go +++ b/general_tests/balance_it_test.go @@ -26,6 +26,7 @@ import ( "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/sessions" "github.com/cgrates/cgrates/utils" ) @@ -259,6 +260,9 @@ cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY,`, // per second. // 2. Process an 3 usage (representing 12 sms, when taking into consideration the balance factor) event. // 3. Ensure that the *sms balance has 2 units left (10 - (2 sms * 4 factor)) and that 1 unit was subtracted from the *monetary balance. +// 4. Do the above steps also for SessionSv1.ProcessCDR. +// 5. Initiate a prepaid session (usage 10s), update it twice (usages 5s and 2s), terminate, and process CDR. +// 6. Check to see if balance_voice was debitted 34s ((10s+5s+2s) * voiceFactor, where voiceFactor is 2). func TestBalanceFactor(t *testing.T) { switch *dbType { case utils.MetaInternal: @@ -298,6 +302,17 @@ func TestBalanceFactor(t *testing.T) { "apiers": { "enabled": true, "scheduler_conns": ["*internal"] +}, + +"sessions": { + "enabled": true, + "cdrs_conns": ["*internal"], + "chargers_conns": ["*internal"], + "rals_conns": ["*internal"] +}, + +"chargers": { + "enabled": true } }` @@ -308,8 +323,12 @@ cgrates.org,1001,PACKAGE_1001,,,`, utils.ActionPlansCsv: `#Id,ActionsId,TimingId,Weight PACKAGE_1001,ACT_TOPUP,*asap,10`, utils.ActionsCsv: `#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] -ACT_TOPUP,*topup_reset,"{""sms"":4}",,balance_sms,*sms,,,,,*unlimited,,10,20,false,false,20 +ACT_TOPUP,*topup_reset,"{""smsFactor"":4}",,balance_sms,*sms,,,,,*unlimited,,10,20,false,false,20 +ACT_TOPUP,*topup_reset,"{""voiceFactor"":2}",,balance_voice,*voice,call,,,,*unlimited,,100s,20,false,false,20 ACT_TOPUP,*topup_reset,,,balance_monetary,*monetary,,*any,,,*unlimited,,5,10,false,false,20`, + utils.ChargersCsv: `#Id,ActionsId,TimingId,Weight +#Tenant,ID,FilterIDs,ActivationInterval,RunID,AttributeIDs,Weight +cgrates.org,DEFAULT,,,DEFAULT,*none,20`, utils.DestinationRatesCsv: `#Id,DestinationId,RatesTag,RoundingMethod,RoundingDecimals,MaxCost,MaxCostStrategy DR_ANY,*any,RT_ANY,*up,20,0,`, utils.RatesCsv: `#Id,ConnectFee,Rate,RateUnit,RateIncrement,GroupIntervalStart @@ -317,7 +336,8 @@ RT_ANY,0,1,1s,1s,0s`, utils.RatingPlansCsv: `#Id,DestinationRatesId,TimingTag,Weight RP_ANY,DR_ANY,*any,10`, utils.RatingProfilesCsv: `#Tenant,Category,Subject,ActivationTime,RatingPlanId,RatesFallbackSubject -cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY,`, +cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY, +cgrates.org,call,1001,2014-01-14T00:00:00Z,RP_ANY,`, } testEnv := TestEnvironment{ @@ -339,10 +359,11 @@ cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY,`, if err := client.Call(context.Background(), utils.APIerSv2GetAccount, attrs, &acnt); err != nil { t.Fatal(err) } - if len(acnt.BalanceMap) != 2 || + if len(acnt.BalanceMap) != 3 || len(acnt.BalanceMap[utils.MetaMonetary]) != 1 || - len(acnt.BalanceMap[utils.MetaSMS]) != 1 { - t.Fatalf("expected account to have one balance of type *monetary and one of type *sms, received %v", acnt) + len(acnt.BalanceMap[utils.MetaSMS]) != 1 || + len(acnt.BalanceMap[utils.MetaVoice]) != 1 { + t.Fatalf("expected account to have one balance of type *monetary, one of type *sms and one of type *voice, received %v", acnt) } smsBalance := acnt.BalanceMap[utils.MetaSMS][0] if smsBalance.ID != "balance_sms" || smsBalance.Value != 10 { @@ -352,29 +373,34 @@ cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY,`, if monetaryBalance.ID != "balance_monetary" || monetaryBalance.Value != 5 { t.Fatalf("received account with unexpected *monetary balance: %v", monetaryBalance) } + voiceBalance := acnt.BalanceMap[utils.MetaVoice][0] + if voiceBalance.ID != "balance_voice" || voiceBalance.Value != float64(100*time.Second) { + t.Fatalf("received account with unexpected *voice balance: %v", voiceBalance) + } }) - t.Run("ProcessCDRAndCheckBalance", func(t *testing.T) { + t.Run("CDRsV1ProcessCDR", func(t *testing.T) { var reply string if err := client.Call(context.Background(), utils.CDRsV1ProcessEvent, &engine.ArgV1ProcessEvent{ Flags: []string{utils.MetaRALs}, CGREvent: utils.CGREvent{ Tenant: "cgrates.org", - ID: "event1", + ID: "CDRsV1ProcessCDR", Event: map[string]any{ - utils.RunID: "*default", - utils.Tenant: "cgrates.org", - utils.Category: "sms", - utils.ToR: utils.MetaSMS, - utils.OriginID: "processCDR", - utils.OriginHost: "127.0.0.1", - 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: 3, + utils.RunID: "*default", + utils.Tenant: "cgrates.org", + utils.Category: "sms", + utils.ToR: utils.MetaSMS, + utils.OriginID: "processCDR1", + utils.OriginHost: "127.0.0.1", + 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: 3, + utils.BalanceFactorID: "smsFactor", }, }, }, &reply); err != nil { @@ -383,7 +409,7 @@ cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY,`, var cdrs []*engine.CDR if err := client.Call(context.Background(), utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithAPIOpts{ RPCCDRsFilter: &utils.RPCCDRsFilter{ - RunIDs: []string{"*default"}, + OriginIDs: []string{"processCDR1"}, }}, &cdrs); err != nil { t.Fatal(err) } @@ -398,7 +424,7 @@ cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY,`, if smsBalanceValue != 2. { t.Errorf("unexpected balance value: expected %v, received %v", 2., smsBalanceValue) } - monetaryBalanceValue, err := cdrs[0].CostDetails.FieldAsInterface([]string{"AccountSummary", "BalanceSummaries[1]", "Value"}) + monetaryBalanceValue, err := cdrs[0].CostDetails.FieldAsInterface([]string{"AccountSummary", "BalanceSummaries[2]", "Value"}) if err != nil { t.Fatalf("could not retrieve *sms balance current value: %v", err) } @@ -406,6 +432,216 @@ cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY,`, t.Errorf("unexpected balance value: expected %v, received %v", 4., monetaryBalanceValue) } }) + + t.Run("SessionSv1ProcessCDR", func(t *testing.T) { + var reply string + if err := client.Call(context.Background(), utils.SessionSv1ProcessCDR, + &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "SessionSv1ProcessCDR", + Event: map[string]any{ + utils.RunID: "*default", + utils.Tenant: "cgrates.org", + utils.Category: "sms", + utils.ToR: utils.MetaSMS, + utils.OriginID: "processCDR2", + utils.OriginHost: "127.0.0.1", + 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: 3, + utils.BalanceFactorID: "smsFactor", + }, + }, &reply); err != nil { + t.Fatal(err) + } + var cdrs []*engine.CDR + if err := client.Call(context.Background(), utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithAPIOpts{ + RPCCDRsFilter: &utils.RPCCDRsFilter{ + OriginIDs: []string{"processCDR2"}, + }}, &cdrs); err != nil { + t.Fatal(err) + } + + if len(cdrs) != 1 { + t.Fatalf("expected to receive only one CDR: %v", utils.ToJSON(cdrs)) + } + smsBalanceValue, err := cdrs[0].CostDetails.FieldAsInterface([]string{"AccountSummary", "BalanceSummaries[0]", "Value"}) + if err != nil { + t.Fatalf("could not retrieve *sms balance current value: %v", err) + } + if smsBalanceValue != 2. { + t.Errorf("unexpected balance value: expected %v, received %v", 2., smsBalanceValue) + } + monetaryBalanceValue, err := cdrs[0].CostDetails.FieldAsInterface([]string{"AccountSummary", "BalanceSummaries[2]", "Value"}) + if err != nil { + t.Fatalf("could not retrieve *sms balance current value: %v", err) + } + if monetaryBalanceValue != 3. { + t.Errorf("unexpected balance value: expected %v, received %v", 4., monetaryBalanceValue) + } + }) + + t.Run("PrepaidSession", func(t *testing.T) { + var replyInit sessions.V1InitSessionReply + if err := client.Call(context.Background(), utils.SessionSv1InitiateSession, + &sessions.V1InitSessionArgs{ + InitSession: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "SessionSv1InitiateSession", + Event: map[string]any{ + utils.OriginID: "prepaidSession", + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2023, time.February, 28, 8, 59, 50, 0, time.UTC), + utils.AnswerTime: time.Date(2023, time.February, 28, 9, 0, 0, 0, time.UTC), + utils.Usage: 10 * time.Second, + utils.BalanceFactorID: "voiceFactor", + }, + APIOpts: map[string]any{ + utils.OptsDebitInterval: 0, + }, + }, + }, &replyInit); err != nil { + t.Error(err) + } + + var replyUpdate sessions.V1UpdateSessionReply + if err := client.Call(context.Background(), utils.SessionSv1UpdateSession, + &sessions.V1UpdateSessionArgs{ + UpdateSession: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "SessionSv1UpdateSession1", + Event: map[string]any{ + utils.OriginID: "prepaidSession", + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2023, time.February, 28, 8, 59, 50, 0, time.UTC), + utils.AnswerTime: time.Date(2023, time.February, 28, 9, 0, 0, 0, time.UTC), + utils.Usage: 5 * time.Second, + utils.BalanceFactorID: "voiceFactor", + }, + APIOpts: map[string]any{ + utils.OptsDebitInterval: 0, + }, + }, + }, &replyUpdate); err != nil { + t.Error(err) + } + + if err := client.Call(context.Background(), utils.SessionSv1UpdateSession, + &sessions.V1UpdateSessionArgs{ + UpdateSession: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "SessionSv1UpdateSession2", + Event: map[string]any{ + utils.OriginID: "prepaidSession", + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2023, time.February, 28, 8, 59, 50, 0, time.UTC), + utils.AnswerTime: time.Date(2023, time.February, 28, 9, 0, 0, 0, time.UTC), + utils.Usage: 2 * time.Second, + utils.BalanceFactorID: "voiceFactor", + }, + APIOpts: map[string]any{ + utils.OptsDebitInterval: 0, + }, + }, + }, &replyUpdate); err != nil { + t.Error(err) + } + + var replyTerminate string + if err := client.Call(context.Background(), utils.SessionSv1TerminateSession, + &sessions.V1TerminateSessionArgs{ + TerminateSession: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "SessionSv1TerminateSession", + Event: map[string]any{ + utils.OriginID: "prepaidSession", + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2023, time.February, 28, 8, 59, 50, 0, time.UTC), + utils.AnswerTime: time.Date(2023, time.February, 28, 9, 0, 0, 0, time.UTC), + }, + APIOpts: map[string]any{ + utils.OptsDebitInterval: 0, + }, + }, + }, &replyTerminate); err != nil { + t.Error(err) + } + + var replyProcessCDR string + if err := client.Call(context.Background(), utils.SessionSv1ProcessCDR, + &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "testSesRnd2PrepaidProcessCDR", + Event: map[string]any{ + utils.OriginID: "prepaidSession", + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2023, time.February, 28, 8, 59, 50, 0, time.UTC), + utils.AnswerTime: time.Date(2023, time.February, 28, 9, 0, 0, 0, time.UTC), + utils.Usage: 0, + }, + APIOpts: map[string]any{ + utils.OptsDebitInterval: 0, + }, + }, &replyProcessCDR); err != nil { + t.Error(err) + } + + var cdrs []*engine.CDR + if err := client.Call(context.Background(), utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithAPIOpts{ + RPCCDRsFilter: &utils.RPCCDRsFilter{ + OriginIDs: []string{"prepaidSession"}, + }}, &cdrs); err != nil { + t.Fatal(err) + } + + if len(cdrs) != 1 { + t.Fatalf("expected to receive only one CDR: %v", utils.ToJSON(cdrs)) + } + voiceBalanceValue, err := cdrs[0].CostDetails.FieldAsInterface([]string{"AccountSummary", "BalanceSummaries[1]", "Value"}) + if err != nil { + t.Fatalf("could not retrieve *voice balance current value: %v", err) + } + if voiceBalanceValue != float64(66*time.Second) { + t.Errorf("unexpected balance value: expected %v, received %v", float64(66*time.Second), voiceBalanceValue) + } + }) } // TestBalanceCDRLog tests the usage of balance related actions together with a "*cdrlog" action. diff --git a/utils/consts.go b/utils/consts.go index 6d3898d55..f98f02fbb 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -510,6 +510,7 @@ const ( BalanceSharedGroups = "BalanceSharedGroups" BalanceBlocker = "BalanceBlocker" BalanceDisabled = "BalanceDisabled" + BalanceFactorID = "BalanceFactorID" Units = "Units" AccountUpdate = "AccountUpdate" BalanceUpdate = "BalanceUpdate"