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.
This commit is contained in:
ionutboangiu
2024-03-01 12:24:47 -05:00
committed by Dan Christian Bogos
parent 0aea8ac641
commit 3269393141
6 changed files with 278 additions and 25 deletions

View File

@@ -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{

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,

View File

@@ -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.

View File

@@ -510,6 +510,7 @@ const (
BalanceSharedGroups = "BalanceSharedGroups"
BalanceBlocker = "BalanceBlocker"
BalanceDisabled = "BalanceDisabled"
BalanceFactorID = "BalanceFactorID"
Units = "Units"
AccountUpdate = "AccountUpdate"
BalanceUpdate = "BalanceUpdate"