From d2e04360bd8b4f88a20bfe79f56cfc593a513a67 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 15 Feb 2021 08:13:26 +0200 Subject: [PATCH] Added *sessionChargeable session option. Fixes #1702 --- accounts/libaccounts.go | 2 +- engine/eventcost.go | 159 ++++++++----- engine/libeventcost.go | 29 +-- engine/mapevent.go | 13 ++ general_tests/session5_it_test.go | 367 ++++++++++++++++++++++++++++++ packages/debian/changelog | 1 + sessions/session.go | 1 + sessions/sessions.go | 72 ++++-- sessions/sessionscover_it_test.go | 21 ++ utils/consts.go | 14 +- 10 files changed, 585 insertions(+), 94 deletions(-) create mode 100644 general_tests/session5_it_test.go diff --git a/accounts/libaccounts.go b/accounts/libaccounts.go index 0045a44d3..5332c783c 100644 --- a/accounts/libaccounts.go +++ b/accounts/libaccounts.go @@ -102,7 +102,7 @@ func processAttributeS(connMgr *engine.ConnManager, cgrEv *utils.CGREvent, attrArgs := &engine.AttrArgsProcessEvent{ Context: utils.StringPointer(utils.FirstNonEmpty( engine.MapEvent(cgrEv.Opts).GetStringIgnoreErrors(utils.OptsContext), - utils.MetaAccountS)), + utils.MetaAccounts)), CGREvent: cgrEv, AttributeIDs: attrIDs, ProcessRuns: procRuns, diff --git a/engine/eventcost.go b/engine/eventcost.go index 6d82d8f94..160bef0e4 100644 --- a/engine/eventcost.go +++ b/engine/eventcost.go @@ -54,15 +54,16 @@ func NewEventCostFromCallCost(cc *CallCost, cgrID, runID string) (ec *EventCost) cIl := &ChargingInterval{CompressFactor: ts.CompressFactor} rf := RatingMatchedFilters{"Subject": ts.MatchedSubject, "DestinationPrefix": ts.MatchedPrefix, "DestinationID": ts.MatchedDestId, "RatingPlanID": ts.RatingPlanId} - cIl.RatingID = ec.ratingIDForRateInterval(ts.RateInterval, rf) + isPause := ts.RatingPlanId == utils.MetaPause + cIl.RatingID = ec.ratingIDForRateInterval(ts.RateInterval, rf, isPause) if len(ts.Increments) != 0 { cIl.Increments = make([]*ChargingIncrement, 0, len(ts.Increments)+1) } for _, incr := range ts.Increments { - cIl.Increments = append(cIl.Increments, ec.newChargingIncrement(incr, rf, false)) + cIl.Increments = append(cIl.Increments, ec.newChargingIncrement(incr, rf, false, isPause)) } if ts.RoundIncrement != nil { - rIncr := ec.newChargingIncrement(ts.RoundIncrement, rf, true) + rIncr := ec.newChargingIncrement(ts.RoundIncrement, rf, true, false) rIncr.Cost = -rIncr.Cost cIl.Increments = append(cIl.Increments, rIncr) } @@ -73,7 +74,7 @@ func NewEventCostFromCallCost(cc *CallCost, cgrID, runID string) (ec *EventCost) // newChargingIncrement creates ChargingIncrement from a Increment // special case if is the roundIncrement the rateID is *rounding -func (ec *EventCost) newChargingIncrement(incr *Increment, rf RatingMatchedFilters, roundedIncrement bool) (cIt *ChargingIncrement) { +func (ec *EventCost) newChargingIncrement(incr *Increment, rf RatingMatchedFilters, roundedIncrement, isPause bool) (cIt *ChargingIncrement) { cIt = &ChargingIncrement{ Usage: incr.Duration, Cost: incr.Cost, @@ -82,6 +83,9 @@ func (ec *EventCost) newChargingIncrement(incr *Increment, rf RatingMatchedFilte if incr.BalanceInfo == nil { return } + if roundedIncrement { + isPause = false + } rateID := utils.MetaRounding //AccountingID if incr.BalanceInfo.Unit != nil { @@ -89,40 +93,53 @@ func (ec *EventCost) newChargingIncrement(incr *Increment, rf RatingMatchedFilte ecUUID := utils.MetaNone // populate no matter what due to Unit not nil if incr.BalanceInfo.Monetary != nil { if !roundedIncrement { - rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf) + rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf, isPause) } - if uuid := ec.Accounting.GetIDWithSet( - &BalanceCharge{ - AccountID: incr.BalanceInfo.AccountID, - BalanceUUID: incr.BalanceInfo.Monetary.UUID, - Units: incr.Cost, - RatingID: rateID, - }); uuid != "" { - ecUUID = uuid - } - } - if !roundedIncrement { - rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Unit.RateInterval, rf) - } - cIt.AccountingID = ec.Accounting.GetIDWithSet( - &BalanceCharge{ - AccountID: incr.BalanceInfo.AccountID, - BalanceUUID: incr.BalanceInfo.Unit.UUID, - Units: incr.BalanceInfo.Unit.Consumed, - RatingID: rateID, - ExtraChargeID: ecUUID, - }) - } else if incr.BalanceInfo.Monetary != nil { // Only monetary - if !roundedIncrement { - rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf) - } - cIt.AccountingID = ec.Accounting.GetIDWithSet( - &BalanceCharge{ + bc := &BalanceCharge{ AccountID: incr.BalanceInfo.AccountID, BalanceUUID: incr.BalanceInfo.Monetary.UUID, Units: incr.Cost, RatingID: rateID, - }) + } + if isPause { + ecUUID = utils.MetaPause + ec.Accounting[ecUUID] = bc + } else { + ecUUID = ec.Accounting.GetIDWithSet(bc) + } + } + if !roundedIncrement { + rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Unit.RateInterval, rf, isPause) + } + bc := &BalanceCharge{ + AccountID: incr.BalanceInfo.AccountID, + BalanceUUID: incr.BalanceInfo.Unit.UUID, + Units: incr.BalanceInfo.Unit.Consumed, + RatingID: rateID, + ExtraChargeID: ecUUID, + } + if isPause { + cIt.AccountingID = utils.MetaPause + ec.Accounting[utils.MetaPause] = bc + } else { + cIt.AccountingID = ec.Accounting.GetIDWithSet(bc) + } + } else if incr.BalanceInfo.Monetary != nil { // Only monetary + if !roundedIncrement { + rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf, isPause) + } + bc := &BalanceCharge{ + AccountID: incr.BalanceInfo.AccountID, + BalanceUUID: incr.BalanceInfo.Monetary.UUID, + Units: incr.Cost, + RatingID: rateID, + } + if isPause { + cIt.AccountingID = utils.MetaPause + ec.Accounting[utils.MetaPause] = bc + } else { + cIt.AccountingID = ec.Accounting.GetIDWithSet(bc) + } } return } @@ -151,39 +168,54 @@ func (ec *EventCost) initCache() { } } -func (ec *EventCost) ratingIDForRateInterval(ri *RateInterval, rf RatingMatchedFilters) string { +func (ec *EventCost) ratingIDForRateInterval(ri *RateInterval, rf RatingMatchedFilters, isPause bool) string { if ri == nil || ri.Rating == nil { return "" } var rfUUID string if rf != nil { - rfUUID = ec.RatingFilters.GetIDWithSet(rf) + if isPause { + rfUUID = utils.MetaPause + ec.RatingFilters[rfUUID] = rf + } else { + rfUUID = ec.RatingFilters.GetIDWithSet(rf) + } } var tmID string if ri.Timing != nil { - tmID = ec.Timings.GetIDWithSet( - &ChargedTiming{ - Years: ri.Timing.Years, - Months: ri.Timing.Months, - MonthDays: ri.Timing.MonthDays, - WeekDays: ri.Timing.WeekDays, - StartTime: ri.Timing.StartTime}) + // timingID can have random UUID to be reused by other Rates from EventCost + tmID = ec.Timings.GetIDWithSet(&ChargedTiming{ + Years: ri.Timing.Years, + Months: ri.Timing.Months, + MonthDays: ri.Timing.MonthDays, + WeekDays: ri.Timing.WeekDays, + StartTime: ri.Timing.StartTime, + }) } var rtUUID string if len(ri.Rating.Rates) != 0 { - rtUUID = ec.Rates.GetIDWithSet(ri.Rating.Rates) + if isPause { + rtUUID = utils.MetaPause + ec.Rates[rtUUID] = ri.Rating.Rates + } else { + rtUUID = ec.Rates.GetIDWithSet(ri.Rating.Rates) + } } - return ec.Rating.GetIDWithSet( - &RatingUnit{ - ConnectFee: ri.Rating.ConnectFee, - RoundingMethod: ri.Rating.RoundingMethod, - RoundingDecimals: ri.Rating.RoundingDecimals, - MaxCost: ri.Rating.MaxCost, - MaxCostStrategy: ri.Rating.MaxCostStrategy, - TimingID: tmID, - RatesID: rtUUID, - RatingFiltersID: rfUUID, - }) + ru := &RatingUnit{ + ConnectFee: ri.Rating.ConnectFee, + RoundingMethod: ri.Rating.RoundingMethod, + RoundingDecimals: ri.Rating.RoundingDecimals, + MaxCost: ri.Rating.MaxCost, + MaxCostStrategy: ri.Rating.MaxCostStrategy, + TimingID: tmID, + RatesID: rtUUID, + RatingFiltersID: rfUUID, + } + if isPause { + ec.Rating[utils.MetaPause] = ru + return utils.MetaPause + } + return ec.Rating.GetIDWithSet(ru) } func (ec *EventCost) rateIntervalForRatingID(ratingID string) (ri *RateInterval) { @@ -459,8 +491,18 @@ func (ec *EventCost) newIntervalFromCharge(cInc *ChargingIncrement) (incr *Incre // ratingGetIDFomEventCost retrieves UUID based on data from another EventCost func (ec *EventCost) ratingGetIDFomEventCost(oEC *EventCost, oRatingID string) string { - if oRatingID == "" { - return "" + if oRatingID == utils.EmptyString { + return utils.EmptyString + } else if oRatingID == utils.MetaPause { + oCIlRating := oEC.Rating[oRatingID].Clone() // clone so we don't influence the original data + oCIlRating.TimingID = utils.MetaPause + ec.Timings[utils.MetaPause] = oEC.Timings[oCIlRating.TimingID] + oCIlRating.RatingFiltersID = utils.MetaPause + ec.RatingFilters[utils.MetaPause] = oEC.RatingFilters[oCIlRating.RatingFiltersID] + oCIlRating.RatesID = utils.MetaPause + ec.Rates[utils.MetaPause] = oEC.Rates[oCIlRating.RatesID] + ec.Rating[utils.MetaPause] = oCIlRating + return utils.MetaPause } oCIlRating := oEC.Rating[oRatingID].Clone() // clone so we don't influence the original data oCIlRating.TimingID = ec.Timings.GetIDWithSet(oEC.Timings[oCIlRating.TimingID]) @@ -473,6 +515,11 @@ func (ec *EventCost) ratingGetIDFomEventCost(oEC *EventCost, oRatingID string) s func (ec *EventCost) accountingGetIDFromEventCost(oEC *EventCost, oAccountingID string) string { if oAccountingID == "" || oAccountingID == utils.MetaNone { return "" + } else if oAccountingID == utils.MetaPause { // *pause represent a pause in debited session + oBC := oEC.Accounting[oAccountingID].Clone() + oBC.RatingID = ec.ratingGetIDFomEventCost(oEC, oBC.RatingID) + ec.Accounting[utils.MetaPause] = oBC + return utils.MetaPause } oBC := oEC.Accounting[oAccountingID].Clone() oBC.RatingID = ec.ratingGetIDFomEventCost(oEC, oBC.RatingID) diff --git a/engine/libeventcost.go b/engine/libeventcost.go index 218947376..bcff58077 100644 --- a/engine/libeventcost.go +++ b/engine/libeventcost.go @@ -632,11 +632,11 @@ func NewFreeEventCost(cgrID, runID, account string, tStart time.Time, usage time StartTime: tStart, Cost: utils.Float64Pointer(0), Charges: []*ChargingInterval{{ - RatingID: "*free", + RatingID: utils.MetaPause, Increments: []*ChargingIncrement{ { Usage: usage, - AccountingID: "*free", + AccountingID: utils.MetaPause, CompressFactor: 1, }, }, @@ -644,39 +644,40 @@ func NewFreeEventCost(cgrID, runID, account string, tStart time.Time, usage time }}, Rating: Rating{ - "*free": { + utils.MetaPause: { RoundingMethod: "*up", RoundingDecimals: 5, - RatesID: "*free", - RatingFiltersID: "*free", - TimingID: "*free", + RatesID: utils.MetaPause, + RatingFiltersID: utils.MetaPause, + TimingID: utils.MetaPause, }, }, Accounting: Accounting{ - "*free": { + utils.MetaPause: { AccountID: account, // BalanceUUID: "", - RatingID: "*free", + RatingID: utils.MetaPause, }, }, RatingFilters: RatingFilters{ - "*free": { + utils.MetaPause: { utils.Subject: "", utils.DestinationPrefixName: "", utils.DestinationID: "", - utils.RatingPlanID: "", + utils.RatingPlanID: utils.MetaPause, }, }, Rates: ChargedRates{ - "*free": { + utils.MetaPause: { { - RateIncrement: usage, - RateUnit: usage, + RateIncrement: 1, + RateUnit: 1, }, }, }, Timings: ChargedTimings{ - "*free": { + utils.MetaPause: { + StartTime: "00:00:00", }, }, diff --git a/engine/mapevent.go b/engine/mapevent.go index f59c1daed..a4d72768e 100644 --- a/engine/mapevent.go +++ b/engine/mapevent.go @@ -285,3 +285,16 @@ func (me MapEvent) AsCDR(cfg *config.CGRConfig, tnt, tmz string) (cdr *CDR, err func (me MapEvent) Data() map[string]interface{} { return me } + +// GetBoolOrDefault returns the value as a bool or dflt if not present in map +func (me MapEvent) GetBoolOrDefault(fldName string, dflt bool) (out bool) { + fldIface, has := me[fldName] + if !has { + return dflt + } + out, err := utils.IfaceAsBool(fldIface) + if err != nil { + return dflt + } + return out +} diff --git a/general_tests/session5_it_test.go b/general_tests/session5_it_test.go new file mode 100644 index 000000000..7e70079b6 --- /dev/null +++ b/general_tests/session5_it_test.go @@ -0,0 +1,367 @@ +// +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 ( + "encoding/json" + "net/rpc" + "path" + "reflect" + "testing" + "time" + + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/sessions" + "github.com/cgrates/cgrates/utils" +) + +var ( + ses5CfgDir string + ses5CfgPath string + ses5Cfg *config.CGRConfig + ses5RPC *rpc.Client + + ses5Tests = []func(t *testing.T){ + testSes5ItLoadConfig, + testSes5ItResetDataDB, + testSes5ItResetStorDb, + testSes5ItStartEngine, + testSes5ItRPCConn, + testSes5ItLoadFromFolder, + + testSes5ItAllPause, + + testSes5ItStopCgrEngine, + } +) + +func TestSes5ItSessions(t *testing.T) { + switch *dbType { + case utils.MetaInternal: + ses5CfgDir = "tutinternal" + case utils.MetaMySQL: + ses5CfgDir = "tutmysql" + case utils.MetaMongo: + ses5CfgDir = "tutmongo" + case utils.MetaPostgres: + t.SkipNow() + default: + t.Fatal("Unknown Database type") + } + for _, stest := range ses5Tests { + t.Run(ses5CfgDir, stest) + } +} + +func testSes5ItLoadConfig(t *testing.T) { + ses5CfgPath = path.Join(*dataDir, "conf", "samples", ses5CfgDir) + if ses5Cfg, err = config.NewCGRConfigFromPath(ses5CfgPath); err != nil { + t.Error(err) + } +} + +func testSes5ItResetDataDB(t *testing.T) { + if err := engine.InitDataDb(ses5Cfg); err != nil { + t.Fatal(err) + } +} + +func testSes5ItResetStorDb(t *testing.T) { + if err := engine.InitStorDb(ses5Cfg); err != nil { + t.Fatal(err) + } +} + +func testSes5ItStartEngine(t *testing.T) { + if _, err := engine.StopStartEngine(ses5CfgPath, *waitRater); err != nil { + t.Fatal(err) + } +} + +func testSes5ItRPCConn(t *testing.T) { + var err error + ses5RPC, err = newRPCClient(ses5Cfg.ListenCfg()) + if err != nil { + t.Fatal(err) + } +} + +func testSes5ItLoadFromFolder(t *testing.T) { + var reply string + attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "tutorial")} + if err := ses5RPC.Call(utils.APIerSv1LoadTariffPlanFromFolder, attrs, &reply); err != nil { + t.Error(err) + } + time.Sleep(100 * time.Millisecond) +} + +func testSes5ItInitSession(t *testing.T, chargeable bool) { + args1 := &sessions.V1InitSessionArgs{ + InitSession: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Category: utils.Call, + utils.ToR: utils.MetaVoice, + utils.OriginID: "TestDebitIterval", + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2018, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2018, time.January, 7, 16, 60, 10, 0, time.UTC), + utils.Usage: time.Second, + }, + Opts: map[string]interface{}{ + utils.OptsDebitInterval: "0s", + utils.OptsChargeable: chargeable, + }, + }, + } + var rply1 sessions.V1InitSessionReply + if err := ses5RPC.Call(utils.SessionSv1InitiateSession, + args1, &rply1); err != nil { + t.Error(err) + return + } else if rply1.MaxUsage != nil && *rply1.MaxUsage != time.Second { + t.Errorf("Unexpected MaxUsage: %v", rply1.MaxUsage) + } +} + +func testSes5ItUpdateSession(t *testing.T, chargeable bool) { + usage := 2 * time.Second + updtArgs := &sessions.V1UpdateSessionArgs{ + UpdateSession: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Category: utils.Call, + utils.ToR: utils.MetaVoice, + utils.OriginID: "TestDebitIterval", + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2018, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2018, time.January, 7, 16, 60, 10, 0, time.UTC), + utils.Usage: usage, + }, + Opts: map[string]interface{}{ + utils.OptsChargeable: chargeable, + }, + }, + } + + var updtRpl sessions.V1UpdateSessionReply + if err := ses5RPC.Call(utils.SessionSv1UpdateSession, updtArgs, &updtRpl); err != nil { + t.Error(err) + } + if updtRpl.MaxUsage == nil || *updtRpl.MaxUsage != usage { + t.Errorf("Expecting : %+v, received: %+v", usage, updtRpl.MaxUsage) + } +} + +func testSes5ItTerminateSession(t *testing.T, chargeable bool) { + args := &sessions.V1TerminateSessionArgs{ + TerminateSession: true, + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Category: utils.Call, + utils.ToR: utils.MetaVoice, + utils.OriginID: "TestDebitIterval", + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1002", + utils.Destination: "1002", + utils.SetupTime: time.Date(2018, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2018, time.January, 7, 16, 60, 10, 0, time.UTC), + utils.Usage: 5 * time.Second, + }, + Opts: map[string]interface{}{ + utils.OptsChargeable: chargeable, + }, + }, + } + var rply string + if err := ses5RPC.Call(utils.SessionSv1TerminateSession, + args, &rply); err != nil { + t.Error(err) + } + if rply != utils.OK { + t.Errorf("Unexpected reply: %s", rply) + } + if err := ses5RPC.Call(utils.SessionSv1ProcessCDR, &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "testSes5ItProccesCDR", + Event: map[string]interface{}{ + utils.Category: utils.Call, + utils.ToR: utils.MetaVoice, + utils.OriginID: "TestDebitIterval", + utils.RequestType: utils.MetaPrepaid, + utils.AccountField: "1001", + utils.Subject: "1002", + utils.Destination: "1002", + utils.SetupTime: time.Date(2018, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2018, time.January, 7, 16, 60, 10, 0, time.UTC), + utils.Usage: 5 * time.Second, + }, + }, &rply); err != nil { + t.Error(err) + } else if rply != utils.OK { + t.Errorf("Received reply: %s", rply) + } +} + +func testSes5ItAllPause(t *testing.T) { + testSes5ItInitSession(t, false) + testSes5ItUpdateSession(t, false) + testSes5ItTerminateSession(t, false) + time.Sleep(20 * time.Millisecond) + var cdrs []*engine.ExternalCDR + req := utils.RPCCDRsFilter{RequestTypes: []string{utils.MetaPrepaid}} + if err := ses5RPC.Call(utils.APIerSv2GetCDRs, &req, &cdrs); err != nil { + t.Error("Unexpected error: ", err.Error()) + } + + exp := []*engine.ExternalCDR{{ + CGRID: "1b676c7583ceb27ad7c991ded73b2417faa29a6a", + RunID: "*default", + OrderID: 0, + OriginHost: "", + Source: "", + OriginID: "TestDebitIterval", + ToR: "*voice", + RequestType: "*prepaid", + Tenant: "cgrates.org", + Category: "call", + Account: "1001", + Subject: "1001", + Destination: "1002", + SetupTime: "2018-01-07T19:00:00+02:00", + AnswerTime: "2018-01-07T19:00:10+02:00", + Usage: "5s", + ExtraFields: map[string]string{}, + CostSource: "*sessions", + Cost: 0, + CostDetails: "", + ExtraInfo: "", + PreRated: false, + }} + if len(cdrs) == 0 { + t.Fatal("No cdrs returned") + } + cdrs[0].OrderID = 0 + cdString := cdrs[0].CostDetails + cdrs[0].CostDetails = "" + if !reflect.DeepEqual(exp, cdrs) { + t.Errorf("Expected %s \n received: %s", utils.ToJSON(exp), utils.ToJSON(cdrs)) + } + + var cd engine.EventCost + if err := json.Unmarshal([]byte(cdString), &cd); err != nil { + t.Fatal(err) + } + evCost := engine.EventCost{ + CGRID: "1b676c7583ceb27ad7c991ded73b2417faa29a6a", + RunID: "*default", + StartTime: time.Date(2018, time.January, 7, 16, 60, 10, 0, time.UTC), + Usage: utils.DurationPointer(5 * time.Second), + Cost: utils.Float64Pointer(0), + Charges: []*engine.ChargingInterval{{ + RatingID: utils.MetaPause, + Increments: []*engine.ChargingIncrement{{ + Usage: time.Second, + Cost: 0, + AccountingID: utils.MetaPause, + CompressFactor: 1, + }}, + CompressFactor: 1, + }, { + RatingID: utils.MetaPause, + Increments: []*engine.ChargingIncrement{{ + Usage: 2 * time.Second, + Cost: 0, + AccountingID: utils.MetaPause, + CompressFactor: 1, + }}, + CompressFactor: 2, + }}, + Rating: engine.Rating{ + utils.MetaPause: { + ConnectFee: 0, + RoundingMethod: "*up", + RoundingDecimals: 5, + MaxCost: 0, + MaxCostStrategy: "", + TimingID: "", + RatesID: utils.MetaPause, + RatingFiltersID: utils.MetaPause, + }, + }, + Accounting: engine.Accounting{ + utils.MetaPause: { + AccountID: "1001", + BalanceUUID: "", + RatingID: utils.MetaPause, + Units: 0, + ExtraChargeID: "", + }, + }, + RatingFilters: engine.RatingFilters{ + utils.MetaPause: { + utils.DestinationID: "", + utils.DestinationPrefixName: "", + utils.RatingPlanID: utils.MetaPause, + utils.Subject: "", + }, + }, + Rates: engine.ChargedRates{ + utils.MetaPause: {{ + GroupIntervalStart: 0, + Value: 0, + RateIncrement: 1, + RateUnit: 1, + }}, + }, + Timings: engine.ChargedTimings{ + "": { + StartTime: "00:00:00", + }, + }, + } + // the Timings are not relevant for this test + for _, r := range cd.Rating { + r.TimingID = "" + } + cd.Timings = evCost.Timings + if !reflect.DeepEqual(evCost, cd) { + t.Errorf("Expected %s \n received: %s", utils.ToJSON(evCost), utils.ToJSON(cd)) + } + +} + +func testSes5ItStopCgrEngine(t *testing.T) { + if err := engine.KillEngine(100); err != nil { + t.Error(err) + } +} diff --git a/packages/debian/changelog b/packages/debian/changelog index c810602cb..b5ceb2fa6 100644 --- a/packages/debian/changelog +++ b/packages/debian/changelog @@ -142,6 +142,7 @@ cgrates (0.11.0~dev) UNRELEASED; urgency=medium * [FilterS] Optimized the automated index fields matching * [AgentS] Added *cfg as DataProvider for AgentRequest * [AgentS] Added *routes_maxcost flag + * [SessionS] Added *sessionChargeable session option to control session charging -- DanB Wed, 19 Feb 2020 13:25:52 +0200 diff --git a/sessions/session.go b/sessions/session.go index 9df332d3f..0763bde97 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -81,6 +81,7 @@ type Session struct { debitStop chan struct{} sTerminator *sTerminator // automatic timeout for the session + chargeable bool } // Lock exported function from sync.RWMutex diff --git a/sessions/sessions.go b/sessions/sessions.go index 81ab0f204..d676dae34 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -425,6 +425,11 @@ func (sS *SessionS) debitSession(s *Session, sRunIdx int, dur time.Duration, return } sr := s.SRuns[sRunIdx] + if !s.chargeable { + sS.pause(sr, dur) + sr.TotalUsage += sr.LastUsage + return dur, nil + } rDur := sr.debitReserve(dur, lastUsed) // debit out of reserve, rDur is still to be debited if rDur == time.Duration(0) { return dur, nil // complete debit out of reserve @@ -498,6 +503,27 @@ func (sS *SessionS) debitSession(s *Session, sRunIdx int, dur time.Duration, return } +func (sS *SessionS) pause(sr *SRun, dur time.Duration) { + if sr.CD.LoopIndex > 0 { + sr.CD.TimeStart = sr.CD.TimeEnd + } + sr.CD.TimeEnd = sr.CD.TimeStart.Add(dur) + sr.CD.DurationIndex += dur + ec := engine.NewFreeEventCost(sr.CD.CgrID, sr.CD.RunID, sr.CD.Account, sr.CD.TimeStart, dur) + sr.LastUsage = dur + sr.CD.LoopIndex++ + if sr.EventCost == nil { // is the first increment + // when we start the call with debit interval 0 + // but later we update this value with one greater than 0 + sr.EventCost = ec + } else { // we already debited something + // copy the old AccountSummary as in Merge the old one is overwriten by the new one + ec.AccountSummary = sr.EventCost.AccountSummary + // similar to the debit merge the event costs + sr.EventCost.Merge(ec) + } +} + // debitLoopSession will periodically debit sessions, ie: automatic prepaid // threadSafe since it will run into it's own goroutine func (sS *SessionS) debitLoopSession(s *Session, sRunIdx int, @@ -1121,6 +1147,7 @@ func (sS *SessionS) newSession(cgrEv *utils.CGREvent, resID, clntConnID string, ClientConnID: clntConnID, DebitInterval: dbtItval, } + s.chargeable = s.OptsStart.GetBoolOrDefault(utils.OptsChargeable, true) if !isMsg && sS.isIndexed(s, false) { // check if already exists return nil, utils.ErrExists } @@ -1367,7 +1394,7 @@ func (sS *SessionS) initSessionDebitLoops(s *Session) { return } for i, sr := range s.SRuns { - if s.DebitInterval != 0 && + if s.DebitInterval > 0 && sr.Event.GetStringIgnoreErrors(utils.RequestType) == utils.MetaPrepaid { if s.debitStop == nil { // init the debitStop only for the first sRun with DebitInterval and RequestType MetaPrepaid s.debitStop = make(chan struct{}) @@ -1450,6 +1477,7 @@ func (sS *SessionS) updateSession(s *Session, updtEv, opts engine.MapEvent, isMs s.updateSRuns(updtEv, sS.cgrCfg.SessionSCfg().AlterableFields) sS.setSTerminator(s, opts) // reset the terminator } + s.chargeable = opts.GetBoolOrDefault(utils.OptsChargeable, true) //init has no updtEv if updtEv == nil { updtEv = engine.MapEvent(s.EventStart.Clone()) @@ -1517,20 +1545,24 @@ func (sS *SessionS) endSession(s *Session, tUsage, lastUsage *time.Duration, if sr.EventCost != nil { if !isMsg { // in case of one time charge there is no need of corrections if notCharged := sUsage - sr.EventCost.GetUsage(); notCharged > 0 { // we did not charge enough, make a manual debit here - if sr.CD.LoopIndex > 0 { - sr.CD.TimeStart = sr.CD.TimeEnd - } - sr.CD.TimeEnd = sr.CD.TimeStart.Add(notCharged) - sr.CD.DurationIndex += notCharged - cc := new(engine.CallCost) - if err = sS.connMgr.Call(sS.cgrCfg.SessionSCfg().RALsConns, nil, utils.ResponderDebit, - &engine.CallDescriptorWithOpts{ - CallDescriptor: sr.CD, - Opts: s.OptsStart, - }, cc); err == nil { - sr.EventCost.Merge( - engine.NewEventCostFromCallCost(cc, s.CGRID, - sr.Event.GetStringIgnoreErrors(utils.RunID))) + if !s.chargeable { + sS.pause(sr, notCharged) + } else { + if sr.CD.LoopIndex > 0 { + sr.CD.TimeStart = sr.CD.TimeEnd + } + sr.CD.TimeEnd = sr.CD.TimeStart.Add(notCharged) + sr.CD.DurationIndex += notCharged + cc := new(engine.CallCost) + if err = sS.connMgr.Call(sS.cgrCfg.SessionSCfg().RALsConns, nil, utils.ResponderDebit, + &engine.CallDescriptorWithOpts{ + CallDescriptor: sr.CD, + Opts: s.OptsStart, + }, cc); err == nil { + sr.EventCost.Merge( + engine.NewEventCostFromCallCost(cc, s.CGRID, + sr.Event.GetStringIgnoreErrors(utils.RunID))) + } } } else if notCharged < 0 { // charged too much, try refund if err = sS.refundSession(s, sRunIdx, -notCharged); err != nil { @@ -2585,7 +2617,7 @@ func (sS *SessionS) BiRPCv1TerminateSession(clnt rpcclient.ClientConnector, dbtItvl, isMsg, args.ForceDuration); err != nil { return utils.NewErrRALs(err) } - if _, err = sS.updateSession(s, ev, args.Opts, isMsg); err != nil { + if _, err = sS.updateSession(s, ev, opts, isMsg); err != nil { return err } break @@ -2593,6 +2625,9 @@ func (sS *SessionS) BiRPCv1TerminateSession(clnt rpcclient.ClientConnector, if !isMsg { s.UpdateSRuns(ev, sS.cgrCfg.SessionSCfg().AlterableFields) } + s.Lock() + s.chargeable = opts.GetBoolOrDefault(utils.OptsChargeable, true) + s.Unlock() if err = sS.terminateSession(s, ev.GetDurationPtrIgnoreErrors(utils.Usage), ev.GetDurationPtrIgnoreErrors(utils.LastUsed), @@ -3396,6 +3431,10 @@ func (sS *SessionS) BiRPCv1ProcessEvent(clnt rpcclient.ClientConnector, dbtItvl, false, ralsOpts.Has(utils.MetaFD)); err != nil { return err } + } else { + s.Lock() + s.chargeable = opts.GetBoolOrDefault(utils.OptsChargeable, true) + s.Unlock() } if err = sS.terminateSession(s, ev.GetDurationPtrIgnoreErrors(utils.Usage), @@ -3637,7 +3676,6 @@ func (sS *SessionS) BiRPCv1DeactivateSessions(clnt rpcclient.ClientConnector, } func (sS *SessionS) processCDR(cgrEv *utils.CGREvent, flags []string, rply *string, clnb bool) (err error) { - ev := engine.MapEvent(cgrEv.Event) cgrID := GetSetCGRID(ev) s := sS.getRelocateSession(cgrID, diff --git a/sessions/sessionscover_it_test.go b/sessions/sessionscover_it_test.go index f807c8a7c..95eb7b965 100644 --- a/sessions/sessionscover_it_test.go +++ b/sessions/sessionscover_it_test.go @@ -308,6 +308,7 @@ func testForceSTerminatorManualTermination(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } cfg := config.NewDefaultCGRConfig() @@ -348,6 +349,7 @@ func testForceSTerminatorPostCDRs(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } expected := "INTERNALLY_DISCONNECTED" @@ -384,6 +386,7 @@ func testForceSTerminatorReleaseSession(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } expected := "MANDATORY_IE_MISSING: [connIDs]" @@ -432,6 +435,7 @@ func testForceSTerminatorClientCall(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } expected := "MANDATORY_IE_MISSING: [connIDs]" @@ -464,6 +468,7 @@ func testDebitSession(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } //RunIdx cannot be higher than the length of sessions runs @@ -551,6 +556,7 @@ func testDebitSessionResponderMaxDebit(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } if maxDur, err := sessions.debitSession(ss, 0, 5*time.Second, @@ -614,6 +620,7 @@ func testDebitSessionResponderMaxDebitError(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } if maxDur, err := sessions.debitSession(ss, 0, 5*time.Minute, @@ -668,6 +675,7 @@ func testInitSessionDebitLoops(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } sessions.initSessionDebitLoops(ss) @@ -713,6 +721,7 @@ func testDebitLoopSessionErrorDebiting(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } // session already closed @@ -800,6 +809,7 @@ func testDebitLoopSession(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } go func() { if _, err := sessions.debitLoopSession(ss, 0, time.Second); err != nil { @@ -860,6 +870,7 @@ func testDebitLoopSessionFrcDiscLowerDbtInterval(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } go func() { if _, err := sessions.debitLoopSession(ss, 0, time.Second); err != nil { @@ -913,6 +924,7 @@ func testDebitLoopSessionLowBalance(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } sessions.cgrCfg.SessionSCfg().MinDurLowBalance = 10 * time.Second @@ -970,6 +982,7 @@ func testDebitLoopSessionWarningSessions(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } // will disconnect faster, MinDurLowBalance higher than the debit interval @@ -1031,6 +1044,7 @@ func testDebitLoopSessionDisconnectSession(t *testing.T) { NextAutoDebit: utils.TimePointer(time.Date(2020, time.April, 18, 23, 0, 0, 0, time.UTC)), }, }, + chargeable: true, } // will disconnect faster @@ -1085,6 +1099,7 @@ func testStoreSCost(t *testing.T) { }, }, }, + chargeable: true, } if err := sessions.storeSCost(ss, 0); err != nil { @@ -1132,6 +1147,7 @@ func testRefundSession(t *testing.T) { }, }, }, + chargeable: true, } expectedErr := "no event cost" @@ -1266,6 +1282,7 @@ func testRoundCost(t *testing.T) { }, }, }, + chargeable: true, } //mocking an error API Call @@ -1288,6 +1305,7 @@ func testDisconnectSession(t *testing.T) { TotalUsage: time.Minute, }, }, + chargeable: true, } sTestMock := &testMockClientConn{} @@ -1410,6 +1428,7 @@ func testNewSession(t *testing.T) { }, }, }, + chargeable: true, } if rcv, err := sessions.newSession(cgrEv, "resourceID", "clientConnID", time.Second, false, false); err != nil { @@ -2080,6 +2099,7 @@ func testEndSession(t *testing.T) { }, }, }, + chargeable: true, } activationTime := time.Date(2020, 21, 07, 10, 0, 0, 0, time.UTC) @@ -2196,6 +2216,7 @@ func testBiRPCv1GetActivePassiveSessions(t *testing.T) { }, }, }, + chargeable: true, } sr2[utils.ToR] = utils.MetaSMS sr2[utils.Subject] = "subject2" diff --git a/utils/consts.go b/utils/consts.go index 64b722cc8..e8f16afb7 100755 --- a/utils/consts.go +++ b/utils/consts.go @@ -370,6 +370,7 @@ const ( MetaMaxCostFree = "*free" MetaMaxCostDisconnect = "*disconnect" MetaOut = "*out" + MetaPause = "*pause" MetaVoice = "*voice" ACD = "ACD" @@ -989,7 +990,6 @@ const ( MetaActionProfiles = "*action_profiles" MetaAccountProfiles = "*account_profiles" MetaLoadIDs = "*load_ids" - MetaAccountS = "*accounts" ) // MetaMetrics @@ -2500,11 +2500,12 @@ var ( ) // CGROptionsSet the possible cgr options -var CGROptionsSet = NewStringSet([]string{OptsRatesStartTime, OptsRatesUsage, OptsSessionTTL, OptsSessionTTLMaxDelay, - OptsSessionTTLLastUsed, OptsSessionTTLLastUsage, OptsSessionTTLUsage, OptsDebitInterval, OptsStirATest, - OptsStirPayloadMaxDuration, OptsStirIdentity, OptsStirOriginatorTn, OptsStirOriginatorURI, - OptsStirDestinationTn, OptsStirDestinationURI, OptsStirPublicKeyPath, OptsStirPrivateKeyPath, - OptsAPIKey, OptsRouteID, OptsContext, OptsAttributesProcessRuns, OptsRoutesLimit, OptsRoutesOffset}) +var CGROptionsSet = NewStringSet([]string{OptsRatesStartTime, OptsRatesUsage, OptsSessionTTL, + OptsSessionTTLMaxDelay, OptsSessionTTLLastUsed, OptsSessionTTLLastUsage, OptsSessionTTLUsage, + OptsDebitInterval, OptsStirATest, OptsStirPayloadMaxDuration, OptsStirIdentity, + OptsStirOriginatorTn, OptsStirOriginatorURI, OptsStirDestinationTn, OptsStirDestinationURI, + OptsStirPublicKeyPath, OptsStirPrivateKeyPath, OptsAPIKey, OptsRouteID, OptsContext, + OptsAttributesProcessRuns, OptsRoutesLimit, OptsRoutesOffset, OptsChargeable}) // EventExporter metrics const ( @@ -2536,6 +2537,7 @@ const ( OptsSessionTTLLastUsage = "*sessionTTLLastUsage" OptsSessionTTLUsage = "*sessionTTLUsage" OptsDebitInterval = "*sessionDebitInterval" + OptsChargeable = "*sessionChargeable" // STIR OptsStirATest = "*stirATest" OptsStirPayloadMaxDuration = "*stirPayloadMaxDuration"