Added *sessionChargeable session option. Fixes #1702

This commit is contained in:
Trial97
2021-02-15 08:13:26 +02:00
committed by Dan Christian Bogos
parent a2688b9536
commit d2e04360bd
10 changed files with 585 additions and 94 deletions

View File

@@ -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,

View File

@@ -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)

View File

@@ -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",
},
},

View File

@@ -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
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*/
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)
}
}

View File

@@ -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 <danb@cgrates.org> Wed, 19 Feb 2020 13:25:52 +0200

View File

@@ -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

View File

@@ -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,

View File

@@ -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"

View File

@@ -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"