From fa1b866931684ff72188f6a93e9479c072c503c3 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Fri, 22 Jan 2016 20:50:01 +0200 Subject: [PATCH] first version of set balance api --- apier/v1/accounts.go | 73 +++++++++-- console/{balance_debit.go => balance_add.go} | 41 +++---- console/balance_remove.go | 65 ++++++++++ console/balance_set.go | 16 +-- engine/account.go | 120 +++++++++++++++---- engine/action.go | 17 ++- engine/actions_test.go | 69 +++++++++++ 7 files changed, 337 insertions(+), 64 deletions(-) rename console/{balance_debit.go => balance_add.go} (51%) create mode 100644 console/balance_remove.go diff --git a/apier/v1/accounts.go b/apier/v1/accounts.go index bc527df95..f4b1a1fff 100644 --- a/apier/v1/accounts.go +++ b/apier/v1/accounts.go @@ -388,10 +388,14 @@ type AttrAddBalance struct { Weight float64 SharedGroups string Overwrite bool // When true it will reset if the balance is already there + Blocker bool Disabled bool } func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error { + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } expTime, err := utils.ParseTimeDetectLayout(attr.ExpiryTime, self.Config.DefaultTimezone) if err != nil { *reply = err.Error() @@ -411,12 +415,9 @@ func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error { at := &engine.ActionTiming{} at.SetAccountIDs(utils.StringMap{accID: true}) - aType := engine.DEBIT - // reverse the sign as it is a debit - attr.Value = -attr.Value - + aType := engine.TOPUP if attr.Overwrite { - aType = engine.DEBIT_RESET + aType = engine.TOPUP_RESET } at.SetActions(engine.Actions{ &engine.Action{ @@ -433,6 +434,7 @@ func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error { Categories: utils.ParseStringMap(attr.Categories), Weight: attr.Weight, SharedGroups: utils.ParseStringMap(attr.SharedGroups), + Blocker: attr.Blocker, Disabled: attr.Disabled, }, }, @@ -446,7 +448,10 @@ func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error { } func (self *ApierV1) EnableDisableBalance(attr *AttrAddBalance, reply *string) error { - expTime, err := utils.ParseDate(attr.ExpiryTime) + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + expTime, err := utils.ParseTimeDetectLayout(attr.ExpiryTime, self.Config.DefaultTimezone) if err != nil { *reply = err.Error() return err @@ -468,10 +473,12 @@ func (self *ApierV1) EnableDisableBalance(attr *AttrAddBalance, reply *string) e Value: attr.Value, ExpirationDate: expTime, RatingSubject: attr.RatingSubject, + Categories: utils.ParseStringMap(attr.Categories), Directions: utils.ParseStringMap(attr.Directions), DestinationIds: utils.ParseStringMap(attr.DestinationIds), Weight: attr.Weight, SharedGroups: utils.ParseStringMap(attr.SharedGroups), + Blocker: attr.Blocker, Disabled: attr.Disabled, }, }, @@ -485,7 +492,10 @@ func (self *ApierV1) EnableDisableBalance(attr *AttrAddBalance, reply *string) e } func (self *ApierV1) RemoveBalances(attr *AttrAddBalance, reply *string) error { - expTime, err := utils.ParseDate(attr.ExpiryTime) + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + expTime, err := utils.ParseTimeDetectLayout(attr.ExpiryTime, self.Config.DefaultTimezone) if err != nil { *reply = err.Error() return err @@ -509,8 +519,57 @@ func (self *ApierV1) RemoveBalances(attr *AttrAddBalance, reply *string) error { RatingSubject: attr.RatingSubject, Directions: utils.ParseStringMap(attr.Directions), DestinationIds: utils.ParseStringMap(attr.DestinationIds), + Categories: utils.ParseStringMap(attr.Categories), Weight: attr.Weight, SharedGroups: utils.ParseStringMap(attr.SharedGroups), + Blocker: attr.Blocker, + Disabled: attr.Disabled, + }, + }, + }) + if err := at.Execute(); err != nil { + *reply = err.Error() + return err + } + *reply = OK + return nil +} + +func (self *ApierV1) SetBalance(attr *AttrAddBalance, reply *string) error { + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + if attr.BalanceId == "" && attr.BalanceUuid == "" { + return utils.NewErrMandatoryIeMissing("BalanceId", "or", "BalanceUuid") + } + expTime, err := utils.ParseTimeDetectLayout(attr.ExpiryTime, self.Config.DefaultTimezone) + 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.ActionTiming{} + at.SetAccountIDs(utils.StringMap{accID: true}) + + at.SetActions(engine.Actions{ + &engine.Action{ + ActionType: engine.SET_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), + Categories: utils.ParseStringMap(attr.Categories), + Weight: attr.Weight, + SharedGroups: utils.ParseStringMap(attr.SharedGroups), + Blocker: true, Disabled: attr.Disabled, }, }, diff --git a/console/balance_debit.go b/console/balance_add.go similarity index 51% rename from console/balance_debit.go rename to console/balance_add.go index ab6f45ec8..b60a59380 100644 --- a/console/balance_debit.go +++ b/console/balance_add.go @@ -18,51 +18,48 @@ along with this program. If not, see package console -import "github.com/cgrates/cgrates/engine" +import ( + "github.com/cgrates/cgrates/apier/v1" + "github.com/cgrates/cgrates/utils" +) func init() { - c := &CmdDebitBalance{ - name: "balance_debit", - rpcMethod: "Responder.Debit", - rpcParams: &engine.CallDescriptor{Direction: "*out"}, - clientArgs: []string{"Direction", "Category", "TOR", "Tenant", "Subject", "Account", "Destination", "TimeStart", "TimeEnd", "CallDuration", "FallbackSubject"}, + c := &CmdAddBalance{ + name: "balance_add", + rpcMethod: "ApierV1.AddBalance", } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} } // Commander implementation -type CmdDebitBalance struct { - name string - rpcMethod string - rpcParams *engine.CallDescriptor - clientArgs []string +type CmdAddBalance struct { + name string + rpcMethod string + rpcParams *v1.AttrAddBalance *CommandExecuter } -func (self *CmdDebitBalance) Name() string { +func (self *CmdAddBalance) Name() string { return self.name } -func (self *CmdDebitBalance) RpcMethod() string { +func (self *CmdAddBalance) RpcMethod() string { return self.rpcMethod } -func (self *CmdDebitBalance) RpcParams(reset bool) interface{} { +func (self *CmdAddBalance) RpcParams(reset bool) interface{} { if reset || self.rpcParams == nil { - self.rpcParams = &engine.CallDescriptor{Direction: "*out"} + self.rpcParams = &v1.AttrAddBalance{BalanceType: utils.MONETARY, Overwrite: false} } return self.rpcParams } -func (self *CmdDebitBalance) PostprocessRpcParams() error { +func (self *CmdAddBalance) PostprocessRpcParams() error { return nil } -func (self *CmdDebitBalance) RpcResult() interface{} { - return &engine.CallCost{} -} - -func (self *CmdDebitBalance) ClientArgs() []string { - return self.clientArgs +func (self *CmdAddBalance) RpcResult() interface{} { + var s string + return &s } diff --git a/console/balance_remove.go b/console/balance_remove.go new file mode 100644 index 000000000..b01324f6f --- /dev/null +++ b/console/balance_remove.go @@ -0,0 +1,65 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2012-2015 ITsysCOM + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package console + +import ( + "github.com/cgrates/cgrates/apier/v1" + "github.com/cgrates/cgrates/utils" +) + +func init() { + c := &CmdRemoveBalance{ + name: "balance_remove", + rpcMethod: "ApierV1.RemoveBalances", + } + commands[c.Name()] = c + c.CommandExecuter = &CommandExecuter{c} +} + +// Commander implementation +type CmdRemoveBalance struct { + name string + rpcMethod string + rpcParams *v1.AttrAddBalance + *CommandExecuter +} + +func (self *CmdRemoveBalance) Name() string { + return self.name +} + +func (self *CmdRemoveBalance) RpcMethod() string { + return self.rpcMethod +} + +func (self *CmdRemoveBalance) RpcParams(reset bool) interface{} { + if reset || self.rpcParams == nil { + self.rpcParams = &v1.AttrAddBalance{BalanceType: utils.MONETARY, Overwrite: false} + } + return self.rpcParams +} + +func (self *CmdRemoveBalance) PostprocessRpcParams() error { + return nil +} + +func (self *CmdRemoveBalance) RpcResult() interface{} { + var s string + return &s +} diff --git a/console/balance_set.go b/console/balance_set.go index 89f6a70c5..eee047382 100644 --- a/console/balance_set.go +++ b/console/balance_set.go @@ -24,42 +24,42 @@ import ( ) func init() { - c := &CmdAddBalance{ + c := &CmdSetBalance{ name: "balance_set", - rpcMethod: "ApierV1.AddBalance", + rpcMethod: "ApierV1.SetBalance", } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} } // Commander implementation -type CmdAddBalance struct { +type CmdSetBalance struct { name string rpcMethod string rpcParams *v1.AttrAddBalance *CommandExecuter } -func (self *CmdAddBalance) Name() string { +func (self *CmdSetBalance) Name() string { return self.name } -func (self *CmdAddBalance) RpcMethod() string { +func (self *CmdSetBalance) RpcMethod() string { return self.rpcMethod } -func (self *CmdAddBalance) RpcParams(reset bool) interface{} { +func (self *CmdSetBalance) RpcParams(reset bool) interface{} { if reset || self.rpcParams == nil { self.rpcParams = &v1.AttrAddBalance{BalanceType: utils.MONETARY, Overwrite: false} } return self.rpcParams } -func (self *CmdAddBalance) PostprocessRpcParams() error { +func (self *CmdSetBalance) PostprocessRpcParams() error { return nil } -func (self *CmdAddBalance) RpcResult() interface{} { +func (self *CmdSetBalance) RpcResult() interface{} { var s string return &s } diff --git a/engine/account.go b/engine/account.go index 7ae6b8083..9044639b1 100644 --- a/engine/account.go +++ b/engine/account.go @@ -88,6 +88,83 @@ func (ub *Account) getCreditForPrefix(cd *CallDescriptor) (duration time.Duratio return } +// sets all the fields of the balance +func (ub *Account) setBalanceAction(a *Action) error { + if a == nil { + return errors.New("nil action") + } + bClone := a.Balance.Clone() + if bClone == nil { + return errors.New("nil balance") + } + // load ValueFactor if defined in extra parametrs + if a.ExtraParameters != "" { + vf := ValueFactor{} + err := json.Unmarshal([]byte(a.ExtraParameters), &vf) + if err == nil { + bClone.Factor = vf + } else { + utils.Logger.Warning(fmt.Sprintf("Could load value factor from actions: extra parametrs: %s", a.ExtraParameters)) + } + } + if ub.BalanceMap == nil { + ub.BalanceMap = make(map[string]BalanceChain, 1) + } + found := false + balanceType := a.BalanceType + var previousSharedGroups utils.StringMap // kept for comparison + for _, b := range ub.BalanceMap[balanceType] { + if b.IsExpired() { + continue // just to be safe (cleaned expired balances above) + } + b.account = ub + if b.MatchFilter(a.Balance, false) { // for Id or Uuid only + bClone.Id = b.Id + bClone.Uuid = b.Uuid + previousSharedGroups = b.SharedGroups + *b = *bClone + found = true + break // only set one balance + } + } + // if it is not found then we add it to the list + if !found { + // check if the Id is *default (user trying to create the default balance) + // use only it's value value + if bClone.Id == utils.META_DEFAULT { + bClone = &Balance{ + Id: utils.META_DEFAULT, + Value: bClone.GetValue(), + } + } + bClone.dirty = true // Mark the balance as dirty since we have modified and it should be checked by action triggers + bClone.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency + ub.BalanceMap[balanceType] = append(ub.BalanceMap[balanceType], bClone) + } + if !found || !previousSharedGroups.Equal(bClone.SharedGroups) { + for sgId := range a.Balance.SharedGroups { + // add shared group member + sg, err := ratingStorage.GetSharedGroup(sgId, false) + if err != nil || sg == nil { + //than is problem + utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgId)) + } else { + if _, found := sg.MemberIds[ub.Id]; !found { + // add member and save + if sg.MemberIds == nil { + sg.MemberIds = make(utils.StringMap) + } + sg.MemberIds[ub.Id] = true + ratingStorage.SetSharedGroup(sg) + } + } + } + } + ub.InitCounters() + ub.executeActionTriggers(nil) + return nil +} + // Debits some amount of user's specified balance adding the balance if it does not exists. // Returns the remaining credit in user's balance. func (ub *Account) debitBalanceAction(a *Action, reset bool) error { @@ -102,8 +179,8 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { ub.BalanceMap = make(map[string]BalanceChain, 1) } found := false - id := a.BalanceType - for _, b := range ub.BalanceMap[id] { + balanceType := a.BalanceType + for _, b := range ub.BalanceMap[balanceType] { if b.IsExpired() { continue // just to be safe (cleaned expired balances above) } @@ -113,6 +190,7 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { b.SetValue(0) } b.SubstractValue(bClone.GetValue()) + b.dirty = true found = true } } @@ -131,9 +209,8 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { } } bClone.dirty = true // Mark the balance as dirty since we have modified and it should be checked by action triggers - if bClone.Uuid == "" { - bClone.Uuid = utils.GenUUID() - } + + bClone.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency // load ValueFactor if defined in extra parametrs if a.ExtraParameters != "" { vf := ValueFactor{} @@ -144,27 +221,28 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { utils.Logger.Warning(fmt.Sprintf("Could load value factor from actions: extra parametrs: %s", a.ExtraParameters)) } } - ub.BalanceMap[id] = append(ub.BalanceMap[id], bClone) - } - for sgId := range a.Balance.SharedGroups { - // add shared group member - sg, err := ratingStorage.GetSharedGroup(sgId, false) - if err != nil || sg == nil { - //than is problem - utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgId)) - } else { - if _, found := sg.MemberIds[ub.Id]; !found { - // add member and save - if sg.MemberIds == nil { - sg.MemberIds = make(utils.StringMap) + ub.BalanceMap[balanceType] = append(ub.BalanceMap[balanceType], bClone) + + for sgId := range a.Balance.SharedGroups { + // add shared group member + sg, err := ratingStorage.GetSharedGroup(sgId, false) + if err != nil || sg == nil { + //than is problem + utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgId)) + } else { + if _, found := sg.MemberIds[ub.Id]; !found { + // add member and save + if sg.MemberIds == nil { + sg.MemberIds = make(utils.StringMap) + } + sg.MemberIds[ub.Id] = true + ratingStorage.SetSharedGroup(sg) } - sg.MemberIds[ub.Id] = true - ratingStorage.SetSharedGroup(sg) } } } ub.executeActionTriggers(nil) - return nil //ub.BalanceMap[id].GetTotalValue() + return nil } func (ub *Account) enableDisableBalanceAction(a *Action) error { diff --git a/engine/action.go b/engine/action.go index 147b176a7..b579482fc 100644 --- a/engine/action.go +++ b/engine/action.go @@ -57,6 +57,7 @@ const ( DENY_NEGATIVE = "*deny_negative" RESET_ACCOUNT = "*reset_account" REMOVE_ACCOUNT = "*remove_account" + SET_BALANCE = "*set_balance" REMOVE_BALANCE = "*remove_balance" TOPUP_RESET = "*topup_reset" TOPUP = "*topup" @@ -135,8 +136,10 @@ func getActionFunc(typ string) (actionTypeFunc, bool) { return removeAccountAction, true case REMOVE_BALANCE: return removeBalanceAction, true + case SET_BALANCE: + return setBalanceAction, true case TRANSFER_MONETARY_DEFAULT: - return transferMonetaryDefault, true + return transferMonetaryDefaultAction, true } return nil, false } @@ -588,11 +591,14 @@ func removeBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Ac if !found { return utils.ErrNotFound } - // update account in storage - return accountingStorage.SetAccount(ub) + return nil } -func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { +func setBalanceAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + return acc.setBalanceAction(a) +} + +func transferMonetaryDefaultAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { if acc == nil { utils.Logger.Err("*transfer_monetary_default called without account") return utils.ErrAccountNotFound @@ -612,8 +618,7 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a } } } - // update account in storage - return accountingStorage.SetAccount(acc) + return nil } // Structure to store actions according to weight diff --git a/engine/actions_test.go b/engine/actions_test.go index d2cd20538..662da3d65 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -1759,6 +1759,75 @@ func TestActionConditionalTopupExistingBalance(t *testing.T) { } } +func TestActionSetBalance(t *testing.T) { + err := accountingStorage.SetAccount( + &Account{ + Id: "cgrates.org:setb", + BalanceMap: map[string]BalanceChain{ + utils.MONETARY: BalanceChain{ + &Balance{ + Id: "m1", + Uuid: utils.GenUUID(), + Value: 1, + Weight: 10, + }, + &Balance{ + Id: "m2", + Uuid: utils.GenUUID(), + Value: 6, + Weight: 20, + }, + }, + utils.VOICE: BalanceChain{ + &Balance{ + Id: "v1", + Uuid: utils.GenUUID(), + Value: 10, + Weight: 10, + }, + &Balance{ + Id: "v2", + Uuid: utils.GenUUID(), + Value: 100, + Weight: 20, + }, + }, + }, + }) + if err != nil { + t.Errorf("error setting account: %v", err) + } + + a := &Action{ + ActionType: SET_BALANCE, + BalanceType: utils.MONETARY, + Balance: &Balance{ + Id: "m2", + Value: 11, + Weight: 10, + }, + } + + at := &ActionTiming{ + accountIDs: utils.StringMap{"cgrates.org:setb": true}, + actions: Actions{a}, + } + at.Execute() + + afterUb, err := accountingStorage.GetAccount("cgrates.org:setb") + if err != nil { + t.Error("account not found: ", err, afterUb) + } + if len(afterUb.BalanceMap[utils.MONETARY]) != 2 || + afterUb.BalanceMap[utils.MONETARY][1].Value != 11 || + afterUb.BalanceMap[utils.MONETARY][1].Weight != 10 { + for _, b := range afterUb.BalanceMap[utils.MONETARY] { + t.Logf("B: %+v", b) + } + t.Errorf("Balance: %+v", afterUb.BalanceMap[utils.MONETARY][1]) + } +} + /**************** Benchmarks ********************************/ func BenchmarkUUID(b *testing.B) {