Update *remove actions to support multiple balance types at once

This commit is contained in:
ionutboangiu
2024-03-12 12:25:00 -04:00
committed by Dan Christian Bogos
parent fe39eec4d9
commit 5ac08799e1
8 changed files with 111 additions and 83 deletions

View File

@@ -210,7 +210,7 @@ func (acc *Account) debitBalanceAction(a *Action, reset, resetIfNegative bool, f
continue // just to be safe (cleaned expired balances above)
}
b.account = acc
if b.MatchFilter(a.Balance, false, false) {
if b.MatchFilter(a.Balance, "", false, false) {
if reset || (resetIfNegative && b.Value < 0) {
b.SetValue(0)
}

View File

@@ -348,33 +348,32 @@ func cdrLogAction(acc *Account, a *Action, acs Actions, _ *FilterS, extraData an
}
// Function to process balances and append CDR if conditions are met.
processBalances := func(checkFunc func(*Balance) bool) error {
processBalances := func(checkFunc func(*Balance, string) bool) error {
if acc == nil {
return fmt.Errorf("nil account for action %s", utils.ToJSON(action))
}
balanceChain, exists := acc.BalanceMap[action.Balance.GetType()]
if !exists {
return utils.ErrNotFound
}
found := false
for _, balance := range balanceChain {
if checkFunc(balance) {
// Create a new CDR instance for each balance that meets the condition.
newCDR := *cdr // Copy CDR's values to a new CDR instance.
newCDR.Cost = balance.Value
newCDR.OriginID = utils.GenUUID() // OriginID must be unique for every CDR.
newCDR.CGRID = utils.Sha1(newCDR.OriginID, newCDR.OriginHost)
for bType, bChain := range acc.BalanceMap {
for _, balance := range bChain {
if checkFunc(balance, bType) {
// Create a new CDR instance for each balance that meets the condition.
newCDR := *cdr // Copy CDR's values to a new CDR instance.
newCDR.Cost = balance.Value
newCDR.OriginID = utils.GenUUID() // OriginID must be unique for every CDR.
newCDR.CGRID = utils.Sha1(newCDR.OriginID, newCDR.OriginHost)
newCDR.ToR = bType
// Clone the ExtraFields map to avoid changing its value in
// CDRs appended previously.
newCDR.ExtraFields = make(map[string]string, len(cdr.ExtraFields)+1)
for key, val := range cdr.ExtraFields {
newCDR.ExtraFields[key] = val
// Clone the ExtraFields map to avoid changing its value in
// CDRs appended previously.
newCDR.ExtraFields = make(map[string]string, len(cdr.ExtraFields)+1)
for key, val := range cdr.ExtraFields {
newCDR.ExtraFields[key] = val
}
newCDR.ExtraFields[utils.BalanceID] = balance.ID
cdrs = append(cdrs, &newCDR) // Append the address of the new instance.
found = true
}
newCDR.ExtraFields[utils.BalanceID] = balance.ID
cdrs = append(cdrs, &newCDR) // Append the address of the new instance.
found = true
}
}
if !found {
@@ -387,16 +386,16 @@ func cdrLogAction(acc *Account, a *Action, acs Actions, _ *FilterS, extraData an
// assign the balance values to the CDR cost and append to the list of CDRs.
switch action.ActionType {
case utils.MetaRemoveBalance:
if err = processBalances(func(b *Balance) bool {
return b.MatchFilter(action.Balance, false, false)
if err = processBalances(func(b *Balance, typ string) bool {
return b.MatchFilter(action.Balance, typ, false, false)
}); err != nil {
return err
}
continue
case utils.MetaRemoveExpired:
if err = processBalances(func(b *Balance) bool {
if err = processBalances(func(b *Balance, typ string) bool {
return b.IsExpiredAt(currentTime) &&
b.MatchFilter(action.Balance, false, true)
b.MatchFilter(action.Balance, typ, false, true)
}); err != nil {
return err
}
@@ -758,25 +757,24 @@ func removeAccountAction(ub *Account, a *Action, acs Actions, _ *FilterS, extraD
}, config.CgrConfig().GeneralCfg().LockingTimeout, utils.ActionPlanPrefix)
}
func removeBalanceAction(ub *Account, a *Action, acs Actions, _ *FilterS, extraData any, _ ActionConnCfg) error {
if ub == nil {
func removeBalanceAction(acc *Account, a *Action, _ Actions, _ *FilterS, _ any,
_ ActionConnCfg) error {
if acc == nil {
return fmt.Errorf("nil account for %s action", utils.ToJSON(a))
}
if _, exists := ub.BalanceMap[a.Balance.GetType()]; !exists {
return utils.ErrNotFound
}
bChain := ub.BalanceMap[a.Balance.GetType()]
found := false
for i := 0; i < len(bChain); i++ {
if bChain[i].MatchFilter(a.Balance, false, false) {
// delete without preserving order
bChain[i] = bChain[len(bChain)-1]
bChain = bChain[:len(bChain)-1]
i--
found = true
for bType, bChain := range acc.BalanceMap {
for i := 0; i < len(bChain); i++ {
if bChain[i].MatchFilter(a.Balance, bType, false, false) {
// Remove balance without preserving order.
bChain[i] = bChain[len(bChain)-1]
bChain = bChain[:len(bChain)-1]
i--
found = true
}
}
acc.BalanceMap[bType] = bChain
}
ub.BalanceMap[a.Balance.GetType()] = bChain
if !found {
return utils.ErrNotFound
}
@@ -803,7 +801,7 @@ func transferMonetaryDefaultAction(ub *Account, a *Action, acs Actions, _ *Filte
for _, balance := range bChain {
if balance.Uuid != defaultBalance.Uuid &&
balance.ID != defaultBalance.ID && // extra caution
balance.MatchFilter(a.Balance, false, false) {
balance.MatchFilter(a.Balance, "", false, false) {
if balance.Value > 0 {
defaultBalance.Value += balance.Value
balance.Value = 0
@@ -1024,7 +1022,7 @@ func setExpiryAction(ub *Account, a *Action, acs Actions, _ *FilterS, extraData
}
balanceType := a.Balance.GetType()
for _, b := range ub.BalanceMap[balanceType] {
if b.MatchFilter(a.Balance, false, true) {
if b.MatchFilter(a.Balance, "", false, true) {
b.ExpirationDate = a.Balance.GetExpirationDate()
}
}
@@ -1196,28 +1194,27 @@ func removeSessionCosts(_ *Account, action *Action, _ Actions, _ *FilterS, _ any
return cdrStorage.RemoveSMCosts(smcFilter)
}
func removeExpired(acc *Account, action *Action, _ Actions, _ *FilterS, extraData any, _ ActionConnCfg) error {
func removeExpired(acc *Account, action *Action, _ Actions, _ *FilterS, _ any, _ ActionConnCfg) error {
if acc == nil {
return fmt.Errorf("nil account for %s action", utils.ToJSON(action))
}
bChain, exists := acc.BalanceMap[action.Balance.GetType()]
if !exists {
return utils.ErrNotFound
}
found := false
for i := 0; i < len(bChain); i++ {
if bChain[i].IsExpiredAt(time.Now()) &&
bChain[i].MatchFilter(action.Balance, false, true) {
for bType, bChain := range acc.BalanceMap {
for i := 0; i < len(bChain); i++ {
if bChain[i].IsExpiredAt(time.Now()) &&
bChain[i].MatchFilter(action.Balance, bType, false, false) {
// Delete balance without preserving order.
bChain[i] = bChain[len(bChain)-1] // assign last balance to current balance
bChain = bChain[:len(bChain)-1] // remove last balance
i-- // subtract 1 from index to avoid skipping next balance
found = true
// Remove balance without maintaining order.
bChain[i] = bChain[len(bChain)-1]
bChain = bChain[:len(bChain)-1]
i--
found = true
}
}
acc.BalanceMap[bType] = bChain
}
acc.BalanceMap[action.Balance.GetType()] = bChain
if !found {
return utils.ErrNotFound
}

View File

@@ -139,7 +139,7 @@ func (at *ActionTrigger) Match(a *Action) bool {
}
thresholdType = t.ThresholdType == "" || at.ThresholdType == t.ThresholdType
}
return thresholdType && at.Balance.CreateBalance().MatchFilter(a.Balance, false, false)
return thresholdType && at.Balance.CreateBalance().MatchFilter(a.Balance, "", false, false)
}
func (at *ActionTrigger) CreateBalance() *Balance {

View File

@@ -3938,19 +3938,15 @@ func TestRemoveBalanceActionErr(t *testing.T) {
},
}
acs := &Action{
Balance: &BalanceFilter{},
Balance: &BalanceFilter{
ExpirationDate: utils.TimePointer(time.Date(2022, 11, 12, 2, 0, 0, 0, time.UTC)),
Type: utils.StringPointer(utils.MetaMonetary),
Value: &utils.ValueFormula{Static: 10},
},
}
if err := removeBalanceAction(nil, acs, nil, nil, nil, ActionConnCfg{}); err == nil {
t.Error(err)
}
if err := removeBalanceAction(acc, acs, nil, nil, nil, ActionConnCfg{}); err == nil {
t.Error(err)
}
acs.Balance = &BalanceFilter{
ExpirationDate: utils.TimePointer(time.Date(2022, 11, 12, 2, 0, 0, 0, time.UTC)),
Type: utils.StringPointer(utils.MetaMonetary),
Value: &utils.ValueFormula{Static: 10},
}
if err := removeBalanceAction(acc, acs, nil, nil, nil, ActionConnCfg{}); err == nil || err != utils.ErrNotFound {
t.Error(err)
}

View File

@@ -70,10 +70,15 @@ func (b *Balance) Equal(o *Balance) bool {
b.Blocker == o.Blocker
}
func (b *Balance) MatchFilter(o *BalanceFilter, skipIds, skipExpiry bool) bool {
func (b *Balance) MatchFilter(o *BalanceFilter, bType string, skipIds, skipExpiry bool) bool {
if o == nil {
return true
}
if bType != "" && o.Type != nil && *o.Type != "" {
if bType != *o.Type {
return false
}
}
if !skipIds && o.Uuid != nil && *o.Uuid != "" {
return b.Uuid == *o.Uuid
}

View File

@@ -119,12 +119,12 @@ func TestBalanceEqual(t *testing.T) {
func TestBalanceMatchFilter(t *testing.T) {
mb1 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIDs: utils.StringMap{}}
mb2 := &BalanceFilter{Weight: utils.Float64Pointer(1), RatingSubject: nil, DestinationIDs: nil}
if !mb1.MatchFilter(mb2, false, false) {
if !mb1.MatchFilter(mb2, "", false, false) {
t.Errorf("Match filter failure: %+v == %+v", mb1, mb2)
}
mb1.Uuid, mb2.Uuid = "id", utils.StringPointer("id")
if !mb1.MatchFilter(mb2, false, false) {
if !mb1.MatchFilter(mb2, "", false, false) {
t.Errorf("Match filter failure: %+v == %+v", mb1, mb2)
}
@@ -133,7 +133,7 @@ func TestBalanceMatchFilter(t *testing.T) {
func TestBalanceMatchFilterEmpty(t *testing.T) {
mb1 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIDs: utils.StringMap{}}
mb2 := &BalanceFilter{}
if !mb1.MatchFilter(mb2, false, false) {
if !mb1.MatchFilter(mb2, "", false, false) {
t.Errorf("Match filter failure: %+v == %+v", mb1, mb2)
}
}
@@ -141,7 +141,7 @@ func TestBalanceMatchFilterEmpty(t *testing.T) {
func TestBalanceMatchFilterId(t *testing.T) {
mb1 := &Balance{ID: "T1", Weight: 2, precision: 2, RatingSubject: "2", DestinationIDs: utils.NewStringMap("NAT")}
mb2 := &BalanceFilter{ID: utils.StringPointer("T1"), Weight: utils.Float64Pointer(1), RatingSubject: utils.StringPointer("1"), DestinationIDs: nil}
if !mb1.MatchFilter(mb2, false, false) {
if !mb1.MatchFilter(mb2, "", false, false) {
t.Errorf("Match filter failure: %+v == %+v", mb1, mb2)
}
}
@@ -149,7 +149,7 @@ func TestBalanceMatchFilterId(t *testing.T) {
func TestBalanceMatchFilterDiffId(t *testing.T) {
mb1 := &Balance{ID: "T1", Weight: 1, precision: 1, RatingSubject: "1", DestinationIDs: utils.StringMap{}}
mb2 := &BalanceFilter{ID: utils.StringPointer("T2"), Weight: utils.Float64Pointer(1), RatingSubject: utils.StringPointer("1"), DestinationIDs: nil}
if mb1.MatchFilter(mb2, false, false) {
if mb1.MatchFilter(mb2, "", false, false) {
t.Errorf("Match filter failure: %+v != %+v", mb1, mb2)
}
}

View File

@@ -128,7 +128,7 @@ func (ucs UnitCounters) addUnits(amount float64, kind string, cc *CallCost, b *B
continue
}
if uc.CounterType == utils.MetaBalance && b != nil && b.MatchFilter(c.Filter, true, false) {
if uc.CounterType == utils.MetaBalance && b != nil && b.MatchFilter(c.Filter, "", true, false) {
c.Value += amount
continue
}

View File

@@ -700,9 +700,9 @@ PACKAGE_ACC_TEST,ACT_TOPUP_SMS,*asap,10`,
ACT_REMOVE_BALANCE_MONETARY,*cdrlog,,,,,,,,,,,,,,,
ACT_REMOVE_BALANCE_MONETARY,*remove_balance,,,balance_monetary,*monetary,,,,,,,,,,,
ACT_REMOVE_EXPIRED_WITH_CATEGORY,*cdrlog,,,,,,,,,,,,,,,
ACT_REMOVE_EXPIRED_WITH_CATEGORY,*remove_expired,,,,*monetary,category2,,,,,,,,,,
ACT_REMOVE_EXPIRED_WITH_CATEGORY,*remove_expired,,,,,category2,,,,,,,,,,
ACT_REMOVE_EXPIRED,*cdrlog,,,,,,,,,,,,,,,
ACT_REMOVE_EXPIRED,*remove_expired,,,,*monetary,,,,,,,,,,,
ACT_REMOVE_EXPIRED,*remove_expired,,,,,,,,,,,,,,,
ACT_TOPUP_MONETARY,*cdrlog,"{""BalanceID"":""~*acnt.BalanceID""}",,,,,,,,,,,,,,
ACT_TOPUP_MONETARY,*topup_reset,,,balance_monetary,*monetary,,*any,,,*unlimited,,150,20,false,false,20
ACT_TOPUP_SMS,*topup_reset,,,balance_sms,*sms,,*any,,,*unlimited,,1000,10,false,false,10`,
@@ -855,6 +855,15 @@ ACT_TOPUP_SMS,*topup_reset,,,balance_sms,*sms,,*any,,,*unlimited,,1000,10,false,
utils.Categories: "category2",
},
},
{
// will be removed
BalanceType: utils.MetaSMS,
Value: 16,
Balance: map[string]any{
utils.ID: "ExpiredSMSBalance",
utils.ExpiryTime: expiryTime,
},
},
},
}
var reply string
@@ -878,7 +887,7 @@ ACT_TOPUP_SMS,*topup_reset,,,balance_sms,*sms,,*any,,,*unlimited,,1000,10,false,
t.Fatal(err)
}
if len(cdrs) != 4 ||
if len(cdrs) != 5 ||
cdrs[0].Cost != 11 ||
cdrs[0].ExtraFields[utils.BalanceID] != "ExpiredBalanceNotMatching1" ||
cdrs[1].Cost != 12 ||
@@ -886,19 +895,25 @@ ACT_TOPUP_SMS,*topup_reset,,,balance_sms,*sms,,*any,,,*unlimited,,1000,10,false,
cdrs[2].Cost != 13 ||
cdrs[2].ExtraFields[utils.BalanceID] != "ExpiredBalanceNotMatching3" ||
cdrs[3].Cost != 14 ||
cdrs[3].ExtraFields[utils.BalanceID] != "MatchingExpiredBalance" {
cdrs[3].ExtraFields[utils.BalanceID] != "MatchingExpiredBalance" ||
cdrs[4].Cost != 16 ||
cdrs[4].ExtraFields[utils.BalanceID] != "ExpiredSMSBalance" {
t.Errorf("unexpected cdrs received: %v", utils.ToJSON(cdrs))
}
assertCommonCDRFields := func(t *testing.T, cdr *engine.CDR) {
assertCommonCDRFields := func(t *testing.T, cdr *engine.CDR, expectedType string) {
if cdr.RunID != utils.MetaRemoveExpired ||
cdr.Source != utils.CDRLog ||
cdr.ToR != utils.MetaMonetary {
cdr.ToR != expectedType {
t.Fatalf("unexpected cdrs received: %v", utils.ToJSON(cdrs))
}
}
for _, cdr := range cdrs {
assertCommonCDRFields(t, cdr)
expType := utils.MetaMonetary
for i, cdr := range cdrs {
if i == len(cdrs)-1 {
expType = utils.MetaSMS
}
assertCommonCDRFields(t, cdr, expType)
}
})
@@ -972,6 +987,16 @@ ACT_TOPUP_SMS,*topup_reset,,,balance_sms,*sms,,*any,,,*unlimited,,1000,10,false,
utils.Categories: "category2",
},
},
{
// will be removed
BalanceType: utils.MetaSMS,
Value: 16,
Balance: map[string]any{
utils.ID: "ExpiredSMSBalance",
utils.ExpiryTime: expiryTime,
utils.Categories: "category1;category2",
},
},
},
}
if err := client.Call(context.Background(), utils.APIerSv1SetBalances, &attrSetBalance, &reply); err != nil {
@@ -994,12 +1019,17 @@ ACT_TOPUP_SMS,*topup_reset,,,balance_sms,*sms,,*any,,,*unlimited,,1000,10,false,
t.Fatal(err)
}
if len(cdrs) != 1 ||
if len(cdrs) != 2 ||
cdrs[0].Cost != 14 ||
cdrs[0].ExtraFields[utils.BalanceID] != "MatchingExpiredBalance" ||
cdrs[0].RunID != utils.MetaRemoveExpired ||
cdrs[0].Source != utils.CDRLog ||
cdrs[0].ToR != utils.MetaMonetary {
cdrs[0].ToR != utils.MetaMonetary ||
cdrs[1].Cost != 16 ||
cdrs[1].ExtraFields[utils.BalanceID] != "ExpiredSMSBalance" ||
cdrs[1].RunID != utils.MetaRemoveExpired ||
cdrs[1].Source != utils.CDRLog ||
cdrs[1].ToR != utils.MetaSMS {
t.Errorf("unexpected cdrs received: %v", utils.ToJSON(cdrs))
}
})