diff --git a/accounts/abstractbalance.go b/accounts/abstractbalance.go index 26dd6fe2b..1504467df 100644 --- a/accounts/abstractbalance.go +++ b/accounts/abstractbalance.go @@ -130,9 +130,9 @@ func (aB *abstractBalance) rateSCostForEvent(cgrEv *utils.CGREvent) (rplyCost *e } // debitUsageFromConcrete attempts to debit the usage out of concrete balances -// returns error if complete usage cannot be debitted -func (aB *abstractBalance) debitUsageFromConcrete(usage *utils.Decimal, costIcrm *utils.CostIncrement, - cgrEv *utils.CGREvent) (err error) { +// returns utils.ErrInsufficientCredit if complete usage cannot be debitted +func (aB *abstractBalance) debitUsageFromConcrete(cBs []*concreteBalance, usage *utils.Decimal, + costIcrm *utils.CostIncrement, cgrEv *utils.CGREvent) (err error) { if costIcrm.RecurrentFee.Cmp(decimal.New(-1, 0)) == 0 && costIcrm.FixedFee == nil { var rplyCost *engine.RateProfileCost @@ -141,19 +141,36 @@ func (aB *abstractBalance) debitUsageFromConcrete(usage *utils.Decimal, costIcrm } costIcrm.FixedFee = utils.NewDecimalFromFloat64(rplyCost.Cost) } - var tCost *utils.Decimal + var tCost *decimal.Big if costIcrm.FixedFee != nil { - tCost = costIcrm.FixedFee + tCost = costIcrm.FixedFee.Big } // RecurrentFee is configured, used it with increments - if costIcrm.RecurrentFee.Cmp(decimal.New(-1, 0)) != 0 { + if costIcrm.RecurrentFee.Big.Cmp(decimal.New(-1, 0)) != 0 { rcrntCost := utils.MultiplyBig( utils.DivideBig(usage.Big, costIcrm.Increment.Big), costIcrm.RecurrentFee.Big) - tCost = &utils.Decimal{utils.SumBig(tCost.Big, rcrntCost)} + if tCost == nil { + tCost = rcrntCost + } else { + tCost = utils.SumBig(tCost, rcrntCost) + } } - fmt.Println(tCost) - return + for _, cB := range cBs { + ev := utils.MapStorage{ + utils.MetaOpts: cgrEv.Opts, + utils.MetaReq: cgrEv.Event, + } + var dbted *utils.Decimal + if dbted, _, err = cB.debitUnits(&utils.Decimal{tCost}, cgrEv.Tenant, ev); err != nil { + return + } + tCost = utils.SubstractBig(tCost, dbted.Big) + if tCost.Cmp(decimal.New(0, 0)) <= 0 { + return // have debitted all, total is smaller or equal to 0 + } + } + return utils.ErrInsufficientCredit } // debitUsage implements the balanceOperator interface diff --git a/accounts/abstractbalance_test.go b/accounts/abstractbalance_test.go new file mode 100644 index 000000000..2149ab0d2 --- /dev/null +++ b/accounts/abstractbalance_test.go @@ -0,0 +1,106 @@ +/* +Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +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 accounts + +import ( + "testing" + "time" + + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + +func TestABDebitUsageFromConcrete(t *testing.T) { + cBs := []*concreteBalance{ + { + blnCfg: &utils.Balance{ + ID: "CB1", + Type: utils.MetaConcrete, + Opts: map[string]interface{}{ + utils.MetaBalanceLimit: utils.NewDecimal(-200, 0), + }, + UnitFactors: []*utils.UnitFactor{ + { + Factor: utils.NewDecimal(100, 0), // EuroCents + }, + }, + Units: utils.NewDecimal(500, 0), // 500 EURcents + }, + fltrS: new(engine.FilterS), + }, + { + blnCfg: &utils.Balance{ + ID: "CB2", + Type: utils.MetaConcrete, + Opts: map[string]interface{}{ + utils.MetaBalanceLimit: utils.NewDecimal(-1, 0), + }, + Units: utils.NewDecimal(125, 2), + }, + fltrS: new(engine.FilterS), + }, + } + // consume only from first balance + if err := new(abstractBalance).debitUsageFromConcrete(cBs, + utils.NewDecimal(int64(time.Duration(5*time.Minute)), 0), + &utils.CostIncrement{ + Increment: utils.NewDecimal(int64(time.Duration(time.Minute)), 0), + RecurrentFee: utils.NewDecimal(1, 0)}, + new(utils.CGREvent)); err != nil { + t.Error(err) + } else if cBs[0].blnCfg.Units.Compare(utils.NewDecimal(0, 0)) != 0 { + t.Errorf("Unexpected units in first balance: %s", cBs[0].blnCfg.Units) + } else if cBs[1].blnCfg.Units.Compare(utils.NewDecimal(125, 2)) != 0 { + t.Errorf("Unexpected units in first balance: %s", cBs[1].blnCfg.Units) + } + + // consume from second also, remaining in second + cBs[0].blnCfg.Units = utils.NewDecimal(500, 0) + cBs[1].blnCfg.Units = utils.NewDecimal(125, 2) + + if err := new(abstractBalance).debitUsageFromConcrete(cBs, + utils.NewDecimal(int64(time.Duration(9*time.Minute)), 0), + &utils.CostIncrement{ + Increment: utils.NewDecimal(int64(time.Duration(time.Minute)), 0), + RecurrentFee: utils.NewDecimal(1, 0)}, + new(utils.CGREvent)); err != nil { + t.Error(err) + } else if cBs[0].blnCfg.Units.Compare(utils.NewDecimal(-200, 0)) != 0 { + t.Errorf("Unexpected units in first balance: %s", cBs[0].blnCfg.Units) + } else if cBs[1].blnCfg.Units.Compare(utils.NewDecimal(-75, 2)) != 0 { + t.Errorf("Unexpected units in second balance: %s", cBs[1].blnCfg.Units) + } + + // not enough balance + cBs[0].blnCfg.Units = utils.NewDecimal(500, 0) + cBs[1].blnCfg.Units = utils.NewDecimal(125, 2) + + if err := new(abstractBalance).debitUsageFromConcrete(cBs, + utils.NewDecimal(int64(time.Duration(10*time.Minute)), 0), + &utils.CostIncrement{ + Increment: utils.NewDecimal(int64(time.Duration(time.Minute)), 0), + RecurrentFee: utils.NewDecimal(1, 0)}, + new(utils.CGREvent)); err == nil || err != utils.ErrInsufficientCredit { + t.Error(err) + } else if cBs[0].blnCfg.Units.Compare(utils.NewDecimal(-200, 0)) != 0 { + t.Errorf("Unexpected units in first balance: %s", cBs[0].blnCfg.Units) + } else if cBs[1].blnCfg.Units.Compare(utils.NewDecimal(-1, 0)) != 0 { + t.Errorf("Unexpected units in first balance: %s", cBs[1].blnCfg.Units) + } +} diff --git a/accounts/concretebalance.go b/accounts/concretebalance.go index 84d3f37b9..67743969b 100644 --- a/accounts/concretebalance.go +++ b/accounts/concretebalance.go @@ -19,7 +19,6 @@ along with this program. If not, see package accounts import ( - "fmt" "time" "github.com/cgrates/cgrates/engine" @@ -113,8 +112,8 @@ func (cB *concreteBalance) debitUsage(usage *utils.Decimal, startTime time.Time, } // debitUnits is a direct debit of balance units -func (cB *concreteBalance) debitUnits(dUnts *utils.Decimal, incrm *utils.Decimal, - tnt string, ev utils.DataProvider) (dbted *utils.Decimal, uF *utils.UnitFactor, err error) { +func (cB *concreteBalance) debitUnits(dUnts *utils.Decimal, tnt string, + ev utils.DataProvider) (dbted *utils.Decimal, uF *utils.UnitFactor, err error) { // pass the general balance filters var pass bool @@ -125,47 +124,48 @@ func (cB *concreteBalance) debitUnits(dUnts *utils.Decimal, incrm *utils.Decimal } // unitFactor + var hasUF bool if uF, err = cB.unitFactor(tnt, ev); err != nil { return } - - var hasUF bool if uF != nil && uF.Factor.Cmp(decimal.New(1, 0)) != 0 { - dUnts = &utils.Decimal{utils.MultiplyBig(dUnts.Big, uF.Factor.Big)} - incrm = &utils.Decimal{utils.MultiplyBig(incrm.Big, uF.Factor.Big)} hasUF = true + dUnts = &utils.Decimal{utils.MultiplyBig(dUnts.Big, uF.Factor.Big)} } - blcVal := cB.blnCfg.Units - // balanceLimit var hasLmt bool blncLmt := cB.balanceLimit() if blncLmt != nil && blncLmt.Big.Cmp(decimal.New(0, 0)) != 0 { - blcVal = &utils.Decimal{utils.SubstractBig(blcVal.Big, blncLmt.Big)} + cB.blnCfg.Units.Big = utils.SubstractBig(cB.blnCfg.Units.Big, blncLmt.Big) hasLmt = true } - if blcVal.Compare(dUnts) == -1 && blncLmt != nil { // balance smaller than debit - // will use special rounding to 0 since otherwise we go negative (ie: 0.05 as increment) - maxIncrm := &utils.Decimal{ - decimal.WithContext( - decimal.Context{RoundingMode: decimal.ToZero}).Quo(blcVal.Big, - incrm.Big).RoundToInt()} - dUnts = utils.MultiplyDecimal(incrm, maxIncrm) - } - rmain := &utils.Decimal{utils.SubstractBig(blcVal.Big, dUnts.Big)} - if hasLmt { - rmain = &utils.Decimal{utils.SumBig(rmain.Big, blncLmt.Big)} - } - if hasUF { - dbted = &utils.Decimal{utils.DivideBig(dUnts.Big, uF.Factor.Big)} + + if cB.blnCfg.Units.Compare(dUnts) <= 0 && blncLmt != nil { // balance smaller than debit and limited + dbted = &utils.Decimal{cB.blnCfg.Units.Big} + cB.blnCfg.Units.Big = blncLmt.Big } else { + cB.blnCfg.Units.Big = utils.SubstractBig(cB.blnCfg.Units.Big, dUnts.Big) + if hasLmt { // put back the limit + cB.blnCfg.Units.Big = utils.SumBig(cB.blnCfg.Units.Big, blncLmt.Big) + } dbted = dUnts } - rmainFlt64, ok := rmain.Float64() - if !ok { - return nil, nil, fmt.Errorf("failed representing decimal <%s> as float64", rmain) + if hasUF { + dbted.Big = utils.DivideBig(dbted.Big, uF.Factor.Big) + } + + return +} + +// cloneUnitsFromConcretes returns cloned units from the concrete balances passed as parameters +func cloneUnitsFromConcretes(cBs []*concreteBalance) (clnUnits []*utils.Decimal) { + if cBs == nil { + return + } + clnUnits = make([]*utils.Decimal, len(cBs)) + for i := range cBs { + clnUnits[i] = cBs[i].blnCfg.Units.Clone() } - cB.blnCfg.Units = utils.NewDecimalFromFloat64(rmainFlt64) return } diff --git a/accounts/concretebalance_test.go b/accounts/concretebalance_test.go index 1f051f71c..fec478d08 100644 --- a/accounts/concretebalance_test.go +++ b/accounts/concretebalance_test.go @@ -46,7 +46,7 @@ func TestCBDebitUnits(t *testing.T) { fltrS: new(engine.FilterS), } toDebit := utils.NewDecimal(6, 0) - if dbted, uFctr, err := cb.debitUnits(toDebit, utils.NewDecimal(1, 0), + if dbted, uFctr, err := cb.debitUnits(toDebit, "cgrates.org", utils.MapStorage{}); err != nil { t.Error(err) } else if !reflect.DeepEqual(cb.blnCfg.UnitFactors[0], uFctr) { @@ -54,8 +54,9 @@ func TestCBDebitUnits(t *testing.T) { } else if dbted.Compare(toDebit) != 0 { t.Errorf("debited: %s", dbted) } else if cb.blnCfg.Units.Cmp(decimal.New(-100, 0)) != 0 { - t.Errorf("balance remaining: %f", cb.blnCfg.Units) + t.Errorf("balance remaining: %s", cb.blnCfg.Units) } + //with increment and not enough balance cb = &concreteBalance{ blnCfg: &utils.Balance{ @@ -64,20 +65,20 @@ func TestCBDebitUnits(t *testing.T) { Opts: map[string]interface{}{ utils.MetaBalanceLimit: utils.NewDecimal(-1, 0), }, - Units: utils.NewDecimal(125, 2), + Units: utils.NewDecimal(125, 2), // 1.25 }, fltrS: new(engine.FilterS), } - if dbted, _, err := cb.debitUnits( - utils.NewDecimal(25, 1), //2.5 - utils.NewDecimal(1, 1), //0.1 + toDebit = utils.NewDecimal(25, 1) //2.5 + if dbted, _, err := cb.debitUnits(toDebit, "cgrates.org", utils.MapStorage{}); err != nil { t.Error(err) - } else if dbted.Cmp(decimal.New(22, 1)) != 0 { // only 1.2 is possible due to increment - t.Errorf("debited: %s, cmp: %v", dbted, dbted.Cmp(new(decimal.Big).SetFloat64(1.2))) - } else if cb.blnCfg.Units.Cmp(decimal.New(-95, 2)) != 0 { - t.Errorf("balance remaining: %f", cb.blnCfg.Units) + } else if dbted.Cmp(decimal.New(225, 2)) != 0 { // 2.25 debited + t.Errorf("debited: %s", dbted) + } else if cb.blnCfg.Units.Cmp(decimal.New(-1, 0)) != 0 { + t.Errorf("balance remaining: %s", cb.blnCfg.Units) } + //with increment and unlimited balance cb = &concreteBalance{ blnCfg: &utils.Balance{ @@ -86,20 +87,20 @@ func TestCBDebitUnits(t *testing.T) { Opts: map[string]interface{}{ utils.MetaBalanceUnlimited: true, }, - Units: &utils.Decimal{decimal.New(125, 2)}, + Units: utils.NewDecimal(125, 2), // 1.25 }, fltrS: new(engine.FilterS), } - if dbted, _, err := cb.debitUnits( - utils.NewDecimal(25, 1), //2.5 - utils.NewDecimal(1, 1), //0.1 + toDebit = utils.NewDecimal(25, 1) // 2.5 + if dbted, _, err := cb.debitUnits(toDebit, "cgrates.org", utils.MapStorage{}); err != nil { t.Error(err) - } else if dbted.Cmp(decimal.New(25, 1)) != 0 { // only 1.2 is possible due to increment - t.Errorf("debited: %s, cmp: %v", dbted, dbted.Cmp(new(decimal.Big).SetFloat64(1.2))) + } else if dbted.Cmp(decimal.New(25, 1)) != 0 { // debit more than available since we have unlimited + t.Errorf("debited: %s", dbted) } else if cb.blnCfg.Units.Cmp(decimal.New(-125, 2)) != 0 { - t.Errorf("balance remaining: %f", cb.blnCfg.Units) + t.Errorf("balance remaining: %s", cb.blnCfg.Units) } + //with increment and positive limit cb = &concreteBalance{ blnCfg: &utils.Balance{ @@ -108,18 +109,18 @@ func TestCBDebitUnits(t *testing.T) { Opts: map[string]interface{}{ utils.MetaBalanceLimit: utils.NewDecimal(5, 1), // 0.5 as limit }, - Units: &utils.Decimal{decimal.New(125, 2)}, + Units: utils.NewDecimal(125, 2), // 1.25 }, fltrS: new(engine.FilterS), } - if dbted, _, err := cb.debitUnits( - utils.NewDecimal(25, 1), //2.5 - utils.NewDecimal(1, 1), //0.1 + toDebit = utils.NewDecimal(25, 1) //2.5 + if dbted, _, err := cb.debitUnits(toDebit, "cgrates.org", utils.MapStorage{}); err != nil { t.Error(err) - } else if dbted.Cmp(decimal.New(7, 1)) != 0 { // only 1.2 is possible due to increment - t.Errorf("debited: %s, cmp: %v", dbted, dbted.Cmp(new(decimal.Big).SetFloat64(1.2))) - } else if cb.blnCfg.Units.Cmp(decimal.New(55, 2)) != 0 { - t.Errorf("balance remaining: %f", cb.blnCfg.Units) + } else if dbted.Cmp(decimal.New(75, 2)) != 0 { // limit is 0.5 + t.Errorf("debited: %s", dbted) + } else if cb.blnCfg.Units.Cmp(decimal.New(5, 1)) != 0 { + t.Errorf("balance remaining: %s", cb.blnCfg.Units) } + }