diff --git a/accounts/accounts.go b/accounts/accounts.go index bd55e9a69..cdf00ab17 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -30,7 +30,8 @@ import ( ) // NewAccountS instantiates the AccountS -func NewAccountS(cfg *config.CGRConfig, fltrS *engine.FilterS, connMgr *engine.ConnManager, dm *engine.DataManager) *AccountS { +func NewAccountS(cfg *config.CGRConfig, fltrS *engine.FilterS, + connMgr *engine.ConnManager, dm *engine.DataManager) *AccountS { return &AccountS{cfg, fltrS, connMgr, dm} } @@ -90,7 +91,7 @@ func (aS *AccountS) matchingAccountsForEvent(ctx *context.Context, tnt string, c for _, acntID := range acntIDs { var refID string if lked { - refID = guardian.Guardian.GuardIDs("", + refID = guardian.Guardian.GuardIDs(utils.EmptyString, aS.cfg.GeneralCfg().LockingTimeout, utils.ConcatenatedKey(utils.CacheAccounts, tnt, acntID)) // RPC caching needs to be atomic } @@ -132,14 +133,16 @@ func (aS *AccountS) matchingAccountsForEvent(ctx *context.Context, tnt string, c } // accountsDebit will debit an usage out of multiple accounts +// concretes parameter limits the debits to concrete only balances +// store is used for simulate only or complete debit func (aS *AccountS) accountsDebit(ctx *context.Context, acnts []*utils.AccountWithWeight, cgrEv *utils.CGREvent, concretes, store bool) (ec *utils.EventCharges, err error) { - var usage *decimal.Big + var usage *decimal.Big // total event usage if usage, err = engine.GetDecimalBigOpts(ctx, cgrEv.Tenant, cgrEv, aS.fltrS, aS.cfg.AccountSCfg().Opts.Usage, config.AccountsUsageDftOpt, utils.OptsAccountsUsage, utils.MetaUsage); err != nil { return } - dbted := decimal.New(0, 0) + dbted := decimal.New(0, 0) // amount debited so far acntBkps := make([]utils.AccountBalancesBackup, len(acnts)) for i, acnt := range acnts { if usage.Cmp(decimal.New(0, 0)) == 0 { @@ -235,6 +238,59 @@ func (aS *AccountS) accountDebit(ctx *context.Context, acnt *utils.Account, usag return } +// refundCharges implements the mechanism of refunding the charges into accounts +func (aS *AccountS) refundCharges(ctx *context.Context, tnt string, ecs *utils.EventCharges) (err error) { + acnts := make(utils.AccountsWithWeight, 0, len(ecs.Accounts)) + acntsIdxed := make(map[string]*utils.Account) // so we can access Account easier + alteredAcnts := make(utils.StringSet) // hold here the list of modified accounts + for acntID := range ecs.Accounts { + refID := guardian.Guardian.GuardIDs(utils.EmptyString, + aS.cfg.GeneralCfg().LockingTimeout, + utils.ConcatenatedKey(utils.CacheAccounts, tnt, acntID)) + var qAcnt *utils.Account + if qAcnt, err = aS.dm.GetAccount(ctx, tnt, acntID); err != nil { + guardian.Guardian.UnguardIDs(refID) + if err == utils.ErrNotFound { // Account was removed in the mean time + err = nil + continue + } + unlockAccounts(acnts) // in case of errors will not have unlocks in upper layers + return + } + acnts = append(acnts, &utils.AccountWithWeight{qAcnt, 0, refID}) + acntsIdxed[acntID] = qAcnt + } + acntBkps := make([]utils.AccountBalancesBackup, len(acnts)) // so we can restore in case of issues + for i, acnt := range acnts { + acntBkps[i] = acnt.AccountBalancesBackup() + } + for _, chrg := range ecs.Charges { + acntChrg := ecs.Accounting[chrg.ChargingID] + refundUnitsOnAccount( + acntsIdxed[acntChrg.AccountID], + uncompressUnits(acntChrg.Units, chrg.CompressFactor), + ecs.Accounts[acntChrg.AccountID].Balances[acntChrg.BalanceID]) + alteredAcnts.Add(acntChrg.AccountID) + for _, chrgID := range acntChrg.JoinedChargeIDs { // refund extra charges + extraChrg := ecs.Accounting[chrgID] + refundUnitsOnAccount( + acntsIdxed[extraChrg.AccountID], + uncompressUnits(extraChrg.Units, chrg.CompressFactor), + ecs.Accounts[acntChrg.AccountID].Balances[extraChrg.BalanceID]) + alteredAcnts.Add(extraChrg.AccountID) + } + } + for acntID := range alteredAcnts { + if err = aS.dm.SetAccount(ctx, acntsIdxed[acntID], false); err != nil { + restoreAccounts(ctx, aS.dm, acnts, acntBkps) + return + } + } + + unlockAccounts(acnts) // in case of errors will not have unlocks in upper layers + return +} + // V1AccountsForEvent returns the matching Accounts for Event func (aS *AccountS) V1AccountsForEvent(ctx *context.Context, args *utils.CGREvent, aps *[]*utils.Account) (err error) { var accIDs []string @@ -379,6 +435,15 @@ func (aS *AccountS) V1DebitConcretes(ctx *context.Context, args *utils.CGREvent, return } +// V1RefundCharges will refund charges recorded inside EventCharges +func (aS *AccountS) V1RefundCharges(ctx *context.Context, args *utils.APIEventCharges, rply *string) (err error) { + if err = aS.refundCharges(ctx, args.Tenant, args.EventCharges); err != nil { + return + } + *rply = utils.OK + return +} + // V1ActionSetBalance performs an update for a specific balance in account func (aS *AccountS) V1ActionSetBalance(ctx *context.Context, args *utils.ArgsActSetBalance, rply *string) (err error) { if args.AccountID == utils.EmptyString { diff --git a/accounts/libaccounts.go b/accounts/libaccounts.go index 18770f2d4..aa8fb856d 100644 --- a/accounts/libaccounts.go +++ b/accounts/libaccounts.go @@ -352,3 +352,27 @@ func unlockAccounts(acnts utils.AccountsWithWeight) { guardian.Guardian.UnguardIDs(lkID) } } + +// uncompressUnits returns the uncompressed value of the units if compressFactor is provided +func uncompressUnits(units *utils.Decimal, cmprsFctr int) (tU *utils.Decimal) { + tU = units + if cmprsFctr > 1 { + tU = &utils.Decimal{utils.MultiplyBig(tU.Big, + decimal.New(int64(cmprsFctr), 0))} + } + return +} + +// refundUnitsOnAccount is responsible for returning the units back to the balance +// origBlnc is used for both it's ID as well as as a configuration backup in case when the balance is not longer present +func refundUnitsOnAccount(acnt *utils.Account, units *utils.Decimal, origBlnc *utils.Balance) { + if _, has := acnt.Balances[origBlnc.ID]; has { + acnt.Balances[origBlnc.ID].Units = &utils.Decimal{ + utils.SumBig( + acnt.Balances[origBlnc.ID].Units.Big, + units.Big)} + } else { + acnt.Balances[origBlnc.ID] = origBlnc.Clone() + acnt.Balances[origBlnc.ID].Units = &utils.Decimal{utils.CloneDecimalBig(units.Big)} + } +} diff --git a/apis/accounts.go b/apis/accounts.go index 8333b75bf..4b9c4a9fa 100644 --- a/apis/accounts.go +++ b/apis/accounts.go @@ -213,6 +213,12 @@ func (aSv1 *AccountSv1) DebitConcretes(ctx *context.Context, args *utils.CGREven return aSv1.aS.V1DebitConcretes(ctx, args, eEc) } +// RefundCharges will refund charges recorded inside EventCharges +func (aSv1 *AccountSv1) RefundCharges(ctx *context.Context, + args *utils.APIEventCharges, rply *string) (err error) { + return aSv1.aS.V1RefundCharges(ctx, args, rply) +} + // ActionSetBalance performs a set balance action func (aSv1 *AccountSv1) ActionSetBalance(ctx *context.Context, args *utils.ArgsActSetBalance, eEc *string) (err error) { diff --git a/utils/account.go b/utils/account.go index 63551d4f2..cc9abf869 100644 --- a/utils/account.go +++ b/utils/account.go @@ -462,6 +462,17 @@ func (apWws AccountsWithWeight) LockIDs() (lkIDs []string) { return } +// Account returns the Account object with ID +func (apWws AccountsWithWeight) Account(acntID string) (acnt *Account) { + for _, aWw := range apWws { + if aWw.Account.ID == acntID { + acnt = aWw.Account + break + } + } + return +} + // BalanceWithWeight attaches static Weight to Balance type BalanceWithWeight struct { *Balance diff --git a/utils/eventcharges.go b/utils/eventcharges.go index 63928977c..ce190256d 100644 --- a/utils/eventcharges.go +++ b/utils/eventcharges.go @@ -287,3 +287,10 @@ func (ac *AccountCharge) equals(nAc *AccountCharge) (eq bool) { } return true } + +// APIEventCharges is used in APIs, ie: refundCharges +type APIEventCharges struct { + Tenant string + APIOpts map[string]interface{} + *EventCharges +}