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) + } +}