mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
Implement new *transfer_balance action
Added possibility to mock datamanager account functions. Fixed typo in SubtractValue function name. Added unit & integration tests.
This commit is contained in:
committed by
Dan Christian Bogos
parent
57413b1346
commit
b76d61281c
@@ -214,7 +214,7 @@ func (acc *Account) debitBalanceAction(a *Action, reset, resetIfNegative bool, f
|
||||
if reset || (resetIfNegative && b.Value < 0) {
|
||||
b.SetValue(0)
|
||||
}
|
||||
b.SubstractValue(bClone.GetValue())
|
||||
b.SubtractValue(bClone.GetValue())
|
||||
b.dirty = true
|
||||
found = true
|
||||
a.balanceValue = b.GetValue()
|
||||
@@ -527,7 +527,7 @@ func (acc *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun bo
|
||||
}
|
||||
cost := increment.Cost
|
||||
defaultBalance := acc.GetDefaultMoneyBalance()
|
||||
defaultBalance.SubstractValue(cost)
|
||||
defaultBalance.SubtractValue(cost)
|
||||
|
||||
increment.BalanceInfo.Monetary = &MonetaryInfo{
|
||||
UUID: defaultBalance.Uuid,
|
||||
@@ -868,7 +868,7 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, ufMoneyBalances Balances, c
|
||||
var connectFeePaid bool
|
||||
for _, b := range ufMoneyBalances {
|
||||
if b.GetValue() >= connectFee {
|
||||
b.SubstractValue(connectFee)
|
||||
b.SubtractValue(connectFee)
|
||||
// the conect fee is not refundable!
|
||||
if count {
|
||||
acc.countUnits(connectFee, utils.MetaMonetary, cc, b, fltrS)
|
||||
@@ -886,7 +886,7 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, ufMoneyBalances Balances, c
|
||||
cc.negativeConnectFee = true
|
||||
// there are no money for the connect fee; go negative
|
||||
b := acc.GetDefaultMoneyBalance()
|
||||
b.SubstractValue(connectFee)
|
||||
b.SubtractValue(connectFee)
|
||||
debitedBalance = *b
|
||||
// the conect fee is not refundable!
|
||||
if count {
|
||||
|
||||
@@ -91,6 +91,7 @@ func init() {
|
||||
actionFuncMap[utils.MetaTopUp] = topupAction
|
||||
actionFuncMap[utils.MetaDebitReset] = debitResetAction
|
||||
actionFuncMap[utils.MetaDebit] = debitAction
|
||||
actionFuncMap[utils.MetaTransferBalance] = transferBalanceAction
|
||||
actionFuncMap[utils.MetaResetCounters] = resetCountersAction
|
||||
actionFuncMap[utils.MetaEnableAccount] = enableAccountAction
|
||||
actionFuncMap[utils.MetaDisableAccount] = disableAccountAction
|
||||
@@ -122,6 +123,84 @@ func RegisterActionFunc(action string, f actionTypeFunc) {
|
||||
actionFuncMap[action] = f
|
||||
}
|
||||
|
||||
// transferBalanceAction transfers units between accounts' balances.
|
||||
// It ensures both source and destination balances are of the same type and non-expired.
|
||||
// Destination account and balance IDs are obtained from Action's ExtraParameters.
|
||||
// ExtraParameters should be a JSON string containing keys 'DestAccountID' and 'DestBalanceID',
|
||||
// which identify the destination account and balance for the transfer.
|
||||
func transferBalanceAction(srcAcc *Account, act *Action, _ Actions, fltrS *FilterS, _ any) error {
|
||||
if srcAcc == nil {
|
||||
return errors.New("source account is nil")
|
||||
}
|
||||
if act.Balance.Type == nil {
|
||||
return errors.New("balance type is missing")
|
||||
}
|
||||
if act.Balance.ID == nil {
|
||||
return errors.New("source balance ID is missing")
|
||||
}
|
||||
if act.ExtraParameters == "" {
|
||||
return errors.New("ExtraParameters used to identify the destination balance are missing")
|
||||
}
|
||||
if len(srcAcc.BalanceMap) == 0 {
|
||||
return fmt.Errorf("account %s has no balances to transfer from", srcAcc.ID)
|
||||
}
|
||||
|
||||
srcBalance := srcAcc.GetBalanceWithID(*act.Balance.Type, *act.Balance.ID)
|
||||
if srcBalance == nil || srcBalance.IsExpiredAt(time.Now()) {
|
||||
return errors.New("source balance not found or expired")
|
||||
}
|
||||
|
||||
transferUnits := act.Balance.GetValue()
|
||||
if transferUnits == 0 {
|
||||
return errors.New("balance value is missing or 0")
|
||||
}
|
||||
if transferUnits > srcBalance.Value {
|
||||
return utils.ErrInsufficientCredit
|
||||
}
|
||||
|
||||
accDestInfo := struct {
|
||||
DestAccountID string
|
||||
DestBalanceID string
|
||||
}{}
|
||||
if err := json.Unmarshal([]byte(act.ExtraParameters), &accDestInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This guard is meant to lock the destination account as we are making changes to it. It was not needed
|
||||
// for the source account due to it being locked from outside this function.
|
||||
guardErr := guardian.Guardian.Guard(func() error {
|
||||
destAcc, err := dm.GetAccount(accDestInfo.DestAccountID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving destination account failed: %w", err)
|
||||
}
|
||||
destBalance := destAcc.GetBalanceWithID(*act.Balance.Type, accDestInfo.DestBalanceID)
|
||||
if destBalance == nil || destBalance.IsExpiredAt(time.Now()) {
|
||||
return errors.New("destination balance not found or expired")
|
||||
}
|
||||
|
||||
srcBalance.SubtractValue(transferUnits)
|
||||
srcBalance.dirty = true
|
||||
destBalance.AddValue(transferUnits)
|
||||
destBalance.dirty = true
|
||||
|
||||
destAcc.InitCounters()
|
||||
destAcc.ExecuteActionTriggers(act, fltrS)
|
||||
|
||||
if err := dm.SetAccount(destAcc); err != nil {
|
||||
return fmt.Errorf("updating destination account failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, config.CgrConfig().GeneralCfg().LockingTimeout, utils.AccountPrefix+accDestInfo.DestAccountID)
|
||||
if guardErr != nil {
|
||||
return guardErr
|
||||
}
|
||||
|
||||
// Execute action triggers for the source account. This account will be updated in the parent function.
|
||||
srcAcc.InitCounters()
|
||||
srcAcc.ExecuteActionTriggers(act, fltrS)
|
||||
return nil
|
||||
}
|
||||
|
||||
func logAction(ub *Account, a *Action, acs Actions, _ *FilterS, extraData any) (err error) {
|
||||
switch {
|
||||
case ub != nil:
|
||||
|
||||
@@ -4197,3 +4197,403 @@ func TestRemoveAccountActionLogg(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestActionsTransferBalance(t *testing.T) {
|
||||
cfg := config.NewDefaultCGRConfig()
|
||||
var srcAcc *Account
|
||||
var destAcc *Account
|
||||
db := &DataDBMock{
|
||||
SetAccountDrvF: func(acc *Account) error {
|
||||
switch {
|
||||
case strings.HasSuffix(acc.ID, "_FAIL"):
|
||||
return utils.ErrServerError
|
||||
case srcAcc.ID == acc.ID:
|
||||
srcAcc = acc
|
||||
case destAcc.ID == acc.ID:
|
||||
destAcc = acc
|
||||
default:
|
||||
return utils.ErrAccountNotFound
|
||||
}
|
||||
return nil
|
||||
},
|
||||
GetAccountDrvF: func(id string) (*Account, error) {
|
||||
if destAcc == nil || destAcc.ID != id {
|
||||
return nil, utils.ErrNotFound
|
||||
}
|
||||
return destAcc, nil
|
||||
},
|
||||
}
|
||||
mockedDM := NewDataManager(db, cfg.CacheCfg(), nil)
|
||||
originalDM := dm
|
||||
defer func() { dm = originalDM }()
|
||||
dm = mockedDM
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
srcAcc *Account
|
||||
destAcc *Account
|
||||
act *Action
|
||||
expectedSrcBalance float64
|
||||
expectedDestBalance float64
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "SuccessfulTransfer",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_SRC",
|
||||
Value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destAcc: &Account{
|
||||
ID: "cgrates.org:ACC_DEST",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_DEST",
|
||||
Value: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ActionType: utils.MetaTransferBalance,
|
||||
ExtraParameters: `{
|
||||
"DestAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestBalanceID": "BALANCE_DEST"
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSrcBalance: 7,
|
||||
expectedDestBalance: 8,
|
||||
},
|
||||
{
|
||||
name: "NilAccount",
|
||||
expectedErr: "source account is nil",
|
||||
},
|
||||
{
|
||||
name: "UnspecifiedBalanceType",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
Balance: &BalanceFilter{},
|
||||
},
|
||||
expectedErr: "balance type is missing",
|
||||
},
|
||||
{
|
||||
name: "UnspecifiedBalanceID",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
},
|
||||
},
|
||||
expectedErr: "source balance ID is missing",
|
||||
},
|
||||
{
|
||||
name: "MissingDestinationParameters",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
},
|
||||
},
|
||||
expectedErr: "ExtraParameters used to identify the destination balance are missing",
|
||||
},
|
||||
{
|
||||
name: "MissingSourceAccountBalances",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ExtraParameters: "invalid_params",
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
},
|
||||
},
|
||||
expectedErr: "account cgrates.org:ACC_SRC has no balances to transfer from",
|
||||
},
|
||||
{
|
||||
name: "SourceAccountBalanceNotFound",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "*default",
|
||||
Value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ExtraParameters: "invalid_params",
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
},
|
||||
},
|
||||
expectedErr: "source balance not found or expired",
|
||||
},
|
||||
{
|
||||
name: "InvalidExtraParameters",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_SRC",
|
||||
Value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ExtraParameters: "invalid_params",
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "invalid character 'i' looking for beginning of value",
|
||||
},
|
||||
{
|
||||
name: "DestinationAccountNotFound",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_SRC",
|
||||
Value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ExtraParameters: `{
|
||||
"DestAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestBalanceID": "BALANCE_DEST"
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "retrieving destination account failed: NOT_FOUND",
|
||||
},
|
||||
{
|
||||
name: "DestinationBalanceNotFound",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_SRC",
|
||||
Value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destAcc: &Account{
|
||||
ID: "cgrates.org:ACC_DEST",
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ExtraParameters: `{
|
||||
"DestAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestBalanceID": "BALANCE_DEST"
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "destination balance not found or expired",
|
||||
},
|
||||
{
|
||||
name: "TransferUnitsNotSpecifiedOr0",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_SRC",
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destAcc: &Account{
|
||||
ID: "cgrates.org:ACC_DEST",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_DEST",
|
||||
Value: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ExtraParameters: `{
|
||||
"DestAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestBalanceID": "BALANCE_DEST"
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "balance value is missing or 0",
|
||||
},
|
||||
{
|
||||
name: "NotEnoughFundsInSourceBalance",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_SRC",
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destAcc: &Account{
|
||||
ID: "cgrates.org:ACC_DEST",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_DEST",
|
||||
Value: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ExtraParameters: `{
|
||||
"DestAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestBalanceID": "BALANCE_DEST"
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "INSUFFICIENT_CREDIT",
|
||||
},
|
||||
{
|
||||
name: "DestinationBalanceFailedUpdate",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_SRC",
|
||||
Value: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destAcc: &Account{
|
||||
ID: "cgrates.org:ACC_DEST_FAIL",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_DEST",
|
||||
Value: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ExtraParameters: `{
|
||||
"DestAccountID": "cgrates.org:ACC_DEST_FAIL",
|
||||
"DestBalanceID": "BALANCE_DEST"
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
Type: utils.StringPointer(utils.MetaMonetary),
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "updating destination account failed: SERVER_ERROR",
|
||||
},
|
||||
}
|
||||
|
||||
verifyBalance := func(t *testing.T, acc *Account, expected float64, accType, balanceType string, balanceIdx int) {
|
||||
t.Helper()
|
||||
balance := acc.BalanceMap[balanceType]
|
||||
balanceValue := balance[balanceIdx].Value
|
||||
if balanceValue != expected {
|
||||
t.Errorf("received wrong %s balance value: expected %v, received %v",
|
||||
accType, expected, balanceValue)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srcAcc = tc.srcAcc
|
||||
destAcc = tc.destAcc
|
||||
err := transferBalanceAction(srcAcc, tc.act, nil, nil, nil)
|
||||
if tc.expectedErr != "" {
|
||||
if err == nil || err.Error() != tc.expectedErr {
|
||||
t.Errorf("expected error %v, received %v", tc.expectedErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyBalance(t, srcAcc, tc.expectedSrcBalance, "source", utils.MetaMonetary, 0)
|
||||
verifyBalance(t, destAcc, tc.expectedDestBalance, "destination", utils.MetaMonetary, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ func (b *Balance) AddValue(amount float64) {
|
||||
b.SetValue(b.GetValue() + amount)
|
||||
}
|
||||
|
||||
func (b *Balance) SubstractValue(amount float64) {
|
||||
func (b *Balance) SubtractValue(amount float64) {
|
||||
b.SetValue(b.GetValue() - amount)
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Bala
|
||||
globalRoundingDecimals, utils.MetaRoundingUp)
|
||||
}
|
||||
if b.GetValue() >= amount {
|
||||
b.SubstractValue(amount)
|
||||
b.SubtractValue(amount)
|
||||
inc.BalanceInfo.Unit = &UnitInfo{
|
||||
UUID: b.Uuid,
|
||||
ID: b.ID,
|
||||
@@ -477,7 +477,7 @@ func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Bala
|
||||
moneyBal = ub.GetDefaultMoneyBalance()
|
||||
}
|
||||
if b.GetValue() >= amount && (moneyBal != nil || cost == 0) {
|
||||
b.SubstractValue(amount)
|
||||
b.SubtractValue(amount)
|
||||
inc.BalanceInfo.Unit = &UnitInfo{
|
||||
UUID: b.Uuid,
|
||||
ID: b.ID,
|
||||
@@ -489,7 +489,7 @@ func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Bala
|
||||
}
|
||||
inc.BalanceInfo.AccountID = ub.ID
|
||||
if cost != 0 {
|
||||
moneyBal.SubstractValue(cost)
|
||||
moneyBal.SubtractValue(cost)
|
||||
inc.BalanceInfo.Monetary = &MonetaryInfo{
|
||||
UUID: moneyBal.Uuid,
|
||||
ID: moneyBal.ID,
|
||||
@@ -635,7 +635,7 @@ func (b *Balance) debitMoney(cd *CallDescriptor, ub *Account, moneyBalances Bala
|
||||
}
|
||||
|
||||
if b.GetValue() >= amount {
|
||||
b.SubstractValue(amount)
|
||||
b.SubtractValue(amount)
|
||||
cd.MaxCostSoFar += amount
|
||||
inc.BalanceInfo.Monetary = &MonetaryInfo{
|
||||
UUID: b.Uuid,
|
||||
@@ -880,7 +880,7 @@ func (b *Balance) debit(cd *CallDescriptor, ub *Account, moneyBalances Balances,
|
||||
globalRoundingDecimals, utils.MetaRoundingUp)
|
||||
}
|
||||
if b.GetValue() >= amount {
|
||||
b.SubstractValue(amount)
|
||||
b.SubtractValue(amount)
|
||||
inc.BalanceInfo.Unit = &UnitInfo{
|
||||
UUID: b.Uuid,
|
||||
ID: b.ID,
|
||||
@@ -1049,7 +1049,7 @@ func (b *Balance) debit(cd *CallDescriptor, ub *Account, moneyBalances Balances,
|
||||
}
|
||||
|
||||
if isUnitBal { // unit balance
|
||||
b.SubstractValue(amount)
|
||||
b.SubtractValue(amount)
|
||||
inc.BalanceInfo.Unit = &UnitInfo{
|
||||
UUID: b.Uuid,
|
||||
ID: b.ID,
|
||||
@@ -1061,7 +1061,7 @@ func (b *Balance) debit(cd *CallDescriptor, ub *Account, moneyBalances Balances,
|
||||
}
|
||||
inc.BalanceInfo.AccountID = ub.ID
|
||||
if cost != 0 {
|
||||
moneyBal.SubstractValue(cost)
|
||||
moneyBal.SubtractValue(cost)
|
||||
inc.BalanceInfo.Monetary = &MonetaryInfo{
|
||||
UUID: moneyBal.Uuid,
|
||||
ID: moneyBal.ID,
|
||||
@@ -1076,7 +1076,7 @@ func (b *Balance) debit(cd *CallDescriptor, ub *Account, moneyBalances Balances,
|
||||
}
|
||||
}
|
||||
} else { // monetary balance
|
||||
b.SubstractValue(cost)
|
||||
b.SubtractValue(cost)
|
||||
cd.MaxCostSoFar += cost
|
||||
inc.BalanceInfo.Monetary = &MonetaryInfo{
|
||||
UUID: b.Uuid,
|
||||
|
||||
@@ -43,6 +43,9 @@ type DataDBMock struct {
|
||||
RemoveActionPlanDrvF func(key string) (err error)
|
||||
GetRouteProfileDrvF func(tenant, id string) (rp *RouteProfile, err error)
|
||||
RemoveRouteProfileDrvF func(tenant, id string) error
|
||||
GetAccountDrvF func(id string) (*Account, error)
|
||||
SetAccountDrvF func(acc *Account) error
|
||||
RemoveAccountDrvF func(id string) error
|
||||
}
|
||||
|
||||
// Storage methods
|
||||
@@ -213,15 +216,24 @@ func (dbM *DataDBMock) PopTask() (*Task, error) {
|
||||
return nil, utils.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (dbM *DataDBMock) GetAccountDrv(string) (*Account, error) {
|
||||
func (dbM *DataDBMock) GetAccountDrv(id string) (*Account, error) {
|
||||
if dbM.GetAccountDrvF != nil {
|
||||
return dbM.GetAccountDrvF(id)
|
||||
}
|
||||
return nil, utils.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (dbM *DataDBMock) SetAccountDrv(*Account) error {
|
||||
func (dbM *DataDBMock) SetAccountDrv(acc *Account) error {
|
||||
if dbM.SetAccountDrvF != nil {
|
||||
return dbM.SetAccountDrvF(acc)
|
||||
}
|
||||
return utils.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (dbM *DataDBMock) RemoveAccountDrv(string) error {
|
||||
func (dbM *DataDBMock) RemoveAccountDrv(id string) error {
|
||||
if dbM.RemoveAccountDrvF != nil {
|
||||
return dbM.RemoveAccountDrvF(id)
|
||||
}
|
||||
return utils.ErrNotImplemented
|
||||
}
|
||||
|
||||
|
||||
166
general_tests/transfer_balance_it_test.go
Normal file
166
general_tests/transfer_balance_it_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
package general_tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/cgrates/birpc/context"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
// TestTransferBalance tests the implementation of the "*transfer_balance" action.
|
||||
//
|
||||
// The test steps are as follows:
|
||||
// 1. Create two accounts with a single *monetary balance of 10 units.
|
||||
// 2. Set a "*transfer_balance" action that takes 4 units from the source balance and adds them to the destination balance.
|
||||
// 3. Execute that action using the method "APIerSv1.ExecuteAction".
|
||||
// 4. Check the balances; the source balance should now have 6 units while the destination balance should have 14.
|
||||
|
||||
func TestTransferBalance(t *testing.T) {
|
||||
switch *dbType {
|
||||
case utils.MetaInternal:
|
||||
case utils.MetaMySQL, utils.MetaMongo, utils.MetaPostgres:
|
||||
t.SkipNow()
|
||||
default:
|
||||
t.Fatal("unsupported dbtype value")
|
||||
}
|
||||
|
||||
content := `{
|
||||
|
||||
"data_db": {
|
||||
"db_type": "*internal"
|
||||
},
|
||||
|
||||
"stor_db": {
|
||||
"db_type": "*internal"
|
||||
},
|
||||
|
||||
"schedulers": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
"apiers": {
|
||||
"enabled": true,
|
||||
"scheduler_conns": ["*internal"]
|
||||
}
|
||||
|
||||
}`
|
||||
|
||||
tpFiles := map[string]string{
|
||||
utils.AccountActionsCsv: `#Tenant,Account,ActionPlanId,ActionTriggersId,AllowNegative,Disabled
|
||||
cgrates.org,ACC_SRC,PACKAGE_ACC_SRC,,,
|
||||
cgrates.org,ACC_DEST,PACKAGE_ACC_DEST,,,`,
|
||||
utils.ActionPlansCsv: `#Id,ActionsId,TimingId,Weight
|
||||
PACKAGE_ACC_SRC,ACT_TOPUP_SRC,*asap,10
|
||||
PACKAGE_ACC_DEST,ACT_TOPUP_DEST,*asap,10`,
|
||||
utils.ActionsCsv: `#ActionsId[0],Action[1],ExtraParameters[2],Filter[3],BalanceId[4],BalanceType[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingIds[11],Units[12],BalanceWeight[13],BalanceBlocker[14],BalanceDisabled[15],Weight[16]
|
||||
ACT_TOPUP_SRC,*topup_reset,,,balance_src,*monetary,,*any,,,*unlimited,,10,20,false,false,20
|
||||
ACT_TOPUP_DEST,*topup_reset,,,balance_dest,*monetary,,*any,,,*unlimited,,10,10,false,false,10
|
||||
ACT_TRANSFER,*transfer_balance,"{""DestAccountID"":""cgrates.org:ACC_DEST"",""DestBalanceID"":""balance_dest""}",,balance_src,*monetary,,,,,*unlimited,,4,,,,`,
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
testEnv := TestEnvironment{
|
||||
Name: "TestTransferBalance",
|
||||
// Encoding: *encoding,
|
||||
ConfigJSON: content,
|
||||
TpFiles: tpFiles,
|
||||
LogBuffer: buf,
|
||||
}
|
||||
client, _, shutdown, err := testEnv.Setup(t, *waitRater)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer shutdown()
|
||||
|
||||
t.Run("CheckInitialBalances", func(t *testing.T) {
|
||||
var acnts []*engine.Account
|
||||
if err := client.Call(context.Background(), utils.APIerSv2GetAccounts,
|
||||
&utils.AttrGetAccounts{
|
||||
Tenant: "cgrates.org",
|
||||
}, &acnts); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(acnts) != 2 {
|
||||
t.Fatal("expecting 2 accounts to be retrieved")
|
||||
}
|
||||
sort.Slice(acnts, func(i, j int) bool {
|
||||
return acnts[i].ID > acnts[j].ID
|
||||
})
|
||||
if len(acnts[0].BalanceMap) != 1 || len(acnts[0].BalanceMap[utils.MetaMonetary]) != 1 {
|
||||
t.Fatalf("expected account to have only one balance of type *monetary, received %v", acnts[0])
|
||||
}
|
||||
balance := acnts[0].BalanceMap[utils.MetaMonetary][0]
|
||||
if balance.ID != "balance_src" || balance.Value != 10 {
|
||||
t.Fatalf("received account with unexpected balance: %v", balance)
|
||||
}
|
||||
if len(acnts[1].BalanceMap) != 1 || len(acnts[1].BalanceMap[utils.MetaMonetary]) != 1 {
|
||||
t.Fatalf("expected account to have only one balance of type *monetary, received %v", acnts[1])
|
||||
}
|
||||
balance = acnts[1].BalanceMap[utils.MetaMonetary][0]
|
||||
if balance.ID != "balance_dest" || balance.Value != 10 {
|
||||
t.Fatalf("received account with unexpected balance: %v", balance)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TransferBalance", func(t *testing.T) {
|
||||
var reply string
|
||||
attrsEA := &utils.AttrExecuteAction{Tenant: "cgrates.org", Account: "ACC_SRC", ActionsId: "ACT_TRANSFER"}
|
||||
if err := client.Call(context.Background(), utils.APIerSv1ExecuteAction, attrsEA, &reply); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CheckFinalBalances", func(t *testing.T) {
|
||||
var acnts []*engine.Account
|
||||
if err := client.Call(context.Background(), utils.APIerSv2GetAccounts,
|
||||
&utils.AttrGetAccounts{
|
||||
Tenant: "cgrates.org",
|
||||
}, &acnts); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(acnts) != 2 {
|
||||
t.Fatal("expecting 2 accounts to be retrieved")
|
||||
}
|
||||
sort.Slice(acnts, func(i, j int) bool {
|
||||
return acnts[i].ID > acnts[j].ID
|
||||
})
|
||||
if len(acnts[0].BalanceMap) != 1 || len(acnts[0].BalanceMap[utils.MetaMonetary]) != 1 {
|
||||
t.Errorf("expected account to have only one balance of type *monetary, received %v", acnts[0])
|
||||
}
|
||||
balance := acnts[0].BalanceMap[utils.MetaMonetary][0]
|
||||
if balance.ID != "balance_src" || balance.Value != 6 {
|
||||
t.Errorf("received account with unexpected balance: %v", balance)
|
||||
}
|
||||
if len(acnts[1].BalanceMap) != 1 || len(acnts[1].BalanceMap[utils.MetaMonetary]) != 1 {
|
||||
t.Errorf("expected account to have only one balance of type *monetary, received %v", acnts[1])
|
||||
}
|
||||
balance = acnts[1].BalanceMap[utils.MetaMonetary][0]
|
||||
if balance.ID != "balance_dest" || balance.Value != 14 {
|
||||
t.Errorf("received account with unexpected balance: %v", balance)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1034,6 +1034,7 @@ const (
|
||||
MetaTopUp = "*topup"
|
||||
MetaDebitReset = "*debit_reset"
|
||||
MetaDebit = "*debit"
|
||||
MetaTransferBalance = "*transfer_balance"
|
||||
MetaResetCounters = "*reset_counters"
|
||||
MetaEnableAccount = "*enable_account"
|
||||
MetaDisableAccount = "*disable_account"
|
||||
|
||||
Reference in New Issue
Block a user