From b76d61281c62691cda5e42eb96d9204c38cea23c Mon Sep 17 00:00:00 2001 From: ionutboangiu Date: Tue, 9 Jan 2024 08:08:16 -0500 Subject: [PATCH] Implement new *transfer_balance action Added possibility to mock datamanager account functions. Fixed typo in SubtractValue function name. Added unit & integration tests. --- engine/account.go | 8 +- engine/action.go | 79 +++++ engine/actions_test.go | 400 ++++++++++++++++++++++ engine/balances.go | 18 +- engine/datadbmock.go | 18 +- general_tests/transfer_balance_it_test.go | 166 +++++++++ utils/consts.go | 1 + 7 files changed, 674 insertions(+), 16 deletions(-) create mode 100644 general_tests/transfer_balance_it_test.go diff --git a/engine/account.go b/engine/account.go index dfdfa19de..7402804c4 100644 --- a/engine/account.go +++ b/engine/account.go @@ -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 { diff --git a/engine/action.go b/engine/action.go index b94ad7663..770c40b45 100644 --- a/engine/action.go +++ b/engine/action.go @@ -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: diff --git a/engine/actions_test.go b/engine/actions_test.go index e51960ab5..2f330a00f 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -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) + }) + } +} diff --git a/engine/balances.go b/engine/balances.go index b12087faf..cc983d3e0 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -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, diff --git a/engine/datadbmock.go b/engine/datadbmock.go index 57a9ed4f4..5c9637210 100644 --- a/engine/datadbmock.go +++ b/engine/datadbmock.go @@ -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 } diff --git a/general_tests/transfer_balance_it_test.go b/general_tests/transfer_balance_it_test.go new file mode 100644 index 000000000..2151713a5 --- /dev/null +++ b/general_tests/transfer_balance_it_test.go @@ -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 +*/ +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) + } + }) +} diff --git a/utils/consts.go b/utils/consts.go index bee24fe8c..e69631421 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1034,6 +1034,7 @@ const ( MetaTopUp = "*topup" MetaDebitReset = "*debit_reset" MetaDebit = "*debit" + MetaTransferBalance = "*transfer_balance" MetaResetCounters = "*reset_counters" MetaEnableAccount = "*enable_account" MetaDisableAccount = "*disable_account"