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.
This commit is contained in:
ionutboangiu
2023-04-19 03:49:51 -04:00
committed by Dan Christian Bogos
parent 425590d733
commit 4cd2dc3de8
7 changed files with 673 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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