mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
Add reference value functionality to *transfer_balance action
The *transfer_balance action can now use a reference value to ensure the destination balance reaches a specified amount. If the destination balance exceeds the reference value, the excess is transferred back to the source balance. If the destination balance is below the reference value, the required amount is transferred from the source balance to the destination balance to reach the specified reference value. An error is returned if the transfer cannot achieve the specified reference value. Used by specifying DestinationReferenceValue inside ExtraParameters. Other *transfer_balance changes: - used json tags when unmarshaling ExtraParameters in order to be able to shorten the names of the fields - lock the destination account only if it's different from the source account. It is still passed to the Guard function but without a lock key and with 0 timeout. - if the transfer happens within the same account, update the account and execute its ActionTriggers only once. - moved transfer units validation after retrieving/creating the destination balance *cdrlog action has been updated to create cdrs for reference *transfer_balance actions, although improvements are needed and the functionality is not completely tested. APIerSv1.TransferBalance has been updated to take into account the ReferenceValue parameter. Added new *transfer_balance action unit tests to account for the new changes. Added integration tests (incomplete for now, but functionality has been tested manually).
This commit is contained in:
committed by
Dan Christian Bogos
parent
1fd77cafa5
commit
49f6c5982e
@@ -714,7 +714,7 @@ func (apierSv1 *APIerSv1) RemoveBalances(ctx *context.Context, attr *utils.AttrS
|
||||
}
|
||||
|
||||
// TransferBalance initiates a balance transfer between accounts, immediately executing the configured actions.
|
||||
func (apierSv1 *APIerSv1) TransferBalance(ctx *context.Context, attr utils.AttrTransferBalance, reply *string) (err error) {
|
||||
func (apierSv1 *APIerSv1) TransferBalance(ctx *context.Context, attr utils.AttrTransferBalance, reply *string) error {
|
||||
|
||||
// Check for missing mandatory fields in the request attributes.
|
||||
if missing := utils.MissingStructFields(&attr, []string{
|
||||
@@ -729,12 +729,18 @@ func (apierSv1 *APIerSv1) TransferBalance(ctx *context.Context, attr utils.AttrT
|
||||
attr.Tenant = apierSv1.Config.GeneralCfg().DefaultTenant
|
||||
}
|
||||
|
||||
// Marshal extra parameters including the destination account and balance ID.
|
||||
var extraParams []byte
|
||||
extraParams, err = json.Marshal(map[string]string{
|
||||
utils.DestinationAccountID: attr.Tenant + ":" + attr.DestinationAccountID,
|
||||
utils.DestinationBalanceID: attr.DestinationBalanceID,
|
||||
})
|
||||
// Marshal extra parameters including the destination account, balance ID,
|
||||
// and optional reference value.
|
||||
tmp := struct {
|
||||
DestinationAccountID string
|
||||
DestinationBalanceID string
|
||||
DestinationReferenceValue *float64
|
||||
}{
|
||||
DestinationAccountID: attr.Tenant + ":" + attr.DestinationAccountID,
|
||||
DestinationBalanceID: attr.DestinationBalanceID,
|
||||
DestinationReferenceValue: attr.DestinationReferenceValue,
|
||||
}
|
||||
extraParams, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
return utils.NewErrServerError(err)
|
||||
}
|
||||
|
||||
128
engine/action.go
128
engine/action.go
@@ -152,9 +152,9 @@ func RegisterActionFunc(action string, f actionTypeFunc) {
|
||||
|
||||
// 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.
|
||||
// Destination account and balance IDs, and optionally a reference value, are obtained from Action's ExtraParameters.
|
||||
// If a reference value is specified, the transfer ensures the destination balance reaches this value.
|
||||
// If the destination account is different from the source, it is locked during the transfer.
|
||||
func transferBalanceAction(srcAcc *Account, act *Action, _ Actions, fltrS *FilterS, _ any, _ time.Time, _ ActionConnCfg) error {
|
||||
if srcAcc == nil {
|
||||
return errors.New("source account is nil")
|
||||
@@ -174,66 +174,92 @@ func transferBalanceAction(srcAcc *Account, act *Action, _ Actions, fltrS *Filte
|
||||
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 srcBalance.ID != utils.MetaDefault && transferUnits > srcBalance.Value {
|
||||
return utils.ErrInsufficientCredit
|
||||
}
|
||||
|
||||
accDestInfo := struct {
|
||||
DestinationAccountID string
|
||||
DestinationBalanceID string
|
||||
destInfo := struct {
|
||||
AccID string `json:"DestinationAccountID"`
|
||||
BalID string `json:"DestinationBalanceID"`
|
||||
RefVal *float64 `json:"DestinationReferenceValue"`
|
||||
}{}
|
||||
if err := json.Unmarshal([]byte(act.ExtraParameters), &accDestInfo); err != nil {
|
||||
if err := json.Unmarshal([]byte(act.ExtraParameters), &destInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lock the destination account if different from source, otherwise
|
||||
// pass without lock key and timeout.
|
||||
diffAcnts := srcAcc.ID != destInfo.AccID
|
||||
var lockTimeout time.Duration
|
||||
lockKeys := make([]string, 0, 1)
|
||||
if diffAcnts {
|
||||
lockTimeout = config.CgrConfig().GeneralCfg().LockingTimeout
|
||||
lockKeys = append(lockKeys, utils.AccountPrefix+destInfo.AccID)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// to it. It is 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.DestinationAccountID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving destination account failed: %w", err)
|
||||
|
||||
var destAcc *Account
|
||||
switch diffAcnts {
|
||||
case true:
|
||||
var err error
|
||||
if destAcc, err = dm.GetAccount(destInfo.AccID); err != nil {
|
||||
return fmt.Errorf("retrieving destination account failed: %w", err)
|
||||
}
|
||||
case false:
|
||||
destAcc = srcAcc
|
||||
}
|
||||
|
||||
if destAcc.BalanceMap == nil {
|
||||
destAcc.BalanceMap = make(map[string]Balances)
|
||||
}
|
||||
|
||||
// We look for the destination balance only through balances of the same
|
||||
// type as the source balance.
|
||||
destBalance := destAcc.GetBalanceWithID(srcBalanceType, accDestInfo.DestinationBalanceID)
|
||||
// Look for the destination balance only through balances of the same type as the source balance.
|
||||
destBalance := destAcc.GetBalanceWithID(srcBalanceType, destInfo.BalID)
|
||||
if destBalance != nil && destBalance.IsExpiredAt(time.Now()) {
|
||||
return errors.New("destination balance expired")
|
||||
}
|
||||
|
||||
if destBalance == nil {
|
||||
// Destination Balance was not found. It will be
|
||||
// created and added to the balance map.
|
||||
// Destination Balance was not found. Create it and add it to the balance map.
|
||||
destBalance = &Balance{
|
||||
ID: accDestInfo.DestinationBalanceID,
|
||||
ID: destInfo.BalID,
|
||||
Uuid: utils.GenUUID(),
|
||||
}
|
||||
destAcc.BalanceMap[srcBalanceType] = append(destAcc.BalanceMap[srcBalanceType], destBalance)
|
||||
}
|
||||
|
||||
// If DestinationReferenceValue is specified adjust transferUnits to make the
|
||||
// destination balance match the DestinationReferenceValue.
|
||||
transferUnits := act.Balance.GetValue()
|
||||
if destInfo.RefVal != nil {
|
||||
transferUnits = *destInfo.RefVal - destBalance.Value
|
||||
}
|
||||
if transferUnits == 0 {
|
||||
return errors.New("transfer amount is missing or 0")
|
||||
}
|
||||
if srcBalance.ID != utils.MetaDefault && transferUnits > srcBalance.Value {
|
||||
return fmt.Errorf("insufficient credits in source balance %q (account %q) for transfer of %.2f units",
|
||||
srcBalance.ID, srcAcc.ID, transferUnits)
|
||||
}
|
||||
if destBalance.ID != utils.MetaDefault && -transferUnits > destBalance.Value {
|
||||
return fmt.Errorf("insufficient credits in destination balance %q (account %q) for transfer of %.2f units",
|
||||
destBalance.ID, destAcc.ID, transferUnits)
|
||||
}
|
||||
|
||||
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)
|
||||
if diffAcnts {
|
||||
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.DestinationAccountID)
|
||||
}, lockTimeout, lockKeys...)
|
||||
if guardErr != nil {
|
||||
return guardErr
|
||||
}
|
||||
@@ -401,18 +427,40 @@ func cdrLogAction(acc *Account, a *Action, acs Actions, _ *FilterS, extraData an
|
||||
}
|
||||
continue
|
||||
case utils.MetaTransferBalance:
|
||||
cdr.Cost = action.Balance.GetValue()
|
||||
cdr.Account = utils.SplitConcatenatedKey(acc.ID)[1] // Extract ID from TenantID.
|
||||
accDestInfo := struct {
|
||||
DestinationAccountID string
|
||||
DestinationBalanceID string
|
||||
destInfo := struct {
|
||||
AccID string `json:"DestinationAccountID"`
|
||||
BalID string `json:"DestinationBalanceID"`
|
||||
RefVal *float64 `json:"DestinationReferenceValue"`
|
||||
}{}
|
||||
if err := json.Unmarshal([]byte(action.ExtraParameters), &accDestInfo); err != nil {
|
||||
if err := json.Unmarshal([]byte(action.ExtraParameters), &destInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
cdr.Destination = utils.SplitConcatenatedKey(accDestInfo.DestinationAccountID)[1] // Extract ID from TenantID.
|
||||
|
||||
transferUnits := action.Balance.GetValue()
|
||||
if destInfo.RefVal != nil {
|
||||
tmpDestVal := 0.0
|
||||
_, srcBalanceType := acc.FindBalanceByID(*action.Balance.ID)
|
||||
destAcc := acc
|
||||
if acc.ID != destInfo.AccID {
|
||||
destAcc, err = dm.GetAccount(destInfo.AccID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving destination account failed: %w", err)
|
||||
}
|
||||
}
|
||||
if destAcc.BalanceMap != nil {
|
||||
destBalance := destAcc.GetBalanceWithID(srcBalanceType, destInfo.BalID)
|
||||
if destBalance != nil {
|
||||
tmpDestVal = destBalance.Value
|
||||
}
|
||||
}
|
||||
transferUnits = *destInfo.RefVal - tmpDestVal
|
||||
}
|
||||
cdr.Cost = transferUnits
|
||||
cdr.Destination = utils.SplitConcatenatedKey(destInfo.AccID)[1] // Extract ID from TenantID.
|
||||
cdr.ExtraFields[utils.SourceBalanceID] = *action.Balance.ID
|
||||
cdr.ExtraFields[utils.DestinationBalanceID] = accDestInfo.DestinationBalanceID
|
||||
cdr.ExtraFields[utils.DestinationBalanceID] = destInfo.BalID
|
||||
cdr.ExtraFields[utils.DestinationAccountID] = destInfo.AccID
|
||||
}
|
||||
|
||||
cdrs = append(cdrs, cdr)
|
||||
|
||||
@@ -4274,6 +4274,169 @@ func TestActionsTransferBalance(t *testing.T) {
|
||||
expectedSrcBalance: 7,
|
||||
expectedDestBalance: 8,
|
||||
},
|
||||
{
|
||||
name: "SuccessfulTransferNegativeSrcBalance",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "*default",
|
||||
Value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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: `{
|
||||
"DestinationAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestinationBalanceID": "BALANCE_DEST"
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
ID: utils.StringPointer("*default"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSrcBalance: -1,
|
||||
expectedDestBalance: 8,
|
||||
},
|
||||
{
|
||||
name: "SuccessfulTransferNegativeDestBalance",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "BALANCE_SRC",
|
||||
Value: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destAcc: &Account{
|
||||
ID: "cgrates.org:ACC_DEST",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "*default",
|
||||
Value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ActionType: utils.MetaTransferBalance,
|
||||
ExtraParameters: `{
|
||||
"DestinationAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestinationBalanceID": "*default"
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: -3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSrcBalance: 8,
|
||||
expectedDestBalance: -1,
|
||||
},
|
||||
{
|
||||
name: "SuccessfulTransferRefNegativeSrcBalance",
|
||||
srcAcc: &Account{
|
||||
ID: "cgrates.org:ACC_SRC",
|
||||
BalanceMap: map[string]Balances{
|
||||
utils.MetaMonetary: {
|
||||
{
|
||||
ID: "*default",
|
||||
Value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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: `{
|
||||
"DestinationAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestinationBalanceID": "BALANCE_DEST",
|
||||
"DestinationReferenceValue": 8
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
ID: utils.StringPointer("*default"),
|
||||
},
|
||||
},
|
||||
expectedSrcBalance: -1,
|
||||
expectedDestBalance: 8,
|
||||
},
|
||||
{
|
||||
name: "SuccessfulTransferRefNegativeDestBalance",
|
||||
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: "*default",
|
||||
Value: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
act: &Action{
|
||||
Id: "ACT_TRANSFER_BALANCE",
|
||||
ActionType: utils.MetaTransferBalance,
|
||||
ExtraParameters: `{
|
||||
"DestinationAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestinationBalanceID": "*default",
|
||||
"DestinationReferenceValue": -0.01
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSrcBalance: 15.01,
|
||||
expectedDestBalance: -0.01,
|
||||
},
|
||||
{
|
||||
name: "NilAccount",
|
||||
expectedErr: "source account is nil",
|
||||
@@ -4460,7 +4623,7 @@ func TestActionsTransferBalance(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "balance value is missing or 0",
|
||||
expectedErr: "transfer amount is missing or 0",
|
||||
},
|
||||
{
|
||||
name: "NotEnoughFundsInSourceBalance",
|
||||
@@ -4499,7 +4662,47 @@ func TestActionsTransferBalance(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "INSUFFICIENT_CREDIT",
|
||||
expectedErr: `insufficient credits in source balance "BALANCE_SRC" (account "cgrates.org:ACC_SRC") for transfer of 3.00 units`,
|
||||
},
|
||||
{
|
||||
name: "NotEnoughFundsInDestBalance",
|
||||
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: `{
|
||||
"DestinationAccountID": "cgrates.org:ACC_DEST",
|
||||
"DestinationBalanceID": "BALANCE_DEST",
|
||||
"DestinationReferenceValue": -0.01
|
||||
}`,
|
||||
Balance: &BalanceFilter{
|
||||
ID: utils.StringPointer("BALANCE_SRC"),
|
||||
Value: &utils.ValueFormula{ // should be ignored (overwritten by the ReferenceValue)
|
||||
Static: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: `insufficient credits in destination balance "BALANCE_DEST" (account "cgrates.org:ACC_DEST") for transfer of -5.01 units`,
|
||||
},
|
||||
{
|
||||
name: "DestinationBalanceFailedUpdate",
|
||||
|
||||
@@ -21,6 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package general_tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -304,3 +305,181 @@ ACT_TRANSFER,*transfer_balance,"{""DestinationAccountID"":""cgrates.org:ACC_DEST
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestATExportAndTransfer(t *testing.T) {
|
||||
switch *utils.DBType {
|
||||
case utils.MetaInternal:
|
||||
case utils.MetaMySQL, utils.MetaMongo, utils.MetaPostgres:
|
||||
t.SkipNow()
|
||||
default:
|
||||
t.Fatal("unsupported dbtype value")
|
||||
}
|
||||
|
||||
content := `{
|
||||
|
||||
"general": {
|
||||
"log_level": 7,
|
||||
"reply_timeout": "30s"
|
||||
},
|
||||
|
||||
"data_db": {
|
||||
"db_type": "*internal"
|
||||
},
|
||||
|
||||
"stor_db": {
|
||||
"db_type": "*internal"
|
||||
},
|
||||
|
||||
"cdrs": {
|
||||
"enabled": true,
|
||||
"rals_conns": ["*localhost"]
|
||||
},
|
||||
|
||||
"rals": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
"schedulers": {
|
||||
"enabled": true,
|
||||
"cdrs_conns": ["*internal"]
|
||||
},
|
||||
|
||||
"apiers": {
|
||||
"enabled": true,
|
||||
"scheduler_conns": ["*internal"],
|
||||
"ees_conns": ["*localhost"]
|
||||
},
|
||||
|
||||
"ees": {
|
||||
"enabled": true,
|
||||
"exporters": [
|
||||
{
|
||||
"id": "test_exporter",
|
||||
"type": "*virt",
|
||||
"flags": ["*log"],
|
||||
"attempts": 1,
|
||||
"synchronous": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}`
|
||||
|
||||
tpFiles := map[string]string{
|
||||
utils.AccountActionsCsv: `#Tenant,Account,ActionPlanId,ActionTriggersId,AllowNegative,Disabled
|
||||
cgrates.org,1001,PACKAGE_1001,EMPTY_BALANCE_TRIGGER,,`,
|
||||
utils.ActionPlansCsv: `#Id,ActionsId,TimingId,Weight
|
||||
PACKAGE_1001,ACT_TOPUP_INITIAL,*asap,
|
||||
#PACKAGE_1001,ACT_TOPUP_BUFFER,*asap,
|
||||
#PACKAGE_1001,ACT_TOPUP_TRANSFER,*monthly,`,
|
||||
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_INITIAL,*topup_reset,,,main,*data,,,,,*unlimited,,11000,10,,,20
|
||||
ACT_TOPUP_INITIAL,*transfer_balance,"{""DestinationAccountID"":""cgrates.org:1001"",""DestinationBalanceID"":""buffer"",""DestinationReferenceValue"":10000}",,main,,,,,,,,,,,,10
|
||||
ACT_TOPUP_INITIAL,*cdrlog,"{""ToR"":""*data""}",,,,,,,,,,,,,,0
|
||||
ACT_EXPORT,*export,test_exporter,,,,,,,,,,,,,,
|
||||
ACT_TOPUP_TRANSFER,*topup,,,main,*data,,,,,,,5000,,,,30
|
||||
ACT_TOPUP_TRANSFER,*transfer_balance,"{""DestinationAccountID"":""cgrates.org:1001"",""DestinationBalanceID"":""buffer"",""DestinationReferenceValue"":10000}",,main,,,,,,,,,,,,20
|
||||
ACT_TOPUP_TRANSFER,*cdrlog,"{""ToR"":""*data""}",,,,,,,,,,,,,,10
|
||||
ACT_TOPUP_TRANSFER,*reset_triggers,,,,,,,,,,,,,,,0`,
|
||||
utils.ActionTriggersCsv: `#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],ExpiryTime[6],ActivationTime[7],BalanceTag[8],BalanceType[9],BalanceCategories[10],BalanceDestinationIds[11],BalanceRatingSubject[12],BalanceSharedGroup[13],BalanceExpiryTime[14],BalanceTimingIds[15],BalanceWeight[16],BalanceBlocker[17],BalanceDisabled[18],ActionsId[19],Weight[20]
|
||||
EMPTY_BALANCE_TRIGGER,,*min_balance,9999,false,0,,,buffer,*data,,,,,,,,,,ACT_EXPORT,
|
||||
#EMPTY_BALANCE_TRIGGER,,*min_balance,0,false,0,,,buffer,*data,,,,,,,,,,ACT_EXPORT,`,
|
||||
}
|
||||
|
||||
testEnv := TestEnvironment{
|
||||
ConfigJSON: content,
|
||||
TpFiles: tpFiles,
|
||||
// LogBuffer: &bytes.Buffer{},
|
||||
}
|
||||
// defer fmt.Println(testEnv.LogBuffer)
|
||||
client, _ := testEnv.Setup(t, 0)
|
||||
|
||||
t.Run("ProcessCDRs", func(t *testing.T) {
|
||||
i := 0
|
||||
processCDRs := func(t *testing.T, count int, amount int) {
|
||||
// fmt.Println("===========ProcessCDRs===========")
|
||||
t.Helper()
|
||||
var reply string
|
||||
for range count {
|
||||
i++
|
||||
if err := client.Call(context.Background(), utils.CDRsV1ProcessEvent,
|
||||
&engine.ArgV1ProcessEvent{
|
||||
Flags: []string{utils.MetaRALs},
|
||||
CGREvent: utils.CGREvent{
|
||||
Tenant: "cgrates.org",
|
||||
ID: fmt.Sprintf("event%d", i),
|
||||
Event: map[string]any{
|
||||
utils.RunID: "*default",
|
||||
utils.Tenant: "cgrates.org",
|
||||
utils.Category: "data",
|
||||
utils.ToR: utils.MetaData,
|
||||
utils.OriginID: fmt.Sprintf("processCDR%d", i),
|
||||
utils.OriginHost: "127.0.0.1",
|
||||
utils.RequestType: utils.MetaPostpaid,
|
||||
utils.AccountField: "1001",
|
||||
utils.Destination: "1002",
|
||||
utils.SetupTime: time.Date(2021, time.February, 2, 16, 14, 50, 0, time.UTC),
|
||||
utils.AnswerTime: time.Date(2021, time.February, 2, 16, 15, 0, 0, time.UTC),
|
||||
utils.Usage: amount,
|
||||
},
|
||||
},
|
||||
}, &reply); err != nil {
|
||||
t.Errorf("CDRsV1ProcessEvent(%d) err: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkAccountAndCDRs := func(t *testing.T) {
|
||||
// fmt.Println("===========CheckAcc===========")
|
||||
t.Helper()
|
||||
var acnts []*engine.Account
|
||||
err := client.Call(context.Background(), utils.APIerSv2GetAccounts,
|
||||
&utils.AttrGetAccounts{
|
||||
Tenant: "cgrates.org",
|
||||
}, &acnts)
|
||||
t.Logf("APIerSv2GetAccounts err: %v", err)
|
||||
var cdrs []*engine.CDR
|
||||
err = client.Call(context.Background(), utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithAPIOpts{
|
||||
RPCCDRsFilter: &utils.RPCCDRsFilter{
|
||||
RunIDs: []string{utils.MetaTopUp, utils.MetaTransferBalance},
|
||||
}}, &cdrs)
|
||||
t.Logf("CDRsV1GetCDRs err: %v", err)
|
||||
|
||||
// fmt.Println(utils.ToJSON(acnts[0].BalanceMap["*data"]))
|
||||
// fmt.Println(utils.ToJSON(cdrs))
|
||||
}
|
||||
|
||||
executeAction := func(t *testing.T, id string) {
|
||||
var reply string
|
||||
attrsEA := &utils.AttrExecuteAction{Tenant: "cgrates.org", Account: "1001", ActionsId: id}
|
||||
if err := client.Call(context.Background(), utils.APIerSv1ExecuteAction, attrsEA, &reply); err != nil {
|
||||
t.Errorf("APIerSv1ExecuteAction err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
checkAccountAndCDRs(t) // main 1000 buffer 10000 total 11000
|
||||
processCDRs(t, 2, 600) // -1200 + export
|
||||
checkAccountAndCDRs(t) // main 0 buffer 9800 total 9800
|
||||
executeAction(t, "ACT_TOPUP_TRANSFER") // +5000
|
||||
checkAccountAndCDRs(t) // main 4800 buffer 10000 total 14800
|
||||
processCDRs(t, 8, 600) // -4800 (no export)
|
||||
checkAccountAndCDRs(t) // main 0 buffer 10000 total 10000
|
||||
processCDRs(t, 1, 600) // -600 + export
|
||||
checkAccountAndCDRs(t) // main 0 buffer 9400 total 9400
|
||||
|
||||
var reply string
|
||||
if err := client.Call(context.Background(), utils.APIerSv1TransferBalance, utils.AttrTransferBalance{
|
||||
Tenant: "cgrates.org",
|
||||
SourceAccountID: "1001",
|
||||
SourceBalanceID: "main",
|
||||
DestinationAccountID: "1001",
|
||||
DestinationBalanceID: "buffer",
|
||||
DestinationReferenceValue: utils.Float64Pointer(5000),
|
||||
Cdrlog: true,
|
||||
}, &reply); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
checkAccountAndCDRs(t) // main 4400 buffer 5000 total 9400
|
||||
})
|
||||
}
|
||||
|
||||
@@ -880,14 +880,15 @@ type AttrBalance struct {
|
||||
}
|
||||
|
||||
type AttrTransferBalance struct {
|
||||
Tenant string
|
||||
SourceAccountID string
|
||||
SourceBalanceID string
|
||||
DestinationAccountID string
|
||||
DestinationBalanceID string
|
||||
Units float64
|
||||
Cdrlog bool
|
||||
APIOpts map[string]any
|
||||
Tenant string
|
||||
SourceAccountID string
|
||||
SourceBalanceID string
|
||||
DestinationAccountID string
|
||||
DestinationBalanceID string
|
||||
DestinationReferenceValue *float64
|
||||
Units float64
|
||||
Cdrlog bool
|
||||
APIOpts map[string]any
|
||||
}
|
||||
|
||||
// TPResourceProfile is used in APIs to manage remotely offline ResourceProfile
|
||||
|
||||
Reference in New Issue
Block a user