From 0dd7a37f9b8e386dd53ab25128a04c1f3c74107d Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 23 Sep 2021 10:53:52 +0300 Subject: [PATCH] Updated EventCost Rounding. Fixes #3018 --- apier/v1/api_interfaces.go | 2 +- apier/v1/dispatcher.go | 2 +- dispatchers/responder.go | 2 +- engine/callcost.go | 4 - engine/calldesc.go | 20 +- engine/cdrs.go | 11 +- engine/eventcost.go | 5 +- engine/eventcost_test.go | 4 +- engine/responder.go | 10 +- engine/responder_test.go | 4 +- general_tests/session_rounding_it_test.go | 395 ++++++++++++++++++++++ sessions/sessions.go | 15 +- sessions/sessions_voice_it_test.go | 4 +- 13 files changed, 448 insertions(+), 30 deletions(-) create mode 100644 general_tests/session_rounding_it_test.go diff --git a/apier/v1/api_interfaces.go b/apier/v1/api_interfaces.go index 8288dc50f..4594d745c 100644 --- a/apier/v1/api_interfaces.go +++ b/apier/v1/api_interfaces.go @@ -98,7 +98,7 @@ type ResponderInterface interface { Debit(arg *engine.CallDescriptorWithArgDispatcher, reply *engine.CallCost) (err error) MaxDebit(arg *engine.CallDescriptorWithArgDispatcher, reply *engine.CallCost) (err error) RefundIncrements(arg *engine.CallDescriptorWithArgDispatcher, reply *engine.Account) (err error) - RefundRounding(arg *engine.CallDescriptorWithArgDispatcher, reply *float64) (err error) + RefundRounding(arg *engine.CallDescriptorWithArgDispatcher, reply *engine.Account) (err error) GetMaxSessionTime(arg *engine.CallDescriptorWithArgDispatcher, reply *time.Duration) (err error) Shutdown(arg *utils.TenantWithArgDispatcher, reply *string) (err error) GetCostOnRatingPlans(arg *utils.GetCostOnRatingPlansArgs, reply *map[string]interface{}) (err error) diff --git a/apier/v1/dispatcher.go b/apier/v1/dispatcher.go index f573aac89..95c035eaa 100755 --- a/apier/v1/dispatcher.go +++ b/apier/v1/dispatcher.go @@ -523,7 +523,7 @@ func (dS *DispatcherResponder) RefundIncrements(args *engine.CallDescriptorWithA return dS.dS.ResponderRefundIncrements(args, reply) } -func (dS *DispatcherResponder) RefundRounding(args *engine.CallDescriptorWithArgDispatcher, reply *float64) error { +func (dS *DispatcherResponder) RefundRounding(args *engine.CallDescriptorWithArgDispatcher, reply *engine.Account) error { return dS.dS.ResponderRefundRounding(args, reply) } diff --git a/dispatchers/responder.go b/dispatchers/responder.go index 414d05847..04573b6e9 100644 --- a/dispatchers/responder.go +++ b/dispatchers/responder.go @@ -128,7 +128,7 @@ func (dS *DispatcherService) ResponderRefundIncrements(args *engine.CallDescript } func (dS *DispatcherService) ResponderRefundRounding(args *engine.CallDescriptorWithArgDispatcher, - reply *float64) (err error) { + reply *engine.Account) (err error) { if len(dS.cfg.DispatcherSCfg().AttributeSConns) != 0 { if args.ArgDispatcher == nil { return utils.NewErrMandatoryIeMissing(utils.ArgDispatcherField) diff --git a/engine/callcost.go b/engine/callcost.go index 2c2d2b3fd..6a6cb0c76 100644 --- a/engine/callcost.go +++ b/engine/callcost.go @@ -157,10 +157,6 @@ func (cc *CallCost) GetLongestRounding() (roundingDecimals int, roundingMethod s return } -func (cc *CallCost) AsJSON() string { - return utils.ToJSON(cc) -} - // public function to update final (merged) callcost func (cc *CallCost) UpdateCost() { cc.deductConnectFee = true diff --git a/engine/calldesc.go b/engine/calldesc.go index 02c2e8f30..aec90d6f9 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -777,7 +777,7 @@ func (cd *CallDescriptor) debit(account *Account, dryRun bool, goNegative bool) if len(roundIncrements) != 0 { rcd := cc.CreateCallDescriptor() rcd.Increments = roundIncrements - rcd.refundRounding() + rcd.refundRounding(cd.account) } } //log.Printf("OUT CC: ", cc) @@ -952,10 +952,14 @@ func (cd *CallDescriptor) RefundIncrements() (acnt *Account, err error) { return } -func (cd *CallDescriptor) refundRounding() (err error) { +func (cd *CallDescriptor) refundRounding(old *Account) (accountsCache map[string]*Account, err error) { // get account list for locking // all must be locked in order to use cache - accountsCache := make(map[string]*Account) + accountsCache = make(map[string]*Account) + if old != nil { + accountsCache[old.ID] = old + defer dm.SetAccount(old) + } for _, increment := range cd.Increments { account, found := accountsCache[increment.BalanceInfo.AccountID] if !found { @@ -983,13 +987,17 @@ func (cd *CallDescriptor) refundRounding() (err error) { return } -func (cd *CallDescriptor) RefundRounding() (err error) { +func (cd *CallDescriptor) RefundRounding() (acc *Account, err error) { accMap := make(utils.StringMap) for _, inc := range cd.Increments { accMap[utils.ACCOUNT_PREFIX+inc.BalanceInfo.AccountID] = true } - _, err = guardian.Guardian.Guard(func() (iface interface{}, err error) { - err = cd.refundRounding() + guardian.Guardian.Guard(func() (_ interface{}, _ error) { + var accCache map[string]*Account + if accCache, err = cd.refundRounding(nil); err != nil { + return + } + acc = accCache[utils.ConcatenatedKey(cd.Tenant, cd.Account)] return }, config.CgrConfig().GeneralCfg().LockingTimeout, accMap.Slice()...) return diff --git a/engine/cdrs.go b/engine/cdrs.go index 38cedcdf3..4a30f7ef3 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -810,6 +810,10 @@ func (cdrS *CDRServer) V2StoreSessionCost(args *ArgsV2CDRSStoreSMCost, reply *st } // end of RPC caching cc := args.Cost.CostDetails.AsCallCost(utils.EmptyString) + if args.Cost.CostDetails.AccountSummary != nil { + cc.Tenant = args.Cost.CostDetails.AccountSummary.Tenant + cc.Account = args.Cost.CostDetails.AccountSummary.ID + } cc.Round() roundIncrements := cc.GetRoundIncrements() if len(roundIncrements) != 0 { @@ -817,15 +821,18 @@ func (cdrS *CDRServer) V2StoreSessionCost(args *ArgsV2CDRSStoreSMCost, reply *st cd.CgrID = args.Cost.CGRID cd.RunID = args.Cost.RunID cd.Increments = roundIncrements - var response float64 + response := new(Account) if err := cdrS.connMgr.Call(cdrS.cgrCfg.CdrsCfg().RaterConns, nil, utils.ResponderRefundRounding, &CallDescriptorWithArgDispatcher{CallDescriptor: cd}, - &response); err != nil { + response); err != nil { utils.Logger.Warning( fmt.Sprintf(" RefundRounding for cc: %+v, got error: %s", cc, err.Error())) } + if response != nil { + cc.AccountSummary = response.AsAccountSummary() + } } if err = cdrS.storeSMCost( &SMCost{ diff --git a/engine/eventcost.go b/engine/eventcost.go index e580ff306..4b621e8e3 100644 --- a/engine/eventcost.go +++ b/engine/eventcost.go @@ -62,9 +62,7 @@ func NewEventCostFromCallCost(cc *CallCost, cgrID, runID string) (ec *EventCost) cIl.Increments = append(cIl.Increments, ec.newChargingIncrement(incr, rf, false)) } if ts.RoundIncrement != nil { - rIncr := ec.newChargingIncrement(ts.RoundIncrement, rf, true) - rIncr.Cost = -rIncr.Cost - cIl.Increments = append(cIl.Increments, rIncr) + cIl.Increments = append(cIl.Increments, ec.newChargingIncrement(ts.RoundIncrement, rf, true)) } ec.Charges[i] = cIl } @@ -401,7 +399,6 @@ func (ec *EventCost) AsCallCost(tor string) *CallCost { l-- incrs = incrs[:l] ts.RoundIncrement = ec.newIntervalFromCharge(cIl.Increments[l-1]) - ts.RoundIncrement.Cost = -ts.RoundIncrement.Cost } ts.Increments = make(Increments, l) } diff --git a/engine/eventcost_test.go b/engine/eventcost_test.go index a9f5fe51a..3b9d71801 100644 --- a/engine/eventcost_test.go +++ b/engine/eventcost_test.go @@ -354,7 +354,7 @@ func TestNewEventCostFromCallCost(t *testing.T) { RatingPlanId: "RPL_RETAIL1", CompressFactor: 1, RoundIncrement: &Increment{ - Cost: 0.1, + Cost: -0.1, BalanceInfo: &DebitInfo{ Monetary: &MonetaryInfo{UUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010", ID: utils.MetaDefault, @@ -586,7 +586,7 @@ func TestNewEventCostFromCallCost(t *testing.T) { AccountID: "cgrates.org:dan", BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010", RatingID: "*rounding", - Units: 0.1, + Units: -0.1, ExtraChargeID: "", }, }, diff --git a/engine/responder.go b/engine/responder.go index e174881ec..f2715db75 100644 --- a/engine/responder.go +++ b/engine/responder.go @@ -267,7 +267,7 @@ func (rs *Responder) RefundIncrements(arg *CallDescriptorWithArgDispatcher, repl return } -func (rs *Responder) RefundRounding(arg *CallDescriptorWithArgDispatcher, reply *float64) (err error) { +func (rs *Responder) RefundRounding(arg *CallDescriptorWithArgDispatcher, reply *Account) (err error) { // RPC caching if config.CgrConfig().CacheCfg()[utils.CacheRPCResponses].Limit != 0 { cacheKey := utils.ConcatenatedKey(utils.ResponderRefundRounding, arg.CgrID) @@ -278,7 +278,7 @@ func (rs *Responder) RefundRounding(arg *CallDescriptorWithArgDispatcher, reply if itm, has := Cache.Get(utils.CacheRPCResponses, cacheKey); has { cachedResp := itm.(*utils.CachedRPCResponse) if cachedResp.Error == nil { - *reply = *cachedResp.Result.(*float64) + *reply = *cachedResp.Result.(*Account) } return cachedResp.Error } @@ -294,7 +294,11 @@ func (rs *Responder) RefundRounding(arg *CallDescriptorWithArgDispatcher, reply err = utils.ErrMaxUsageExceeded return } - err = arg.RefundRounding() + var acc *Account + if acc, err = arg.RefundRounding(); err != nil || acc == nil { + return + } + *reply = *acc return } diff --git a/engine/responder_test.go b/engine/responder_test.go index 2f86a8964..d59d76303 100644 --- a/engine/responder_test.go +++ b/engine/responder_test.go @@ -387,7 +387,7 @@ func TestResponderRefundRoundingMaxUsageANY(t *testing.T) { TimeEnd: tEnd, }, } - var reply float64 + var reply Account if err := rsponder.RefundRounding(cd, &reply); err == nil || err.Error() != utils.ErrMaxUsageExceeded.Error() { t.Errorf("Expected %+v, received : %+v", utils.ErrMaxUsageExceeded, err) @@ -414,7 +414,7 @@ func TestResponderRefundRoundingMaxUsageVOICE(t *testing.T) { TimeEnd: tEnd, }, } - var reply float64 + var reply Account if err := rsponder.RefundRounding(cd, &reply); err == nil || err.Error() != utils.ErrMaxUsageExceeded.Error() { t.Errorf("Expected %+v, received : %+v", utils.ErrMaxUsageExceeded, err) diff --git a/general_tests/session_rounding_it_test.go b/general_tests/session_rounding_it_test.go new file mode 100644 index 000000000..1059c0651 --- /dev/null +++ b/general_tests/session_rounding_it_test.go @@ -0,0 +1,395 @@ +//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 ( + "net/rpc" + "path" + "testing" + "time" + + v1 "github.com/cgrates/cgrates/apier/v1" + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/sessions" + "github.com/cgrates/cgrates/utils" +) + +var ( + sesRndCfgPath string + sesRndCfgDIR string + sesRndCfg *config.CGRConfig + sesRndRPC *rpc.Client + sesRndAccount = "testAccount" + sesRndTenant = "cgrates.org" + + sesRndExpCost float64 + sesRndExpMaxUsage time.Duration + sesRndExpBalanceValue float64 + + sesRndCgrEv = &utils.CGREvent{ + Tenant: sesRndTenant, + Event: map[string]interface{}{ + utils.Tenant: sesRndTenant, + utils.Category: utils.CALL, + utils.ToR: utils.VOICE, + utils.Account: sesRndAccount, + utils.Destination: "TEST", + 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: 10 * time.Second, + utils.SessionTTL: 0, + utils.CGRDebitInterval: time.Second, + }, + } + + sTestSesRndIt = []func(t *testing.T){ + testSesRndItLoadConfig, + testSesRndItResetDataDB, + testSesRndItResetStorDb, + testSesRndItStartEngine, + testSesRndItRPCConn, + testSesRndItLoadRating, + testSesRndItAddCharger, + + testSesRndItPreparePostpaidUP, + testSesRndItAddVoiceBalance, + testSesRndItPrepareCDRs, + testSesRndItCheckCdrs, + + testSesRndItPreparePostpaidDOWN, + testSesRndItAddVoiceBalance, + testSesRndItPrepareCDRs, + testSesRndItCheckCdrs, + + testSesRndItPreparePrepaidUP, + testSesRndItAddVoiceBalance, + testSesRndItPrepareCDRs, + testSesRndItCheckCdrs, + + testSesRndItPreparePrepaidDOWN, + testSesRndItAddVoiceBalance, + testSesRndItPrepareCDRs, + testSesRndItCheckCdrs, + + testSesRndItStopCgrEngine, + } +) + +func TestSesRndIt(t *testing.T) { + switch *dbType { + case utils.MetaInternal: + sesRndCfgDIR = "sessions_internal" + case utils.MetaMySQL: + sesRndCfgDIR = "sessions_mysql" + case utils.MetaMongo: + sesRndCfgDIR = "sessions_mongo" + case utils.MetaPostgres: + t.SkipNow() + default: + t.Fatal("Unknown Database type") + } + for _, stest := range sTestSesRndIt { + t.Run(sesRndCfgDIR, stest) + } +} + +func testSesRndItPreparePostpaidUP(t *testing.T) { + sesRndCgrEv.Event[utils.Subject] = "up" + sesRndCgrEv.Event[utils.RequestType] = utils.META_POSTPAID + sesRndCgrEv.Event[utils.OriginID] = "RndupMETA_POSTPAID" + sesRndExpMaxUsage = 10 * time.Second + sesRndExpCost = 0.4 + sesRndExpBalanceValue = 3599999999999.5977 +} + +func testSesRndItPreparePostpaidDOWN(t *testing.T) { + sesRndCgrEv.Event[utils.Subject] = "down" + sesRndCgrEv.Event[utils.RequestType] = utils.META_POSTPAID + sesRndCgrEv.Event[utils.OriginID] = "RnddownMETA_POSTPAID" + sesRndExpMaxUsage = 10 * time.Second + sesRndExpCost = 0.3 + sesRndExpBalanceValue = 3599999999999.697 +} + +func testSesRndItPreparePrepaidUP(t *testing.T) { + sesRndCgrEv.Event[utils.Subject] = "up" + sesRndCgrEv.Event[utils.RequestType] = utils.META_PREPAID + sesRndCgrEv.Event[utils.OriginID] = "RndupMETA_PREPAID" + sesRndExpMaxUsage = 3 * time.Hour + sesRndExpCost = 0.4 + sesRndExpBalanceValue = 3599999999999.5977 +} + +func testSesRndItPreparePrepaidDOWN(t *testing.T) { + sesRndCgrEv.Event[utils.Subject] = "down" + sesRndCgrEv.Event[utils.RequestType] = utils.META_PREPAID + sesRndCgrEv.Event[utils.OriginID] = "RnddownMETA_PREPAID" + sesRndExpMaxUsage = 3 * time.Hour + sesRndExpCost = 0.3 + sesRndExpBalanceValue = 3599999999999.697 +} + +// test for 0 balance with sesRndsion terminate with 1s usage +func testSesRndItLoadConfig(t *testing.T) { + sesRndCfgPath = path.Join(*dataDir, "conf", "samples", sesRndCfgDIR) + if sesRndCfg, err = config.NewCGRConfigFromPath(sesRndCfgPath); err != nil { + t.Error(err) + } +} + +func testSesRndItResetDataDB(t *testing.T) { + if err := engine.InitDataDb(sesRndCfg); err != nil { + t.Fatal(err) + } +} + +func testSesRndItResetStorDb(t *testing.T) { + if err := engine.InitStorDb(sesRndCfg); err != nil { + t.Fatal(err) + } +} + +func testSesRndItStartEngine(t *testing.T) { + if _, err := engine.StopStartEngine(sesRndCfgPath, *waitRater); err != nil { + t.Fatal(err) + } +} + +func testSesRndItRPCConn(t *testing.T) { + var err error + if sesRndRPC, err = newRPCClient(sesRndCfg.ListenCfg()); err != nil { + t.Fatal(err) + } +} + +func testSesRndItLoadRating(t *testing.T) { + var reply string + if err := sesRndRPC.Call(utils.APIerSv1SetTPRate, &utils.TPRate{ + TPid: utils.TEST_SQL, + ID: "RT1", + RateSlots: []*utils.RateSlot{ + {ConnectFee: 0, Rate: 0.033, RateUnit: "1s", RateIncrement: "1s", GroupIntervalStart: "0s"}, + }, + }, &reply); err != nil { + t.Error("Got error on APIerSv1.SetTPRate: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received when calling APIerSv1.SetTPRate: ", reply) + } + + dr := &utils.TPDestinationRate{ + TPid: utils.TEST_SQL, + ID: "DR_UP", + DestinationRates: []*utils.DestinationRate{ + {DestinationId: utils.ANY, RateId: "RT1", RoundingMethod: utils.ROUNDING_UP, RoundingDecimals: 1}, + }, + } + if err := sesRndRPC.Call(utils.APIerSv1SetTPDestinationRate, dr, &reply); err != nil { + t.Error("Got error on APIerSv1.SetTPDestinationRate: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received when calling APIerSv1.SetTPDestinationRate: ", reply) + } + dr.ID = "DR_DOWN" + dr.DestinationRates[0].RoundingMethod = utils.ROUNDING_DOWN + if err := sesRndRPC.Call(utils.APIerSv1SetTPDestinationRate, dr, &reply); err != nil { + t.Error("Got error on APIerSv1.SetTPDestinationRate: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received when calling APIerSv1.SetTPDestinationRate: ", reply) + } + + rp := &utils.TPRatingPlan{ + TPid: utils.TEST_SQL, + ID: "RP_UP", + RatingPlanBindings: []*utils.TPRatingPlanBinding{ + {DestinationRatesId: "DR_UP", TimingId: utils.ANY, Weight: 10}, + }, + } + if err := sesRndRPC.Call(utils.APIerSv1SetTPRatingPlan, rp, &reply); err != nil { + t.Error("Got error on APIerSv1.SetTPRatingPlan: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received when calling APIerSv1.SetTPRatingPlan: ", reply) + } + rp.ID = "RP_DOWN" + rp.RatingPlanBindings[0].DestinationRatesId = "DR_DOWN" + if err := sesRndRPC.Call(utils.APIerSv1SetTPRatingPlan, rp, &reply); err != nil { + t.Error("Got error on APIerSv1.SetTPRatingPlan: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received when calling APIerSv1.SetTPRatingPlan: ", reply) + } + + rpf := &utils.TPRatingProfile{ + TPid: utils.TEST_SQL, + LoadId: utils.TEST_SQL, + Tenant: sesRndTenant, + Category: utils.CALL, + Subject: "up", + RatingPlanActivations: []*utils.TPRatingActivation{{ + RatingPlanId: "RP_UP", + FallbackSubjects: utils.EmptyString, + }}, + } + if err := sesRndRPC.Call(utils.APIerSv1SetTPRatingProfile, rpf, &reply); err != nil { + t.Error("Got error on APIerSv1.SetTPRatingProfile: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received when calling APIerSv1.SetTPRatingProfile: ", reply) + } + rpf.Subject = "down" + rpf.RatingPlanActivations[0].RatingPlanId = "RP_DOWN" + if err := sesRndRPC.Call(utils.APIerSv1SetTPRatingProfile, rpf, &reply); err != nil { + t.Error("Got error on APIerSv1.SetTPRatingProfile: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received when calling APIerSv1.SetTPRatingProfile: ", reply) + } + + if err := sesRndRPC.Call(utils.APIerSv1LoadRatingPlan, &v1.AttrLoadRatingPlan{TPid: utils.TEST_SQL}, &reply); err != nil { + t.Error("Got error on APIerSv1.LoadRatingPlan: ", err.Error()) + } else if reply != utils.OK { + t.Error("Calling APIerSv1.LoadRatingPlan got reply: ", reply) + } + + if err := sesRndRPC.Call(utils.APIerSv1LoadRatingProfile, &utils.TPRatingProfile{ + TPid: utils.TEST_SQL, LoadId: utils.TEST_SQL, + Tenant: sesRndTenant, Category: utils.CALL}, &reply); err != nil { + t.Error("Got error on APIerSv1.VOICE: ", err.Error()) + } else if reply != utils.OK { + t.Error("Calling APIerSv1.LoadRatingProfile got reply: ", reply) + } + +} + +func testSesRndItAddCharger(t *testing.T) { + //add a default charger + var result string + if err := sesRndRPC.Call(utils.APIerSv1SetChargerProfile, &engine.ChargerProfile{ + Tenant: sesRndTenant, + ID: "default", + RunID: utils.MetaDefault, + AttributeIDs: []string{utils.META_NONE}, + Weight: 20, + }, &result); err != nil { + t.Error(err) + } else if result != utils.OK { + t.Error("Unexpected reply returned", result) + } +} + +func testSesRndItAddVoiceBalance(t *testing.T) { + var reply string + if err := sesRndRPC.Call(utils.APIerSv2SetBalance, utils.AttrSetBalance{ + Tenant: sesRndTenant, + Account: sesRndAccount, + BalanceType: utils.MONETARY, + Value: float64(time.Hour), + Balance: map[string]interface{}{ + utils.ID: "TestSesBal1", + }, + }, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Errorf("Received: %s", reply) + } + + var acnt engine.Account + if err := sesRndRPC.Call(utils.APIerSv2GetAccount, + &utils.AttrGetAccount{ + Tenant: sesRndTenant, + Account: sesRndAccount, + }, &acnt); err != nil { + t.Fatal(err) + } + expected := float64(time.Hour) + if rply := acnt.BalanceMap[utils.MONETARY].GetTotalValue(); rply != expected { + t.Errorf("Expected: %v, received: %v", expected, rply) + } +} + +func testSesRndItPrepareCDRs(t *testing.T) { + var reply sessions.V1InitSessionReply + if err := sesRndRPC.Call(utils.SessionSv1InitiateSession, + &sessions.V1InitSessionArgs{ + InitSession: true, + CGREvent: sesRndCgrEv, + }, &reply); err != nil { + t.Error(err) + return + } else if reply.MaxUsage != sesRndExpMaxUsage { + t.Errorf("Unexpected MaxUsage: %v", reply.MaxUsage) + } + time.Sleep(50 * time.Millisecond) + + var rply string + if err := sesRndRPC.Call(utils.SessionSv1TerminateSession, + &sessions.V1TerminateSessionArgs{ + TerminateSession: true, + CGREvent: sesRndCgrEv, + }, &rply); err != nil { + t.Error(err) + } else if rply != utils.OK { + t.Errorf("Unexpected reply: %s", rply) + } + + if err := sesRndRPC.Call(utils.SessionSv1ProcessCDR, + sesRndCgrEv, &rply); err != nil { + t.Error(err) + } else if rply != utils.OK { + t.Errorf("Received reply: %s", rply) + } + time.Sleep(20 * time.Millisecond) +} + +func testSesRndItCheckCdrs(t *testing.T) { + var cdrs []*engine.ExternalCDR + req := utils.RPCCDRsFilter{Accounts: []string{sesRndAccount}, OriginIDs: []string{utils.IfaceAsString(sesRndCgrEv.Event[utils.OriginID])}} + if err := sesRndRPC.Call(utils.APIerSv2GetCDRs, req, &cdrs); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(cdrs) != 1 { + t.Fatal("Wrong number of CDRs") + } else if cd, err := engine.IfaceAsEventCost(cdrs[0].CostDetails); err != nil { + t.Fatal(err) + } else if cd.Cost == nil || + *cd.Cost != sesRndExpCost || + *cd.Cost != cdrs[0].Cost { + t.Errorf("CDR cost= %v", utils.ToJSON(cdrs[0].Cost)) + t.Errorf("CostDetails cost= %v", utils.ToJSON(cd.Cost)) + t.Errorf("Expected cost=%v", utils.ToJSON(sesRndExpCost)) + t.Log(cdrs[0].CostDetails) + } else if len(cd.AccountSummary.BalanceSummaries) != 1 || + cd.AccountSummary.BalanceSummaries[0].Value != sesRndExpBalanceValue { + t.Errorf("Unexpected AccountSummary: %v", utils.ToJSON(cd.AccountSummary)) + } + var acnt engine.Account + if err := sesRndRPC.Call(utils.APIerSv2GetAccount, + &utils.AttrGetAccount{ + Tenant: sesRndTenant, + Account: sesRndAccount, + }, &acnt); err != nil { + t.Fatal(err) + } + if rply := acnt.BalanceMap[utils.MONETARY].GetTotalValue(); rply != sesRndExpBalanceValue { + t.Errorf("Expected: %+v, received: %v", utils.ToJSON(sesRndExpBalanceValue), utils.ToJSON(rply)) + } +} + +func testSesRndItStopCgrEngine(t *testing.T) { + if err := engine.KillEngine(100); err != nil { + t.Error(err) + } +} diff --git a/sessions/sessions.go b/sessions/sessions.go index b351a0fda..9f7cce17f 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -651,19 +651,30 @@ func (sS *SessionS) roundCost(s *Session, sRunIdx int) (err error) { sr := s.SRuns[sRunIdx] runID := sr.Event.GetStringIgnoreErrors(utils.RunID) cc := sr.EventCost.AsCallCost(utils.EmptyString) + if sr.CD != nil { + cc.Category = sr.CD.Category + cc.Subject = sr.CD.Subject + cc.Tenant = sr.CD.Tenant + cc.Account = sr.CD.Account + cc.Destination = sr.CD.Destination + cc.ToR = sr.CD.ToR + } cc.Round() if roundIncrements := cc.GetRoundIncrements(); len(roundIncrements) != 0 { cd := cc.CreateCallDescriptor() cd.CgrID = s.CGRID cd.RunID = runID cd.Increments = roundIncrements - var response float64 + response := new(engine.Account) if err = sS.connMgr.Call(sS.cgrCfg.SessionSCfg().RALsConns, nil, utils.ResponderRefundRounding, &engine.CallDescriptorWithArgDispatcher{CallDescriptor: cd}, - &response); err != nil { + response); err != nil { return } + if response != nil { + cc.AccountSummary = response.AsAccountSummary() + } } sr.EventCost = engine.NewEventCostFromCallCost(cc, s.CGRID, runID) return diff --git a/sessions/sessions_voice_it_test.go b/sessions/sessions_voice_it_test.go index d0dfcc53a..cd008a149 100644 --- a/sessions/sessions_voice_it_test.go +++ b/sessions/sessions_voice_it_test.go @@ -934,8 +934,8 @@ func testSessionsVoiceSessionTTL(t *testing.T) { if cdrs[0].Usage != "2m30.05s" { t.Errorf("Unexpected CDR Usage received, cdr: %v %+v ", cdrs[0].Usage, cdrs[0]) } - if cdrs[0].Cost != 1.5332 { - t.Errorf("Unexpected CDR Cost received, cdr: %v %+v ", cdrs[0].Cost, cdrs[0]) + if cdrs[0].Cost != 1.5334 { + t.Errorf("Unexpected CDR Cost received, cdr: %v %+v ", cdrs[0].Cost, utils.ToJSON(cdrs[0])) } } }