diff --git a/apier/v1/apier.go b/apier/v1/apier.go index ac49eff35..44f8d6579 100644 --- a/apier/v1/apier.go +++ b/apier/v1/apier.go @@ -211,6 +211,45 @@ func (self *ApierV1) EnableDisableBalance(attr *AttrAddBalance, reply *string) e return nil } +func (self *ApierV1) RemoveBalances(attr *AttrAddBalance, reply *string) error { + expTime, err := utils.ParseDate(attr.ExpiryTime) + if err != nil { + *reply = err.Error() + return err + } + accId := utils.ConcatenatedKey(attr.Tenant, attr.Account) + if _, err := self.AccountDb.GetAccount(accId); err != nil { + return utils.ErrNotFound + } + at := &engine.ActionPlan{ + AccountIds: []string{accId}, + } + at.SetActions(engine.Actions{ + &engine.Action{ + ActionType: engine.REMOVE_BALANCE, + BalanceType: attr.BalanceType, + Balance: &engine.Balance{ + Uuid: attr.BalanceUuid, + Id: attr.BalanceId, + Value: attr.Value, + ExpirationDate: expTime, + RatingSubject: attr.RatingSubject, + Directions: utils.ParseStringMap(attr.Directions), + DestinationIds: utils.ParseStringMap(attr.DestinationIds), + Weight: attr.Weight, + SharedGroups: utils.ParseStringMap(attr.SharedGroups), + Disabled: attr.Disabled, + }, + }, + }) + if err := at.Execute(); err != nil { + *reply = err.Error() + return err + } + *reply = OK + return nil +} + func (self *ApierV1) ExecuteAction(attr *utils.AttrExecuteAction, reply *string) error { accId := utils.AccountKey(attr.Tenant, attr.Account) at := &engine.ActionPlan{ diff --git a/engine/account.go b/engine/account.go index c7d1a8879..5c5a414bc 100644 --- a/engine/account.go +++ b/engine/account.go @@ -97,7 +97,6 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { if bClone == nil { return errors.New("nil balance") } - if ub.BalanceMap == nil { ub.BalanceMap = make(map[string]BalanceChain, 1) } diff --git a/engine/action.go b/engine/action.go index f9ef1680b..a3f6a640a 100644 --- a/engine/action.go +++ b/engine/action.go @@ -56,6 +56,7 @@ const ( DENY_NEGATIVE = "*deny_negative" RESET_ACCOUNT = "*reset_account" REMOVE_ACCOUNT = "*remove_account" + REMOVE_BALANCE = "*remove_balance" TOPUP_RESET = "*topup_reset" TOPUP = "*topup" DEBIT_RESET = "*debit_reset" @@ -128,6 +129,8 @@ func getActionFunc(typ string) (actionTypeFunc, bool) { return mailAsync, true case SET_DDESTINATIONS: return setddestinations, true + case REMOVE_BALANCE: + return removeBalance, true } return nil, false } @@ -525,6 +528,29 @@ func setddestinations(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actio return nil } +func removeBalance(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + if _, exists := ub.BalanceMap[a.BalanceType]; !exists { + return utils.ErrNotFound + } + bChain := ub.BalanceMap[a.BalanceType] + found := false + for i := 0; i < len(bChain); i++ { + if bChain[i].MatchFilter(a.Balance, false) { + // delete without preserving order + bChain[i] = bChain[len(bChain)-1] + bChain = bChain[:len(bChain)-1] + i -= 1 + found = true + } + } + ub.BalanceMap[a.BalanceType] = bChain + if !found { + return utils.ErrNotFound + } + // update account in storage + return accountingStorage.SetAccount(ub) +} + // Structure to store actions according to weight type Actions []*Action diff --git a/engine/actions_test.go b/engine/actions_test.go index ed52e141b..d3799f35d 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -21,7 +21,6 @@ package engine import ( "encoding/json" "fmt" - "log" "reflect" "testing" "time" @@ -1351,21 +1350,94 @@ func TestActionTransactionBalanceType(t *testing.T) { } } -func TestActionExecuteActionNonExistingAccount(t *testing.T) { +func TestActionWithExpireWithoutExpire(t *testing.T) { + err := accountingStorage.SetAccount(&Account{ + Id: "cgrates.org:exp", + BalanceMap: map[string]BalanceChain{ + utils.MONETARY: BalanceChain{&Balance{ + Value: 10, + }}, + }, + }) + if err != nil { + t.Error("Error setting account: ", err) + } at := &ActionPlan{ - AccountIds: []string{"cgrates.org:exe"}, + AccountIds: []string{"cgrates.org:exp"}, Timing: &RateInterval{}, actions: []*Action{ &Action{ ActionType: TOPUP, - BalanceType: utils.MONETARY, - Balance: &Balance{Value: 1.1}, + BalanceType: utils.VOICE, + Balance: &Balance{ + Value: 15, + }, + }, + &Action{ + ActionType: TOPUP, + BalanceType: utils.VOICE, + Balance: &Balance{ + Value: 30, + ExpirationDate: time.Date(2025, time.November, 11, 22, 39, 0, 0, time.UTC), + }, }, }, } err = at.Execute() - if err == nil { - log.Print("Fail to return error on action execute: ", err) + acc, err := accountingStorage.GetAccount("cgrates.org:exp") + if err != nil || acc == nil { + t.Errorf("Error getting account: %+v: %v", acc, err) + } + if len(acc.BalanceMap) != 2 || + len(acc.BalanceMap[utils.VOICE]) != 2 { + t.Errorf("Error debiting expir and unexpire: %+v", acc.BalanceMap[utils.VOICE][0]) + } +} + +func TestActionRemoveBalance(t *testing.T) { + err := accountingStorage.SetAccount(&Account{ + Id: "cgrates.org:rembal", + BalanceMap: map[string]BalanceChain{ + utils.MONETARY: BalanceChain{ + &Balance{ + Value: 10, + }, + &Balance{ + Value: 10, + DestinationIds: utils.NewStringMap("NAT", "RET"), + ExpirationDate: time.Date(2025, time.November, 11, 22, 39, 0, 0, time.UTC), + }, + &Balance{ + Value: 10, + DestinationIds: utils.NewStringMap("NAT", "RET"), + }, + }, + }, + }) + if err != nil { + t.Error("Error setting account: ", err) + } + at := &ActionPlan{ + AccountIds: []string{"cgrates.org:rembal"}, + Timing: &RateInterval{}, + actions: []*Action{ + &Action{ + ActionType: REMOVE_BALANCE, + BalanceType: utils.MONETARY, + Balance: &Balance{ + DestinationIds: utils.NewStringMap("NAT", "RET"), + }, + }, + }, + } + err = at.Execute() + acc, err := accountingStorage.GetAccount("cgrates.org:rembal") + if err != nil || acc == nil { + t.Errorf("Error getting account: %+v: %v", acc, err) + } + if len(acc.BalanceMap) != 1 || + len(acc.BalanceMap[utils.MONETARY]) != 1 { + t.Errorf("Error removing balance: %+v", acc.BalanceMap[utils.MONETARY]) } }