diff --git a/engine/action.go b/engine/action.go index 770c40b45..007017d4b 100644 --- a/engine/action.go +++ b/engine/action.go @@ -215,7 +215,7 @@ func logAction(ub *Account, a *Action, acs Actions, _ *FilterS, extraData any) ( func cdrLogAction(acc *Account, a *Action, acs Actions, _ *FilterS, extraData any) (err error) { if len(config.CgrConfig().SchedulerCfg().CDRsConns) == 0 { - return fmt.Errorf("No connection with CDR Server") + return errors.New("No connection with CDR Server") } defaultTemplate := map[string]config.RSRParsers{ utils.ToR: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAcnt+utils.NestingSep+utils.BalanceType, utils.InfieldSep), @@ -224,7 +224,6 @@ func cdrLogAction(acc *Account, a *Action, acs Actions, _ *FilterS, extraData an utils.Tenant: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAcnt+utils.NestingSep+utils.Tenant, utils.InfieldSep), utils.AccountField: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAcnt+utils.NestingSep+utils.AccountField, utils.InfieldSep), utils.Subject: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAcnt+utils.NestingSep+utils.AccountField, utils.InfieldSep), - utils.Cost: config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAct+utils.NestingSep+utils.ActionValue, utils.InfieldSep), } template := make(map[string]string) // overwrite default template @@ -251,11 +250,14 @@ func cdrLogAction(acc *Account, a *Action, acs Actions, _ *FilterS, extraData an // set stored cdr values var cdrs []*CDR for _, action := range acs { - if !slices.Contains([]string{utils.MetaDebit, utils.MetaDebitReset, utils.MetaSetBalance, utils.MetaTopUp, utils.MetaTopUpReset}, action.ActionType) || - action.Balance == nil { + if !slices.Contains( + []string{utils.MetaDebit, utils.MetaDebitReset, + utils.MetaTopUp, utils.MetaTopUpReset, + utils.MetaSetBalance, utils.MetaRemoveBalance, + }, action.ActionType) || action.Balance == nil { continue // Only log specific actions } - cdrLogProvider := newCdrLogProvider(acc, action) + cdr := &CDR{ RunID: action.ActionType, Source: utils.CDRLog, @@ -267,6 +269,34 @@ func cdrLogAction(acc *Account, a *Action, acs Actions, _ *FilterS, extraData an Usage: time.Duration(1), } cdr.CGRID = utils.Sha1(cdr.OriginID, cdr.OriginHost) + + // If the action is of type *remove_balance, retrieve the balance value from the account + // and assign it to the CDR's Cost field. + if action.ActionType == utils.MetaRemoveBalance { + if acc == nil { + return fmt.Errorf("nil account for action %s", utils.ToJSON(action)) + } + balanceChain, exists := acc.BalanceMap[action.Balance.GetType()] + if !exists { + return utils.ErrNotFound + } + found := false + for _, balance := range balanceChain { + if balance.MatchFilter(action.Balance, false, false) { + cdr.Cost = balance.Value + found = true + break + } + } + if !found { + return utils.ErrNotFound + } + } else { + // Otherwise, update the template to retrieve it from Action's BalanceValue. + defaultTemplate[utils.Cost] = config.NewRSRParsersMustCompile(utils.DynamicDataPrefix+utils.MetaAct+utils.NestingSep+utils.ActionValue, utils.InfieldSep) + } + + cdrLogProvider := newCdrLogProvider(acc, action) elem := reflect.ValueOf(cdr).Elem() for key, rsrFlds := range defaultTemplate { parsedValue, err := rsrFlds.ParseDataProvider(cdrLogProvider) diff --git a/general_tests/balance_it_test.go b/general_tests/balance_it_test.go index 6c485ab71..f251c59f1 100644 --- a/general_tests/balance_it_test.go +++ b/general_tests/balance_it_test.go @@ -407,3 +407,150 @@ cgrates.org,sms,1001,2014-01-14T00:00:00Z,RP_ANY,`, } }) } + +// TestBalanceCDRLog tests the usage of balance related actions together with a "*cdrlog" action. +// +// The test steps are as follows: +// 1. Create an account with 2 balances of types *sms and *monetary. The topup action for the *monetary one will also include +// the creation of a CDR. +// 2. Set an action bundle with both "*remove_balance" and "*cdrlog" actions. +// 3. Retrieve both CDRs and check whether the their fields are set correctly. +func TestBalanceCDRLog(t *testing.T) { + switch *dbType { + case utils.MetaInternal: + case utils.MetaMySQL, utils.MetaMongo, utils.MetaPostgres: + t.SkipNow() + default: + t.Fatal("unsupported dbtype value") + } + + content := `{ + +"general": { + "log_level": 7 +}, + +"data_db": { + "db_type": "*internal" +}, + +"stor_db": { + "db_type": "*internal" +}, + +"cdrs": { + "enabled": true, +}, + +"schedulers": { + "enabled": true, + "cdrs_conns": ["*localhost"] +}, + +"apiers": { + "enabled": true, + "scheduler_conns": ["*internal"] +} + +}` + + tpFiles := map[string]string{ + utils.AccountActionsCsv: `#Tenant,Account,ActionPlanId,ActionTriggersId,AllowNegative,Disabled +cgrates.org,ACC_TEST,PACKAGE_ACC_TEST,,,`, + utils.ActionPlansCsv: `#Id,ActionsId,TimingId,Weight +PACKAGE_ACC_TEST,ACT_TOPUP_MONETARY,*asap,10 +PACKAGE_ACC_TEST,ACT_TOPUP_SMS,*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_REMOVE_BALANCE_MONETARY,*cdrlog,"{""BalanceID"":""~*acnt.BalanceID""}",,,,,,,,,,,,,, +ACT_REMOVE_BALANCE_MONETARY,*remove_balance,,,balance_monetary,*monetary,,,,,,,,,,, +ACT_TOPUP_MONETARY,*cdrlog,"{""BalanceID"":""~*acnt.BalanceID""}",,,,,,,,,,,,,, +ACT_TOPUP_MONETARY,*topup_reset,,,balance_monetary,*monetary,,*any,,,*unlimited,,150,20,false,false,20 +ACT_TOPUP_SMS,*topup_reset,,,balance_sms,*sms,,*any,,,*unlimited,,1000,10,false,false,10`, + } + + testEnv := TestEnvironment{ + Name: "TestBalanceCDRLog", + // Encoding: *encoding, + ConfigJSON: content, + TpFiles: tpFiles, + } + client, _, shutdown, err := testEnv.Setup(t, *waitRater) + if err != nil { + t.Fatal(err) + } + + defer shutdown() + + t.Run("CheckInitialBalances", func(t *testing.T) { + time.Sleep(time.Second) + var acnt engine.Account + if err := client.Call(context.Background(), utils.APIerSv2GetAccount, + &utils.AttrGetAccount{ + Tenant: "cgrates.org", + Account: "ACC_TEST", + }, &acnt); err != nil { + t.Fatal(err) + } + if len(acnt.BalanceMap) != 2 || len(acnt.BalanceMap[utils.MetaMonetary]) != 1 || len(acnt.BalanceMap[utils.MetaSMS]) != 1 { + t.Errorf("unexpected accont received: %v", utils.ToJSON(acnt)) + } + }) + + t.Run("CheckTopupCDR", func(t *testing.T) { + var cdrs []*engine.CDR + if err := client.Call(context.Background(), utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithAPIOpts{ + RPCCDRsFilter: &utils.RPCCDRsFilter{}}, &cdrs); err != nil { + t.Fatal(err) + } + + if len(cdrs) != 1 || + cdrs[0].RunID != utils.MetaTopUpReset || + cdrs[0].Source != utils.CDRLog || + cdrs[0].ToR != utils.MetaMonetary || + cdrs[0].ExtraFields["BalanceID"] != "balance_monetary" || + cdrs[0].Cost != 150 { + t.Errorf("unexpected cdr received: %v", utils.ToJSON(cdrs)) + } + }) + + t.Run("RemoveMonetaryBalance", func(t *testing.T) { + var reply string + attrsEA := &utils.AttrExecuteAction{Tenant: "cgrates.org", Account: "ACC_TEST", ActionsId: "ACT_REMOVE_BALANCE_MONETARY"} + if err := client.Call(context.Background(), utils.APIerSv1ExecuteAction, attrsEA, &reply); err != nil { + t.Error(err) + } + }) + + t.Run("CheckRemoveBalanceCDR", func(t *testing.T) { + var cdrs []*engine.CDR + if err := client.Call(context.Background(), utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithAPIOpts{ + RPCCDRsFilter: &utils.RPCCDRsFilter{ + RunIDs: []string{"*remove_balance"}, + }}, &cdrs); err != nil { + t.Fatal(err) + } + + if len(cdrs) != 1 || + cdrs[0].RunID != utils.MetaRemoveBalance || + cdrs[0].Source != utils.CDRLog || + cdrs[0].ToR != utils.MetaMonetary || + cdrs[0].ExtraFields["BalanceID"] != "balance_monetary" || + cdrs[0].Cost != 150 { + t.Errorf("unexpected cdr received: %v", utils.ToJSON(cdrs)) + } + }) + + t.Run("CheckFinalBalances", func(t *testing.T) { + var acnt engine.Account + if err := client.Call(context.Background(), utils.APIerSv2GetAccount, + &utils.AttrGetAccount{ + Tenant: "cgrates.org", + Account: "ACC_TEST", + }, &acnt); err != nil { + t.Error(err) + } + if len(acnt.BalanceMap) != 2 || len(acnt.BalanceMap[utils.MetaSMS]) != 1 || len(acnt.BalanceMap[utils.MetaMonetary]) != 0 { + t.Errorf("unexpected account received: %v", utils.ToJSON(acnt)) + } + }) +}