From 4cd2dc3de8d8da471f6e91ca476b9466744167a1 Mon Sep 17 00:00:00 2001 From: ionutboangiu Date: Wed, 19 Apr 2023 03:49:51 -0400 Subject: [PATCH] Revise CDR rerating The (*CDRServer).processEvent function is now called processEvents and can be passed an array of CGREvents instead of only one. This was done because when calling the RateCDRs API we want to first refund all CDRs before starting to debit again. The rerate parameter is now no longer hardcoded to true for the RateCDRs API.If required, the "*rerate" flag must be provided by the caller. Now, the refundEventCost function returns an additional boolean, that signals whether the refund occured or didn't. If the reRate parameter is set to true, also set refund to true. In case CostDetails is not populated, retrieve it from StorDB if possible and add it to the CGREvent before converting to CDRs. Set CostDetails back to nil once the refund goes through. Remove the refund logic from within the store block. Now that the refund happens before the debit, revise the expected values for the "testV1CDRsProcessEventWithRefund" subtest within the apier/v1/cdrs_it_test.go file. Add an integration test for the following scenario: -create one account with one balance of 1 free minute and rating for the rest. -send one CDR of two minutes with ProcessEvent. This should consume 60s out of the free balance and charge 60s. The SetupTime in the CDR should be 1 hour after the second CDR. -send the second CDR with an usage of 2m. This should be charged entirely. -send a RateCDR API call with OrderBy: "SetupTime". This should rerate the two CDRs from above and change their order of rating. --- apier/v1/cdrs_it_test.go | 4 +- apier/v2/cdrs_it_test.go | 4 +- .../samples/rerate_cdrs_internal/cgrates.json | 42 ++ .../samples/rerate_cdrs_mongo/cgrates.json | 47 ++ .../samples/rerate_cdrs_mysql/cgrates.json | 44 ++ engine/cdrs.go | 121 +++-- general_tests/rerate_cdrs_it_test.go | 463 ++++++++++++++++++ 7 files changed, 673 insertions(+), 52 deletions(-) create mode 100644 data/conf/samples/rerate_cdrs_internal/cgrates.json create mode 100644 data/conf/samples/rerate_cdrs_mongo/cgrates.json create mode 100644 data/conf/samples/rerate_cdrs_mysql/cgrates.json create mode 100644 general_tests/rerate_cdrs_it_test.go diff --git a/apier/v1/cdrs_it_test.go b/apier/v1/cdrs_it_test.go index 4461164b9..1c1f63247 100644 --- a/apier/v1/cdrs_it_test.go +++ b/apier/v1/cdrs_it_test.go @@ -241,9 +241,9 @@ func testV1CDRsProcessEventWithRefund(t *testing.T) { } if err := cdrsRpc.Call(utils.APIerSv2GetAccount, acntAttrs, &acnt); err != nil { t.Error(err) - } else if blc1 := acnt.GetBalanceWithID(utils.VOICE, "BALANCE1"); blc1.Value != 120000000000 { // refund is done after debit + } else if blc1 := acnt.GetBalanceWithID(utils.VOICE, "BALANCE1"); blc1.Value != 60000000000 { t.Errorf("Balance1 is: %s", utils.ToIJSON(blc1)) - } else if blc2 := acnt.GetBalanceWithID(utils.VOICE, "BALANCE2"); blc2.Value != 120000000000 { + } else if blc2 := acnt.GetBalanceWithID(utils.VOICE, "BALANCE2"); blc2.Value != 180000000000 { t.Errorf("Balance2 is: %s", utils.ToIJSON(blc2)) } } diff --git a/apier/v2/cdrs_it_test.go b/apier/v2/cdrs_it_test.go index 954a3e66f..e6a80d38e 100644 --- a/apier/v2/cdrs_it_test.go +++ b/apier/v2/cdrs_it_test.go @@ -277,7 +277,7 @@ func testV2CDRsRateCDRs(t *testing.T) { if err := cdrsRpc.Call(utils.CDRsV1RateCDRs, &engine.ArgRateCDRs{ RPCCDRsFilter: utils.RPCCDRsFilter{NotRunIDs: []string{utils.MetaRaw}}, - Flags: []string{"*chargers:false"}, + Flags: []string{"*chargers:false", utils.MetaRerate}, }, &reply); err != nil { t.Error("Unexpected error: ", err.Error()) } else if reply != utils.OK { @@ -651,7 +651,7 @@ func testV2CDRsRateCDRsWithRatingPlan(t *testing.T) { if err := cdrsRpc.Call(utils.CDRsV1RateCDRs, &engine.ArgRateCDRs{ RPCCDRsFilter: utils.RPCCDRsFilter{NotRunIDs: []string{utils.MetaRaw}, Accounts: []string{"testV2CDRsProcessCDR4"}}, - Flags: []string{"*chargers:true"}, + Flags: []string{"*chargers:true", utils.MetaRerate}, }, &reply); err != nil { t.Error("Unexpected error: ", err.Error()) } else if reply != utils.OK { diff --git a/data/conf/samples/rerate_cdrs_internal/cgrates.json b/data/conf/samples/rerate_cdrs_internal/cgrates.json new file mode 100644 index 000000000..4dbae3dea --- /dev/null +++ b/data/conf/samples/rerate_cdrs_internal/cgrates.json @@ -0,0 +1,42 @@ +{ + +"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, + "max_increments":3000000 +}, + +"cdrs": { + "enabled": true, + "rals_conns": ["*internal"] +}, + +"sessions": { + "enabled": true, + "rals_conns": ["*internal"], + "cdrs_conns": ["*internal"] +}, + +"apiers": { + "enabled": true +} + +} \ No newline at end of file diff --git a/data/conf/samples/rerate_cdrs_mongo/cgrates.json b/data/conf/samples/rerate_cdrs_mongo/cgrates.json new file mode 100644 index 000000000..ec3b82225 --- /dev/null +++ b/data/conf/samples/rerate_cdrs_mongo/cgrates.json @@ -0,0 +1,47 @@ +{ + +"general": { + "log_level": 7, + "reply_timeout": "50s" +}, + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080" +}, + +"data_db": { + "db_type": "mongo", + "db_name": "10", + "db_port": 27017 +}, + +"stor_db": { + "db_type": "mongo", + "db_name": "cgrates", + "db_port": 27017 +}, + + +"rals": { + "enabled": true, + "max_increments":3000000 +}, + +"cdrs": { + "enabled": true, + "rals_conns": ["*localhost"] +}, + +"sessions": { + "enabled": true, + "rals_conns": ["*localhost"], + "cdrs_conns": ["*localhost"] +}, + +"apiers": { + "enabled": true +} + +} \ No newline at end of file diff --git a/data/conf/samples/rerate_cdrs_mysql/cgrates.json b/data/conf/samples/rerate_cdrs_mysql/cgrates.json new file mode 100644 index 000000000..412616646 --- /dev/null +++ b/data/conf/samples/rerate_cdrs_mysql/cgrates.json @@ -0,0 +1,44 @@ +{ + +"general": { + "log_level": 7, + "reply_timeout": "50s" +}, + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080" +}, + +"data_db": { + "db_type": "redis", + "db_port": 6379, + "db_name": "10" +}, + +"stor_db": { + "db_password": "CGRateS.org" +}, + +"rals": { + "enabled": true, + "max_increments":3000000 +}, + +"cdrs": { + "enabled": true, + "rals_conns": ["*localhost"] +}, + +"sessions": { + "enabled": true, + "rals_conns": ["*localhost"], + "cdrs_conns": ["*localhost"] +}, + +"apiers": { + "enabled": true +} + +} \ No newline at end of file diff --git a/engine/cdrs.go b/engine/cdrs.go index 080b90717..2e301a1d0 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -178,7 +178,7 @@ func (cdrS *CDRServer) rateCDR(cdr *CDRWithArgDispatcher) ([]*CDR, error) { if cdr.Usage == 0 { cdrClone.Usage = smCost.Usage } else if smCost.Usage != cdr.Usage { - if err = cdrS.refundEventCost(smCost.CostDetails, + if _, err = cdrS.refundEventCost(smCost.CostDetails, cdrClone.RequestType, cdrClone.ToR); err != nil { return nil, err } @@ -212,7 +212,7 @@ func (cdrS *CDRServer) rateCDR(cdr *CDRWithArgDispatcher) ([]*CDR, error) { cdr.CostDetails.Compute() return []*CDR{cdr.CDR}, nil } - if err = cdrS.refundEventCost(cdr.CostDetails, + if _, err = cdrS.refundEventCost(cdr.CostDetails, cdr.RequestType, cdr.ToR); err != nil { return nil, err } @@ -287,9 +287,9 @@ func (cdrS *CDRServer) rateCDRWithErr(cdr *CDRWithArgDispatcher) (ratedCDRs []*C } // refundEventCost will refund the EventCost using RefundIncrements -func (cdrS *CDRServer) refundEventCost(ec *EventCost, reqType, tor string) (err error) { +func (cdrS *CDRServer) refundEventCost(ec *EventCost, reqType, tor string) (rfnd bool, err error) { if len(cdrS.cgrCfg.CdrsCfg().RaterConns) == 0 { - return utils.NewErrNotConnected(utils.RALService) + return false, utils.NewErrNotConnected(utils.RALService) } if ec == nil || !utils.AccountableRequestTypes.Has(reqType) { return // non refundable @@ -304,7 +304,7 @@ func (cdrS *CDRServer) refundEventCost(ec *EventCost, reqType, tor string) (err &CallDescriptorWithArgDispatcher{CallDescriptor: cd}, &acnt); err != nil { return } - return + return true, nil } // chrgrSProcessEvent forks CGREventWithArgDispatcher into multiples based on matching ChargerS profiles @@ -404,29 +404,39 @@ func (cdrS *CDRServer) exportCDRs(cdrs []*CDR) (err error) { return } -// processEvent processes a CGREvent based on arguments -func (cdrS *CDRServer) processEvent(ev *utils.CGREventWithArgDispatcher, +// processEvents processes a CGREvent based on arguments +func (cdrS *CDRServer) processEvents(evs []*utils.CGREventWithArgDispatcher, chrgS, attrS, refund, ralS, store, reRate, export, thdS, stS bool) (err error) { + if reRate { + refund = true + } if attrS { - if err = cdrS.attrSProcessEvent(ev); err != nil { - utils.Logger.Warning( - fmt.Sprintf("<%s> error: <%s> processing event %+v with %s", - utils.CDRs, err.Error(), utils.ToJSON(ev), utils.AttributeS)) - err = utils.ErrPartiallyExecuted - return + for _, ev := range evs { + if err = cdrS.attrSProcessEvent(ev); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> error: <%s> processing event %+v with %s", + utils.CDRs, err.Error(), utils.ToJSON(ev), utils.AttributeS)) + err = utils.ErrPartiallyExecuted + return + } } } var cgrEvs []*utils.CGREventWithArgDispatcher if chrgS { - if cgrEvs, err = cdrS.chrgrSProcessEvent(ev); err != nil { - utils.Logger.Warning( - fmt.Sprintf("<%s> error: <%s> processing event %+v with %s", - utils.CDRs, err.Error(), utils.ToJSON(ev), utils.ChargerS)) - err = utils.ErrPartiallyExecuted - return + for _, ev := range evs { + var chrgEvs []*utils.CGREventWithArgDispatcher + if chrgEvs, err = cdrS.chrgrSProcessEvent(ev); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> error: <%s> processing event %+v with %s", + utils.CDRs, err.Error(), utils.ToJSON(ev), utils.ChargerS)) + err = utils.ErrPartiallyExecuted + return + } else { + cgrEvs = append(cgrEvs, chrgEvs...) + } } } else { // ChargerS not requested, charge the original event - cgrEvs = []*utils.CGREventWithArgDispatcher{ev} + cgrEvs = evs } // Check if the unique ID was not already processed if !refund { @@ -456,6 +466,30 @@ func (cdrS *CDRServer) processEvent(ev *utils.CGREventWithArgDispatcher, cdrs := make([]*CDR, len(cgrEvs)) if refund || ralS || store || reRate || export { for i, cgrEv := range cgrEvs { + if refund { + if _, has := cgrEv.Event[utils.CostDetails]; !has { + // if CostDetails is not populated or is nil, look for it inside the previously stored cdr + var cgrID string // prepare CGRID to filter for previous CDR + if val, has := cgrEv.Event[utils.CGRID]; !has { + cgrID = utils.Sha1(utils.IfaceAsString(cgrEv.Event[utils.OriginID]), + utils.IfaceAsString(cgrEv.Event[utils.OriginHost])) + } else { + cgrID = utils.IfaceAsString(val) + } + var prevCDRs []*CDR // only one should be returned + if prevCDRs, _, err = cdrS.cdrDb.GetCDRs( + &utils.CDRsFilter{CGRIDs: []string{cgrID}, + RunIDs: []string{utils.IfaceAsString(cgrEv.Event[utils.RunID])}}, false); err != nil { + utils.Logger.Err( + fmt.Sprintf("<%s> could not retrieve previously stored CDR, error: <%s>", + utils.CDRs, err.Error())) + err = utils.ErrPartiallyExecuted + return + } else { + cgrEv.Event[utils.CostDetails] = prevCDRs[0].CostDetails + } + } + } if cdrs[i], err = NewMapEvent(cgrEv.Event).AsCDR(cdrS.cgrCfg, cgrEv.Tenant, cdrS.cgrCfg.GeneralCfg().DefaultTimezone); err != nil { utils.Logger.Warning( @@ -468,12 +502,13 @@ func (cdrS *CDRServer) processEvent(ev *utils.CGREventWithArgDispatcher, } if refund { for _, cdr := range cdrs { - if errRfd := cdrS.refundEventCost(cdr.CostDetails, + if rfnd, errRfd := cdrS.refundEventCost(cdr.CostDetails, cdr.RequestType, cdr.ToR); errRfd != nil { utils.Logger.Warning( fmt.Sprintf("<%s> error: <%s> refunding CDR %+v", utils.CDRs, errRfd.Error(), cdr)) - + } else if rfnd { + cdr.CostDetails = nil // this makes sure that the rater will recalculate (and debit) the cost } } } @@ -481,10 +516,10 @@ func (cdrS *CDRServer) processEvent(ev *utils.CGREventWithArgDispatcher, for i, cdr := range cdrs { for j, rtCDR := range cdrS.rateCDRWithErr( &CDRWithArgDispatcher{CDR: cdr, - ArgDispatcher: ev.ArgDispatcher}) { + ArgDispatcher: cgrEvs[i].ArgDispatcher}) { cgrEv := &utils.CGREventWithArgDispatcher{ CGREvent: rtCDR.AsCGREvent(), - ArgDispatcher: ev.ArgDispatcher, + ArgDispatcher: cgrEvs[i].ArgDispatcher, } if j == 0 { // the first CDR will replace the events we got already as a small optimization cdrs[i] = rtCDR @@ -499,7 +534,7 @@ func (cdrS *CDRServer) processEvent(ev *utils.CGREventWithArgDispatcher, if store { refundCDRCosts := func() { // will be used to refund all CDRs on errors for _, cdr := range cdrs { // refund what we have charged since duplicates are not allowed - if errRfd := cdrS.refundEventCost(cdr.CostDetails, + if _, errRfd := cdrS.refundEventCost(cdr.CostDetails, cdr.RequestType, cdr.ToR); errRfd != nil { utils.Logger.Warning( fmt.Sprintf("<%s> error: <%s> refunding CDR %+v", @@ -513,21 +548,6 @@ func (cdrS *CDRServer) processEvent(ev *utils.CGREventWithArgDispatcher, refundCDRCosts() return } - // CDR was found in StorDB - // reRate is allowed, refund the previous CDR - var prevCDRs []*CDR // only one should be returned - if prevCDRs, _, err = cdrS.cdrDb.GetCDRs( - &utils.CDRsFilter{CGRIDs: []string{cdr.CGRID}, - RunIDs: []string{cdr.RunID}}, false); err != nil { - refundCDRCosts() - return - } - if err = cdrS.refundEventCost(prevCDRs[0].CostDetails, - cdr.RequestType, cdr.ToR); err != nil { - refundCDRCosts() - return - } - // after refund we can force update if err = cdrS.cdrDb.SetCDR(cdr, true); err != nil { utils.Logger.Warning( fmt.Sprintf("<%s> error: <%s> updating CDR %+v", @@ -644,7 +664,7 @@ func (cdrS *CDRServer) V1ProcessCDR(cdr *CDRWithArgDispatcher, reply *string) (e ArgDispatcher: cdr.ArgDispatcher, } - if err = cdrS.processEvent(cgrEv, + if err = cdrS.processEvents([]*utils.CGREventWithArgDispatcher{cgrEv}, len(cdrS.cgrCfg.CdrsCfg().ChargerSConns) != 0 && !cdr.PreRated, len(cdrS.cgrCfg.CdrsCfg().AttributeSConns) != 0, false, @@ -745,7 +765,7 @@ func (cdrS *CDRServer) V1ProcessEvent(arg *ArgV1ProcessEvent, reply *string) (er CGREvent: &arg.CGREvent, ArgDispatcher: arg.ArgDispatcher, } - if err = cdrS.processEvent(cgrEv, chrgS, attrS, refund, + if err = cdrS.processEvents([]*utils.CGREventWithArgDispatcher{cgrEv}, chrgS, attrS, refund, ralS, store, reRate, export, thdS, stS); err != nil { return } @@ -902,23 +922,28 @@ func (cdrS *CDRServer) V1RateCDRs(arg *ArgRateCDRs, reply *string) (err error) { if flgs.HasKey(utils.MetaAttributes) { attrS = flgs.GetBool(utils.MetaAttributes) } + var reRate bool + if flgs.HasKey(utils.MetaRerate) { + reRate = flgs.GetBool(utils.MetaRerate) + } if chrgS && len(cdrS.cgrCfg.CdrsCfg().ChargerSConns) == 0 { return utils.NewErrNotConnected(utils.ChargerS) } - for _, cdr := range cdrs { + cgrEvs := make([]*utils.CGREventWithArgDispatcher, len(cdrs)) + for i, cdr := range cdrs { cdr.Cost = -1 // the cost will be recalculated if cdr.Tenant == utils.EmptyString { cdr.Tenant = cdrS.cgrCfg.GeneralCfg().DefaultTenant } - cgrEv := &utils.CGREventWithArgDispatcher{ + cgrEvs[i] = &utils.CGREventWithArgDispatcher{ CGREvent: cdr.AsCGREvent(), ArgDispatcher: arg.ArgDispatcher, } - if err = cdrS.processEvent(cgrEv, chrgS, attrS, false, - true, store, true, export, thdS, statS); err != nil { - return utils.NewErrServerError(err) - } + } + if err = cdrS.processEvents(cgrEvs, chrgS, attrS, false, + true, store, reRate, export, thdS, statS); err != nil { + return utils.NewErrServerError(err) } *reply = utils.OK return diff --git a/general_tests/rerate_cdrs_it_test.go b/general_tests/rerate_cdrs_it_test.go new file mode 100644 index 000000000..1a2d09411 --- /dev/null +++ b/general_tests/rerate_cdrs_it_test.go @@ -0,0 +1,463 @@ +//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 ( + "math" + "net/rpc" + "os" + "path" + "reflect" + "testing" + "time" + + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + +var ( + rrCdrsCfgPath string + rrCdrsCfg *config.CGRConfig + rrCdrsRPC *rpc.Client + rrCdrsConfDIR string //run tests for specific configuration + rrCdrsDelay int + rrCdrsUUID = utils.GenUUID() + + rrCdrsTests = []func(t *testing.T){ + testRerateCDRsRemoveFolders, + testRerateCDRsCreateFolders, + testRerateCDRsLoadConfig, + testRerateCDRsInitDataDb, + testRerateCDRsResetStorDb, + testRerateCDRsStartEngine, + testRerateCDRsRPCConn, + testRerateCDRsLoadTPs, + + testRerateCDRsSetBalance, + testRerateCDRsGetAccountAfterBalanceSet, + + testRerateCDRsProcessEventCDR1, + testRerateCDRsCheckCDRCostAfterProcessEvent1, + testRerateCDRsGetAccountAfterProcessEvent1, + + testRerateCDRsProcessEventCDR2, + testRerateCDRsCheckCDRCostAfterProcessEvent2, + testRerateCDRsGetAccountAfterProcessEvent2, + + testRerateCDRsRerateCDRs, + testRerateCDRsCheckCDRCostsAfterRerate, + testRerateCDRsGetAccountAfterRerate, + + testRerateCDRsStopEngine, + testRerateCDRsRemoveFolders, + } +) + +// Test start here +func TestRerateCDRs(t *testing.T) { + switch *dbType { + case utils.MetaInternal: + rrCdrsConfDIR = "rerate_cdrs_internal" + case utils.MetaMySQL: + rrCdrsConfDIR = "rerate_cdrs_mysql" + case utils.MetaMongo: + rrCdrsConfDIR = "rerate_cdrs_mongo" + case utils.MetaPostgres: + t.SkipNow() + default: + t.Fatal("Unknown Database type") + } + + for _, stest := range rrCdrsTests { + t.Run(rrCdrsConfDIR, stest) + } +} + +func testRerateCDRsLoadConfig(t *testing.T) { + var err error + rrCdrsCfgPath = path.Join(*dataDir, "conf", "samples", rrCdrsConfDIR) + if rrCdrsCfg, err = config.NewCGRConfigFromPath(rrCdrsCfgPath); err != nil { + t.Error(err) + } + rrCdrsDelay = 1000 +} + +func testRerateCDRsInitDataDb(t *testing.T) { + if err := engine.InitDataDb(rrCdrsCfg); err != nil { + t.Fatal(err) + } +} + +func testRerateCDRsResetStorDb(t *testing.T) { + if err := engine.InitStorDb(rrCdrsCfg); err != nil { + t.Fatal(err) + } +} + +func testRerateCDRsStartEngine(t *testing.T) { + if _, err := engine.StopStartEngine(rrCdrsCfgPath, rrCdrsDelay); err != nil { + t.Fatal(err) + } +} + +func testRerateCDRsRPCConn(t *testing.T) { + var err error + rrCdrsRPC, err = newRPCClient(rrCdrsCfg.ListenCfg()) // We connect over JSON so we can also troubleshoot if needed + if err != nil { + t.Fatal("Could not connect to rater: ", err.Error()) + } +} + +func testRerateCDRsLoadTPs(t *testing.T) { + writeFile := func(fileName, data string) error { + csvFile, err := os.Create(path.Join("/tmp/TestRerateCDRs", fileName)) + if err != nil { + return err + } + defer csvFile.Close() + _, err = csvFile.WriteString(data) + if err != nil { + return err + + } + return csvFile.Sync() + } + + // Create and populate DestinationRates.csv + if err := writeFile(utils.DestinationRatesCsv, ` +#Id,DestinationId,RatesTag,RoundingMethod,RoundingDecimals,MaxCost,MaxCostStrategy +DR_ANY,*any,RT_ANY,*up,20,0, +`); err != nil { + t.Fatal(err) + } + + // Create and populate Rates.csv + if err := writeFile(utils.RatesCsv, ` +#Id,ConnectFee,Rate,RateUnit,RateIncrement,GroupIntervalStart +RT_ANY,0,0.6,60s,1s,0s +`); err != nil { + t.Fatal(err) + } + + // Create and populate RatingPlans.csv + if err := writeFile(utils.RatingPlansCsv, ` +#Id,DestinationRatesId,TimingTag,Weight +RP_ANY,DR_ANY,*any,10 +`); err != nil { + t.Fatal(err) + } + + // Create and populate RatingProfiles.csv + if err := writeFile(utils.RatingProfilesCsv, ` +#Tenant,Category,Subject,ActivationTime,RatingPlanId,RatesFallbackSubject +cgrates.org,call,1001,2014-01-14T00:00:00Z,RP_ANY, +`); err != nil { + t.Fatal(err) + } + + var loadInst string + if err := rrCdrsRPC.Call(utils.APIerSv1LoadTariffPlanFromFolder, + &utils.AttrLoadTpFromFolder{FolderPath: "/tmp/TestRerateCDRs"}, &loadInst); err != nil { + t.Error(err) + } +} + +func testRerateCDRsStopEngine(t *testing.T) { + if err := engine.KillEngine(rrCdrsDelay); err != nil { + t.Error(err) + } +} + +func testRerateCDRsSetBalance(t *testing.T) { + attrSetBalance := utils.AttrSetBalance{ + Tenant: "cgrates.org", + Account: "1001", + Value: float64(time.Minute), + BalanceType: utils.VOICE, + Balance: map[string]interface{}{ + utils.ID: "1001", + }, + } + var reply string + if err := rrCdrsRPC.Call(utils.APIerSv2SetBalance, attrSetBalance, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Errorf("Received: %s", reply) + } +} + +func testRerateCDRsGetAccountAfterBalanceSet(t *testing.T) { + expAcnt := engine.Account{ + ID: "cgrates.org:1001", + BalanceMap: map[string]engine.Balances{ + utils.VOICE: { + { + ID: "1001", + Value: float64(time.Minute), + }, + }, + }, + } + var acnt engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + if err := rrCdrsRPC.Call(utils.APIerSv2GetAccount, attrs, &acnt); err != nil { + t.Error(err) + } else { + expAcnt.UpdateTime = acnt.UpdateTime + expAcnt.BalanceMap[utils.VOICE][0].Uuid = acnt.BalanceMap[utils.VOICE][0].Uuid + if !reflect.DeepEqual(acnt, expAcnt) { + t.Errorf("expected: <%+v>,\nreceived: <%+v>", utils.ToJSON(expAcnt), utils.ToJSON(acnt)) + } + } +} + +func testRerateCDRsProcessEventCDR1(t *testing.T) { + argsEv := &engine.ArgV1ProcessEvent{ + Flags: []string{utils.MetaRALs}, + CGREvent: utils.CGREvent{ + Tenant: "cgrates.org", + ID: "event1", + Event: map[string]interface{}{ + utils.RunID: "run_1", + utils.CGRID: rrCdrsUUID, + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.VOICE, + utils.OriginID: "processCDR1", + utils.OriginHost: "OriginHost1", + utils.RequestType: utils.META_PSEUDOPREPAID, + utils.Account: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2021, time.February, 2, 16, 14, 50, 0, time.UTC), + utils.AnswerTime: time.Date(2021, time.February, 2, 16, 15, 0, 0, time.UTC), + utils.Usage: 2 * time.Minute, + }, + }, + } + var reply string + if err := rrCdrsRPC.Call(utils.CDRsV1ProcessEvent, argsEv, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Error("Unexpected reply received: ", reply) + } + +} + +func testRerateCDRsCheckCDRCostAfterProcessEvent1(t *testing.T) { + var cdrs []*engine.CDR + if err := rrCdrsRPC.Call(utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithArgDispatcher{ + RPCCDRsFilter: &utils.RPCCDRsFilter{ + RunIDs: []string{"run_1"}, + }}, &cdrs); err != nil { + t.Error(err) + } else if cdrs[0].Usage != 2*time.Minute { + t.Errorf("expected usage to be <%+v>, received <%+v>", 2*time.Minute, cdrs[0].Usage) + } else if cdrs[0].Cost != 0.6 { + t.Errorf("expected cost to be <%+v>, received <%+v>", 0.6, cdrs[0].Cost) + } +} + +func testRerateCDRsGetAccountAfterProcessEvent1(t *testing.T) { + expAcnt := engine.Account{ + ID: "cgrates.org:1001", + BalanceMap: map[string]engine.Balances{ + utils.VOICE: { + { + ID: "1001", + Value: 0, + }, + }, + utils.MONETARY: { + { + ID: utils.MetaDefault, + Value: -0.6, + }, + }, + }, + } + var acnt engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + if err := rrCdrsRPC.Call(utils.APIerSv2GetAccount, attrs, &acnt); err != nil { + t.Error(err) + } else { + expAcnt.UpdateTime = acnt.UpdateTime + expAcnt.BalanceMap[utils.VOICE][0].Uuid = acnt.BalanceMap[utils.VOICE][0].Uuid + expAcnt.BalanceMap[utils.MONETARY][0].Uuid = acnt.BalanceMap[utils.MONETARY][0].Uuid + acnt.BalanceMap[utils.MONETARY][0].Value = math.Round(acnt.BalanceMap[utils.MONETARY][0].Value*10) / 10 + if !reflect.DeepEqual(acnt, expAcnt) { + t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ToJSON(expAcnt), utils.ToJSON(acnt)) + } + } +} + +func testRerateCDRsProcessEventCDR2(t *testing.T) { + argsEv := &engine.ArgV1ProcessEvent{ + Flags: []string{utils.MetaRALs}, + CGREvent: utils.CGREvent{ + Tenant: "cgrates.org", + ID: "event2", + Event: map[string]interface{}{ + utils.RunID: "run_2", + utils.CGRID: rrCdrsUUID, + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.VOICE, + utils.OriginID: "processCDR2", + utils.OriginHost: "OriginHost2", + utils.RequestType: utils.META_PSEUDOPREPAID, + utils.Account: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2021, time.February, 2, 15, 14, 50, 0, time.UTC), + utils.AnswerTime: time.Date(2021, time.February, 2, 15, 15, 0, 0, time.UTC), + utils.Usage: 2 * time.Minute, + }, + }, + } + var reply string + if err := rrCdrsRPC.Call(utils.CDRsV1ProcessEvent, argsEv, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Error("Unexpected reply received: ", reply) + } + +} + +func testRerateCDRsCheckCDRCostAfterProcessEvent2(t *testing.T) { + var cdrs []*engine.CDR + if err := rrCdrsRPC.Call(utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithArgDispatcher{ + RPCCDRsFilter: &utils.RPCCDRsFilter{ + RunIDs: []string{"run_2"}, + }}, &cdrs); err != nil { + t.Error(err) + } else if cdrs[0].Usage != 2*time.Minute { + t.Errorf("expected usage to be <%+v>, received <%+v>", 2*time.Minute, cdrs[0].Usage) + } else if cdrs[0].Cost != 1.2 { + t.Errorf("expected cost to be <%+v>, received <%+v>", 1.2, cdrs[0].Cost) + } +} + +func testRerateCDRsGetAccountAfterProcessEvent2(t *testing.T) { + expAcnt := engine.Account{ + ID: "cgrates.org:1001", + BalanceMap: map[string]engine.Balances{ + utils.VOICE: { + { + ID: "1001", + Value: 0, + }, + }, + utils.MONETARY: { + { + ID: utils.MetaDefault, + Value: -1.8, + }, + }, + }, + } + var acnt engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + if err := rrCdrsRPC.Call(utils.APIerSv2GetAccount, attrs, &acnt); err != nil { + t.Error(err) + } else { + expAcnt.UpdateTime = acnt.UpdateTime + expAcnt.BalanceMap[utils.VOICE][0].Uuid = acnt.BalanceMap[utils.VOICE][0].Uuid + expAcnt.BalanceMap[utils.MONETARY][0].Uuid = acnt.BalanceMap[utils.MONETARY][0].Uuid + acnt.BalanceMap[utils.MONETARY][0].Value = math.Round(acnt.BalanceMap[utils.MONETARY][0].Value*10) / 10 + if !reflect.DeepEqual(acnt, expAcnt) { + t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ToJSON(expAcnt), utils.ToJSON(acnt)) + } + } +} + +func testRerateCDRsRerateCDRs(t *testing.T) { + var reply string + if err := rrCdrsRPC.Call(utils.CDRsV1RateCDRs, &engine.ArgRateCDRs{ + Flags: []string{utils.MetaRerate}, + RPCCDRsFilter: utils.RPCCDRsFilter{ + OrderBy: utils.AnswerTime, + CGRIDs: []string{rrCdrsUUID}, + }}, &reply); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received: ", reply) + } +} + +func testRerateCDRsCheckCDRCostsAfterRerate(t *testing.T) { + var cdrs []*engine.CDR + if err := rrCdrsRPC.Call(utils.CDRsV1GetCDRs, &utils.RPCCDRsFilterWithArgDispatcher{ + RPCCDRsFilter: &utils.RPCCDRsFilter{ + CGRIDs: []string{rrCdrsUUID}, + OrderBy: utils.AnswerTime, + }}, &cdrs); err != nil { + t.Error(err) + } else if cdrs[0].Cost != 0.6 { + t.Errorf("expected cost to be <%+v>, received <%+v>", 0.6, cdrs[0].Cost) + } else if cdrs[1].Cost != 1.2 { + t.Errorf("expected cost to be <%+v>, received <%+v>", 1.2, cdrs[1].Cost) + } +} + +func testRerateCDRsGetAccountAfterRerate(t *testing.T) { + expAcnt := engine.Account{ + ID: "cgrates.org:1001", + BalanceMap: map[string]engine.Balances{ + utils.VOICE: { + { + ID: "1001", + Value: 0, + }, + }, + utils.MONETARY: { + { + ID: utils.MetaDefault, + Value: -1.8, + }, + }, + }, + } + var acnt engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + if err := rrCdrsRPC.Call(utils.APIerSv2GetAccount, attrs, &acnt); err != nil { + t.Error(err) + } else { + expAcnt.UpdateTime = acnt.UpdateTime + expAcnt.BalanceMap[utils.VOICE][0].Uuid = acnt.BalanceMap[utils.VOICE][0].Uuid + expAcnt.BalanceMap[utils.MONETARY][0].Uuid = acnt.BalanceMap[utils.MONETARY][0].Uuid + acnt.BalanceMap[utils.MONETARY][0].Value = math.Round(acnt.BalanceMap[utils.MONETARY][0].Value*10) / 10 + if !reflect.DeepEqual(acnt, expAcnt) { + t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ToJSON(expAcnt), utils.ToJSON(acnt)) + } + } +} + +func testRerateCDRsCreateFolders(t *testing.T) { + if err := os.MkdirAll("/tmp/TestRerateCDRs", 0755); err != nil { + t.Error(err) + } +} + +func testRerateCDRsRemoveFolders(t *testing.T) { + if err := os.RemoveAll("/tmp/TestRerateCDRs"); err != nil { + t.Error(err) + } +}