diff --git a/apier/v1/accounts.go b/apier/v1/accounts.go index 3a23278a1..4b39ccab6 100644 --- a/apier/v1/accounts.go +++ b/apier/v1/accounts.go @@ -573,6 +573,86 @@ func (apierSv1 *APIerSv1) SetBalance(attr *utils.AttrSetBalance, reply *string) return } +// SetBalances sets multiple balances for the given account +// if the account is not already created it will create the account also +func (apierSv1 *APIerSv1) SetBalances(attr *utils.AttrSetBalances, reply *string) (err error) { + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "Balances"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + + accID := utils.ConcatenatedKey(attr.Tenant, attr.Account) + if _, err = apierSv1.DataManager.GetAccount(accID); err != nil { + // create account if not exists + account := &engine.Account{ + ID: accID, + } + if err = apierSv1.DataManager.SetAccount(account); err != nil { + return + } + } + for _, bal := range attr.Balances { + at := &engine.ActionTiming{} + + var balFltr *engine.BalanceFilter + if balFltr, err = engine.NewBalanceFilter(bal.Balance, apierSv1.Config.GeneralCfg().DefaultTimezone); err != nil { + return + } + balFltr.Type = utils.StringPointer(bal.BalanceType) + if bal.Value != 0 { + balFltr.Value = &utils.ValueFormula{Static: math.Abs(bal.Value)} + } + if (balFltr.ID == nil || *balFltr.ID == "") && + (balFltr.Uuid == nil || *balFltr.Uuid == "") { + return utils.NewErrMandatoryIeMissing("BalanceID", "or", "BalanceUUID") + } + + //check if we have extra data + if bal.ActionExtraData != nil && len(*bal.ActionExtraData) != 0 { + at.ExtraData = *bal.ActionExtraData + } + + at.SetAccountIDs(utils.StringMap{accID: true}) + if balFltr.TimingIDs != nil { + for _, timingID := range balFltr.TimingIDs.Slice() { + var tmg *utils.TPTiming + if tmg, err = apierSv1.DataManager.GetTiming(timingID, false, utils.NonTransactional); err != nil { + return + } + balFltr.Timings = append(balFltr.Timings, &engine.RITiming{ + ID: tmg.ID, + Years: tmg.Years, + Months: tmg.Months, + MonthDays: tmg.MonthDays, + WeekDays: tmg.WeekDays, + StartTime: tmg.StartTime, + EndTime: tmg.EndTime, + }) + } + } + + a := &engine.Action{ + ActionType: utils.SET_BALANCE, + Balance: balFltr, + } + publishAction := &engine.Action{ + ActionType: utils.MetaPublishBalance, + } + acts := engine.Actions{a, publishAction} + if bal.Cdrlog { + acts = engine.Actions{a, publishAction, &engine.Action{ + ActionType: utils.CDRLOG, + }} + } + at.SetActions(acts) + if err = at.Execute(nil, nil); err != nil { + return + } + } + + *reply = utils.OK + return +} + // RemoveBalances remove the matching balances for the account func (apierSv1 *APIerSv1) RemoveBalances(attr *utils.AttrSetBalance, reply *string) (err error) { if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType"}); len(missing) != 0 { diff --git a/apier/v1/routes_it_test.go b/apier/v1/routes_it_test.go index b822a6bd6..eb79a093b 100644 --- a/apier/v1/routes_it_test.go +++ b/apier/v1/routes_it_test.go @@ -73,6 +73,7 @@ var ( testV1RoutesOneRouteWithoutDestination, testV1RouteRoutePing, testV1RouteMultipleRouteSameID, + testV1RouteAccountWithRatingPlan, testV1RouteStopEngine, } ) @@ -1237,6 +1238,215 @@ func testV1RouteMultipleRouteSameID(t *testing.T) { } } +func testV1RouteAccountWithRatingPlan(t *testing.T) { + splPrf = &RouteWithCache{ + RouteProfile: &engine.RouteProfile{ + Tenant: "cgrates.org", + ID: "RouteWithAccAndRP", + FilterIDs: []string{"*string:~*req.EventType:testV1RouteAccountWithRatingPlan"}, + Sorting: utils.MetaLC, + Routes: []*engine.Route{ + { + ID: "RouteWithAccAndRP", + AccountIDs: []string{"AccWithVoice"}, + RatingPlanIDs: []string{"RP_ANY2CNT_SEC"}, + Weight: 20, + }, + { + ID: "RouteWithRP", + RatingPlanIDs: []string{"RP_ANY1CNT_SEC"}, + Weight: 10, + }, + }, + Weight: 100, + }, + } + + var result string + if err := splSv1Rpc.Call(utils.APIerSv1SetRouteProfile, splPrf, &result); err != nil { + t.Error(err) + } else if result != utils.OK { + t.Error("Unexpected reply returned", result) + } + + attrSetBalance := utils.AttrSetBalance{ + Tenant: "cgrates.org", + Account: "AccWithVoice", + BalanceType: utils.VOICE, + Value: 30 * float64(time.Second), + Balance: map[string]interface{}{ + utils.ID: "VoiceBalance", + }, + } + var reply string + if err := splSv1Rpc.Call(utils.APIerSv2SetBalance, &attrSetBalance, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Errorf("Received: %s", reply) + } + var acnt *engine.Account + attrAcc := &utils.AttrGetAccount{ + Tenant: "cgrates.org", + Account: "AccWithVoice", + } + if err := splSv1Rpc.Call(utils.APIerSv2GetAccount, attrAcc, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.VOICE].GetTotalValue() != 30*float64(time.Second) { + t.Errorf("Unexpected balance received : %+v", acnt.BalanceMap[utils.VOICE].GetTotalValue()) + } + + // test for 30 seconds usage + // we expect that the route with account to have cost 0 + tNow := time.Now() + ev := &engine.ArgsGetRoutes{ + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + Time: &tNow, + ID: "testV1RouteAccountWithRatingPlan", + Event: map[string]interface{}{ + utils.Account: "RandomAccount", + utils.Destination: "+135876", + utils.SetupTime: utils.MetaNow, + utils.Usage: "30s", + "EventType": "testV1RouteAccountWithRatingPlan", + }, + }, + } + eSpls := &engine.SortedRoutes{ + ProfileID: "RouteWithAccAndRP", + Sorting: utils.MetaLC, + Count: 2, + SortedRoutes: []*engine.SortedRoute{ + { + RouteID: "RouteWithAccAndRP", + SortingData: map[string]interface{}{ + utils.Account: "AccWithVoice", + "Cost": 0.0, + "MaxUsage": 30000000000.0, + utils.Weight: 20.0, + }, + }, + { + RouteID: "RouteWithRP", + SortingData: map[string]interface{}{ + utils.Cost: 0.3, + "RatingPlanID": "RP_ANY1CNT_SEC", + utils.Weight: 10.0, + }, + }, + }, + } + var suplsReply *engine.SortedRoutes + if err := splSv1Rpc.Call(utils.RouteSv1GetRoutes, + ev, &suplsReply); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eSpls, suplsReply) { + t.Errorf("Expecting: %s \n received: %s", + utils.ToJSON(eSpls), utils.ToJSON(suplsReply)) + } + + // test for 60 seconds usage + // 30 seconds are covered by account and the remaining will be calculated + ev = &engine.ArgsGetRoutes{ + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + Time: &tNow, + ID: "testV1RouteAccountWithRatingPlan", + Event: map[string]interface{}{ + utils.Account: "RandomAccount", + utils.Destination: "+135876", + utils.SetupTime: utils.MetaNow, + utils.Usage: "60s", + "EventType": "testV1RouteAccountWithRatingPlan", + }, + }, + } + eSpls = &engine.SortedRoutes{ + ProfileID: "RouteWithAccAndRP", + Sorting: utils.MetaLC, + Count: 2, + SortedRoutes: []*engine.SortedRoute{ + { + RouteID: "RouteWithAccAndRP", + SortingData: map[string]interface{}{ + utils.Account: "AccWithVoice", + "Cost": 0.6, + "MaxUsage": 30000000000.0, + "RatingPlanID": "RP_ANY2CNT_SEC", + utils.Weight: 20.0, + }, + }, + { + RouteID: "RouteWithRP", + SortingData: map[string]interface{}{ + "Cost": 0.6, + "RatingPlanID": "RP_ANY1CNT_SEC", + utils.Weight: 10.0, + }, + }, + }, + } + var routeRply *engine.SortedRoutes + if err := splSv1Rpc.Call(utils.RouteSv1GetRoutes, + ev, &routeRply); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eSpls, routeRply) { + t.Errorf("Expecting: %s \n received: %s", + utils.ToJSON(eSpls), utils.ToJSON(routeRply)) + } + + // test for 61 seconds usage + // 30 seconds are covered by account and the remaining will be calculated + ev = &engine.ArgsGetRoutes{ + CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + Time: &tNow, + ID: "testV1RouteAccountWithRatingPlan", + Event: map[string]interface{}{ + utils.Account: "RandomAccount", + utils.Destination: "+135876", + utils.SetupTime: utils.MetaNow, + utils.Usage: "1m1s", + "EventType": "testV1RouteAccountWithRatingPlan", + }, + }, + } + eSpls = &engine.SortedRoutes{ + ProfileID: "RouteWithAccAndRP", + Sorting: utils.MetaLC, + Count: 2, + SortedRoutes: []*engine.SortedRoute{ + { + RouteID: "RouteWithRP", + SortingData: map[string]interface{}{ + "Cost": 0.61, + "RatingPlanID": "RP_ANY1CNT_SEC", + utils.Weight: 10.0, + }, + }, + { + RouteID: "RouteWithAccAndRP", + SortingData: map[string]interface{}{ + utils.Account: "AccWithVoice", + "Cost": 0.62, + "MaxUsage": 30000000000.0, + "RatingPlanID": "RP_ANY2CNT_SEC", + utils.Weight: 20.0, + }, + }, + }, + } + var routeRply2 *engine.SortedRoutes + if err := splSv1Rpc.Call(utils.RouteSv1GetRoutes, + ev, &routeRply2); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eSpls, routeRply2) { + t.Errorf("Expecting: %s \n received: %s", + utils.ToJSON(eSpls), utils.ToJSON(routeRply2)) + } + +} + func testV1RouteStopEngine(t *testing.T) { if err := engine.KillEngine(100); err != nil { t.Error(err) diff --git a/data/tariffplans/testit/DestinationRates.csv b/data/tariffplans/testit/DestinationRates.csv index dcab9792d..ad5fe7181 100644 --- a/data/tariffplans/testit/DestinationRates.csv +++ b/data/tariffplans/testit/DestinationRates.csv @@ -7,4 +7,6 @@ DR_TEST_1,DST_1001,RT_TEST_1,*up,4,0, DR_MOBILE_1CNT,DST_MOBILE,RT_1CNT,*up,5,0, DR_LOCAL_2CNT,DST_LOCAL,RT_2CNT,*up,5,0, DR_LOCAL_2CNT,DST_MOBILE,RT_2CNT,*up,5,0, -DR_FREE,DST_FREE,RT_FREE,*up,5,0, \ No newline at end of file +DR_FREE,DST_FREE,RT_FREE,*up,5,0, +DR_ANY_1CNT_SEC,*any,RT_1CNT_SEC,*up,5,0, +DR_ANY_2CNT_SEC,*any,RT_2CNT_SEC,*up,5,0, \ No newline at end of file diff --git a/data/tariffplans/testit/Rates.csv b/data/tariffplans/testit/Rates.csv index 5332a40c7..d849b7136 100644 --- a/data/tariffplans/testit/Rates.csv +++ b/data/tariffplans/testit/Rates.csv @@ -5,5 +5,7 @@ RT_40CNT,0.8,0.4,60s,30s,0s RT_40CNT,0,0.2,60s,10s,60s RT_TEST_1,0,0.05,60s,60s,60s RT_FREE,0,0,60s,60s,0s +RT_1CNT_SEC,0,0.01,1s,1s,0s +RT_2CNT_SEC,0,0.02,1s,1s,0s diff --git a/data/tariffplans/testit/RatingPlans.csv b/data/tariffplans/testit/RatingPlans.csv index 18fb3324c..0b50535b8 100644 --- a/data/tariffplans/testit/RatingPlans.csv +++ b/data/tariffplans/testit/RatingPlans.csv @@ -7,4 +7,6 @@ RP_ANY1CNT,DR_ANY_1CNT,*any,10 RP_TEST,DR_TEST_1,*any,10 RP_MOBILE,DR_MOBILE_1CNT,*any,10 RP_LOCAL,DR_LOCAL_2CNT,*any,10 -RP_FREE,DR_FREE,*any,10 \ No newline at end of file +RP_FREE,DR_FREE,*any,10 +RP_ANY2CNT_SEC,DR_ANY_2CNT_SEC,*any,10 +RP_ANY1CNT_SEC,DR_ANY_1CNT_SEC,*any,10 \ No newline at end of file diff --git a/data/tariffplans/testit/RatingProfiles.csv b/data/tariffplans/testit/RatingProfiles.csv index 965363455..5458634f6 100644 --- a/data/tariffplans/testit/RatingProfiles.csv +++ b/data/tariffplans/testit/RatingProfiles.csv @@ -6,3 +6,4 @@ cgrates.org,call,SUPPLIER1,2018-01-01T00:00:00Z,RP_ANY1CNT, cgrates.org,Standard,TEST,2017-11-27T00:00:00Z,RP_TEST, cgrates.org,call,RP_RETAIL,2018-01-01T00:00:00Z,RP_RETAIL1, cgrates.org,free,RP_FREE,2018-01-01T00:00:00Z,RP_FREE, +cgrates.org,*routes,*any,2018-01-01T00:00:00Z,RP_TESTIT1, diff --git a/engine/responder.go b/engine/responder.go index 57b13972b..fd94871fb 100644 --- a/engine/responder.go +++ b/engine/responder.go @@ -315,6 +315,7 @@ func (rs *Responder) GetMaxSessionTime(arg *CallDescriptorWithArgDispatcher, rep func (rs *Responder) GetMaxSessionTimeOnAccounts(arg *utils.GetMaxSessionTimeOnAccountsArgs, reply *map[string]interface{}) (err error) { + var maxDur time.Duration for _, anctID := range arg.AccountIDs { cd := &CallDescriptor{ Category: utils.MetaRoutes, @@ -326,11 +327,11 @@ func (rs *Responder) GetMaxSessionTimeOnAccounts(arg *utils.GetMaxSessionTimeOnA TimeEnd: arg.SetupTime.Add(arg.Usage), DurationIndex: arg.Usage, } - if maxDur, err := cd.GetMaxSessionDuration(); err != nil { + if maxDur, err = cd.GetMaxSessionDuration(); err != nil { utils.Logger.Warning( fmt.Sprintf("<%s> ignoring cost for account: %s, err: %s", utils.Responder, anctID, err.Error())) - } else if maxDur >= arg.Usage { + } else { *reply = map[string]interface{}{ utils.CapMaxUsage: maxDur, utils.Cost: 0.0, diff --git a/engine/route_highestcost.go b/engine/route_highestcost.go index 7190bd95e..2767faa3a 100755 --- a/engine/route_highestcost.go +++ b/engine/route_highestcost.go @@ -41,11 +41,11 @@ func (hcs *HightCostSorter) SortRoutes(prflID string, routes []*Route, Sorting: hcs.sorting, SortedRoutes: make([]*SortedRoute, 0)} for _, route := range routes { - if len(route.RatingPlanIDs) == 0 { + if len(route.RatingPlanIDs) == 0 && len(route.AccountIDs) == 0 { utils.Logger.Warning( - fmt.Sprintf("<%s> supplier: <%s> - empty RatingPlanIDs", + fmt.Sprintf("<%s> supplier: <%s> - empty RatingPlanIDs or AccountIDs", utils.RouteS, route.ID)) - return nil, utils.NewErrMandatoryIeMissing("RatingPlanIDs") + return nil, utils.NewErrMandatoryIeMissing("RatingPlanIDs or AccountIDs") } if srtSpl, pass, err := hcs.rS.populateSortingData(ev, route, extraOpts); err != nil { return nil, err diff --git a/engine/route_leastcost.go b/engine/route_leastcost.go index 6ede9aa58..01f5e2012 100644 --- a/engine/route_leastcost.go +++ b/engine/route_leastcost.go @@ -41,11 +41,11 @@ func (lcs *LeastCostSorter) SortRoutes(prflID string, routes []*Route, Sorting: lcs.sorting, SortedRoutes: make([]*SortedRoute, 0)} for _, s := range routes { - if len(s.RatingPlanIDs) == 0 { + if len(s.RatingPlanIDs) == 0 && len(s.AccountIDs) == 0 { utils.Logger.Warning( - fmt.Sprintf("<%s> supplier: <%s> - empty RatingPlanIDs", + fmt.Sprintf("<%s> supplier: <%s> - empty RatingPlanIDs or AccountIDs", utils.RouteS, s.ID)) - return nil, utils.NewErrMandatoryIeMissing("RatingPlanIDs") + return nil, utils.NewErrMandatoryIeMissing("RatingPlanIDs or AccountIDs") } if srtSpl, pass, err := lcs.rS.populateSortingData(ev, s, extraOpts); err != nil { return nil, err diff --git a/engine/routes.go b/engine/routes.go index 436a9d851..bc3661669 100644 --- a/engine/routes.go +++ b/engine/routes.go @@ -244,7 +244,9 @@ func (rpS *RouteService) costForEvent(ev *utils.CGREvent, usage = time.Duration(1 * time.Minute) err = nil } - var rplyVals map[string]interface{} + var accountMaxUsage time.Duration + var acntCost map[string]interface{} + var initialUsage time.Duration if err := rpS.connMgr.Call(rpS.cgrcfg.RouteSCfg().RALsConns, nil, utils.ResponderGetMaxSessionTimeOnAccounts, &utils.GetMaxSessionTimeOnAccountsArgs{ Tenant: ev.Tenant, @@ -253,28 +255,47 @@ func (rpS *RouteService) costForEvent(ev *utils.CGREvent, SetupTime: sTime, Usage: usage, AccountIDs: acntIDs, - }, &rplyVals); err != nil { + }, &acntCost); err != nil { return nil, err } - for k, v := range rplyVals { // do not overwrite the return map - costData[k] = v + if ifaceMaxUsage, has := acntCost[utils.CapMaxUsage]; has { + if accountMaxUsage, err = utils.IfaceAsDuration(ifaceMaxUsage); err != nil { + return nil, err + } + if usage > accountMaxUsage { + // remain usage needs to be covered by rating plans + if len(rpIDs) == 0 { + return nil, fmt.Errorf("no rating plans defined for remaining usage") + } + // update the setup time and the usage + sTime = sTime.Add(accountMaxUsage) + initialUsage = usage + usage = usage - accountMaxUsage + } + for k, v := range acntCost { // update the costData with the infos from AccountS + costData[k] = v + } } - rplyVals = make(map[string]interface{}) // reset the map - if err := rpS.connMgr.Call(rpS.cgrcfg.RouteSCfg().RALsConns, nil, utils.ResponderGetCostOnRatingPlans, - &utils.GetCostOnRatingPlansArgs{ - Tenant: ev.Tenant, - Account: acnt, - Subject: subj, - Destination: dst, - SetupTime: sTime, - Usage: usage, - RatingPlanIDs: rpIDs, - }, &rplyVals); err != nil { - return nil, err - } - for k, v := range rplyVals { - costData[k] = v + + if accountMaxUsage == 0 || accountMaxUsage < initialUsage { + var rpCost map[string]interface{} + if err := rpS.connMgr.Call(rpS.cgrcfg.RouteSCfg().RALsConns, nil, utils.ResponderGetCostOnRatingPlans, + &utils.GetCostOnRatingPlansArgs{ + Tenant: ev.Tenant, + Account: acnt, + Subject: subj, + Destination: dst, + SetupTime: sTime, + Usage: usage, + RatingPlanIDs: rpIDs, + }, &rpCost); err != nil { + return nil, err + } + for k, v := range rpCost { // do not overwrite the return map + costData[k] = v + } } + return } @@ -396,6 +417,7 @@ func (rpS *RouteService) populateSortingData(ev *utils.CGREvent, route *Route, utils.Logger.Warning( fmt.Sprintf("<%s> ignoring route with ID: %s, missing cost information", utils.RouteS, route.ID)) + return nil, false, nil } else { if extraOpts.maxCost != 0 && costData[utils.Cost].(float64) > extraOpts.maxCost { diff --git a/general_tests/route_it_test.go b/general_tests/route_it_test.go index a2828c095..c002a5823 100644 --- a/general_tests/route_it_test.go +++ b/general_tests/route_it_test.go @@ -176,8 +176,8 @@ func testV1SplSSetSupplierProfilesWithoutRatingPlanIDs(t *testing.T) { } var suplsReply engine.SortedRoutes if err := splSv1Rpc.Call(utils.RouteSv1GetRoutes, - ev, &suplsReply); err.Error() != utils.NewErrServerError(utils.NewErrMandatoryIeMissing("RatingPlanIDs")).Error() { - t.Errorf("Expected error MANDATORY_IE_MISSING: [RatingPlanIDs] recieved:%v\n", err) + ev, &suplsReply); err == nil || err.Error() != utils.ErrNotFound.Error() { + t.Error(err) } if err := splSv1Rpc.Call(utils.APIerSv1RemoveRouteProfile, utils.TenantID{ Tenant: splPrf.Tenant, diff --git a/utils/apitpdata.go b/utils/apitpdata.go index 3bdc20d7a..327486fec 100755 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -916,6 +916,20 @@ type AttrSetBalance struct { Cdrlog bool } +type AttrSetBalances struct { + Tenant string + Account string + Balances []*AttrBalance +} + +type AttrBalance struct { + BalanceType string + Value float64 + Balance map[string]interface{} + ActionExtraData *map[string]interface{} + Cdrlog bool +} + // TPResourceProfile is used in APIs to manage remotely offline ResourceProfile type TPResourceProfile struct { TPid string