diff --git a/engine/account.go b/engine/account.go index 3d5f228fd..8c38b577e 100644 --- a/engine/account.go +++ b/engine/account.go @@ -861,6 +861,9 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance //log.Print("CONNECT FEE: %f", connectFee) connectFeePaid := false for _, b := range usefulMoneyBalances { + if !b.IsActiveAt(cc.GetStartTime()) { + continue + } if b.GetValue() >= connectFee { b.SubstractValue(connectFee) // the conect fee is not refundable! diff --git a/general_tests/balance_timings_it_test.go b/general_tests/balance_timings_it_test.go new file mode 100644 index 000000000..f63b15894 --- /dev/null +++ b/general_tests/balance_timings_it_test.go @@ -0,0 +1,249 @@ +//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 ( + "testing" + "time" + + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + +func TestBalanceTimings(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": "50s" +}, +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080" +}, + +"data_db": { + "db_type": "*internal" +}, + +"stor_db": { + "db_type": "*internal" +}, + +"rals": { + "enabled": true, +}, +"schedulers": { + "enabled": true, + "cdrs_conns": ["*internal"], +}, +"cdrs": { + "enabled": true, + "chargers_conns":["*internal"], + "rals_conns": ["*localhost"], + "session_cost_retries": 0, +}, +"attributes": { + "enabled": true, + "apiers_conns": ["*localhost"] +}, +"chargers": { + "enabled": true, + "attributes_conns": ["*internal"] +}, +"sessions": { + "enabled": true, + "attributes_conns": ["*internal"], + "rals_conns": ["*internal"], + "cdrs_conns": ["*internal"], + "chargers_conns": ["*internal"] +}, +"apiers": { + "enabled": true, + "scheduler_conns": ["*internal"] +}, +} +` + + tpFiles := map[string]string{ + utils.AccountActionsCsv: `#Tenant,Account,ActionPlanId,ActionTriggersId,AllowNegative,Disabled +cgrates.org,1001,AP_PACKAGE_10,,, +cgrates.org,1002,AP_PACKAGE_10,,,`, + utils.ActionPlansCsv: `#Id,ActionsId,TimingId,Weight +AP_PACKAGE_10,ACT_TOPUP_RST_10,*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_RST_10,*topup_reset,,,bal1,*monetary,,*any,,,*unlimited,HALF1,10,10,false,false,10 +ACT_TOPUP_RST_10,*topup_reset,,,bal2,*monetary,,*any,,,*unlimited,HALF2,10,10,false,false,99`, + utils.ChargersCsv: `#Tenant,ID,FilterIDs,ActivationInterval,RunID,AttributeIDs,Weight +cgrates.org,DEFAULT,,,*default,*none,0 +cgrates.org,Raw,,,*raw,*constant:*req.RequestType:*none,0`, + utils.DestinationRatesCsv: `#Id,DestinationId,RatesTag,RoundingMethod,RoundingDecimals,MaxCost,MaxCostStrategy +DR_1002_20CNT,DST_1002,RT_20CNT,*up,4,0,`, + utils.DestinationsCsv: `#Id,Prefix +DST_1002,1002 +DST_1001,1001`, + utils.RatesCsv: `#Id,ConnectFee,Rate,RateUnit,RateIncrement,GroupIntervalStart +RT_20CNT,0.4,0.2,60s,60s,0s +RT_20CNT,0,0.1,60s,1s,60s`, + utils.RatingPlansCsv: `#Id,DestinationRatesId,TimingTag,Weight +RP_1001,DR_1002_20CNT,*any,10`, + utils.RatingProfilesCsv: `#Tenant,Category,Subject,ActivationTime,RatingPlanId,RatesFallbackSubject +cgrates.org,call,1001,2014-01-14T00:00:00Z,RP_1001,`, + utils.TimingsCsv: `#Tag,Years,Months,MonthDays,WeekDays,Time +HALF1,*any,*any,*any,*any,00:00:00;11:59:59 +HALF2,*any,*any,*any,*any,12:00:00;23:59:59`, + } + + testEnv := TestEnvironment{ + ConfigJSON: content, + TpFiles: tpFiles, + } + client, _ := testEnv.Setup(t, 0) + time.Sleep(10 * time.Millisecond) + + t.Run("GetAccount", func(t *testing.T) { + var acnt engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + if err := client.Call(utils.APIerSv2GetAccount, attrs, &acnt); err != nil { + t.Fatal(err) + } + + if len(acnt.BalanceMap) != 1 || + len(acnt.BalanceMap[utils.MONETARY]) != 2 { + t.Fatalf("expected account to have 1 balance of type *monetary, received %v", utils.ToJSON(acnt)) + } + + monetaryBalance := acnt.BalanceMap[utils.MONETARY][1] + if monetaryBalance.ID != "bal1" || monetaryBalance.Value != 10 { + t.Fatalf("received account with unexpected *monetary balance: %v", monetaryBalance) + } else if monetaryBalance.ID == "bal1" && !monetaryBalance.TimingIDs["HALF1"] { + t.Fatalf("expected TimingIDs %v, received: %v", "HALF1", monetaryBalance.TimingIDs) + } + monetaryBalance = acnt.BalanceMap[utils.MONETARY][0] + if monetaryBalance.ID != "bal2" || monetaryBalance.Value != 10 { + t.Fatalf("received account with unexpected *monetary balance: %v", monetaryBalance) + } else if monetaryBalance.ID == "bal2" && !monetaryBalance.TimingIDs["HALF2"] { + t.Fatalf("expected TimingIDs %v, received: %v", "HALF2", monetaryBalance.TimingIDs) + } + }) + + t.Run("Half2ProcessExternalCDR", func(t *testing.T) { + var reply string + args := &engine.ExternalCDRWithArgDispatcher{ + ExternalCDR: &engine.ExternalCDR{ + OriginID: "TestBalanceTimings", + ToR: utils.VOICE, + RequestType: utils.PREPAID, + AnswerTime: "2024-08-04T15:00:07Z", + SetupTime: "2024-08-04T15:00:00Z", + Tenant: "cgrates.org", + Account: "1001", + Subject: "1001", + Destination: "1002", + Usage: "1", + }, + } + if err := client.Call(utils.CDRsV1ProcessExternalCDR, args, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Errorf("Received: %s", reply) + } + }) + + t.Run("Half2GetAccount", func(t *testing.T) { + var acnt engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + if err := client.Call(utils.APIerSv2GetAccount, attrs, &acnt); err != nil { + t.Fatal(err) + } + if len(acnt.BalanceMap) != 1 || + len(acnt.BalanceMap[utils.MONETARY]) != 2 { + t.Fatalf("expected account to have 1 balance of type *monetary, received %v", utils.ToJSON(acnt)) + } + + monetaryBalance := acnt.BalanceMap[utils.MONETARY][1] + if monetaryBalance.ID != "bal1" || monetaryBalance.Value != 10 { + t.Fatalf("received account with unexpected *monetary balance: %v", monetaryBalance) + } else if monetaryBalance.ID == "bal1" && !monetaryBalance.TimingIDs["HALF1"] { + t.Fatalf("expected TimingIDs %v, received: %v", "HALF1", monetaryBalance.TimingIDs) + } + monetaryBalance = acnt.BalanceMap[utils.MONETARY][0] + if monetaryBalance.ID != "bal2" || monetaryBalance.Value != 9.4 { + t.Fatalf("received account with unexpected *monetary balance: %v", monetaryBalance) + } else if monetaryBalance.ID == "bal2" && !monetaryBalance.TimingIDs["HALF2"] { + t.Fatalf("expected TimingIDs %v, received: %v", "HALF2", monetaryBalance.TimingIDs) + } + }) + + t.Run("Half1ProcessExternalCDR", func(t *testing.T) { + var reply string + args := &engine.ExternalCDRWithArgDispatcher{ + ExternalCDR: &engine.ExternalCDR{ + OriginID: "TestBalanceTimings2", + ToR: utils.VOICE, + RequestType: utils.PREPAID, + AnswerTime: "2024-08-04T11:00:07Z", + SetupTime: "2024-08-04T11:00:00Z", + Tenant: "cgrates.org", + Account: "1001", + Subject: "1001", + Destination: "1002", + Usage: "1", + }, + } + if err := client.Call(utils.CDRsV1ProcessExternalCDR, args, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Errorf("Received: %s", reply) + } + }) + + t.Run("Half1GetAccount", func(t *testing.T) { + var acnt engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + if err := client.Call(utils.APIerSv2GetAccount, attrs, &acnt); err != nil { + t.Fatal(err) + } + if len(acnt.BalanceMap) != 1 || + len(acnt.BalanceMap[utils.MONETARY]) != 2 { + t.Fatalf("expected account to have 1 balance of type *monetary, received %v", utils.ToJSON(acnt)) + } + + monetaryBalance := acnt.BalanceMap[utils.MONETARY][1] + if monetaryBalance.ID != "bal1" || monetaryBalance.Value != 9.4 { + t.Fatalf("received account with unexpected *monetary balance: %v", utils.ToJSON(monetaryBalance)) + } else if monetaryBalance.ID == "bal1" && !monetaryBalance.TimingIDs["HALF1"] { + t.Fatalf("expected TimingIDs %v, received: %v", "HALF1", monetaryBalance.TimingIDs) + } + monetaryBalance = acnt.BalanceMap[utils.MONETARY][0] + if monetaryBalance.ID != "bal2" || monetaryBalance.Value != 9.4 { + t.Fatalf("received account with unexpected *monetary balance: %v", monetaryBalance) + } else if monetaryBalance.ID == "bal2" && !monetaryBalance.TimingIDs["HALF2"] { + t.Fatalf("expected TimingIDs %v, received: %v", "HALF2", monetaryBalance.TimingIDs) + } + }) + +}