diff --git a/.travis.yml b/.travis.yml index 404a07a24..936b99045 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.5 + - 1.6 script: $TRAVIS_BUILD_DIR/test.sh diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 488bba6f3..b2a3821bc 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -41,6 +41,7 @@ information, please see the [`CONTRIBUTING.md`](CONTRIBUTING.md) file. | @rinor | Rinor Hoxha | | @bhepp | Brice Heppner | | @noahmehl | Noah Mehl | +| @elfranne | Tom Braarup Cuykens | diff --git a/agents/dmtagent.go b/agents/dmtagent.go index 23314e69c..9fcf97365 100644 --- a/agents/dmtagent.go +++ b/agents/dmtagent.go @@ -21,8 +21,10 @@ package agents import ( "fmt" "strconv" + "strings" "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" "github.com/cgrates/rpcclient" "github.com/fiorix/go-diameter/diam" @@ -30,8 +32,8 @@ import ( "github.com/fiorix/go-diameter/diam/sm" ) -func NewDiameterAgent(cgrCfg *config.CGRConfig, smg rpcclient.RpcClientConnection) (*DiameterAgent, error) { - da := &DiameterAgent{cgrCfg: cgrCfg, smg: smg} +func NewDiameterAgent(cgrCfg *config.CGRConfig, smg rpcclient.RpcClientConnection, pubsubs rpcclient.RpcClientConnection) (*DiameterAgent, error) { + da := &DiameterAgent{cgrCfg: cgrCfg, smg: smg, pubsubs: pubsubs} dictsDir := cgrCfg.DiameterAgentCfg().DictionariesDir if len(dictsDir) != 0 { if err := loadDictionaries(dictsDir, "DiameterAgent"); err != nil { @@ -42,8 +44,9 @@ func NewDiameterAgent(cgrCfg *config.CGRConfig, smg rpcclient.RpcClientConnectio } type DiameterAgent struct { - cgrCfg *config.CGRConfig - smg rpcclient.RpcClientConnection // Connection towards CGR-SMG component + cgrCfg *config.CGRConfig + smg rpcclient.RpcClientConnection // Connection towards CGR-SMG component + pubsubs rpcclient.RpcClientConnection // Connection towards CGR-PubSub component } // Creates the message handlers @@ -66,51 +69,80 @@ func (self *DiameterAgent) handlers() diam.Handler { return dSM } -func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestProcessor) *CCA { +func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestProcessor, cca *CCA) (bool, error) { passesAllFilters := true for _, fldFilter := range reqProcessor.RequestFilter { - if passes, _ := passesFieldFilter(ccr.diamMessage, fldFilter); !passes { + if passes, _ := passesFieldFilter(ccr.diamMessage, fldFilter, nil); !passes { passesAllFilters = false } } if !passesAllFilters { // Not going with this processor further - return nil + return false, nil } if reqProcessor.DryRun { // DryRun should log the matching processor as well as the received CCR utils.Logger.Info(fmt.Sprintf(" RequestProcessor: %s", reqProcessor.Id)) utils.Logger.Info(fmt.Sprintf(" CCR message: %s", ccr.diamMessage)) } - cca := NewBareCCAFromCCR(ccr, self.cgrCfg.DiameterAgentCfg().OriginHost, self.cgrCfg.DiameterAgentCfg().OriginRealm) + if !reqProcessor.AppendCCA { + *cca = *NewBareCCAFromCCR(ccr, self.cgrCfg.DiameterAgentCfg().OriginHost, self.cgrCfg.DiameterAgentCfg().OriginRealm) + } smgEv, err := ccr.AsSMGenericEvent(reqProcessor.CCRFields) if err != nil { + *cca = *NewBareCCAFromCCR(ccr, self.cgrCfg.DiameterAgentCfg().OriginHost, self.cgrCfg.DiameterAgentCfg().OriginRealm) if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA reply-code, error: %s", ccr.diamMessage, err)) - return nil + return false, err } utils.Logger.Err(fmt.Sprintf(" Processing message: %+v AsSMGenericEvent, error: %s", ccr.diamMessage, err)) - return cca + return false, ErrDiameterRatingFailed + } + if len(reqProcessor.Flags) != 0 { + smgEv[utils.CGRFlags] = reqProcessor.Flags.String() // Populate CGRFlags automatically + } + if reqProcessor.PublishEvent && self.pubsubs != nil { + evt, err := smgEv.AsMapStringString() + if err != nil { + *cca = *NewBareCCAFromCCR(ccr, self.cgrCfg.DiameterAgentCfg().OriginHost, self.cgrCfg.DiameterAgentCfg().OriginRealm) + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + return false, err + } + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v failed converting SMGEvent to pubsub one, error: %s", ccr.diamMessage, err)) + return false, ErrDiameterRatingFailed + } + var reply string + if err := self.pubsubs.Call("PubSubV1.Publish", engine.CgrEvent(evt), &reply); err != nil { + *cca = *NewBareCCAFromCCR(ccr, self.cgrCfg.DiameterAgentCfg().OriginHost, self.cgrCfg.DiameterAgentCfg().OriginRealm) + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + return false, err + } + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v failed publishing event, error: %s", ccr.diamMessage, err)) + return false, ErrDiameterRatingFailed + } } var maxUsage float64 + processorVars := make(map[string]string) + processorVars[CGRResultCode] = strconv.Itoa(diam.Success) + processorVars[CGRError] = "" if reqProcessor.DryRun { // DryRun does not send over network utils.Logger.Info(fmt.Sprintf(" SMGenericEvent: %+v", smgEv)) - if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.LimitedSuccess), - false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) - return nil - } + processorVars[CGRResultCode] = strconv.Itoa(diam.LimitedSuccess) } else { // Find out maxUsage over APIs switch ccr.CCRequestType { case 1: err = self.smg.Call("SMGenericV1.SessionStart", smgEv, &maxUsage) case 2: err = self.smg.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage) - case 3, 4: + case 3, 4: // Handle them together since we generate CDR for them var rpl string if ccr.CCRequestType == 3 { err = self.smg.Call("SMGenericV1.SessionEnd", smgEv, &rpl) } else if ccr.CCRequestType == 4 { err = self.smg.Call("SMGenericV1.ChargeEvent", smgEv, &maxUsage) + if maxUsage == 0 { + smgEv[utils.USAGE] = 0 // For CDR not to debit + } } if self.cgrCfg.DiameterAgentCfg().CreateCDR { if errCdr := self.smg.Call("SMGenericV1.ProcessCdr", smgEv, &rpl); errCdr != nil { @@ -119,45 +151,36 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro } } if err != nil { - if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), - false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) - return nil - } utils.Logger.Err(fmt.Sprintf(" Processing message: %+v, API error: %s", ccr.diamMessage, err)) - return cca - } - var unauthorizedResultCode bool - if ccr.CCRequestType != 3 && ccr.CCRequestType != 4 && maxUsage == 0 { // Not enough balance, RFC demands 4012 - if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, "4012", - false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) - return nil - } - unauthorizedResultCode = true - } else if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.Success), - false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) - return nil - } - if ccr.CCRequestType != 3 && ccr.CCRequestType != 4 && !unauthorizedResultCode { // For terminate or previously marked unauthorized, we don't add granted-service-unit AVP - if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Granted-Service-Unit", "CC-Time"}, strconv.FormatFloat(maxUsage, 'f', 0, 64), - false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Granted-Service-Unit, error: %s", ccr.diamMessage, err)) - return nil + switch { // Prettify some errors + case strings.HasSuffix(err.Error(), utils.ErrAccountNotFound.Error()): + processorVars[CGRError] = utils.ErrAccountNotFound.Error() + case strings.HasSuffix(err.Error(), utils.ErrUserNotFound.Error()): + processorVars[CGRError] = utils.ErrUserNotFound.Error() + case strings.HasSuffix(err.Error(), utils.ErrInsufficientCredit.Error()): + processorVars[CGRError] = utils.ErrInsufficientCredit.Error() + case strings.HasSuffix(err.Error(), utils.ErrAccountDisabled.Error()): + processorVars[CGRError] = utils.ErrAccountDisabled.Error() + default: // Unknown error + processorVars[CGRError] = err.Error() + processorVars[CGRResultCode] = strconv.Itoa(DiameterRatingFailed) } } + processorVars[CGRMaxUsage] = strconv.FormatFloat(maxUsage, 'f', -1, 64) } - if err := cca.SetProcessorAVPs(reqProcessor, maxUsage); err != nil { + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, processorVars[CGRResultCode], + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + return false, err + } + if err := cca.SetProcessorAVPs(reqProcessor, processorVars); err != nil { if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) - return nil + return false, err } utils.Logger.Err(fmt.Sprintf(" CCA SetProcessorAVPs for message: %+v, error: %s", ccr.diamMessage, err)) - return cca + return false, ErrDiameterRatingFailed } - return cca + return true, nil } func (self *DiameterAgent) handleCCR(c diam.Conn, m *diam.Message) { @@ -166,14 +189,18 @@ func (self *DiameterAgent) handleCCR(c diam.Conn, m *diam.Message) { utils.Logger.Err(fmt.Sprintf(" Unmarshaling message: %s, error: %s", m, err)) return } - var cca *CCA // For now we simply overload in loop, maybe we will find some other use of this + cca := NewBareCCAFromCCR(ccr, self.cgrCfg.DiameterAgentCfg().OriginHost, self.cgrCfg.DiameterAgentCfg().OriginRealm) + var processed bool for _, reqProcessor := range self.cgrCfg.DiameterAgentCfg().RequestProcessors { - cca = self.processCCR(ccr, reqProcessor) - if cca != nil && !reqProcessor.ContinueOnSuccess { + processed, err = self.processCCR(ccr, reqProcessor, cca) + if err != nil || (processed && !reqProcessor.ContinueOnSuccess) { break } } - if cca == nil { + if err != nil && err != ErrDiameterRatingFailed { + utils.Logger.Err(fmt.Sprintf(" CCA SetProcessorAVPs for message: %+v, error: %s", ccr.diamMessage, err)) + return + } else if !processed { utils.Logger.Err(fmt.Sprintf(" No request processor enabled for CCR: %s, ignoring request", ccr.diamMessage)) return } diff --git a/agents/dmtagent_it_test.go b/agents/dmtagent_it_test.go index 3ec68ef73..eff5e81ee 100644 --- a/agents/dmtagent_it_test.go +++ b/agents/dmtagent_it_test.go @@ -154,6 +154,38 @@ func TestDmtAgentCCRAsSMGenericEvent(t *testing.T) { } } +func TestDmtAgentPopulateCCTotalOctets(t *testing.T) { + if !*testIntegration { + return + } + daRP := &config.DARequestProcessor{CCAFields: []*config.CfgCdrField{ + &config.CfgCdrField{Tag: "GrantedUnit", FieldFilter: utils.ParseRSRFieldsMustCompile("CGRError(^$)", utils.INFIELD_SEP), + FieldId: "Multiple-Services-Credit-Control>Granted-Service-Unit>CC-Time", Type: utils.META_COMPOSED, Value: utils.ParseRSRFieldsMustCompile("CGRMaxUsage", utils.INFIELD_SEP), Mandatory: true}, + &config.CfgCdrField{Tag: "GrantedOctet", FieldFilter: utils.ParseRSRFieldsMustCompile("CGRError(^$)", utils.INFIELD_SEP), + FieldId: "Multiple-Services-Credit-Control>Granted-Service-Unit>CC-Total-Octets", Type: utils.META_COMPOSED, Value: utils.ParseRSRFieldsMustCompile("CGRMaxUsage", utils.INFIELD_SEP), Mandatory: true}, + }} + ccr := new(CCR) + ccr.diamMessage = ccr.AsBareDiameterMessage() + cca := NewBareCCAFromCCR(ccr, "cgr-da", "cgrates.org") + if err := cca.SetProcessorAVPs(daRP, map[string]string{CGRError: "", CGRMaxUsage: "153600"}); err != nil { + t.Error(err) + } + if avps, err := cca.diamMessage.FindAVPsWithPath([]interface{}{"Multiple-Services-Credit-Control", "Granted-Service-Unit", "CC-Time"}, dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if len(avps) == 0 { + t.Error("Not found") + } else if strResult := avpValAsString(avps[0]); strResult != "153600" { // Result-Code set in the template + t.Errorf("Expecting 153600, received: %s", strResult) + } + if avps, err := cca.diamMessage.FindAVPsWithPath([]interface{}{"Multiple-Services-Credit-Control", "Granted-Service-Unit", "CC-Total-Octets"}, dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if len(avps) == 0 { + t.Error("Not found") + } else if strResult := avpValAsString(avps[0]); strResult != "153600" { // Result-Code set in the template + t.Errorf("Expecting 153600, received: %s", strResult) + } +} + // Connect rpc client to rater func TestDmtAgentApierRpcConn(t *testing.T) { if !*testIntegration { @@ -179,8 +211,7 @@ func TestDmtAgentTPFromFolder(t *testing.T) { time.Sleep(time.Duration(*waitRater) * time.Millisecond) // Give time for scheduler to execute topups } -// cgr-console 'cost Category="call" Tenant="cgrates.org" Subject="1001" Destination="1004" TimeStart="2015-11-07T08:42:26Z" TimeEnd="2015-11-07T08:47:26Z"' -func TestDmtAgentSendCCRInit(t *testing.T) { +func TestConnectDiameterClient(t *testing.T) { if !*testIntegration { return } @@ -189,11 +220,18 @@ func TestDmtAgentSendCCRInit(t *testing.T) { if err != nil { t.Fatal(err) } - cdr := &engine.CDR{CGRID: utils.Sha1("dsafdsaf", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, - OriginID: "dsafdsaf", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", +} + +// cgr-console 'cost Category="call" Tenant="cgrates.org" Subject="1001" Destination="1004" TimeStart="2015-11-07T08:42:26Z" TimeEnd="2015-11-07T08:47:26Z"' +func TestDmtAgentSendCCRInit(t *testing.T) { + if !*testIntegration { + return + } + cdr := &engine.CDR{CGRID: utils.Sha1("testccr1", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, + OriginID: "testccr1", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1004", Supplier: "SUPPL1", SetupTime: time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2015, 11, 7, 8, 42, 26, 0, time.UTC), RunID: utils.DEFAULT_RUNID, - Usage: time.Duration(0) * time.Second, PDD: time.Duration(7) * time.Second, ExtraFields: map[string]string{"Service-Context-Id": "voice@huawei.com"}, + Usage: time.Duration(0), PDD: time.Duration(7) * time.Second, ExtraFields: map[string]string{"Service-Context-Id": "voice@huawei.com"}, } ccr := storedCdrToCCR(cdr, "UNIT_TEST", daCfg.DiameterAgentCfg().OriginRealm, daCfg.DiameterAgentCfg().VendorId, daCfg.DiameterAgentCfg().ProductName, utils.DIAMETER_FIRMWARE_REVISION, daCfg.DiameterAgentCfg().DebitInterval, false) @@ -220,7 +258,7 @@ func TestDmtAgentSendCCRInit(t *testing.T) { } var acnt *engine.Account attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} - eAcntVal := 9.484 + eAcntVal := 9.5008 if err := apierRpc.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { t.Error(err) } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { @@ -233,8 +271,8 @@ func TestDmtAgentSendCCRUpdate(t *testing.T) { if !*testIntegration { return } - cdr := &engine.CDR{CGRID: utils.Sha1("dsafdsaf", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, - OriginID: "dsafdsaf", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", + cdr := &engine.CDR{CGRID: utils.Sha1("testccr1", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, + OriginID: "testccr1", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1004", Supplier: "SUPPL1", SetupTime: time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2015, 11, 7, 8, 42, 26, 0, time.UTC), RunID: utils.DEFAULT_RUNID, Usage: time.Duration(300) * time.Second, PDD: time.Duration(7) * time.Second, ExtraFields: map[string]string{"Service-Context-Id": "voice@huawei.com"}, @@ -255,11 +293,11 @@ func TestDmtAgentSendCCRUpdate(t *testing.T) { } else if len(avps) == 0 { t.Error("Granted-Service-Unit not found") } else if strCCTime := avpValAsString(avps[0]); strCCTime != "300" { - t.Errorf("Expecting 300, received: %s", strCCTime) + t.Errorf("Expecting 300, received: %s, (%+v)", strCCTime, avps) } var acnt *engine.Account attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} - eAcntVal := 9.214 + eAcntVal := 9.251800 if err := apierRpc.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { t.Error(err) } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { @@ -272,8 +310,8 @@ func TestDmtAgentSendCCRUpdate2(t *testing.T) { if !*testIntegration { return } - cdr := &engine.CDR{CGRID: utils.Sha1("dsafdsaf", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, - OriginID: "dsafdsaf", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", + cdr := &engine.CDR{CGRID: utils.Sha1("testccr1", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, + OriginID: "testccr1", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1004", Supplier: "SUPPL1", SetupTime: time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2015, 11, 7, 8, 42, 26, 0, time.UTC), RunID: utils.DEFAULT_RUNID, Usage: time.Duration(600) * time.Second, PDD: time.Duration(7) * time.Second, ExtraFields: map[string]string{"Service-Context-Id": "voice@huawei.com"}, @@ -298,7 +336,7 @@ func TestDmtAgentSendCCRUpdate2(t *testing.T) { } var acnt *engine.Account attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} - eAcntVal := 8.944000 + eAcntVal := 9.002800 if err := apierRpc.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { t.Error(err) } else if utils.Round(acnt.BalanceMap[utils.MONETARY].GetTotalValue(), 5, utils.ROUNDING_MIDDLE) != eAcntVal { @@ -310,8 +348,8 @@ func TestDmtAgentSendCCRTerminate(t *testing.T) { if !*testIntegration { return } - cdr := &engine.CDR{CGRID: utils.Sha1("dsafdsaf", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, - OriginID: "dsafdsaf", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", + cdr := &engine.CDR{CGRID: utils.Sha1("testccr1", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, + OriginID: "testccr1", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1004", Supplier: "SUPPL1", SetupTime: time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2015, 11, 7, 8, 42, 26, 0, time.UTC), RunID: utils.DEFAULT_RUNID, Usage: time.Duration(610) * time.Second, PDD: time.Duration(7) * time.Second, ExtraFields: map[string]string{"Service-Context-Id": "voice@huawei.com"}, @@ -339,11 +377,11 @@ func TestDmtAgentSendCCRTerminate(t *testing.T) { } var acnt *engine.Account attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} - eAcntVal := 9.205 + eAcntVal := 9.243500 if err := apierRpc.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { t.Error(err) } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { // Should also consider derived charges which double the cost of 6m10s - 2x0.7584 - t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) + t.Errorf("Expected: %v, received: %v", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) } } @@ -352,7 +390,7 @@ func TestDmtAgentSendCCRSMS(t *testing.T) { return } ccr := diam.NewRequest(diam.CreditControl, 4, nil) - ccr.NewAVP(avp.SessionID, avp.Mbit, 0, datatype.UTF8String("cgrates;1451911932;00082")) + ccr.NewAVP(avp.SessionID, avp.Mbit, 0, datatype.UTF8String("testccr2")) ccr.NewAVP(avp.OriginHost, avp.Mbit, 0, datatype.DiameterIdentity("CGR-DA")) ccr.NewAVP(avp.OriginRealm, avp.Mbit, 0, datatype.DiameterIdentity("cgrates.org")) ccr.NewAVP(avp.AuthApplicationID, avp.Mbit, 0, datatype.Unsigned32(4)) @@ -372,6 +410,99 @@ func TestDmtAgentSendCCRSMS(t *testing.T) { }}) ccr.NewAVP(avp.ServiceIdentifier, avp.Mbit, 0, datatype.Unsigned32(0)) ccr.NewAVP(avp.RequestedAction, avp.Mbit, 0, datatype.Enumerated(0)) + ccr.NewAVP(avp.RequestedServiceUnit, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.CCTime, avp.Mbit, 0, datatype.Unsigned32(1))}}) + ccr.NewAVP(873, avp.Mbit, 10415, &diam.GroupedAVP{ // + AVP: []*diam.AVP{ + diam.NewAVP(20300, avp.Mbit, 2011, &diam.GroupedAVP{ // IN-Information + AVP: []*diam.AVP{ + diam.NewAVP(20302, avp.Mbit, 2011, datatype.UTF8String("22509")), // Calling-Vlr-Number + diam.NewAVP(20385, avp.Mbit, 2011, datatype.UTF8String("4002")), // Called-Party-NP + }, + }), + diam.NewAVP(2000, avp.Mbit, 10415, &diam.GroupedAVP{ // SMS-Information + AVP: []*diam.AVP{ + diam.NewAVP(886, avp.Mbit, 10415, &diam.GroupedAVP{ // Originator-Address + AVP: []*diam.AVP{ + diam.NewAVP(899, avp.Mbit, 10415, datatype.Enumerated(1)), // Address-Type + diam.NewAVP(897, avp.Mbit, 10415, datatype.UTF8String("1003")), // Address-Data + }}), + diam.NewAVP(1201, avp.Mbit, 10415, &diam.GroupedAVP{ // Recipient-Address + AVP: []*diam.AVP{ + diam.NewAVP(899, avp.Mbit, 10415, datatype.Enumerated(1)), // Address-Type + diam.NewAVP(897, avp.Mbit, 10415, datatype.UTF8String("1002")), // Address-Data + }}), + }, + }), + }}) + if err := dmtClient.SendMessage(ccr); err != nil { + t.Error(err) + } + + time.Sleep(time.Duration(100) * time.Millisecond) + dmtClient.ReceivedMessage() // Discard the received message so we can test next one + /* + if msg == nil { + t.Fatal("No message returned") + } + if avps, err := msg.FindAVPsWithPath([]interface{}{"Granted-Service-Unit", "CC-Time"}, dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if len(avps) == 0 { + t.Error("Granted-Service-Unit not found") + } else if strCCTime := avpValAsString(avps[0]); strCCTime != "0" { + t.Errorf("Expecting 0, received: %s", strCCTime) + } + var acnt *engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + eAcntVal := 9.205 + if err := apierRpc.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { // Should also consider derived charges which double the cost of 6m10s - 2x0.7584 + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) + } + */ + var cdrs []*engine.ExternalCDR + req := utils.RPCCDRsFilter{RunIDs: []string{utils.META_DEFAULT}, ToRs: []string{utils.SMS}} + if err := apierRpc.Call("ApierV2.GetCdrs", req, &cdrs); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(cdrs) != 1 { + t.Error("Unexpected number of CDRs returned: ", len(cdrs)) + } else { + if cdrs[0].Usage != "1" { + t.Errorf("Unexpected CDR Usage received, cdr: %+v ", cdrs[0]) + } + if cdrs[0].Cost != 0.6 { + t.Errorf("Unexpected CDR Cost received, cdr: %+v ", cdrs[0]) + } + } +} + +func TestDmtAgentSendCCRSMSWrongAccount(t *testing.T) { + if !*testIntegration { + return + } + ccr := diam.NewRequest(diam.CreditControl, 4, nil) + ccr.NewAVP(avp.SessionID, avp.Mbit, 0, datatype.UTF8String("testccr3")) + ccr.NewAVP(avp.OriginHost, avp.Mbit, 0, datatype.DiameterIdentity("CGR-DA")) + ccr.NewAVP(avp.OriginRealm, avp.Mbit, 0, datatype.DiameterIdentity("cgrates.org")) + ccr.NewAVP(avp.AuthApplicationID, avp.Mbit, 0, datatype.Unsigned32(4)) + ccr.NewAVP(avp.ServiceContextID, avp.Mbit, 0, datatype.UTF8String("message@huawei.com")) + ccr.NewAVP(avp.CCRequestType, avp.Mbit, 0, datatype.Enumerated(4)) + ccr.NewAVP(avp.CCRequestNumber, avp.Mbit, 0, datatype.Unsigned32(0)) + ccr.NewAVP(avp.EventTimestamp, avp.Mbit, 0, datatype.Time(time.Date(2016, 1, 5, 11, 30, 10, 0, time.UTC))) + ccr.NewAVP(avp.SubscriptionID, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.SubscriptionIDType, avp.Mbit, 0, datatype.Enumerated(0)), + diam.NewAVP(avp.SubscriptionIDData, avp.Mbit, 0, datatype.UTF8String("non_existent")), // Subscription-Id-Data + }}) + ccr.NewAVP(avp.SubscriptionID, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.SubscriptionIDType, avp.Mbit, 0, datatype.Enumerated(1)), + diam.NewAVP(avp.SubscriptionIDData, avp.Mbit, 0, datatype.UTF8String("104502200011")), // Subscription-Id-Data + }}) + ccr.NewAVP(avp.ServiceIdentifier, avp.Mbit, 0, datatype.Unsigned32(0)) + ccr.NewAVP(avp.RequestedAction, avp.Mbit, 0, datatype.Enumerated(0)) ccr.NewAVP(avp.RequestedServiceUnit, avp.Mbit, 0, &diam.GroupedAVP{ AVP: []*diam.AVP{ diam.NewAVP(avp.CCTime, avp.Mbit, 0, datatype.Unsigned32(1))}}) @@ -401,28 +532,127 @@ func TestDmtAgentSendCCRSMS(t *testing.T) { if err := dmtClient.SendMessage(ccr); err != nil { t.Error(err) } - /* - time.Sleep(time.Duration(100) * time.Millisecond) - msg := dmtClient.ReceivedMessage() - if msg == nil { - t.Fatal("No message returned") - } - if avps, err := msg.FindAVPsWithPath([]interface{}{"Granted-Service-Unit", "CC-Time"}, dict.UndefinedVendorID); err != nil { - t.Error(err) - } else if len(avps) == 0 { - t.Error("Granted-Service-Unit not found") - } else if strCCTime := avpValAsString(avps[0]); strCCTime != "0" { - t.Errorf("Expecting 0, received: %s", strCCTime) - } - var acnt *engine.Account - attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} - eAcntVal := 9.205 - if err := apierRpc.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { - t.Error(err) - } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { // Should also consider derived charges which double the cost of 6m10s - 2x0.7584 - t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) - } - */ + time.Sleep(time.Duration(100) * time.Millisecond) + msg := dmtClient.ReceivedMessage() // Discard the received message so we can test next one + if msg == nil { + t.Fatal("No message returned") + } + if avps, err := msg.FindAVPsWithPath([]interface{}{"Result-Code"}, dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if len(avps) == 0 { + t.Error("Result-Code") + } else if strResult := avpValAsString(avps[0]); strResult != "5030" { // Result-Code set in the template + t.Errorf("Expecting 5030, received: %s", strResult) + } +} + +// cgr-console 'cost Category="call" Tenant="cgrates.org" Subject="1001" Destination="1004" TimeStart="2015-11-07T08:42:26Z" TimeEnd="2015-11-07T08:47:26Z"' +func TestDmtAgentSendCCRInitWrongAccount(t *testing.T) { + if !*testIntegration { + return + } + cdr := &engine.CDR{CGRID: utils.Sha1("testccr4", time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderID: 123, ToR: utils.VOICE, + OriginID: "testccr4", OriginHost: "192.168.1.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, Direction: "*out", + Tenant: "cgrates.org", Category: "call", Account: "non_existent", Subject: "non_existent", Destination: "1004", Supplier: "SUPPL1", + SetupTime: time.Date(2015, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2015, 11, 7, 8, 42, 26, 0, time.UTC), RunID: utils.DEFAULT_RUNID, + Usage: time.Duration(0) * time.Second, PDD: time.Duration(7) * time.Second, ExtraFields: map[string]string{"Service-Context-Id": "voice@huawei.com"}, + } + ccr := storedCdrToCCR(cdr, "UNIT_TEST", daCfg.DiameterAgentCfg().OriginRealm, daCfg.DiameterAgentCfg().VendorId, + daCfg.DiameterAgentCfg().ProductName, utils.DIAMETER_FIRMWARE_REVISION, daCfg.DiameterAgentCfg().DebitInterval, false) + m, err := ccr.AsDiameterMessage() + if err != nil { + t.Error(err) + } + if err := dmtClient.SendMessage(m); err != nil { + t.Error(err) + } + time.Sleep(time.Duration(100) * time.Millisecond) + msg := dmtClient.ReceivedMessage() // Discard the received message so we can test next one + if msg == nil { + t.Fatal("No message returned") + } + if avps, err := msg.FindAVPsWithPath([]interface{}{"Result-Code"}, dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if len(avps) == 0 { + t.Error("Result-Code") + } else if strResult := avpValAsString(avps[0]); strResult != "5030" { // Result-Code set in the template + t.Errorf("Expecting 5030, received: %s", strResult) + } +} + +func TestDmtAgentSendCCRSimpaEvent(t *testing.T) { + if !*testIntegration { + return + } + ccr := diam.NewRequest(diam.CreditControl, 4, nil) + ccr.NewAVP(avp.SessionID, avp.Mbit, 0, datatype.UTF8String("testccr5")) + ccr.NewAVP(avp.OriginHost, avp.Mbit, 0, datatype.DiameterIdentity("CGR-DA")) + ccr.NewAVP(avp.OriginRealm, avp.Mbit, 0, datatype.DiameterIdentity("cgrates.org")) + ccr.NewAVP(avp.DestinationRealm, avp.Mbit, 0, datatype.DiameterIdentity("routing1.huawei.com")) + ccr.NewAVP(avp.AuthApplicationID, avp.Mbit, 0, datatype.Unsigned32(4)) + ccr.NewAVP(avp.ServiceContextID, avp.Mbit, 0, datatype.UTF8String("simpa@huawei.com")) + ccr.NewAVP(avp.CCRequestType, avp.Mbit, 0, datatype.Enumerated(4)) + ccr.NewAVP(avp.CCRequestNumber, avp.Mbit, 0, datatype.Unsigned32(0)) + ccr.NewAVP(avp.EventTimestamp, avp.Mbit, 0, datatype.Time(time.Date(2016, 1, 13, 16, 47, 58, 0, time.UTC))) + ccr.NewAVP(avp.SubscriptionID, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.SubscriptionIDType, avp.Mbit, 0, datatype.Enumerated(0)), + diam.NewAVP(avp.SubscriptionIDData, avp.Mbit, 0, datatype.UTF8String("1001")), // Subscription-Id-Data + }}) + ccr.NewAVP(avp.ServiceIdentifier, avp.Mbit, 0, datatype.Unsigned32(0)) + ccr.NewAVP(avp.RequestedAction, avp.Mbit, 0, datatype.Enumerated(1)) + ccr.NewAVP(avp.RequestedServiceUnit, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.CCMoney, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.UnitValue, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.ValueDigits, avp.Mbit, 0, datatype.Integer64(10000)), + diam.NewAVP(avp.Exponent, avp.Mbit, 0, datatype.Integer32(-5)), + }, + }), + diam.NewAVP(avp.CurrencyCode, avp.Mbit, 0, datatype.Unsigned32(33)), + }, + }), + }, + }) + ccr.NewAVP(873, avp.Mbit, 10415, &diam.GroupedAVP{ // Service-Information + AVP: []*diam.AVP{ + diam.NewAVP(20300, avp.Mbit, 2011, &diam.GroupedAVP{ // IN-Information + AVP: []*diam.AVP{ + diam.NewAVP(20302, avp.Mbit, 2011, datatype.UTF8String("22509")), // Calling-Vlr-Number + diam.NewAVP(20385, avp.Mbit, 2011, datatype.UTF8String("4002")), // Called-Party-NP + }, + }), + diam.NewAVP(29000, avp.Mbit, 2011, &diam.GroupedAVP{ // MC-Information + AVP: []*diam.AVP{ + diam.NewAVP(29001, avp.Mbit, 2011, datatype.OctetString("0x38924012914528")), // HighLayerCharacteristics + diam.NewAVP(29002, avp.Mbit, 2011, datatype.UTF8String("12928471313847173")), // MC-Service-Id + diam.NewAVP(29003, avp.Mbit, 2011, datatype.UTF8String("SPV123456012123")), // TransparentData + diam.NewAVP(1201, avp.Mbit, 10415, &diam.GroupedAVP{ // MC-Information + AVP: []*diam.AVP{ + diam.NewAVP(899, avp.Mbit, 10415, datatype.Enumerated(0)), // Address-Type + diam.NewAVP(897, avp.Mbit, 10415, datatype.UTF8String("33780029555")), // Address-Data + }, + }), + }, + }), + }}) + if err := dmtClient.SendMessage(ccr); err != nil { + t.Error(err) + } + time.Sleep(time.Duration(100) * time.Millisecond) + msg := dmtClient.ReceivedMessage() // Discard the received message so we can test next one + if msg == nil { + t.Fatal("No message returned") + } + if avps, err := msg.FindAVPsWithPath([]interface{}{"Result-Code"}, dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if len(avps) == 0 { + t.Error("Result-Code") + } else if strResult := avpValAsString(avps[0]); strResult != "2001" { // Result-Code set in the template + t.Errorf("Expecting 2001, received: %s", strResult) + } } func TestDmtAgentCdrs(t *testing.T) { @@ -430,7 +660,7 @@ func TestDmtAgentCdrs(t *testing.T) { return } var cdrs []*engine.ExternalCDR - req := utils.RPCCDRsFilter{RunIDs: []string{utils.META_DEFAULT}} + req := utils.RPCCDRsFilter{RunIDs: []string{utils.META_DEFAULT}, ToRs: []string{utils.VOICE}} if err := apierRpc.Call("ApierV2.GetCdrs", req, &cdrs); err != nil { t.Error("Unexpected error: ", err.Error()) } else if len(cdrs) != 1 { @@ -439,12 +669,45 @@ func TestDmtAgentCdrs(t *testing.T) { if cdrs[0].Usage != "610" { t.Errorf("Unexpected CDR Usage received, cdr: %+v ", cdrs[0]) } - if cdrs[0].Cost != 0.5349 { + if cdrs[0].Cost != 0.7565 { t.Errorf("Unexpected CDR Cost received, cdr: %+v ", cdrs[0]) } } } +func TestDmtAgentDryRun1(t *testing.T) { + if !*testIntegration { + return + } + ccr := diam.NewRequest(diam.CreditControl, 4, nil) + ccr.NewAVP(avp.SessionID, avp.Mbit, 0, datatype.UTF8String("cgrates;1451911932;00082")) + ccr.NewAVP(avp.OriginHost, avp.Mbit, 0, datatype.DiameterIdentity("CGR-DA")) + ccr.NewAVP(avp.OriginRealm, avp.Mbit, 0, datatype.DiameterIdentity("cgrates.org")) + ccr.NewAVP(avp.AuthApplicationID, avp.Mbit, 0, datatype.Unsigned32(4)) + ccr.NewAVP(avp.ServiceContextID, avp.Mbit, 0, datatype.UTF8String("pubsub1")) // Match specific DryRun profile + ccr.NewAVP(avp.CCRequestType, avp.Mbit, 0, datatype.Enumerated(4)) + ccr.NewAVP(avp.CCRequestNumber, avp.Mbit, 0, datatype.Unsigned32(0)) + ccr.NewAVP(avp.EventTimestamp, avp.Mbit, 0, datatype.Time(time.Date(2016, 1, 5, 11, 30, 10, 0, time.UTC))) + if _, err := ccr.NewAVP("Framed-IP-Address", avp.Mbit, 0, datatype.UTF8String("10.228.16.4")); err != nil { + t.Error(err) + } + if err := dmtClient.SendMessage(ccr); err != nil { + t.Error(err) + } + time.Sleep(time.Duration(100) * time.Millisecond) + msg := dmtClient.ReceivedMessage() + if msg == nil { + t.Fatal("No message returned") + } + if avps, err := msg.FindAVPsWithPath([]interface{}{"Result-Code"}, dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if len(avps) == 0 { + t.Error("Result-Code") + } else if strResult := avpValAsString(avps[0]); strResult != "300" { // Result-Code set in the template + t.Errorf("Expecting 300, received: %s", strResult) + } +} + func TestDmtAgentStopEngine(t *testing.T) { if !*testIntegration { return diff --git a/agents/libdmt.go b/agents/libdmt.go index fe32fab92..bb60b4f75 100644 --- a/agents/libdmt.go +++ b/agents/libdmt.go @@ -50,9 +50,18 @@ func init() { const ( META_CCR_USAGE = "*ccr_usage" - META_CCA_USAGE = "*cca_usage" + META_VALUE_EXPONENT = "*value_exponent" + META_SUM = "*sum" DIAMETER_CCR = "DIAMETER_CCR" DiameterRatingFailed = 5031 + CGRError = "CGRError" + CGRMaxUsage = "CGRMaxUsage" + CGRResultCode = "CGRResultCode" +) + +var ( + ErrFilterNotPassing = errors.New("Filter not passing") + ErrDiameterRatingFailed = errors.New("Diameter rating failed") ) func loadDictionaries(dictsDir, componentId string) error { @@ -215,6 +224,40 @@ func metaHandler(m *diam.Message, tag, arg string, dur time.Duration) (string, e return "", nil } +// metaValueExponent will multiply the float value with the exponent provided. +// Expects 2 arguments in template separated by | +func metaValueExponent(m *diam.Message, argsTpl utils.RSRFields, roundingDecimals int) (string, error) { + valStr := composedFieldvalue(m, argsTpl, 0, nil) + handlerArgs := strings.Split(valStr, utils.HandlerArgSep) + if len(handlerArgs) != 2 { + return "", errors.New("Unexpected number of arguments") + } + val, err := strconv.ParseFloat(handlerArgs[0], 64) + if err != nil { + return "", err + } + exp, err := strconv.Atoi(handlerArgs[1]) + if err != nil { + return "", err + } + res := val * math.Pow10(exp) + return strconv.FormatFloat(utils.Round(res, roundingDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil +} + +func metaSum(m *diam.Message, argsTpl utils.RSRFields, roundingDecimals int) (string, error) { + valStr := composedFieldvalue(m, argsTpl, 0, nil) + handlerArgs := strings.Split(valStr, utils.HandlerArgSep) + var summed float64 + for _, arg := range handlerArgs { + val, err := strconv.ParseFloat(arg, 64) + if err != nil { + return "", err + } + summed += val + } + return strconv.FormatFloat(utils.Round(summed, roundingDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil +} + // splitIntoInterface is used to split a string into []interface{} instead of []string func splitIntoInterface(content, sep string) []interface{} { spltStr := strings.Split(content, sep) @@ -230,16 +273,19 @@ func avpsWithPath(m *diam.Message, rsrFld *utils.RSRField) ([]*diam.AVP, error) return m.FindAVPsWithPath(splitIntoInterface(rsrFld.Id, utils.HIERARCHY_SEP), dict.UndefinedVendorID) } -// Follows the implementation in the StorCdr -func passesFieldFilter(m *diam.Message, fieldFilter *utils.RSRField) (bool, int) { +func passesFieldFilter(m *diam.Message, fieldFilter *utils.RSRField, processorVars map[string]string) (bool, int) { if fieldFilter == nil { return true, 0 } + if val, hasIt := processorVars[fieldFilter.Id]; hasIt { // ProcessorVars have priority + if fieldFilter.FilterPasses(val) { + return true, 0 + } + return false, 0 + } avps, err := avpsWithPath(m, fieldFilter) if err != nil { return false, 0 - } else if len(avps) == 0 { - return false, 0 // No AVPs with field filter ID } for avpIdx, avpVal := range avps { // First match wins due to index if fieldFilter.FilterPasses(avpValAsString(avpVal)) { @@ -249,12 +295,16 @@ func passesFieldFilter(m *diam.Message, fieldFilter *utils.RSRField) (bool, int) return false, 0 } -func composedFieldvalue(m *diam.Message, outTpl utils.RSRFields, avpIdx int) string { +func composedFieldvalue(m *diam.Message, outTpl utils.RSRFields, avpIdx int, processorVars map[string]string) string { var outVal string for _, rsrTpl := range outTpl { if rsrTpl.IsStatic() { outVal += rsrTpl.ParseValue("") } else { + if val, hasIt := processorVars[rsrTpl.Id]; hasIt { // ProcessorVars have priority + outVal += rsrTpl.ParseValue(val) + continue + } matchingAvps, err := avpsWithPath(m, rsrTpl) if err != nil || len(matchingAvps) == 0 { utils.Logger.Warning(fmt.Sprintf(" Cannot find AVP for field template with id: %s, ignoring.", rsrTpl.Id)) @@ -281,12 +331,18 @@ func serializeAVPValueFromString(dictAVP *dict.AVP, valStr, timezone string) ([] return []byte(valStr), nil case datatype.AddressType: return []byte(net.ParseIP(valStr)), nil - case datatype.EnumeratedType, datatype.Integer32Type, datatype.Integer64Type, datatype.Unsigned32Type, datatype.Unsigned64Type: + case datatype.EnumeratedType, datatype.Integer32Type, datatype.Unsigned32Type: i, err := strconv.Atoi(valStr) if err != nil { return nil, err } return datatype.Enumerated(i).Serialize(), nil + case datatype.Unsigned64Type, datatype.Integer64Type: + i, err := strconv.ParseInt(valStr, 10, 64) + if err != nil { + return nil, err + } + return datatype.Unsigned64(i).Serialize(), nil case datatype.Float32Type: f, err := strconv.ParseFloat(valStr, 32) if err != nil { @@ -314,15 +370,13 @@ func serializeAVPValueFromString(dictAVP *dict.AVP, valStr, timezone string) ([] } } -var ErrFilterNotPassing = errors.New("Filter not passing") - -func fieldOutVal(m *diam.Message, cfgFld *config.CfgCdrField, extraParam interface{}) (fmtValOut string, err error) { +func fieldOutVal(m *diam.Message, cfgFld *config.CfgCdrField, extraParam interface{}, processorVars map[string]string) (fmtValOut string, err error) { var outVal string passAtIndex := -1 passedAllFilters := true for _, fldFilter := range cfgFld.FieldFilter { var pass bool - if pass, passAtIndex = passesFieldFilter(m, fldFilter); !pass { + if pass, passAtIndex = passesFieldFilter(m, fldFilter, processorVars); !pass { passedAllFilters = false break } @@ -340,18 +394,21 @@ func fieldOutVal(m *diam.Message, cfgFld *config.CfgCdrField, extraParam interfa case utils.META_CONSTANT: outVal = cfgFld.Value.Id() case utils.META_HANDLER: - if cfgFld.HandlerId == META_CCA_USAGE { // Exception, usage is passed in the dur variable by CCA - outVal = strconv.FormatFloat(extraParam.(float64), 'f', -1, 64) - } else { + switch cfgFld.HandlerId { + case META_VALUE_EXPONENT: + outVal, err = metaValueExponent(m, cfgFld.Value, 10) // FixMe: add here configured number of decimals + case META_SUM: + outVal, err = metaSum(m, cfgFld.Value, 10) + default: outVal, err = metaHandler(m, cfgFld.HandlerId, cfgFld.Layout, extraParam.(time.Duration)) if err != nil { utils.Logger.Warning(fmt.Sprintf(" Ignoring processing of metafunction: %s, error: %s", cfgFld.HandlerId, err.Error())) } } case utils.META_COMPOSED: - outVal = composedFieldvalue(m, cfgFld.Value, 0) + outVal = composedFieldvalue(m, cfgFld.Value, 0, processorVars) case utils.MetaGrouped: // GroupedAVP - outVal = composedFieldvalue(m, cfgFld.Value, passAtIndex) + outVal = composedFieldvalue(m, cfgFld.Value, passAtIndex, processorVars) } if fmtValOut, err = utils.FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil { utils.Logger.Warning(fmt.Sprintf(" Error when processing field template with tag: %s, error: %s", cfgFld.Tag, err.Error())) @@ -377,7 +434,7 @@ func messageSetAVPsWithPath(m *diam.Message, path []interface{}, avpValStr strin } } if dictAVPs[len(path)-1].Data.Type == diam.GroupedAVPType { - return errors.New("Last AVP in path needs not to be GroupedAVP") + return errors.New("Last AVP in path cannot be GroupedAVP") } var msgAVP *diam.AVP // Keep a reference here towards last AVP lastAVPIdx := len(path) - 1 @@ -388,7 +445,7 @@ func messageSetAVPsWithPath(m *diam.Message, path []interface{}, avpValStr strin if err != nil { return err } - typeVal, err = datatype.Decode(dictAVPs[i].Data.Type, avpValByte) + typeVal, err = datatype.Decode(dictAVPs[i].Data.Type, avpValByte) // Check here if err != nil { return err } @@ -400,7 +457,7 @@ func messageSetAVPsWithPath(m *diam.Message, path []interface{}, avpValStr strin if i == lastAVPIdx-1 && !appnd { // last AVP needs to be appended in group avps, _ := m.FindAVPsWithPath(path[:lastAVPIdx], dict.UndefinedVendorID) if len(avps) != 0 { // Group AVP already in the message - prevGrpData := avps[0].Data.(*diam.GroupedAVP) + prevGrpData := avps[len(avps)-1].Data.(*diam.GroupedAVP) // Take the last avp found to append there prevGrpData.AVP = append(prevGrpData.AVP, msgAVP) m.Header.MessageLength += uint32(msgAVP.Len()) return nil @@ -411,8 +468,8 @@ func messageSetAVPsWithPath(m *diam.Message, path []interface{}, avpValStr strin if !appnd { // Not group AVP, replace the previous set one with this one avps, _ := m.FindAVPsWithPath(path, dict.UndefinedVendorID) if len(avps) != 0 { // Group AVP already in the message - m.Header.MessageLength -= uint32(avps[0].Len()) // decrease message length since we overwrite - *avps[0] = *msgAVP + m.Header.MessageLength -= uint32(avps[len(avps)-1].Len()) // decrease message length since we overwrite + *avps[len(avps)-1] = *msgAVP m.Header.MessageLength += uint32(msgAVP.Len()) return nil } @@ -556,7 +613,7 @@ func (self *CCR) AsSMGenericEvent(cfgFlds []*config.CfgCdrField) (sessionmanager outMap := make(map[string]string) // work with it so we can append values to keys outMap[utils.EVENT_NAME] = DIAMETER_CCR for _, cfgFld := range cfgFlds { - fmtOut, err := fieldOutVal(self.diamMessage, cfgFld, self.debitInterval) + fmtOut, err := fieldOutVal(self.diamMessage, cfgFld, self.debitInterval, nil) if err != nil { if err == ErrFilterNotPassing { continue // Do nothing in case of Filter not passing @@ -621,11 +678,11 @@ func (self *CCA) AsDiameterMessage() *diam.Message { } // SetProcessorAVPs will add AVPs to self.diameterMessage based on template defined in processor.CCAFields -func (self *CCA) SetProcessorAVPs(reqProcessor *config.DARequestProcessor, maxUsage float64) error { +func (self *CCA) SetProcessorAVPs(reqProcessor *config.DARequestProcessor, processorVars map[string]string) error { for _, cfgFld := range reqProcessor.CCAFields { - fmtOut, err := fieldOutVal(self.ccrMessage, cfgFld, maxUsage) + fmtOut, err := fieldOutVal(self.ccrMessage, cfgFld, nil, processorVars) if err == ErrFilterNotPassing { // Field not in or filter not passing, try match in answer - fmtOut, err = fieldOutVal(self.diamMessage, cfgFld, maxUsage) + fmtOut, err = fieldOutVal(self.diamMessage, cfgFld, nil, processorVars) } if err != nil { if err == ErrFilterNotPassing { diff --git a/agents/libdmt_test.go b/agents/libdmt_test.go index 73297edd0..7d6924c42 100644 --- a/agents/libdmt_test.go +++ b/agents/libdmt_test.go @@ -21,6 +21,7 @@ package agents import ( "bytes" "encoding/binary" + "fmt" "reflect" "testing" "time" @@ -86,6 +87,62 @@ func TestAvpValAsString(t *testing.T) { } } +func TestMetaValueExponent(t *testing.T) { + m := diam.NewRequest(diam.CreditControl, 4, nil) + m.NewAVP("Session-Id", avp.Mbit, 0, datatype.UTF8String("simuhuawei;1449573472;00002")) + m.NewAVP(avp.RequestedServiceUnit, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.CCMoney, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.UnitValue, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.ValueDigits, avp.Mbit, 0, datatype.Integer64(10000)), + diam.NewAVP(avp.Exponent, avp.Mbit, 0, datatype.Integer32(-5)), + }, + }), + diam.NewAVP(avp.CurrencyCode, avp.Mbit, 0, datatype.Unsigned32(33)), + }, + }), + }, + }) + if val, err := metaValueExponent(m, utils.ParseRSRFieldsMustCompile("Requested-Service-Unit>CC-Money>Unit-Value>Value-Digits;^|;Requested-Service-Unit>CC-Money>Unit-Value>Exponent", utils.INFIELD_SEP), 10); err != nil { + t.Error(err) + } else if val != "0.1" { + t.Error("Received: ", val) + } + if _, err = metaValueExponent(m, utils.ParseRSRFieldsMustCompile("Requested-Service-Unit>CC-Money>Unit-Value>Value-Digits;Requested-Service-Unit>CC-Money>Unit-Value>Exponent", utils.INFIELD_SEP), 10); err == nil { + t.Error("Should have received error") // Insufficient number arguments + } +} + +func TestMetaSum(t *testing.T) { + m := diam.NewRequest(diam.CreditControl, 4, nil) + m.NewAVP("Session-Id", avp.Mbit, 0, datatype.UTF8String("simuhuawei;1449573472;00002")) + m.NewAVP(avp.RequestedServiceUnit, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.CCMoney, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.UnitValue, avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(avp.ValueDigits, avp.Mbit, 0, datatype.Integer64(10000)), + diam.NewAVP(avp.Exponent, avp.Mbit, 0, datatype.Integer32(-5)), + }, + }), + diam.NewAVP(avp.CurrencyCode, avp.Mbit, 0, datatype.Unsigned32(33)), + }, + }), + }, + }) + if val, err := metaSum(m, utils.ParseRSRFieldsMustCompile("Requested-Service-Unit>CC-Money>Unit-Value>Value-Digits;^|;Requested-Service-Unit>CC-Money>Unit-Value>Exponent", utils.INFIELD_SEP), 10); err != nil { + t.Error(err) + } else if val != "9995" { + t.Error("Received: ", val) + } + if _, err = metaSum(m, utils.ParseRSRFieldsMustCompile("Requested-Service-Unit>CC-Money>Unit-Value>Value-Digits;Requested-Service-Unit>CC-Money>Unit-Value>Exponent", utils.INFIELD_SEP), 10); err == nil { + t.Error("Should have received error") // Insufficient number arguments + } +} + func TestFieldOutVal(t *testing.T) { m := diam.NewRequest(diam.CreditControl, 4, nil) m.NewAVP("Session-Id", avp.Mbit, 0, datatype.UTF8String("simuhuawei;1449573472;00002")) @@ -106,7 +163,7 @@ func TestFieldOutVal(t *testing.T) { cfgFld := &config.CfgCdrField{Tag: "StaticTest", Type: utils.META_COMPOSED, FieldId: utils.TOR, Value: utils.ParseRSRFieldsMustCompile("^*voice", utils.INFIELD_SEP), Mandatory: true} eOut := "*voice" - if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0)); err != nil { + if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0), nil); err != nil { t.Error(err) } else if fldOut != eOut { t.Errorf("Expecting: %s, received: %s", eOut, fldOut) @@ -114,7 +171,20 @@ func TestFieldOutVal(t *testing.T) { cfgFld = &config.CfgCdrField{Tag: "ComposedTest", Type: utils.META_COMPOSED, FieldId: utils.DESTINATION, Value: utils.ParseRSRFieldsMustCompile("Requested-Service-Unit>CC-Time", utils.INFIELD_SEP), Mandatory: true} eOut = "360" - if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0)); err != nil { + if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0), nil); err != nil { + t.Error(err) + } else if fldOut != eOut { + t.Errorf("Expecting: %s, received: %s", eOut, fldOut) + } + // With filter on ProcessorVars + cfgFld = &config.CfgCdrField{Tag: "ComposedTestWithProcessorVarsFilter", Type: utils.META_COMPOSED, FieldId: utils.DESTINATION, + FieldFilter: utils.ParseRSRFieldsMustCompile("CGRError(INSUFFICIENT_CREDIT)", utils.INFIELD_SEP), + Value: utils.ParseRSRFieldsMustCompile("Requested-Service-Unit>CC-Time", utils.INFIELD_SEP), Mandatory: true} + if _, err := fieldOutVal(m, cfgFld, time.Duration(0), nil); err == nil { + t.Error("Should have error") + } + eOut = "360" + if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0), map[string]string{"CGRError": "INSUFFICIENT_CREDIT"}); err != nil { t.Error(err) } else if fldOut != eOut { t.Errorf("Expecting: %s, received: %s", eOut, fldOut) @@ -123,7 +193,7 @@ func TestFieldOutVal(t *testing.T) { cfgFld = &config.CfgCdrField{Tag: "Grouped1", Type: utils.MetaGrouped, FieldId: "Account", Value: utils.ParseRSRFieldsMustCompile("Subscription-Id>Subscription-Id-Data", utils.INFIELD_SEP), Mandatory: true} eOut = "33708000003" - if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0)); err != nil { + if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0), nil); err != nil { t.Error(err) } else if fldOut != eOut { t.Errorf("Expecting: %s, received: %s", eOut, fldOut) @@ -133,7 +203,7 @@ func TestFieldOutVal(t *testing.T) { FieldFilter: utils.ParseRSRFieldsMustCompile("Subscription-Id>Subscription-Id-Type(1)", utils.INFIELD_SEP), Value: utils.ParseRSRFieldsMustCompile("Subscription-Id>Subscription-Id-Data", utils.INFIELD_SEP), Mandatory: true} eOut = "208708000003" - if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0)); err != nil { + if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0), nil); err != nil { t.Error(err) } else if fldOut != eOut { t.Errorf("Expecting: %s, received: %s", eOut, fldOut) @@ -241,6 +311,52 @@ func TestMessageSetAVPsWithPath(t *testing.T) { } else if !reflect.DeepEqual(eMessage, m) { t.Errorf("Expecting: %+v, received: %+v", eMessage, m) } + // Multiple append + eMessage = diam.NewRequest(diam.CreditControl, 4, nil) + eMessage.NewAVP("Multiple-Services-Credit-Control", avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(431, avp.Mbit, 0, &diam.GroupedAVP{ // Granted-Service-Unit + AVP: []*diam.AVP{ + diam.NewAVP(420, avp.Mbit, 0, datatype.Unsigned32(3600)), + diam.NewAVP(421, avp.Mbit, 0, datatype.Unsigned64(153600)), // "CC-Total-Octets" + }, + }), + diam.NewAVP(432, avp.Mbit, 0, datatype.Unsigned32(10)), + }, + }) + eMessage.NewAVP("Multiple-Services-Credit-Control", avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(431, avp.Mbit, 0, &diam.GroupedAVP{ // Granted-Service-Unit + AVP: []*diam.AVP{ + diam.NewAVP(420, avp.Mbit, 0, datatype.Unsigned32(2600)), + diam.NewAVP(421, avp.Mbit, 0, datatype.Unsigned64(143600)), // "CC-Total-Octets" + }, + }), + diam.NewAVP(432, avp.Mbit, 0, datatype.Unsigned32(11)), // Rating-Group + }, + }) + m = diam.NewMessage(diam.CreditControl, diam.RequestFlag, 4, eMessage.Header.HopByHopID, eMessage.Header.EndToEndID, nil) + if err := messageSetAVPsWithPath(m, []interface{}{"Multiple-Services-Credit-Control", "Granted-Service-Unit", "CC-Time"}, "3600", false, "UTC"); err != nil { + t.Error(err) + } + if err := messageSetAVPsWithPath(m, []interface{}{"Multiple-Services-Credit-Control", "Granted-Service-Unit", "CC-Total-Octets"}, "153600", false, "UTC"); err != nil { + t.Error(err) + } + if err := messageSetAVPsWithPath(m, []interface{}{"Multiple-Services-Credit-Control", "Rating-Group"}, "10", false, "UTC"); err != nil { + t.Error(err) + } + if err := messageSetAVPsWithPath(m, []interface{}{"Multiple-Services-Credit-Control", "Granted-Service-Unit", "CC-Time"}, "2600", true, "UTC"); err != nil { + t.Error(err) + } + if err := messageSetAVPsWithPath(m, []interface{}{"Multiple-Services-Credit-Control", "Granted-Service-Unit", "CC-Total-Octets"}, "143600", false, "UTC"); err != nil { + t.Error(err) + } + if err := messageSetAVPsWithPath(m, []interface{}{"Multiple-Services-Credit-Control", "Rating-Group"}, "11", false, "UTC"); err != nil { + t.Error(err) + } + if fmt.Sprintf("%q", eMessage) != fmt.Sprintf("%q", m) { // test with fmt since reflect.DeepEqual does not perform properly here + t.Errorf("Expecting: %+v, received: %+v", eMessage, m) + } } func TestCCASetProcessorAVPs(t *testing.T) { @@ -274,7 +390,7 @@ func TestCCASetProcessorAVPs(t *testing.T) { diam.NewAVP(450, avp.Mbit, 0, datatype.Enumerated(0)), // Subscription-Id-Type diam.NewAVP(444, avp.Mbit, 0, datatype.UTF8String("33708000003")), // Subscription-Id-Data }}) - if err := cca.SetProcessorAVPs(reqProcessor, 0); err != nil { + if err := cca.SetProcessorAVPs(reqProcessor, map[string]string{}); err != nil { t.Error(err) } else if ccaMsg := cca.AsDiameterMessage(); !reflect.DeepEqual(eMessage, ccaMsg) { t.Errorf("Expecting: %+v, received: %+v", eMessage, ccaMsg) diff --git a/apier/v1/accounts.go b/apier/v1/accounts.go index c94a788ca..6d006da25 100644 --- a/apier/v1/accounts.go +++ b/apier/v1/accounts.go @@ -21,7 +21,6 @@ package v1 import ( "fmt" "math" - "regexp" "strings" "time" @@ -94,9 +93,11 @@ func (self *ApierV1) RemActionTiming(attrs AttrRemActionTiming, reply *string) e return 0, utils.ErrNotFound } - if attrs.ActionPlanId != "" { // delete the entire action plan - ap.ActionTimings = nil // will delete the action plan - return 0, self.RatingDb.SetActionPlan(ap.Id, ap) + if attrs.Tenant != "" && attrs.Account != "" { + accID := utils.AccountKey(attrs.Tenant, attrs.Account) + delete(ap.AccountIDs, accID) + err = self.RatingDb.SetActionPlan(ap.Id, ap, true) + goto UPDATE } if attrs.ActionTimingId != "" { // delete only a action timing from action plan @@ -107,15 +108,19 @@ func (self *ApierV1) RemActionTiming(attrs AttrRemActionTiming, reply *string) e break } } - return 0, self.RatingDb.SetActionPlan(ap.Id, ap) + err = self.RatingDb.SetActionPlan(ap.Id, ap, true) + goto UPDATE } - if attrs.Tenant != "" && attrs.Account != "" { - accID := utils.AccountKey(attrs.Tenant, attrs.Account) - delete(ap.AccountIDs, accID) - return 0, self.RatingDb.SetActionPlan(ap.Id, ap) + if attrs.ActionPlanId != "" { // delete the entire action plan + ap.ActionTimings = nil // will delete the action plan + err = self.RatingDb.SetActionPlan(ap.Id, ap, true) + goto UPDATE + } + UPDATE: + if err != nil { + return 0, err } - // update cache self.RatingDb.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: []string{utils.ACTION_PLAN_PREFIX + attrs.ActionPlanId}}) return 0, nil @@ -131,61 +136,6 @@ func (self *ApierV1) RemActionTiming(attrs AttrRemActionTiming, reply *string) e return nil } -// Returns a list of ActionTriggers on an account -func (self *ApierV1) GetAccountActionTriggers(attrs AttrAcntAction, reply *engine.ActionTriggers) error { - if missing := utils.MissingStructFields(&attrs, []string{"Tenant", "Account"}); len(missing) != 0 { - return utils.NewErrMandatoryIeMissing(missing...) - } - if balance, err := self.AccountDb.GetAccount(utils.AccountKey(attrs.Tenant, attrs.Account)); err != nil { - return utils.NewErrServerError(err) - } else { - *reply = balance.ActionTriggers - } - return nil -} - -type AttrRemAcntActionTriggers struct { - Tenant string // Tenant he account belongs to - Account string // Account name - ActionTriggersId string // Id filtering only specific id to remove (can be regexp pattern) - ActionTriggersUniqueId string -} - -// Returns a list of ActionTriggers on an account -func (self *ApierV1) RemAccountActionTriggers(attrs AttrRemAcntActionTriggers, reply *string) error { - if missing := utils.MissingStructFields(&attrs, []string{"Tenant", "Account"}); len(missing) != 0 { - return utils.NewErrMandatoryIeMissing(missing...) - } - accID := utils.AccountKey(attrs.Tenant, attrs.Account) - _, err := engine.Guardian.Guard(func() (interface{}, error) { - ub, err := self.AccountDb.GetAccount(accID) - if err != nil { - return 0, err - } - nactrs := make(engine.ActionTriggers, 0) - for _, actr := range ub.ActionTriggers { - match, _ := regexp.MatchString(attrs.ActionTriggersId, actr.ID) - if len(attrs.ActionTriggersId) != 0 && match { - continue - } - if len(attrs.ActionTriggersUniqueId) != 0 && attrs.ActionTriggersUniqueId == actr.UniqueID { - continue - } - nactrs = append(nactrs, actr) - } - ub.ActionTriggers = nactrs - if err := self.AccountDb.SetAccount(ub); err != nil { - return 0, err - } - return 0, nil - }, 0, accID) - if err != nil { - return utils.NewErrServerError(err) - } - *reply = OK - return nil -} - // Ads a new account into dataDb. If already defined, returns success. func (self *ApierV1) SetAccount(attr utils.AttrSetAccount, reply *string) error { if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { @@ -199,22 +149,47 @@ func (self *ApierV1) SetAccount(attr utils.AttrSetAccount, reply *string) error ub = bal } else { // Not found in db, create it here ub = &engine.Account{ - Id: accID, + ID: accID, } } if len(attr.ActionPlanId) != 0 { _, err := engine.Guardian.Guard(func() (interface{}, error) { + // clean previous action plans + actionPlansMap, err := self.RatingDb.GetAllActionPlans() + if err != nil { + if err == utils.ErrNotFound { // if no action plans just continue + return 0, nil + } + return 0, err + } + var dirtyAps []string + for actionPlanID, ap := range actionPlansMap { + if actionPlanID == attr.ActionPlanId { + // don't remove it if it's the current one + continue + } + if _, exists := ap.AccountIDs[accID]; exists { + delete(ap.AccountIDs, accID) + dirtyAps = append(dirtyAps, utils.ACTION_PLAN_PREFIX+actionPlanID) + } + } + + if len(dirtyAps) > 0 { + // update cache + self.RatingDb.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: dirtyAps}) + schedulerReloadNeeded = true + } + var ap *engine.ActionPlan - var err error ap, err = self.RatingDb.GetActionPlan(attr.ActionPlanId, false) if err != nil { return 0, err } if _, exists := ap.AccountIDs[accID]; !exists { if ap.AccountIDs == nil { - ap.AccountIDs = make(map[string]struct{}) + ap.AccountIDs = make(utils.StringMap) } - ap.AccountIDs[accID] = struct{}{} + ap.AccountIDs[accID] = true schedulerReloadNeeded = true // create tasks for _, at := range ap.ActionTimings { @@ -229,7 +204,7 @@ func (self *ApierV1) SetAccount(attr utils.AttrSetAccount, reply *string) error } } } - if err := self.RatingDb.SetActionPlan(attr.ActionPlanId, ap); err != nil { + if err := self.RatingDb.SetActionPlan(attr.ActionPlanId, ap, true); err != nil { return 0, err } // update cache @@ -279,39 +254,54 @@ func (self *ApierV1) RemoveAccount(attr utils.AttrRemoveAccount, reply *string) if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { return utils.NewErrMandatoryIeMissing(missing...) } + dirtyActionPlans := make(map[string]*engine.ActionPlan) accID := utils.AccountKey(attr.Tenant, attr.Account) - var schedulerReloadNeeded bool _, err := engine.Guardian.Guard(func() (interface{}, error) { // remove it from all action plans - allAPs, err := self.RatingDb.GetAllActionPlans() - if err != nil && err != utils.ErrNotFound { - return 0, err - } - for key, ap := range allAPs { - if _, exists := ap.AccountIDs[accID]; !exists { - schedulerReloadNeeded = true - _, err := engine.Guardian.Guard(func() (interface{}, error) { - // save action plan - self.RatingDb.SetActionPlan(key, ap) - // cache - self.RatingDb.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: []string{utils.ACTION_PLAN_PREFIX + key}}) - return 0, nil - }, 0, utils.ACTION_PLAN_PREFIX) - if err != nil { - return 0, err + _, err := engine.Guardian.Guard(func() (interface{}, error) { + actionPlansMap, err := self.RatingDb.GetAllActionPlans() + if err == utils.ErrNotFound { + // no action plans + return 0, nil + } + if err != nil { + return 0, err + } + + for actionPlanID, ap := range actionPlansMap { + if _, exists := ap.AccountIDs[accID]; exists { + delete(ap.AccountIDs, accID) + dirtyActionPlans[actionPlanID] = ap } } + + var actionPlansCacheIds []string + for actionPlanID, ap := range dirtyActionPlans { + if err := self.RatingDb.SetActionPlan(actionPlanID, ap, true); err != nil { + return 0, err + } + actionPlansCacheIds = append(actionPlansCacheIds, utils.ACTION_PLAN_PREFIX+actionPlanID) + } + if len(actionPlansCacheIds) > 0 { + // update cache + self.RatingDb.CacheRatingPrefixValues(map[string][]string{ + utils.ACTION_PLAN_PREFIX: actionPlansCacheIds}) + } + return 0, nil + }, 0, utils.ACTION_PLAN_PREFIX) + if err != nil { + return 0, err } + if err := self.AccountDb.RemoveAccount(accID); err != nil { return 0, err } return 0, nil }, 0, accID) - // FIXME: remove from all actionplans? if err != nil { return utils.NewErrServerError(err) } - if schedulerReloadNeeded { + if attr.ReloadScheduler && len(dirtyActionPlans) > 0 { // reload scheduler if self.Sched != nil { self.Sched.Reload(true) @@ -376,32 +366,48 @@ func (self *ApierV1) GetAccount(attr *utils.AttrGetAccount, reply *interface{}) type AttrAddBalance struct { Tenant string Account string - BalanceUuid string - BalanceId string + BalanceUuid *string + BalanceId *string BalanceType string - Directions string + Directions *string Value float64 - ExpiryTime string - RatingSubject string - Categories string - DestinationIds string - Weight float64 - SharedGroups string + ExpiryTime *string + RatingSubject *string + Categories *string + DestinationIds *string + TimingIds *string + Weight *float64 + SharedGroups *string Overwrite bool // When true it will reset if the balance is already there - Disabled bool + Blocker *bool + Disabled *bool } func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error { - expTime, err := utils.ParseTimeDetectLayout(attr.ExpiryTime, self.Config.DefaultTimezone) - if err != nil { - *reply = err.Error() - return err + return self.modifyBalance(engine.TOPUP, attr, reply) +} +func (self *ApierV1) DebitBalance(attr *AttrAddBalance, reply *string) error { + return self.modifyBalance(engine.DEBIT, attr, reply) +} + +func (self *ApierV1) modifyBalance(aType string, attr *AttrAddBalance, reply *string) error { + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType", "Value"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + var expTime *time.Time + if attr.ExpiryTime != nil { + expTimeVal, err := utils.ParseTimeDetectLayout(*attr.ExpiryTime, self.Config.DefaultTimezone) + if err != nil { + *reply = err.Error() + return err + } + expTime = &expTimeVal } accID := utils.AccountKey(attr.Tenant, attr.Account) if _, err := self.AccountDb.GetAccount(accID); err != nil { // create account if not exists account := &engine.Account{ - Id: accID, + ID: accID, } if err := self.AccountDb.SetAccount(account); err != nil { *reply = err.Error() @@ -409,34 +415,41 @@ func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error { } } at := &engine.ActionTiming{} - at.SetAccountIDs(map[string]struct{}{accID: struct{}{}}) - - aType := engine.DEBIT - // reverse the sign as it is a debit - attr.Value = -attr.Value + at.SetAccountIDs(utils.StringMap{accID: true}) if attr.Overwrite { - aType = engine.DEBIT_RESET + aType += "_reset" // => *topup_reset/*debit_reset } - at.SetActions(engine.Actions{ - &engine.Action{ - ActionType: aType, - BalanceType: attr.BalanceType, - Balance: &engine.Balance{ - Uuid: attr.BalanceUuid, - Id: attr.BalanceId, - Value: attr.Value, - ExpirationDate: expTime, - RatingSubject: attr.RatingSubject, - Directions: utils.ParseStringMap(attr.Directions), - DestinationIds: utils.ParseStringMap(attr.DestinationIds), - Categories: utils.ParseStringMap(attr.Categories), - Weight: attr.Weight, - SharedGroups: utils.ParseStringMap(attr.SharedGroups), - Disabled: attr.Disabled, - }, + a := &engine.Action{ + ActionType: aType, + Balance: &engine.BalanceFilter{ + Uuid: attr.BalanceUuid, + ID: attr.BalanceId, + Type: utils.StringPointer(attr.BalanceType), + Value: utils.Float64Pointer(attr.Value), + ExpirationDate: expTime, + RatingSubject: attr.RatingSubject, + Weight: attr.Weight, + Blocker: attr.Blocker, + Disabled: attr.Disabled, }, - }) + } + if attr.Directions != nil { + a.Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(*attr.Directions)) + } + if attr.DestinationIds != nil { + a.Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(*attr.DestinationIds)) + } + if attr.Categories != nil { + a.Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(*attr.Categories)) + } + if attr.SharedGroups != nil { + a.Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(*attr.SharedGroups)) + } + if attr.TimingIds != nil { + a.Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(*attr.TimingIds)) + } + at.SetActions(engine.Actions{a}) if err := at.Execute(); err != nil { *reply = err.Error() return err @@ -445,37 +458,86 @@ func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error { return nil } -func (self *ApierV1) EnableDisableBalance(attr *AttrAddBalance, reply *string) error { - expTime, err := utils.ParseDate(attr.ExpiryTime) - if err != nil { - *reply = err.Error() - return err +type AttrSetBalance struct { + Tenant string + Account string + BalanceType string + BalanceUUID *string + BalanceID *string + Directions *string + Value *float64 + ExpiryTime *string + RatingSubject *string + Categories *string + DestinationIds *string + TimingIds *string + Weight *float64 + SharedGroups *string + Blocker *bool + Disabled *bool +} + +func (self *ApierV1) SetBalance(attr *AttrSetBalance, reply *string) error { + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) } - accID := utils.ConcatenatedKey(attr.Tenant, attr.Account) + if (attr.BalanceID == nil || *attr.BalanceID == "") && + (attr.BalanceUUID == nil || *attr.BalanceUUID == "") { + return utils.NewErrMandatoryIeMissing("BalanceID", "or", "BalanceUUID") + } + var expTime *time.Time + if attr.ExpiryTime != nil { + expTimeVal, err := utils.ParseTimeDetectLayout(*attr.ExpiryTime, self.Config.DefaultTimezone) + if err != nil { + *reply = err.Error() + return err + } + expTime = &expTimeVal + } + accID := utils.AccountKey(attr.Tenant, attr.Account) if _, err := self.AccountDb.GetAccount(accID); err != nil { - return utils.ErrNotFound + // create account if not exists + account := &engine.Account{ + ID: accID, + } + if err := self.AccountDb.SetAccount(account); err != nil { + *reply = err.Error() + return err + } } at := &engine.ActionTiming{} - at.SetAccountIDs(map[string]struct{}{accID: struct{}{}}) + at.SetAccountIDs(utils.StringMap{accID: true}) - at.SetActions(engine.Actions{ - &engine.Action{ - ActionType: engine.ENABLE_DISABLE_BALANCE, - BalanceType: attr.BalanceType, - Balance: &engine.Balance{ - Uuid: attr.BalanceUuid, - Id: attr.BalanceId, - Value: attr.Value, - ExpirationDate: expTime, - RatingSubject: attr.RatingSubject, - Directions: utils.ParseStringMap(attr.Directions), - DestinationIds: utils.ParseStringMap(attr.DestinationIds), - Weight: attr.Weight, - SharedGroups: utils.ParseStringMap(attr.SharedGroups), - Disabled: attr.Disabled, - }, + a := &engine.Action{ + ActionType: engine.SET_BALANCE, + Balance: &engine.BalanceFilter{ + Uuid: attr.BalanceUUID, + ID: attr.BalanceID, + Type: utils.StringPointer(attr.BalanceType), + Value: attr.Value, + ExpirationDate: expTime, + RatingSubject: attr.RatingSubject, + Weight: attr.Weight, + Blocker: attr.Blocker, + Disabled: attr.Disabled, }, - }) + } + if attr.Directions != nil { + a.Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(*attr.Directions)) + } + if attr.DestinationIds != nil { + a.Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(*attr.DestinationIds)) + } + if attr.Categories != nil { + a.Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(*attr.Categories)) + } + if attr.SharedGroups != nil { + a.Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(*attr.SharedGroups)) + } + if attr.TimingIds != nil { + a.Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(*attr.TimingIds)) + } + at.SetActions(engine.Actions{a}) if err := at.Execute(); err != nil { *reply = err.Error() return err @@ -484,11 +546,18 @@ func (self *ApierV1) EnableDisableBalance(attr *AttrAddBalance, reply *string) e return nil } -func (self *ApierV1) RemoveBalances(attr *AttrAddBalance, reply *string) error { - expTime, err := utils.ParseDate(attr.ExpiryTime) - if err != nil { - *reply = err.Error() - return err +func (self *ApierV1) RemoveBalances(attr *AttrSetBalance, reply *string) error { + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + var expTime *time.Time + if attr.ExpiryTime != nil { + expTimeVal, err := utils.ParseTimeDetectLayout(*attr.ExpiryTime, self.Config.DefaultTimezone) + if err != nil { + *reply = err.Error() + return err + } + expTime = &expTimeVal } accID := utils.AccountKey(attr.Tenant, attr.Account) if _, err := self.AccountDb.GetAccount(accID); err != nil { @@ -496,25 +565,37 @@ func (self *ApierV1) RemoveBalances(attr *AttrAddBalance, reply *string) error { } at := &engine.ActionTiming{} - at.SetAccountIDs(map[string]struct{}{accID: struct{}{}}) - at.SetActions(engine.Actions{ - &engine.Action{ - ActionType: engine.REMOVE_BALANCE, - BalanceType: attr.BalanceType, - Balance: &engine.Balance{ - Uuid: attr.BalanceUuid, - Id: attr.BalanceId, - Value: attr.Value, - ExpirationDate: expTime, - RatingSubject: attr.RatingSubject, - Directions: utils.ParseStringMap(attr.Directions), - DestinationIds: utils.ParseStringMap(attr.DestinationIds), - Weight: attr.Weight, - SharedGroups: utils.ParseStringMap(attr.SharedGroups), - Disabled: attr.Disabled, - }, + at.SetAccountIDs(utils.StringMap{accID: true}) + a := &engine.Action{ + ActionType: engine.SET_BALANCE, + Balance: &engine.BalanceFilter{ + Uuid: attr.BalanceUUID, + ID: attr.BalanceID, + Type: utils.StringPointer(attr.BalanceType), + Value: attr.Value, + ExpirationDate: expTime, + RatingSubject: attr.RatingSubject, + Weight: attr.Weight, + Blocker: attr.Blocker, + Disabled: attr.Disabled, }, - }) + } + if attr.Directions != nil { + a.Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(*attr.Directions)) + } + if attr.DestinationIds != nil { + a.Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(*attr.DestinationIds)) + } + if attr.Categories != nil { + a.Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(*attr.Categories)) + } + if attr.SharedGroups != nil { + a.Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(*attr.SharedGroups)) + } + if attr.TimingIds != nil { + a.Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(*attr.TimingIds)) + } + at.SetActions(engine.Actions{a}) if err := at.Execute(); err != nil { *reply = err.Error() return err @@ -522,3 +603,96 @@ func (self *ApierV1) RemoveBalances(attr *AttrAddBalance, reply *string) error { *reply = OK return nil } + +/* To be removed after the above one proves reliable +func (self *ApierV1) SetBalance(attr *AttrSetBalance, reply *string) error { + if missing := utils.MissingStructFields(attr, []string{"Tenant", "Account", "BalanceType"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + + var err error + if attr.ExpiryTime != nil { + attr.expTime, err = utils.ParseTimeDetectLayout(*attr.ExpiryTime, self.Config.DefaultTimezone) + if err != nil { + *reply = err.Error() + return err + } + } + accID := utils.ConcatenatedKey(attr.Tenant, attr.Account) + _, err = engine.Guardian.Guard(func() (interface{}, error) { + account, err := self.AccountDb.GetAccount(accID) + if err != nil { + return 0, utils.ErrNotFound + } + + if account.BalanceMap == nil { + account.BalanceMap = make(map[string]engine.Balances, 1) + } + var previousSharedGroups utils.StringMap // kept for comparison + var balance *engine.Balance + var found bool + for _, b := range account.BalanceMap[attr.BalanceType] { + if b.IsExpired() { + continue + } + if (attr.BalanceUUID != nil && b.Uuid == *attr.BalanceUUID) || + (attr.BalanceID != nil && b.Id == *attr.BalanceID) { + previousSharedGroups = b.SharedGroups + balance = b + found = true + break // only set one balance + } + } + + // if it is not found then we add it to the list + if balance == nil { + balance = &engine.Balance{} + balance.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency + account.BalanceMap[attr.BalanceType] = append(account.BalanceMap[attr.BalanceType], balance) + } + + if attr.BalanceID != nil && *attr.BalanceID == utils.META_DEFAULT { + balance.Id = utils.META_DEFAULT + if attr.Value != nil { + balance.Value = *attr.Value + } + } else { + attr.SetBalance(balance) + } + + if !found || !previousSharedGroups.Equal(balance.SharedGroups) { + _, err = engine.Guardian.Guard(func() (interface{}, error) { + for sgID := range balance.SharedGroups { + // add shared group member + sg, err := self.RatingDb.GetSharedGroup(sgID, false) + if err != nil || sg == nil { + //than is problem + utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID)) + } else { + if _, found := sg.MemberIds[account.Id]; !found { + // add member and save + if sg.MemberIds == nil { + sg.MemberIds = make(utils.StringMap) + } + sg.MemberIds[account.Id] = true + self.RatingDb.SetSharedGroup(sg) + } + } + } + return 0, nil + }, 0, balance.SharedGroups.Slice()...) + } + + account.InitCounters() + account.ExecuteActionTriggers(nil) + self.AccountDb.SetAccount(account) + return 0, nil + }, 0, accID) + if err != nil { + *reply = err.Error() + return err + } + *reply = utils.OK + return nil +} +*/ diff --git a/apier/v1/accounts_test.go b/apier/v1/accounts_test.go index 51a5ce573..8520e6e3c 100644 --- a/apier/v1/accounts_test.go +++ b/apier/v1/accounts_test.go @@ -41,16 +41,16 @@ func TestSetAccounts(t *testing.T) { cgrTenant := "cgrates.org" iscTenant := "itsyscom.com" b10 := &engine.Balance{Value: 10, Weight: 10} - cgrAcnt1 := &engine.Account{Id: utils.ConcatenatedKey(cgrTenant, "account1"), - BalanceMap: map[string]engine.BalanceChain{utils.MONETARY + utils.OUT: engine.BalanceChain{b10}}} - cgrAcnt2 := &engine.Account{Id: utils.ConcatenatedKey(cgrTenant, "account2"), - BalanceMap: map[string]engine.BalanceChain{utils.MONETARY + utils.OUT: engine.BalanceChain{b10}}} - cgrAcnt3 := &engine.Account{Id: utils.ConcatenatedKey(cgrTenant, "account3"), - BalanceMap: map[string]engine.BalanceChain{utils.MONETARY + utils.OUT: engine.BalanceChain{b10}}} - iscAcnt1 := &engine.Account{Id: utils.ConcatenatedKey(iscTenant, "account1"), - BalanceMap: map[string]engine.BalanceChain{utils.MONETARY + utils.OUT: engine.BalanceChain{b10}}} - iscAcnt2 := &engine.Account{Id: utils.ConcatenatedKey(iscTenant, "account2"), - BalanceMap: map[string]engine.BalanceChain{utils.MONETARY + utils.OUT: engine.BalanceChain{b10}}} + cgrAcnt1 := &engine.Account{ID: utils.ConcatenatedKey(cgrTenant, "account1"), + BalanceMap: map[string]engine.Balances{utils.MONETARY + utils.OUT: engine.Balances{b10}}} + cgrAcnt2 := &engine.Account{ID: utils.ConcatenatedKey(cgrTenant, "account2"), + BalanceMap: map[string]engine.Balances{utils.MONETARY + utils.OUT: engine.Balances{b10}}} + cgrAcnt3 := &engine.Account{ID: utils.ConcatenatedKey(cgrTenant, "account3"), + BalanceMap: map[string]engine.Balances{utils.MONETARY + utils.OUT: engine.Balances{b10}}} + iscAcnt1 := &engine.Account{ID: utils.ConcatenatedKey(iscTenant, "account1"), + BalanceMap: map[string]engine.Balances{utils.MONETARY + utils.OUT: engine.Balances{b10}}} + iscAcnt2 := &engine.Account{ID: utils.ConcatenatedKey(iscTenant, "account2"), + BalanceMap: map[string]engine.Balances{utils.MONETARY + utils.OUT: engine.Balances{b10}}} for _, account := range []*engine.Account{cgrAcnt1, cgrAcnt2, cgrAcnt3, iscAcnt1, iscAcnt2} { if err := apierAcntsAcntStorage.SetAccount(account); err != nil { t.Error(err) diff --git a/apier/v1/apier.go b/apier/v1/apier.go index 23ae308ec..5c83c9313 100644 --- a/apier/v1/apier.go +++ b/apier/v1/apier.go @@ -19,12 +19,12 @@ along with this program. If not, see package v1 import ( - "encoding/json" "errors" "fmt" "log" "os" "path" + "strconv" "strings" "time" @@ -108,7 +108,7 @@ func (self *ApierV1) ExecuteAction(attr *utils.AttrExecuteAction, reply *string) at := &engine.ActionTiming{ ActionsID: attr.ActionsId, } - at.SetAccountIDs(map[string]struct{}{accID: struct{}{}}) + at.SetAccountIDs(utils.StringMap{accID: true}) if err := at.Execute(); err != nil { *reply = err.Error() return err @@ -503,23 +503,41 @@ func (self *ApierV1) SetActions(attrs utils.AttrSetActions, reply *string) error } storeActions := make(engine.Actions, len(attrs.Actions)) for idx, apiAct := range attrs.Actions { + var units *float64 + if apiAct.Units != "" { + if x, err := strconv.ParseFloat(apiAct.Units, 64); err == nil { + units = &x + } else { + return err + } + } + + var weight *float64 + if apiAct.BalanceWeight != "" { + if x, err := strconv.ParseFloat(apiAct.BalanceWeight, 64); err == nil { + weight = &x + } else { + return err + } + } + a := &engine.Action{ Id: utils.GenUUID(), ActionType: apiAct.Identifier, - BalanceType: apiAct.BalanceType, Weight: apiAct.Weight, ExpirationString: apiAct.ExpiryTime, ExtraParameters: apiAct.ExtraParameters, Filter: apiAct.Filter, - Balance: &engine.Balance{ - Uuid: utils.GenUUID(), - Id: apiAct.BalanceId, - Value: apiAct.Units, - Weight: apiAct.BalanceWeight, - Directions: utils.ParseStringMap(apiAct.Directions), - DestinationIds: utils.ParseStringMap(apiAct.DestinationIds), - RatingSubject: apiAct.RatingSubject, - SharedGroups: utils.ParseStringMap(apiAct.SharedGroups), + Balance: &engine.BalanceFilter{ // TODO: update this part + Uuid: utils.StringPointer(utils.GenUUID()), + ID: utils.StringPointer(apiAct.BalanceId), + Type: utils.StringPointer(apiAct.BalanceType), + Value: units, + Weight: weight, + Directions: utils.StringMapPointer(utils.ParseStringMap(apiAct.Directions)), + DestinationIDs: utils.StringMapPointer(utils.ParseStringMap(apiAct.DestinationIds)), + RatingSubject: utils.StringPointer(apiAct.RatingSubject), + SharedGroups: utils.StringMapPointer(utils.ParseStringMap(apiAct.SharedGroups)), }, } storeActions[idx] = a @@ -544,19 +562,25 @@ func (self *ApierV1) GetActions(actsId string, reply *[]*utils.TPAction) error { } for _, engAct := range engActs { act := &utils.TPAction{Identifier: engAct.ActionType, - BalanceType: engAct.BalanceType, ExpiryTime: engAct.ExpirationString, ExtraParameters: engAct.ExtraParameters, Filter: engAct.Filter, Weight: engAct.Weight, } - if engAct.Balance != nil { - act.Units = engAct.Balance.GetValue() - act.Directions = engAct.Balance.Directions.String() - act.DestinationIds = engAct.Balance.DestinationIds.String() - act.RatingSubject = engAct.Balance.RatingSubject - act.SharedGroups = engAct.Balance.SharedGroups.String() - act.BalanceWeight = engAct.Balance.Weight + bf := engAct.Balance + if bf != nil { + act.BalanceType = bf.GetType() + act.Units = strconv.FormatFloat(bf.GetValue(), 'f', -1, 64) + act.Directions = bf.GetDirections().String() + act.DestinationIds = bf.GetDestinationIDs().String() + act.RatingSubject = bf.GetRatingSubject() + act.SharedGroups = bf.GetSharedGroups().String() + act.BalanceWeight = strconv.FormatFloat(bf.GetWeight(), 'f', -1, 64) + act.TimingTags = bf.GetTimingIDs().String() + act.BalanceId = bf.GetID() + act.Categories = bf.GetCategories().String() + act.BalanceBlocker = strconv.FormatBool(bf.GetBlocker()) + act.BalanceDisabled = strconv.FormatBool(bf.GetDisabled()) } acts = append(acts, act) } @@ -620,7 +644,7 @@ func (self *ApierV1) SetActionPlan(attrs AttrSetActionPlan, reply *string) error ActionsID: apiAtm.ActionsId, }) } - if err := self.RatingDb.SetActionPlan(ap.Id, ap); err != nil { + if err := self.RatingDb.SetActionPlan(ap.Id, ap, true); err != nil { return utils.NewErrServerError(err) } self.RatingDb.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: []string{utils.ACTION_PLAN_PREFIX + attrs.Id}}) @@ -659,139 +683,6 @@ func (self *ApierV1) GetActionPlan(attr AttrGetActionPlan, reply *[]*engine.Acti return nil } -type AttrAddActionTrigger struct { - ActionTriggersId string - ActionTriggersUniqueId string - Tenant string - Account string - ThresholdType string - ThresholdValue float64 - BalanceId string - BalanceType string - BalanceDirection string - BalanceDestinationIds string - BalanceRatingSubject string //ToDo - BalanceWeight float64 - BalanceExpiryTime string - BalanceSharedGroup string //ToDo - Weight float64 - ActionsId string -} - -func (self *ApierV1) AddTriggeredAction(attr AttrAddActionTrigger, reply *string) error { - if attr.BalanceDirection == "" { - attr.BalanceDirection = utils.OUT - } - balExpiryTime, err := utils.ParseTimeDetectLayout(attr.BalanceExpiryTime, self.Config.DefaultTimezone) - if err != nil { - return utils.NewErrServerError(err) - } - at := &engine.ActionTrigger{ - ID: attr.ActionTriggersId, - UniqueID: attr.ActionTriggersUniqueId, - ThresholdType: attr.ThresholdType, - ThresholdValue: attr.ThresholdValue, - BalanceId: attr.BalanceId, - BalanceType: attr.BalanceType, - BalanceDirections: utils.ParseStringMap(attr.BalanceDirection), - BalanceDestinationIds: utils.ParseStringMap(attr.BalanceDestinationIds), - BalanceWeight: attr.BalanceWeight, - BalanceExpirationDate: balExpiryTime, - Weight: attr.Weight, - ActionsId: attr.ActionsId, - Executed: false, - } - - tag := utils.AccountKey(attr.Tenant, attr.Account) - _, err = engine.Guardian.Guard(func() (interface{}, error) { - userBalance, err := self.AccountDb.GetAccount(tag) - if err != nil { - return 0, err - } - - userBalance.ActionTriggers = append(userBalance.ActionTriggers, at) - - if err = self.AccountDb.SetAccount(userBalance); err != nil { - return 0, err - } - return 0, nil - }, 0, tag) - if err != nil { - *reply = err.Error() - return err - } - *reply = OK - return nil -} - -type AttrResetTriggeredAction struct { - Id string - Tenant string - Account string - Directions string - BalanceType string - ThresholdType string - ThresholdValue float64 - DestinationId string - BalanceWeight float64 - BalanceRatingSubject string - BalanceSharedGroup string -} - -func (self *ApierV1) ResetTriggeredActions(attr AttrResetTriggeredAction, reply *string) error { - var a *engine.Action - if attr.Id != "" { - // we can identify the trigger by the id - a = &engine.Action{Id: attr.Id} - } else { - extraParameters, err := json.Marshal(struct { - ThresholdType string - ThresholdValue float64 - DestinationId string - BalanceWeight float64 - BalanceRatingSubject string - BalanceDirections string - BalanceSharedGroup string - }{ - attr.ThresholdType, - attr.ThresholdValue, - attr.DestinationId, - attr.BalanceWeight, - attr.Directions, - attr.BalanceRatingSubject, - attr.BalanceSharedGroup, - }) - if err != nil { - *reply = err.Error() - return err - } - a = &engine.Action{ - BalanceType: attr.BalanceType, - ExtraParameters: string(extraParameters), - } - } - accID := utils.AccountKey(attr.Tenant, attr.Account) - _, err := engine.Guardian.Guard(func() (interface{}, error) { - acc, err := self.AccountDb.GetAccount(accID) - if err != nil { - return 0, err - } - - acc.ResetActionTriggers(a) - - if err = self.AccountDb.SetAccount(acc); err != nil { - return 0, err - } - return 0, nil - }, 0, accID) - if err != nil { - *reply = err.Error() - return err - } - *reply = OK - return nil -} - // Process dependencies and load a specific AccountActions profile from storDb into dataDb. func (self *ApierV1) LoadAccountActions(attrs utils.TPAccountActions, reply *string) error { if len(attrs.TPid) == 0 { diff --git a/apier/v1/apier_local_test.go b/apier/v1/apier_local_test.go index 36b4fc86e..47da716b5 100644 --- a/apier/v1/apier_local_test.go +++ b/apier/v1/apier_local_test.go @@ -52,7 +52,7 @@ README: * Flush tables in storDb to start clean. * Start engine with default configuration and give it some time to listen (here caching can slow down, hence the command argument parameter). * Connect rpc client depending on encoding defined in configuration. - * Execute remote Apis and test their replies(follow prepaid1cent scenario so we can test load in dataDb also). + * Execute remote Apis and test their replies(follow testtp scenario so we can test load in dataDb also). */ var cfgPath string @@ -488,8 +488,8 @@ func TestApierTPActions(t *testing.T) { } reply := "" act := &utils.TPActions{TPid: utils.TEST_SQL, ActionsId: "PREPAID_10", Actions: []*utils.TPAction{ - &utils.TPAction{Identifier: "*topup_reset", BalanceType: "*monetary", Directions: "*out", Units: 10, ExpiryTime: "*unlimited", - DestinationIds: "*any", BalanceWeight: 10, Weight: 10}, + &utils.TPAction{Identifier: "*topup_reset", BalanceType: "*monetary", Directions: "*out", Units: "10", ExpiryTime: "*unlimited", + DestinationIds: "*any", BalanceWeight: "10", Weight: 10}, }} actWarn := &utils.TPActions{TPid: utils.TEST_SQL, ActionsId: "WARN_VIA_HTTP", Actions: []*utils.TPAction{ &utils.TPAction{Identifier: "*call_url", ExtraParameters: "http://localhost:8000", Weight: 10}, @@ -955,7 +955,7 @@ func TestApierSetActions(t *testing.T) { if !*testLocal { return } - act1 := &utils.TPAction{Identifier: engine.TOPUP_RESET, BalanceType: utils.MONETARY, Directions: utils.OUT, Units: 75.0, ExpiryTime: engine.UNLIMITED, Weight: 20.0} + act1 := &utils.TPAction{Identifier: engine.TOPUP_RESET, BalanceType: utils.MONETARY, Directions: utils.OUT, Units: "75", ExpiryTime: engine.UNLIMITED, Weight: 20.0} attrs1 := &utils.AttrSetActions{ActionsId: "ACTS_1", Actions: []*utils.TPAction{act1}} reply1 := "" if err := rater.Call("ApierV1.SetActions", attrs1, &reply1); err != nil { @@ -974,13 +974,13 @@ func TestApierGetActions(t *testing.T) { return } expectActs := []*utils.TPAction{ - &utils.TPAction{Identifier: engine.TOPUP_RESET, BalanceType: utils.MONETARY, Directions: utils.OUT, Units: 75.0, ExpiryTime: engine.UNLIMITED, Weight: 20.0}} + &utils.TPAction{Identifier: engine.TOPUP_RESET, BalanceType: utils.MONETARY, Directions: utils.OUT, Units: "75", BalanceWeight: "0", BalanceBlocker: "false", BalanceDisabled: "false", ExpiryTime: engine.UNLIMITED, Weight: 20.0}} var reply []*utils.TPAction if err := rater.Call("ApierV1.GetActions", "ACTS_1", &reply); err != nil { t.Error("Got error on ApierV1.GetActions: ", err.Error()) } else if !reflect.DeepEqual(expectActs, reply) { - t.Errorf("Expected: %v, received: %v", expectActs, reply) + t.Errorf("Expected: %v, received: %v", utils.ToJSON(expectActs), utils.ToJSON(reply)) } } @@ -1009,20 +1009,19 @@ func TestApierAddTriggeredAction(t *testing.T) { } reply := "" // Add balance to a previously known account - attrs := &AttrAddActionTrigger{ActionTriggersId: "STTR", ActionTriggersUniqueId: "1", Tenant: "cgrates.org", Account: "dan2", BalanceDirection: "*out", BalanceType: "*monetary", - ThresholdType: "*min_balance", ThresholdValue: 2, BalanceDestinationIds: "*any", Weight: 10, ActionsId: "WARN_VIA_HTTP"} - if err := rater.Call("ApierV1.AddTriggeredAction", attrs, &reply); err != nil { - t.Error("Got error on ApierV1.AddTriggeredAction: ", err.Error()) + attrs := &AttrAddAccountActionTriggers{ActionTriggerIDs: &[]string{"STANDARD_TRIGGERS"}, Tenant: "cgrates.org", Account: "dan2"} + if err := rater.Call("ApierV1.AddAccountActionTriggers", attrs, &reply); err != nil { + t.Error("Got error on ApierV1.AddAccountActionTriggers: ", err.Error()) } else if reply != "OK" { - t.Errorf("Calling ApierV1.AddTriggeredAction received: %s", reply) + t.Errorf("Calling ApierV1.AddAccountActionTriggers received: %s", reply) } reply2 := "" - attrs2 := new(AttrAddActionTrigger) + attrs2 := new(AttrAddAccountActionTriggers) *attrs2 = *attrs attrs2.Account = "dan10" // Does not exist so it should error when adding triggers on it // Add trigger to an account which does n exist - if err := rater.Call("ApierV1.AddTriggeredAction", attrs2, &reply2); err == nil || reply2 == "OK" { - t.Error("Expecting error on ApierV1.AddTriggeredAction.", err, reply2) + if err := rater.Call("ApierV1.AddAccountActionTriggers", attrs2, &reply2); err == nil || reply2 == "OK" { + t.Error("Expecting error on ApierV1.AddAccountActionTriggers.", err, reply2) } } @@ -1035,11 +1034,46 @@ func TestApierGetAccountActionTriggers(t *testing.T) { req := AttrAcntAction{Tenant: "cgrates.org", Account: "dan2"} if err := rater.Call("ApierV1.GetAccountActionTriggers", req, &reply); err != nil { t.Error("Got error on ApierV1.GetAccountActionTimings: ", err.Error()) - } else if len(reply) != 1 || reply[0].ActionsId != "WARN_VIA_HTTP" { + } else if len(reply) != 1 || reply[0].ActionsID != "LOG_BALANCE" { t.Errorf("Unexpected action triggers received %v", reply) } } +// Test here SetAccountActionTriggers +func TestApierSetAccountActionTriggers(t *testing.T) { + if !*testLocal { + return + } + // Test first get so we can steal the id which we need to remove + var reply engine.ActionTriggers + req := AttrAcntAction{Tenant: "cgrates.org", Account: "dan2"} + if err := rater.Call("ApierV1.GetAccountActionTriggers", req, &reply); err != nil { + t.Error("Got error on ApierV1.GetAccountActionTimings: ", err.Error()) + } else if len(reply) != 1 || reply[0].ActionsID != "LOG_BALANCE" { + for _, atr := range reply { + t.Logf("ATR: %+v", atr) + } + t.Errorf("Unexpected action triggers received %v", reply) + } + var setReply string + setReq := AttrSetAccountActionTriggers{Tenant: "cgrates.org", Account: "dan2", UniqueID: reply[0].UniqueID, ActivationDate: utils.StringPointer("2016-02-05T18:00:00Z")} + if err := rater.Call("ApierV1.ResetAccountActionTriggers", setReq, &setReply); err != nil { + t.Error("Got error on ApierV1.ResetActionTiming: ", err.Error()) + } else if setReply != OK { + t.Error("Unexpected answer received", setReply) + } + if err := rater.Call("ApierV1.SetAccountActionTriggers", setReq, &setReply); err != nil { + t.Error("Got error on ApierV1.RemoveActionTiming: ", err.Error()) + } else if setReply != OK { + t.Error("Unexpected answer received", setReply) + } + if err := rater.Call("ApierV1.GetAccountActionTriggers", req, &reply); err != nil { + t.Error("Got error on ApierV1.GetAccountActionTriggers: ", err.Error()) + } else if len(reply) != 1 || reply[0].ActivationDate != time.Date(2016, 2, 5, 18, 0, 0, 0, time.UTC) { + t.Errorf("Unexpected action triggers received %+v", reply[0]) + } +} + // Test here RemAccountActionTriggers func TestApierRemAccountActionTriggers(t *testing.T) { if !*testLocal { @@ -1050,13 +1084,21 @@ func TestApierRemAccountActionTriggers(t *testing.T) { req := AttrAcntAction{Tenant: "cgrates.org", Account: "dan2"} if err := rater.Call("ApierV1.GetAccountActionTriggers", req, &reply); err != nil { t.Error("Got error on ApierV1.GetAccountActionTimings: ", err.Error()) - } else if len(reply) != 1 || reply[0].ActionsId != "WARN_VIA_HTTP" { + } else if len(reply) != 1 || reply[0].ActionsID != "LOG_BALANCE" { + for _, atr := range reply { + t.Logf("ATR: %+v", atr) + } t.Errorf("Unexpected action triggers received %v", reply) } var rmReply string - rmReq := AttrRemAcntActionTriggers{Tenant: "cgrates.org", Account: "dan2", ActionTriggersUniqueId: reply[0].UniqueID} - if err := rater.Call("ApierV1.RemAccountActionTriggers", rmReq, &rmReply); err != nil { - t.Error("Got error on ApierV1.RemActionTiming: ", err.Error()) + rmReq := AttrRemoveAccountActionTriggers{Tenant: "cgrates.org", Account: "dan2", UniqueID: reply[0].UniqueID} + if err := rater.Call("ApierV1.ResetAccountActionTriggers", rmReq, &rmReply); err != nil { + t.Error("Got error on ApierV1.ResetActionTiming: ", err.Error()) + } else if rmReply != OK { + t.Error("Unexpected answer received", rmReply) + } + if err := rater.Call("ApierV1.RemoveAccountActionTriggers", rmReq, &rmReply); err != nil { + t.Error("Got error on ApierV1.RemoveActionTiming: ", err.Error()) } else if rmReply != OK { t.Error("Unexpected answer received", rmReply) } @@ -1102,13 +1144,13 @@ func TestApierGetAccountActionPlan(t *testing.T) { t.Error("Unexpected action plan received") } else { if reply[0].ActionPlanId != "ATMS_1" { - t.Errorf("Unexpected ActionPlanId received") + t.Errorf("Unexpected ActionoveAccountPlanId received") } } } -// Test here RemActionTiming -func TestApierRemActionTiming(t *testing.T) { +// Test here RemoveActionTiming +func TestApierRemUniqueIDActionTiming(t *testing.T) { if !*testLocal { return } @@ -1226,7 +1268,7 @@ func TestApierLoadTariffPlanFromFolder(t *testing.T) { t.Error(err) } // Simple test that command is executed without errors - attrs = &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "prepaid1centpsec")} + attrs = &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "testtp")} if err := rater.Call("ApierV1.LoadTariffPlanFromFolder", attrs, &reply); err != nil { t.Error("Got error on ApierV1.LoadTariffPlanFromFolder: ", err.Error()) } else if reply != "OK" { @@ -1255,9 +1297,9 @@ func TestApierResetDataAfterLoadFromFolder(t *testing.T) { if rcvStats.Destinations != 4 || rcvStats.RatingPlans != 3 || rcvStats.RatingProfiles != 3 || - rcvStats.Actions != 5 || + rcvStats.Actions != 6 || rcvStats.DerivedChargers != 2 { - t.Errorf("Calling ApierV1.GetCacheStats received: %v", rcvStats) + t.Errorf("Calling ApierV1.GetCacheStats received: %+v", rcvStats) } } } diff --git a/apier/v1/cdrstatsv1.go b/apier/v1/cdrstatsv1.go index 9820cfa68..6e1f5122e 100644 --- a/apier/v1/cdrstatsv1.go +++ b/apier/v1/cdrstatsv1.go @@ -50,6 +50,14 @@ func (sts *CDRStatsV1) GetQueue(id string, sq *engine.StatsQueue) error { return sts.CdrStats.Call("CDRStatsV1.GetQueue", id, sq) } +func (sts *CDRStatsV1) AddQueue(cs *engine.CdrStats, reply *int) error { + return sts.CdrStats.Call("CDRStatsV1.AddQueue", cs, reply) +} + +func (sts *CDRStatsV1) RemoveQueue(qID string, reply *int) error { + return sts.CdrStats.Call("CDRStatsV1.RemoveQueue", qID, reply) +} + func (sts *CDRStatsV1) GetQueueTriggers(id string, ats *engine.ActionTriggers) error { return sts.CdrStats.Call("CDRStatsV1.GetQueueTriggers", id, ats) } diff --git a/apier/v1/cdrsv1.go b/apier/v1/cdrsv1.go index 21b24ffa0..f2b41d9b7 100644 --- a/apier/v1/cdrsv1.go +++ b/apier/v1/cdrsv1.go @@ -60,7 +60,7 @@ func (self *CdrsV1) RateCdrs(attrs utils.AttrRateCdrs, reply *string) error { } func (self *CdrsV1) LogCallCost(ccl *engine.CallCostLog, reply *string) error { - if err := self.CdrSrv.LocalLogCallCost(ccl); err != nil { + if err := self.CdrSrv.LogCallCost(ccl); err != nil { return utils.NewErrServerError(err) } *reply = utils.OK diff --git a/apier/v1/scheduler.go b/apier/v1/scheduler.go index a91701204..f8ebda45c 100644 --- a/apier/v1/scheduler.go +++ b/apier/v1/scheduler.go @@ -20,9 +20,12 @@ package v1 import ( "errors" + "fmt" + "sort" "strings" "time" + "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" ) @@ -168,3 +171,94 @@ func (self *ApierV1) GetScheduledActions(attrs AttrsGetScheduledActions, reply * *reply = schedActions return nil } + +type AttrsExecuteScheduledActions struct { + ActionPlanID string + TimeStart, TimeEnd time.Time // replay the action timings between the two dates +} + +func (self *ApierV1) ExecuteScheduledActions(attr AttrsExecuteScheduledActions, reply *string) error { + if attr.ActionPlanID != "" { // execute by ActionPlanID + apl, err := self.RatingDb.GetActionPlan(attr.ActionPlanID, false) + if err != nil { + *reply = err.Error() + return err + } + if apl != nil { + // order by weight + engine.ActionTimingWeightOnlyPriorityList(apl.ActionTimings).Sort() + for _, at := range apl.ActionTimings { + if at.IsASAP() { + continue + } + + at.SetAccountIDs(apl.AccountIDs) // copy the accounts + at.SetActionPlanID(apl.Id) + err := at.Execute() + if err != nil { + *reply = err.Error() + return err + } + utils.Logger.Info(fmt.Sprintf(" Executing action %s ", at.ActionsID)) + } + } + } + if !attr.TimeStart.IsZero() && !attr.TimeEnd.IsZero() { // execute between two dates + actionPlans, err := self.RatingDb.GetAllActionPlans() + if err != nil && err != utils.ErrNotFound { + err := fmt.Errorf("cannot get action plans: %v", err) + *reply = err.Error() + return err + } + + // recreate the queue + queue := engine.ActionTimingPriorityList{} + for _, actionPlan := range actionPlans { + for _, at := range actionPlan.ActionTimings { + if at.Timing == nil { + continue + } + if at.IsASAP() { + continue + } + if at.GetNextStartTime(attr.TimeStart).Before(attr.TimeStart) { + // the task is obsolete, do not add it to the queue + continue + } + at.SetAccountIDs(actionPlan.AccountIDs) // copy the accounts + at.SetActionPlanID(actionPlan.Id) + at.ResetStartTimeCache() + queue = append(queue, at) + } + } + sort.Sort(queue) + // start playback execution loop + current := attr.TimeStart + for len(queue) > 0 && current.Before(attr.TimeEnd) { + a0 := queue[0] + current = a0.GetNextStartTime(current) + if current.Before(attr.TimeEnd) || current.Equal(attr.TimeEnd) { + utils.Logger.Info(fmt.Sprintf(" Executing action %s for time %v", a0.ActionsID, current)) + err := a0.Execute() + if err != nil { + *reply = err.Error() + return err + } + // if after execute the next start time is in the past then + // do not add it to the queue + a0.ResetStartTimeCache() + current = current.Add(time.Second) + start := a0.GetNextStartTime(current) + if start.Before(current) || start.After(attr.TimeEnd) { + queue = queue[1:] + } else { + queue = append(queue, a0) + queue = queue[1:] + sort.Sort(queue) + } + } + } + } + *reply = utils.OK + return nil +} diff --git a/apier/v1/smgenericv1.go b/apier/v1/smgenericv1.go index d56c1d431..eed4310fb 100644 --- a/apier/v1/smgenericv1.go +++ b/apier/v1/smgenericv1.go @@ -89,6 +89,45 @@ func (self *SMGenericV1) ProcessCdr(ev sessionmanager.SMGenericEvent, reply *str return nil } +func (self *SMGenericV1) ActiveSessions(attrs utils.AttrSMGGetActiveSessions, reply *[]*sessionmanager.ActiveSession) error { + aSessions := make([]*sessionmanager.ActiveSession, 0) + for _, sGrp := range self.sm.Sessions() { // Add sessions to return with filter + for _, s := range sGrp { + as := s.AsActiveSession(self.sm.Timezone()) + if attrs.ToR != nil && *attrs.ToR != as.TOR { + continue + } + if attrs.RunID != nil && *attrs.RunID != as.RunId { + continue + } + if attrs.RequestType != nil && *attrs.RequestType != as.ReqType { + continue + } + if attrs.Tenant != nil && *attrs.Tenant != as.Tenant { + continue + } + if attrs.Category != nil && *attrs.Category != as.Category { + continue + } + if attrs.Account != nil && *attrs.Account != as.Account { + continue + } + if attrs.Subject != nil && *attrs.Subject != as.Subject { + continue + } + if attrs.Destination != nil && *attrs.Destination != as.Destination { + continue + } + if attrs.Supplier != nil && *attrs.Supplier != as.Supplier { + continue + } + aSessions = append(aSessions, as) + } + } + *reply = aSessions + return nil +} + // rpcclient.RpcClientConnection interface func (self *SMGenericV1) Call(serviceMethod string, args interface{}, reply interface{}) error { switch serviceMethod { diff --git a/apier/v1/smgenericv1_it_test.go b/apier/v1/smgenericv1_it_test.go index c1a8cda8b..6afd87eb4 100644 --- a/apier/v1/smgenericv1_it_test.go +++ b/apier/v1/smgenericv1_it_test.go @@ -116,7 +116,7 @@ func TestSMGV1CacheStats(t *testing.T) { } var rcvStats *utils.CacheStats - expectedStats := &utils.CacheStats{Destinations: 4, RatingPlans: 3, RatingProfiles: 8, Actions: 7, ActionPlans: 4, SharedGroups: 1, Aliases: 1, + expectedStats := &utils.CacheStats{Destinations: 4, RatingPlans: 4, RatingProfiles: 9, Actions: 7, ActionPlans: 4, SharedGroups: 1, Aliases: 1, DerivedChargers: 1, LcrProfiles: 5, CdrStats: 6, Users: 3, LastLoadId: smgV1LoadInst.LoadId, LastLoadTime: smgV1LoadInst.LoadTime.Format(time.RFC3339)} var args utils.AttrCacheStats if err := smgV1Rpc.Call("ApierV2.GetCacheStats", args, &rcvStats); err != nil { @@ -151,7 +151,7 @@ func TestSMGV1GetMaxUsage(t *testing.T) { var maxTime float64 if err := smgV1Rpc.Call("SMGenericV1.GetMaxUsage", setupReq, &maxTime); err != nil { t.Error(err) - } else if maxTime != 2690 { + } else if maxTime != 2700 { t.Errorf("Calling ApierV2.MaxUsage got maxTime: %f", maxTime) } } diff --git a/apier/v1/triggers.go b/apier/v1/triggers.go new file mode 100644 index 000000000..642c137c7 --- /dev/null +++ b/apier/v1/triggers.go @@ -0,0 +1,302 @@ +package v1 + +import ( + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + +// Returns a list of ActionTriggers on an account +func (self *ApierV1) GetAccountActionTriggers(attrs AttrAcntAction, reply *engine.ActionTriggers) error { + if missing := utils.MissingStructFields(&attrs, []string{"Tenant", "Account"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + if account, err := self.AccountDb.GetAccount(utils.AccountKey(attrs.Tenant, attrs.Account)); err != nil { + return utils.NewErrServerError(err) + } else { + ats := account.ActionTriggers + if ats == nil { + ats = engine.ActionTriggers{} + } + *reply = ats + } + return nil +} + +type AttrAddAccountActionTriggers struct { + Tenant string + Account string + ActionTriggerIDs *[]string + ActionTriggerOverwrite bool + ActivationDate string +} + +func (self *ApierV1) AddAccountActionTriggers(attr AttrAddAccountActionTriggers, reply *string) error { + if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + actTime, err := utils.ParseTimeDetectLayout(attr.ActivationDate, self.Config.DefaultTimezone) + if err != nil { + *reply = err.Error() + return err + } + accID := utils.AccountKey(attr.Tenant, attr.Account) + var account *engine.Account + _, err = engine.Guardian.Guard(func() (interface{}, error) { + if acc, err := self.AccountDb.GetAccount(accID); err == nil { + account = acc + } else { + return 0, err + } + if attr.ActionTriggerIDs != nil { + if attr.ActionTriggerOverwrite { + account.ActionTriggers = make(engine.ActionTriggers, 0) + } + for _, actionTriggerID := range *attr.ActionTriggerIDs { + atrs, err := self.RatingDb.GetActionTriggers(actionTriggerID) + if err != nil { + + return 0, err + } + for _, at := range atrs { + var found bool + for _, existingAt := range account.ActionTriggers { + if existingAt.Equals(at) { + found = true + break + } + } + at.ActivationDate = actTime + if !found { + account.ActionTriggers = append(account.ActionTriggers, at) + } + } + } + } + account.InitCounters() + if err := self.AccountDb.SetAccount(account); err != nil { + return 0, err + } + return 0, nil + }, 0, accID) + if err != nil { + *reply = err.Error() + return err + } + *reply = utils.OK + return nil +} + +type AttrRemoveAccountActionTriggers struct { + Tenant string + Account string + GroupID string + UniqueID string +} + +func (self *ApierV1) RemoveAccountActionTriggers(attr AttrRemoveAccountActionTriggers, reply *string) error { + if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + accID := utils.AccountKey(attr.Tenant, attr.Account) + _, err := engine.Guardian.Guard(func() (interface{}, error) { + var account *engine.Account + if acc, err := self.AccountDb.GetAccount(accID); err == nil { + account = acc + } else { + return 0, err + } + var newActionTriggers engine.ActionTriggers + for _, at := range account.ActionTriggers { + if (attr.UniqueID == "" || at.UniqueID == attr.UniqueID) && + (attr.GroupID == "" || at.ID == attr.GroupID) { + // remove action trigger + continue + } + newActionTriggers = append(newActionTriggers, at) + } + account.ActionTriggers = newActionTriggers + account.InitCounters() + if err := self.AccountDb.SetAccount(account); err != nil { + return 0, err + } + return 0, nil + }, 0, accID) + if err != nil { + *reply = err.Error() + return err + } + *reply = utils.OK + return nil +} + +func (self *ApierV1) ResetAccountActionTriggers(attr AttrRemoveAccountActionTriggers, reply *string) error { + + if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + accID := utils.AccountKey(attr.Tenant, attr.Account) + var account *engine.Account + _, err := engine.Guardian.Guard(func() (interface{}, error) { + if acc, err := self.AccountDb.GetAccount(accID); err == nil { + account = acc + } else { + return 0, err + } + for _, at := range account.ActionTriggers { + if (attr.UniqueID == "" || at.UniqueID == attr.UniqueID) && + (attr.GroupID == "" || at.ID == attr.GroupID) { + // reset action trigger + at.Executed = false + } + + } + account.ExecuteActionTriggers(nil) + if err := self.AccountDb.SetAccount(account); err != nil { + return 0, err + } + return 0, nil + }, 0, accID) + if err != nil { + *reply = err.Error() + return err + } + *reply = utils.OK + return nil +} + +type AttrSetAccountActionTriggers struct { + Tenant string + Account string + GroupID string + UniqueID string + ThresholdType *string + ThresholdValue *float64 + Recurrent *bool + MinSleep *string + ExpirationDate *string + ActivationDate *string + BalanceId *string + BalanceType *string + BalanceDirections *[]string + BalanceDestinationIds *[]string + BalanceWeight *float64 + BalanceExpirationDate *string + BalanceTimingTags *[]string + BalanceRatingSubject *string + BalanceCategories *[]string + BalanceSharedGroups *[]string + BalanceBlocker *bool + BalanceDisabled *bool + MinQueuedItems *int + ActionsId *string +} + +func (self *ApierV1) SetAccountActionTriggers(attr AttrSetAccountActionTriggers, reply *string) error { + + if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + accID := utils.AccountKey(attr.Tenant, attr.Account) + var account *engine.Account + _, err := engine.Guardian.Guard(func() (interface{}, error) { + if acc, err := self.AccountDb.GetAccount(accID); err == nil { + account = acc + } else { + return 0, err + } + for _, at := range account.ActionTriggers { + if (attr.UniqueID == "" || at.UniqueID == attr.UniqueID) && + (attr.GroupID == "" || at.ID == attr.GroupID) { + // we have a winner + if attr.ThresholdType != nil { + at.ThresholdType = *attr.ThresholdType + } + if attr.ThresholdValue != nil { + at.ThresholdValue = *attr.ThresholdValue + } + if attr.Recurrent != nil { + at.Recurrent = *attr.Recurrent + } + if attr.MinSleep != nil { + minSleep, err := utils.ParseDurationWithSecs(*attr.MinSleep) + if err != nil { + return 0, err + } + at.MinSleep = minSleep + } + if attr.ExpirationDate != nil { + expTime, err := utils.ParseTimeDetectLayout(*attr.ExpirationDate, self.Config.DefaultTimezone) + if err != nil { + return 0, err + } + at.ExpirationDate = expTime + } + if attr.ActivationDate != nil { + actTime, err := utils.ParseTimeDetectLayout(*attr.ActivationDate, self.Config.DefaultTimezone) + if err != nil { + return 0, err + } + at.ActivationDate = actTime + } + if attr.BalanceId != nil { + at.Balance.ID = attr.BalanceId + } + if attr.BalanceType != nil { + at.Balance.Type = attr.BalanceType + } + if attr.BalanceDirections != nil { + at.Balance.Directions = utils.StringMapPointer(utils.NewStringMap(*attr.BalanceDirections...)) + } + if attr.BalanceDestinationIds != nil { + at.Balance.DestinationIDs = utils.StringMapPointer(utils.NewStringMap(*attr.BalanceDestinationIds...)) + } + if attr.BalanceWeight != nil { + at.Balance.Weight = attr.BalanceWeight + } + if attr.BalanceExpirationDate != nil { + balanceExpTime, err := utils.ParseDate(*attr.BalanceExpirationDate) + if err != nil { + return 0, err + } + at.Balance.ExpirationDate = &balanceExpTime + } + if attr.BalanceTimingTags != nil { + at.Balance.TimingIDs = utils.StringMapPointer(utils.NewStringMap(*attr.BalanceTimingTags...)) + } + if attr.BalanceRatingSubject != nil { + at.Balance.RatingSubject = attr.BalanceRatingSubject + } + if attr.BalanceCategories != nil { + at.Balance.Categories = utils.StringMapPointer(utils.NewStringMap(*attr.BalanceCategories...)) + } + if attr.BalanceSharedGroups != nil { + at.Balance.SharedGroups = utils.StringMapPointer(utils.NewStringMap(*attr.BalanceSharedGroups...)) + } + if attr.BalanceBlocker != nil { + at.Balance.Blocker = attr.BalanceBlocker + } + if attr.BalanceDisabled != nil { + at.Balance.Disabled = attr.BalanceDisabled + } + if attr.MinQueuedItems != nil { + at.MinQueuedItems = *attr.MinQueuedItems + } + if attr.ActionsId != nil { + at.ActionsID = *attr.ActionsId + } + } + + } + account.ExecuteActionTriggers(nil) + if err := self.AccountDb.SetAccount(account); err != nil { + return 0, err + } + return 0, nil + }, 0, accID) + if err != nil { + *reply = err.Error() + return err + } + *reply = utils.OK + return nil +} diff --git a/apier/v2/accounts.go b/apier/v2/accounts.go index d1c49b5fd..c3c495edd 100644 --- a/apier/v2/accounts.go +++ b/apier/v2/accounts.go @@ -47,6 +47,12 @@ func (self *ApierV2) GetAccounts(attr utils.AttrGetAccounts, reply *[]*engine.Ac if len(accountKeys) == 0 { return nil } + if attr.Offset > len(accountKeys) { + attr.Offset = len(accountKeys) + } + if attr.Offset < 0 { + attr.Offset = 0 + } var limitedAccounts []string if attr.Limit != 0 { max := math.Min(float64(attr.Offset+attr.Limit), float64(len(accountKeys))) @@ -79,62 +85,88 @@ func (self *ApierV2) GetAccount(attr *utils.AttrGetAccount, reply *engine.Accoun } type AttrSetAccount struct { - Tenant string - Account string - ActionPlanId string - ActionTriggersId string - AllowNegative *bool - Disabled *bool - ReloadScheduler bool + Tenant string + Account string + ActionPlanIDs *[]string + ActionPlansOverwrite bool + ActionTriggerIDs *[]string + ActionTriggerOverwrite bool + AllowNegative *bool + Disabled *bool + ReloadScheduler bool } func (self *ApierV2) SetAccount(attr AttrSetAccount, reply *string) error { if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { return utils.NewErrMandatoryIeMissing(missing...) } - var schedulerReloadNeeded = false accID := utils.AccountKey(attr.Tenant, attr.Account) + dirtyActionPlans := make(map[string]*engine.ActionPlan) var ub *engine.Account _, err := engine.Guardian.Guard(func() (interface{}, error) { if bal, _ := self.AccountDb.GetAccount(accID); bal != nil { ub = bal } else { // Not found in db, create it here ub = &engine.Account{ - Id: accID, + ID: accID, } } - if len(attr.ActionPlanId) != 0 { + if attr.ActionPlanIDs != nil { _, err := engine.Guardian.Guard(func() (interface{}, error) { - var ap *engine.ActionPlan - var err error - ap, err = self.RatingDb.GetActionPlan(attr.ActionPlanId, false) + actionPlansMap, err := self.RatingDb.GetAllActionPlans() if err != nil { + if err == utils.ErrNotFound { // if no action plans just continue + return 0, nil + } return 0, err } - if _, exists := ap.AccountIDs[accID]; !exists { - if ap.AccountIDs == nil { - ap.AccountIDs = make(map[string]struct{}) + if attr.ActionPlansOverwrite { + // clean previous action plans + for actionPlanID, ap := range actionPlansMap { + if _, exists := ap.AccountIDs[accID]; exists { + delete(ap.AccountIDs, accID) + dirtyActionPlans[actionPlanID] = ap + } } - ap.AccountIDs[accID] = struct{}{} - schedulerReloadNeeded = true - // create tasks - for _, at := range ap.ActionTimings { - if at.IsASAP() { - t := &engine.Task{ - Uuid: utils.GenUUID(), - AccountID: accID, - ActionsID: at.ActionsID, - } - if err = self.RatingDb.PushTask(t); err != nil { - return 0, err + } + + for _, actionPlanID := range *attr.ActionPlanIDs { + ap, ok := actionPlansMap[actionPlanID] + if !ok { + return 0, utils.ErrNotFound + } + + if _, exists := ap.AccountIDs[accID]; !exists { + if ap.AccountIDs == nil { + ap.AccountIDs = make(utils.StringMap) + } + ap.AccountIDs[accID] = true + dirtyActionPlans[actionPlanID] = ap + // create tasks + for _, at := range ap.ActionTimings { + if at.IsASAP() { + t := &engine.Task{ + Uuid: utils.GenUUID(), + AccountID: accID, + ActionsID: at.ActionsID, + } + if err = self.RatingDb.PushTask(t); err != nil { + return 0, err + } } } } - if err := self.RatingDb.SetActionPlan(attr.ActionPlanId, ap); err != nil { + } + var actionPlansCacheIds []string + for actionPlanID, ap := range dirtyActionPlans { + if err := self.RatingDb.SetActionPlan(actionPlanID, ap, true); err != nil { return 0, err } + actionPlansCacheIds = append(actionPlansCacheIds, utils.ACTION_PLAN_PREFIX+actionPlanID) + } + if len(actionPlansCacheIds) > 0 { // update cache - self.RatingDb.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: []string{utils.ACTION_PLAN_PREFIX + attr.ActionPlanId}}) + self.RatingDb.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: actionPlansCacheIds}) } return 0, nil }, 0, utils.ACTION_PLAN_PREFIX) @@ -143,14 +175,30 @@ func (self *ApierV2) SetAccount(attr AttrSetAccount, reply *string) error { } } - if len(attr.ActionTriggersId) != 0 { - atrs, err := self.RatingDb.GetActionTriggers(attr.ActionTriggersId) - if err != nil { - return 0, err + if attr.ActionTriggerIDs != nil { + if attr.ActionTriggerOverwrite { + ub.ActionTriggers = make(engine.ActionTriggers, 0) + } + for _, actionTriggerID := range *attr.ActionTriggerIDs { + atrs, err := self.RatingDb.GetActionTriggers(actionTriggerID) + if err != nil { + return 0, err + } + for _, at := range atrs { + var found bool + for _, existingAt := range ub.ActionTriggers { + if existingAt.Equals(at) { + found = true + break + } + } + if !found { + ub.ActionTriggers = append(ub.ActionTriggers, at) + } + } } - ub.ActionTriggers = atrs - ub.InitCounters() } + ub.InitCounters() if attr.AllowNegative != nil { ub.AllowNegative = *attr.AllowNegative } @@ -166,7 +214,7 @@ func (self *ApierV2) SetAccount(attr AttrSetAccount, reply *string) error { if err != nil { return utils.NewErrServerError(err) } - if attr.ReloadScheduler && schedulerReloadNeeded { + if attr.ReloadScheduler && len(dirtyActionPlans) > 0 { // reload scheduler if self.Sched != nil { self.Sched.Reload(true) diff --git a/cmd/cgr-engine/cgr-engine.go b/cmd/cgr-engine/cgr-engine.go index 49ec1aed5..a50683d99 100644 --- a/cmd/cgr-engine/cgr-engine.go +++ b/cmd/cgr-engine/cgr-engine.go @@ -71,7 +71,7 @@ var ( err error ) -func startCdrcs(internalCdrSChan chan *engine.CdrServer, internalRaterChan chan *engine.Responder, exitChan chan bool) { +func startCdrcs(internalCdrSChan, internalRaterChan chan rpcclient.RpcClientConnection, exitChan chan bool) { cdrcInitialized := false // Control whether the cdrc was already initialized (so we don't reload in that case) var cdrcChildrenChan chan struct{} // Will use it to communicate with the children of one fork for { @@ -102,7 +102,7 @@ func startCdrcs(internalCdrSChan chan *engine.CdrServer, internalRaterChan chan } // Fires up a cdrc instance -func startCdrc(internalCdrSChan chan *engine.CdrServer, internalRaterChan chan *engine.Responder, cdrcCfgs map[string]*config.CdrcConfig, httpSkipTlsCheck bool, +func startCdrc(internalCdrSChan, internalRaterChan chan rpcclient.RpcClientConnection, cdrcCfgs map[string]*config.CdrcConfig, httpSkipTlsCheck bool, closeChan chan struct{}, exitChan chan bool) { var cdrsConn rpcclient.RpcClientConnection var cdrcCfg *config.CdrcConfig @@ -136,50 +136,29 @@ func startCdrc(internalCdrSChan chan *engine.CdrServer, internalRaterChan chan * } } -func startSmGeneric(internalSMGChan chan rpcclient.RpcClientConnection, internalRaterChan chan *engine.Responder, internalCDRSChan chan *engine.CdrServer, server *utils.Server, exitChan chan bool) { +func startSmGeneric(internalSMGChan chan rpcclient.RpcClientConnection, internalRaterChan, internalCDRSChan chan rpcclient.RpcClientConnection, server *utils.Server, exitChan chan bool) { utils.Logger.Info("Starting CGRateS SM-Generic service.") - raterConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - cdrsConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - var client *rpcclient.RpcClient - var err error - // Connect to rater - for _, raterCfg := range cfg.SmGenericConfig.RaterConns { - if raterCfg.Server == utils.INTERNAL { - resp := <-internalRaterChan - raterConn.AddClient(resp) - internalRaterChan <- resp - } else { - client, err = rpcclient.NewRpcClient("tcp", raterCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { //Connected so no need to reiterate - utils.Logger.Crit(fmt.Sprintf(" Could not connect to Rater via RPC: %v", err)) - exitChan <- true - return - } - raterConn.AddClient(client) - } + ralConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.SmGenericConfig.RaterConns, internalRaterChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } - // Connect to CDRS - if reflect.DeepEqual(cfg.SmGenericConfig.CdrsConns, cfg.SmGenericConfig.RaterConns) { - cdrsConn = raterConn - } else if len(cfg.SmGenericConfig.CdrsConns) != 0 { - for _, cdrsCfg := range cfg.SmGenericConfig.CdrsConns { - if cdrsCfg.Server == utils.INTERNAL { - resp := <-internalCDRSChan - cdrsConn.AddClient(resp) - internalCDRSChan <- resp - } else { - client, err = rpcclient.NewRpcClient("tcp", cdrsCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to CDRS via RPC: %v", err)) - exitChan <- true - return - } - cdrsConn.AddClient(client) - } + var cdrsConn *rpcclient.RpcClientPool + if reflect.DeepEqual(cfg.SmGenericConfig.RaterConns, cfg.SmGenericConfig.CdrsConns) { + cdrsConn = ralConn + } else { + cdrsConn, err = engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.SmGenericConfig.CdrsConns, internalCDRSChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } } smg_econns := sessionmanager.NewSMGExternalConnections() - sm := sessionmanager.NewSMGeneric(cfg, raterConn, cdrsConn, cfg.DefaultTimezone, smg_econns) + sm := sessionmanager.NewSMGeneric(cfg, ralConn, cdrsConn, cfg.DefaultTimezone, smg_econns) if err = sm.Connect(); err != nil { utils.Logger.Err(fmt.Sprintf(" error: %s!", err)) } @@ -197,28 +176,23 @@ func startSmGeneric(internalSMGChan chan rpcclient.RpcClientConnection, internal server.BijsonRegisterOnDisconnect(smg_econns.OnClientDisconnect) } -func startDiameterAgent(internalSMGChan chan rpcclient.RpcClientConnection, exitChan chan bool) { +func startDiameterAgent(internalSMGChan, internalPubSubSChan chan rpcclient.RpcClientConnection, exitChan chan bool) { utils.Logger.Info("Starting CGRateS DiameterAgent service.") - smgConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - var client *rpcclient.RpcClient - var err error - for _, smgCfg := range cfg.DiameterAgentCfg().SMGenericConns { - if smgCfg.Server == utils.INTERNAL { - smgRpc := <-internalSMGChan - internalSMGChan <- smgRpc - client, _ = rpcclient.NewRpcClient("", "", 0, 0, rpcclient.INTERNAL_RPC, smgRpc) - smgConn.AddClient(client) - } else { - client, err = rpcclient.NewRpcClient("tcp", smgCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to SMG: %s", err.Error())) - exitChan <- true - return - } - smgConn.AddClient(client) - } + smgConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.DiameterAgentCfg().SMGenericConns, internalSMGChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to SMG: %s", err.Error())) + exitChan <- true + return } - da, err := agents.NewDiameterAgent(cfg, smgConn) + pubsubConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.DiameterAgentCfg().PubSubConns, internalPubSubSChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to PubSubS: %s", err.Error())) + exitChan <- true + return + } + da, err := agents.NewDiameterAgent(cfg, smgConn, pubsubConn) if err != nil { utils.Logger.Err(fmt.Sprintf(" error: %s!", err)) exitChan <- true @@ -230,47 +204,23 @@ func startDiameterAgent(internalSMGChan chan rpcclient.RpcClientConnection, exit exitChan <- true } -func startSmFreeSWITCH(internalRaterChan chan *engine.Responder, internalCDRSChan chan *engine.CdrServer, cdrDb engine.CdrStorage, exitChan chan bool) { +func startSmFreeSWITCH(internalRaterChan, internalCDRSChan chan rpcclient.RpcClientConnection, cdrDb engine.CdrStorage, exitChan chan bool) { utils.Logger.Info("Starting CGRateS SM-FreeSWITCH service.") - raterConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - cdrsConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - var client *rpcclient.RpcClient - var err error - // Connect to rater - for _, raterCfg := range cfg.SmFsConfig.RaterConns { - if raterCfg.Server == utils.INTERNAL { - resp := <-internalRaterChan - raterConn.AddClient(resp) - internalRaterChan <- resp - } else { - client, err = rpcclient.NewRpcClient("tcp", raterCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { //Connected so no need to reiterate - utils.Logger.Crit(fmt.Sprintf(" Could not connect to rater via RPC: %v", err)) - exitChan <- true - return - } - raterConn.AddClient(client) - } + ralConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.SmFsConfig.RaterConns, internalRaterChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } - // Connect to CDRS - if len(cfg.SmFsConfig.CdrsConns) != 0 { - for _, cdrsCfg := range cfg.SmFsConfig.CdrsConns { - if cdrsCfg.Server == utils.INTERNAL { - resp := <-internalCDRSChan - cdrsConn.AddClient(resp) - internalCDRSChan <- resp - } else { - client, err = rpcclient.NewRpcClient("tcp", cdrsCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to CDRS via RPC: %v", err)) - exitChan <- true - return - } - cdrsConn.AddClient(client) - } - } + cdrsConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.SmFsConfig.CdrsConns, internalRaterChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } - sm := sessionmanager.NewFSSessionManager(cfg.SmFsConfig, raterConn, cdrsConn, cfg.DefaultTimezone) + sm := sessionmanager.NewFSSessionManager(cfg.SmFsConfig, ralConn, cdrsConn, cfg.DefaultTimezone) smRpc.SMs = append(smRpc.SMs, sm) if err = sm.Connect(); err != nil { utils.Logger.Err(fmt.Sprintf(" error: %s!", err)) @@ -278,49 +228,23 @@ func startSmFreeSWITCH(internalRaterChan chan *engine.Responder, internalCDRSCha exitChan <- true } -func startSmKamailio(internalRaterChan chan *engine.Responder, internalCDRSChan chan *engine.CdrServer, cdrDb engine.CdrStorage, exitChan chan bool) { +func startSmKamailio(internalRaterChan, internalCDRSChan chan rpcclient.RpcClientConnection, cdrDb engine.CdrStorage, exitChan chan bool) { utils.Logger.Info("Starting CGRateS SM-Kamailio service.") - raterConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - cdrsConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - var client *rpcclient.RpcClient - var err error - // Connect to rater - for _, raterCfg := range cfg.SmKamConfig.RaterConns { - if raterCfg.Server == utils.INTERNAL { - resp := <-internalRaterChan - raterConn.AddClient(resp) - internalRaterChan <- resp - } else { - client, err = rpcclient.NewRpcClient("tcp", raterCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { //Connected so no need to reiterate - utils.Logger.Crit(fmt.Sprintf(" Could not connect to rater via RPC: %v", err)) - exitChan <- true - return - } - raterConn.AddClient(client) - } + ralConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.SmKamConfig.RaterConns, internalRaterChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } - // Connect to CDRS - if reflect.DeepEqual(cfg.SmKamConfig.CdrsConns, cfg.SmKamConfig.RaterConns) { - cdrsConn = raterConn - } else if len(cfg.SmKamConfig.CdrsConns) != 0 { - for _, cdrsCfg := range cfg.SmKamConfig.CdrsConns { - if cdrsCfg.Server == utils.INTERNAL { - resp := <-internalCDRSChan - cdrsConn.AddClient(resp) - internalCDRSChan <- resp - } else { - client, err = rpcclient.NewRpcClient("tcp", cdrsCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to CDRS via RPC: %v", err)) - exitChan <- true - return - } - cdrsConn.AddClient(client) - } - } + cdrsConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.SmKamConfig.CdrsConns, internalRaterChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } - sm, _ := sessionmanager.NewKamailioSessionManager(cfg.SmKamConfig, raterConn, cdrsConn, cfg.DefaultTimezone) + sm, _ := sessionmanager.NewKamailioSessionManager(cfg.SmKamConfig, ralConn, cdrsConn, cfg.DefaultTimezone) smRpc.SMs = append(smRpc.SMs, sm) if err = sm.Connect(); err != nil { utils.Logger.Err(fmt.Sprintf(" error: %s!", err)) @@ -328,49 +252,23 @@ func startSmKamailio(internalRaterChan chan *engine.Responder, internalCDRSChan exitChan <- true } -func startSmOpenSIPS(internalRaterChan chan *engine.Responder, internalCDRSChan chan *engine.CdrServer, cdrDb engine.CdrStorage, exitChan chan bool) { +func startSmOpenSIPS(internalRaterChan, internalCDRSChan chan rpcclient.RpcClientConnection, cdrDb engine.CdrStorage, exitChan chan bool) { utils.Logger.Info("Starting CGRateS SM-OpenSIPS service.") - raterConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - cdrsConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - var client *rpcclient.RpcClient - var err error - // Connect to rater - for _, raterCfg := range cfg.SmOsipsConfig.RaterConns { - if raterCfg.Server == utils.INTERNAL { - resp := <-internalRaterChan - raterConn.AddClient(resp) - internalRaterChan <- resp - } else { - client, err = rpcclient.NewRpcClient("tcp", raterCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { //Connected so no need to reiterate - utils.Logger.Crit(fmt.Sprintf(" Could not connect to rater via RPC: %v", err)) - exitChan <- true - return - } - raterConn.AddClient(client) - } + ralConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.SmOsipsConfig.RaterConns, internalRaterChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } - // Connect to CDRS - if reflect.DeepEqual(cfg.SmOsipsConfig.CdrsConns, cfg.SmOsipsConfig.RaterConns) { - cdrsConn = raterConn - } else if len(cfg.SmOsipsConfig.CdrsConns) != 0 { - for _, cdrsCfg := range cfg.SmOsipsConfig.CdrsConns { - if cdrsCfg.Server == utils.INTERNAL { - resp := <-internalCDRSChan - cdrsConn.AddClient(resp) - internalCDRSChan <- resp - } else { - client, err = rpcclient.NewRpcClient("tcp", cdrsCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to CDRS via RPC: %v", err)) - exitChan <- true - return - } - cdrsConn.AddClient(client) - } - } + cdrsConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.SmOsipsConfig.CdrsConns, internalRaterChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } - sm, _ := sessionmanager.NewOSipsSessionManager(cfg.SmOsipsConfig, cfg.Reconnects, raterConn, cdrsConn, cfg.DefaultTimezone) + sm, _ := sessionmanager.NewOSipsSessionManager(cfg.SmOsipsConfig, cfg.Reconnects, ralConn, cdrsConn, cfg.DefaultTimezone) smRpc.SMs = append(smRpc.SMs, sm) if err := sm.Connect(); err != nil { utils.Logger.Err(fmt.Sprintf(" error: %s!", err)) @@ -378,92 +276,53 @@ func startSmOpenSIPS(internalRaterChan chan *engine.Responder, internalCDRSChan exitChan <- true } -func startCDRS(internalCdrSChan chan *engine.CdrServer, logDb engine.LogStorage, cdrDb engine.CdrStorage, - internalRaterChan chan *engine.Responder, internalPubSubSChan chan rpcclient.RpcClientConnection, +func startCDRS(internalCdrSChan chan rpcclient.RpcClientConnection, logDb engine.LogStorage, cdrDb engine.CdrStorage, + internalRaterChan chan rpcclient.RpcClientConnection, internalPubSubSChan chan rpcclient.RpcClientConnection, internalUserSChan chan rpcclient.RpcClientConnection, internalAliaseSChan chan rpcclient.RpcClientConnection, internalCdrStatSChan chan rpcclient.RpcClientConnection, server *utils.Server, exitChan chan bool) { utils.Logger.Info("Starting CGRateS CDRS service.") - var err error - var client *rpcclient.RpcClient - // Rater connection init - raterConn := rpcclient.NewRpcClientPool(rpcclient.POOL_FIRST) - for _, raterCfg := range cfg.CDRSRaterConns { - if raterCfg.Server == utils.INTERNAL { - responder := <-internalRaterChan // Wait for rater to come up before start querying - raterConn.AddClient(responder) - internalRaterChan <- responder // Put back the connection since there might be other entities waiting for it - } else if len(raterCfg.Server) != 0 { - client, err = rpcclient.NewRpcClient("tcp", raterCfg.Server, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to rater: %s", err.Error())) - exitChan <- true - return - } - raterConn.AddClient(client) - } + // Conn pool towards RAL + ralConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.CDRSRaterConns, internalRaterChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to RAL: %s", err.Error())) + exitChan <- true + return } // Pubsub connection init - var pubSubConn rpcclient.RpcClientConnection - if cfg.CDRSPubSub == utils.INTERNAL { - pubSubs := <-internalPubSubSChan - pubSubConn = pubSubs - internalPubSubSChan <- pubSubs - } else if len(cfg.CDRSPubSub) != 0 { - client, err = rpcclient.NewRpcClient("tcp", cfg.CDRSPubSub, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to pubsub server: %s", err.Error())) - exitChan <- true - return - } - pubSubConn = client + pubSubConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.CDRSPubSubSConns, internalPubSubSChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to PubSubSystem: %s", err.Error())) + exitChan <- true + return } // Users connection init - var usersConn rpcclient.RpcClientConnection - if cfg.CDRSUsers == utils.INTERNAL { - userS := <-internalUserSChan - usersConn = userS - internalUserSChan <- userS - } else if len(cfg.CDRSUsers) != 0 { - client, err = rpcclient.NewRpcClient("tcp", cfg.CDRSUsers, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to users server: %s", err.Error())) - exitChan <- true - return - } - usersConn = client + usersConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.CDRSUserSConns, internalUserSChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to UserS: %s", err.Error())) + exitChan <- true + return } // Aliases connection init - var aliasesConn rpcclient.RpcClientConnection - if cfg.CDRSAliases == utils.INTERNAL { - aliaseS := <-internalAliaseSChan - aliasesConn = aliaseS - internalAliaseSChan <- aliaseS - } else if len(cfg.CDRSAliases) != 0 { - client, err = rpcclient.NewRpcClient("tcp", cfg.CDRSAliases, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to aliases server: %s", err.Error())) - exitChan <- true - return - } - aliasesConn = client + aliasesConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.CDRSAliaseSConns, internalAliaseSChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to AliaseS: %s", err.Error())) + exitChan <- true + return } // Stats connection init - var statsConn rpcclient.RpcClientConnection - if cfg.CDRSStats == utils.INTERNAL { - statS := <-internalCdrStatSChan - statsConn = statS - internalCdrStatSChan <- statS - } else if len(cfg.CDRSStats) != 0 { - client, err = rpcclient.NewRpcClient("tcp", cfg.CDRSStats, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, nil) - if err != nil { - utils.Logger.Crit(fmt.Sprintf(" Could not connect to stats server: %s", err.Error())) - exitChan <- true - return - } - statsConn = client + statsConn, err := engine.NewRPCPool(rpcclient.POOL_FIRST, cfg.ConnectAttempts, cfg.Reconnects, utils.GOB, + cfg.CDRSStatSConns, internalCdrStatSChan) + if err != nil { + utils.Logger.Crit(fmt.Sprintf(" Could not connect to StatS: %s", err.Error())) + exitChan <- true + return } - cdrServer, _ := engine.NewCdrServer(cfg, cdrDb, raterConn, pubSubConn, usersConn, aliasesConn, statsConn) + cdrServer, _ := engine.NewCdrServer(cfg, cdrDb, ralConn, pubSubConn, usersConn, aliasesConn, statsConn) cdrServer.SetTimeToLive(cfg.ResponseCacheTTL, nil) utils.Logger.Info("Registering CDRS HTTP Handlers.") cdrServer.RegisterHandlersToServer(server) @@ -536,12 +395,8 @@ func startUsersServer(internalUserSChan chan rpcclient.RpcClientConnection, acco internalUserSChan <- userServer } -func startRpc(server *utils.Server, internalRaterChan chan *engine.Responder, - internalCdrSChan chan *engine.CdrServer, - internalCdrStatSChan chan rpcclient.RpcClientConnection, - internalHistorySChan chan rpcclient.RpcClientConnection, - internalPubSubSChan chan rpcclient.RpcClientConnection, - internalUserSChan chan rpcclient.RpcClientConnection, +func startRpc(server *utils.Server, internalRaterChan chan rpcclient.RpcClientConnection, + internalCdrSChan, internalCdrStatSChan, internalHistorySChan, internalPubSubSChan, internalUserSChan, internalAliaseSChan chan rpcclient.RpcClientConnection) { select { // Any of the rpc methods will unlock listening to rpc requests case resp := <-internalRaterChan: @@ -562,6 +417,7 @@ func startRpc(server *utils.Server, internalRaterChan chan *engine.Responder, go server.ServeJSON(cfg.RPCJSONListen) go server.ServeGOB(cfg.RPCGOBListen) go server.ServeHTTP(cfg.HTTPListen) + } func writePid() { @@ -616,7 +472,7 @@ func main() { var logDb engine.LogStorage var loadDb engine.LoadStorage var cdrDb engine.CdrStorage - if cfg.RaterEnabled || cfg.SchedulerEnabled { // Only connect to dataDb if necessary + if cfg.RaterEnabled || cfg.SchedulerEnabled || cfg.CDRStatsEnabled { // Only connect to dataDb if necessary ratingDb, err = engine.ConfigureRatingStorage(cfg.TpDbType, cfg.TpDbHost, cfg.TpDbPort, cfg.TpDbName, cfg.TpDbUser, cfg.TpDbPass, cfg.DBDataEncoding) if err != nil { // Cannot configure getter database, show stopper @@ -625,6 +481,8 @@ func main() { } defer ratingDb.Close() engine.SetRatingStorage(ratingDb) + } + if cfg.RaterEnabled || cfg.CDRStatsEnabled || cfg.PubSubServerEnabled || cfg.AliasesServerEnabled || cfg.UserServerEnabled { accountDb, err = engine.ConfigureAccountingStorage(cfg.DataDbType, cfg.DataDbHost, cfg.DataDbPort, cfg.DataDbName, cfg.DataDbUser, cfg.DataDbPass, cfg.DBDataEncoding) if err != nil { // Cannot configure getter database, show stopper @@ -660,10 +518,10 @@ func main() { // Define internal connections via channels internalBalancerChan := make(chan *balancer2go.Balancer, 1) - internalRaterChan := make(chan *engine.Responder, 1) + internalRaterChan := make(chan rpcclient.RpcClientConnection, 1) cacheDoneChan := make(chan struct{}, 1) internalSchedulerChan := make(chan *scheduler.Scheduler, 1) - internalCdrSChan := make(chan *engine.CdrServer, 1) + internalCdrSChan := make(chan rpcclient.RpcClientConnection, 1) internalCdrStatSChan := make(chan rpcclient.RpcClientConnection, 1) internalHistorySChan := make(chan rpcclient.RpcClientConnection, 1) internalPubSubSChan := make(chan rpcclient.RpcClientConnection, 1) @@ -727,7 +585,7 @@ func main() { } if cfg.DiameterAgentCfg().Enabled { - go startDiameterAgent(internalSMGChan, exitChan) + go startDiameterAgent(internalSMGChan, internalPubSubSChan, exitChan) } // Start HistoryS service diff --git a/cmd/cgr-engine/rater.go b/cmd/cgr-engine/rater.go index 08b4f2de7..e56892ee4 100644 --- a/cmd/cgr-engine/rater.go +++ b/cmd/cgr-engine/rater.go @@ -40,7 +40,7 @@ func startBalancer(internalBalancerChan chan *balancer2go.Balancer, stopHandled // Starts rater and reports on chan -func startRater(internalRaterChan chan *engine.Responder, cacheDoneChan chan struct{}, internalBalancerChan chan *balancer2go.Balancer, internalSchedulerChan chan *scheduler.Scheduler, +func startRater(internalRaterChan chan rpcclient.RpcClientConnection, cacheDoneChan chan struct{}, internalBalancerChan chan *balancer2go.Balancer, internalSchedulerChan chan *scheduler.Scheduler, internalCdrStatSChan chan rpcclient.RpcClientConnection, internalHistorySChan chan rpcclient.RpcClientConnection, internalPubSubSChan chan rpcclient.RpcClientConnection, internalUserSChan chan rpcclient.RpcClientConnection, internalAliaseSChan chan rpcclient.RpcClientConnection, server *utils.Server, diff --git a/cmd/cgr-loader/cgr-loader.go b/cmd/cgr-loader/cgr-loader.go index faf3be155..7f26375fb 100644 --- a/cmd/cgr-loader/cgr-loader.go +++ b/cmd/cgr-loader/cgr-loader.go @@ -152,6 +152,17 @@ func main() { log.Print(err.Error()) } } + if strings.Contains(*migrateRC8, "int") { + if err := migratorRC8acc.migrateAccountsInt(); err != nil { + log.Print(err.Error()) + } + if err := migratorRC8rat.migrateActionTriggersInt(); err != nil { + log.Print(err.Error()) + } + if err := migratorRC8rat.migrateActionsInt(); err != nil { + log.Print(err.Error()) + } + } log.Print("Done!") return } diff --git a/cmd/cgr-loader/migrator_rc8.go b/cmd/cgr-loader/migrator_rc8.go index 08eca3650..9d085d2f6 100644 --- a/cmd/cgr-loader/migrator_rc8.go +++ b/cmd/cgr-loader/migrator_rc8.go @@ -173,20 +173,20 @@ func (mig MigratorRC8) migrateAccounts() error { } // transfer data into new structurse newAcc := &engine.Account{ - Id: oldAcc.Id, - BalanceMap: make(map[string]engine.BalanceChain, len(oldAcc.BalanceMap)), + ID: oldAcc.Id, + BalanceMap: make(map[string]engine.Balances, len(oldAcc.BalanceMap)), UnitCounters: make(engine.UnitCounters, len(oldAcc.UnitCounters)), ActionTriggers: make(engine.ActionTriggers, len(oldAcc.ActionTriggers)), AllowNegative: oldAcc.AllowNegative, Disabled: oldAcc.Disabled, } // fix id - idElements := strings.Split(newAcc.Id, utils.CONCATENATED_KEY_SEP) + idElements := strings.Split(newAcc.ID, utils.CONCATENATED_KEY_SEP) if len(idElements) != 3 { log.Printf("Malformed account ID %s", oldAcc.Id) continue } - newAcc.Id = fmt.Sprintf("%s:%s", idElements[1], idElements[2]) + newAcc.ID = fmt.Sprintf("%s:%s", idElements[1], idElements[2]) // balances balanceErr := false for oldBalKey, oldBalChain := range oldAcc.BalanceMap { @@ -198,7 +198,7 @@ func (mig MigratorRC8) migrateAccounts() error { } newBalKey := "*" + keyElements[1] newBalDirection := "*" + keyElements[2] - newAcc.BalanceMap[newBalKey] = make(engine.BalanceChain, len(oldBalChain)) + newAcc.BalanceMap[newBalKey] = make(engine.Balances, len(oldBalChain)) for index, oldBal := range oldBalChain { // check default to set new id if oldBal.IsDefault() { @@ -206,12 +206,12 @@ func (mig MigratorRC8) migrateAccounts() error { } newAcc.BalanceMap[newBalKey][index] = &engine.Balance{ Uuid: oldBal.Uuid, - Id: oldBal.Id, + ID: oldBal.Id, Value: oldBal.Value, Directions: utils.ParseStringMap(newBalDirection), ExpirationDate: oldBal.ExpirationDate, Weight: oldBal.Weight, - DestinationIds: utils.ParseStringMap(oldBal.DestinationIds), + DestinationIDs: utils.ParseStringMap(oldBal.DestinationIds), RatingSubject: oldBal.RatingSubject, Categories: utils.ParseStringMap(oldBal.Category), SharedGroups: utils.ParseStringMap(oldBal.SharedGroup), @@ -227,51 +227,108 @@ func (mig MigratorRC8) migrateAccounts() error { // unit counters for _, oldUc := range oldAcc.UnitCounters { newUc := &engine.UnitCounter{ - BalanceType: oldUc.BalanceType, - Balances: make(engine.BalanceChain, len(oldUc.Balances)), + Counters: make(engine.CounterFilters, len(oldUc.Balances)), } for index, oldUcBal := range oldUc.Balances { - newUc.Balances[index] = &engine.Balance{ - Uuid: oldUcBal.Uuid, - Id: oldUcBal.Id, - Value: oldUcBal.Value, - Directions: utils.ParseStringMap(oldUc.Direction), - ExpirationDate: oldUcBal.ExpirationDate, - Weight: oldUcBal.Weight, - DestinationIds: utils.ParseStringMap(oldUcBal.DestinationIds), - RatingSubject: oldUcBal.RatingSubject, - Categories: utils.ParseStringMap(oldUcBal.Category), - SharedGroups: utils.ParseStringMap(oldUcBal.SharedGroup), - Timings: oldUcBal.Timings, - TimingIDs: utils.ParseStringMap(oldUcBal.TimingIDs), - Disabled: oldUcBal.Disabled, + bf := &engine.BalanceFilter{} + if oldUcBal.Uuid != "" { + bf.Uuid = utils.StringPointer(oldUcBal.Uuid) } + if oldUcBal.Id != "" { + bf.ID = utils.StringPointer(oldUcBal.Id) + } + if oldUc.BalanceType != "" { + bf.Type = utils.StringPointer(oldUc.BalanceType) + } + // the value was used for counter value + /*if oldUcBal.Value != 0 { + bf.Value = utils.Float64Pointer(oldUcBal.Value) + }*/ + if oldUc.Direction != "" { + bf.Directions = utils.StringMapPointer(utils.ParseStringMap(oldUc.Direction)) + } + if !oldUcBal.ExpirationDate.IsZero() { + bf.ExpirationDate = utils.TimePointer(oldUcBal.ExpirationDate) + } + if oldUcBal.Weight != 0 { + bf.Weight = utils.Float64Pointer(oldUcBal.Weight) + } + if oldUcBal.DestinationIds != "" { + bf.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(oldUcBal.DestinationIds)) + } + if oldUcBal.RatingSubject != "" { + bf.RatingSubject = utils.StringPointer(oldUcBal.RatingSubject) + } + if oldUcBal.Category != "" { + bf.Categories = utils.StringMapPointer(utils.ParseStringMap(oldUcBal.Category)) + } + if oldUcBal.SharedGroup != "" { + bf.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(oldUcBal.SharedGroup)) + } + if oldUcBal.TimingIDs != "" { + bf.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(oldUcBal.TimingIDs)) + } + if oldUcBal.Disabled != false { + bf.Disabled = utils.BoolPointer(oldUcBal.Disabled) + } + bf.Timings = oldUcBal.Timings + cf := &engine.CounterFilter{ + Value: oldUcBal.Value, + Filter: bf, + } + newUc.Counters[index] = cf } + newAcc.UnitCounters[oldUc.BalanceType] = append(newAcc.UnitCounters[oldUc.BalanceType], newUc) } // action triggers for index, oldAtr := range oldAcc.ActionTriggers { - newAcc.ActionTriggers[index] = &engine.ActionTrigger{ - UniqueID: oldAtr.Id, - ThresholdType: oldAtr.ThresholdType, - ThresholdValue: oldAtr.ThresholdValue, - Recurrent: oldAtr.Recurrent, - MinSleep: oldAtr.MinSleep, - BalanceId: oldAtr.BalanceId, - BalanceType: oldAtr.BalanceType, - BalanceDirections: utils.ParseStringMap(oldAtr.BalanceDirection), - BalanceDestinationIds: utils.ParseStringMap(oldAtr.BalanceDestinationIds), - BalanceWeight: oldAtr.BalanceWeight, - BalanceExpirationDate: oldAtr.BalanceExpirationDate, - BalanceTimingTags: utils.ParseStringMap(oldAtr.BalanceTimingTags), - BalanceRatingSubject: oldAtr.BalanceRatingSubject, - BalanceCategories: utils.ParseStringMap(oldAtr.BalanceCategory), - BalanceSharedGroups: utils.ParseStringMap(oldAtr.BalanceSharedGroup), - BalanceDisabled: oldAtr.BalanceDisabled, - Weight: oldAtr.Weight, - ActionsId: oldAtr.ActionsId, - MinQueuedItems: oldAtr.MinQueuedItems, - Executed: oldAtr.Executed, + at := &engine.ActionTrigger{ + UniqueID: oldAtr.Id, + ThresholdType: oldAtr.ThresholdType, + ThresholdValue: oldAtr.ThresholdValue, + Recurrent: oldAtr.Recurrent, + MinSleep: oldAtr.MinSleep, + Weight: oldAtr.Weight, + ActionsID: oldAtr.ActionsId, + MinQueuedItems: oldAtr.MinQueuedItems, + Executed: oldAtr.Executed, } + bf := &engine.BalanceFilter{} + if oldAtr.BalanceId != "" { + bf.ID = utils.StringPointer(oldAtr.BalanceId) + } + if oldAtr.BalanceType != "" { + bf.Type = utils.StringPointer(oldAtr.BalanceType) + } + if oldAtr.BalanceRatingSubject != "" { + bf.RatingSubject = utils.StringPointer(oldAtr.BalanceRatingSubject) + } + if oldAtr.BalanceDirection != "" { + bf.Directions = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceDirection)) + } + if oldAtr.BalanceDestinationIds != "" { + bf.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceDestinationIds)) + } + if oldAtr.BalanceTimingTags != "" { + bf.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceTimingTags)) + } + if oldAtr.BalanceCategory != "" { + bf.Categories = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceCategory)) + } + if oldAtr.BalanceSharedGroup != "" { + bf.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceSharedGroup)) + } + if oldAtr.BalanceWeight != 0 { + bf.Weight = utils.Float64Pointer(oldAtr.BalanceWeight) + } + if oldAtr.BalanceDisabled != false { + bf.Disabled = utils.BoolPointer(oldAtr.BalanceDisabled) + } + if !oldAtr.BalanceExpirationDate.IsZero() { + bf.ExpirationDate = utils.TimePointer(oldAtr.BalanceExpirationDate) + } + at.Balance = bf + newAcc.ActionTriggers[index] = at if newAcc.ActionTriggers[index].ThresholdType == "*min_counter" || newAcc.ActionTriggers[index].ThresholdType == "*max_counter" { newAcc.ActionTriggers[index].ThresholdType = strings.Replace(newAcc.ActionTriggers[index].ThresholdType, "_", "_event_", 1) @@ -287,7 +344,7 @@ func (mig MigratorRC8) migrateAccounts() error { if err != nil { return err } - if err := mig.db.Cmd("SET", utils.ACCOUNT_PREFIX+newAcc.Id, result).Err; err != nil { + if err := mig.db.Cmd("SET", utils.ACCOUNT_PREFIX+newAcc.ID, result).Err; err != nil { return err } } @@ -322,28 +379,53 @@ func (mig MigratorRC8) migrateActionTriggers() error { } newAtrs := make(engine.ActionTriggers, len(oldAtrs)) for index, oldAtr := range oldAtrs { - newAtrs[index] = &engine.ActionTrigger{ - UniqueID: oldAtr.Id, - ThresholdType: oldAtr.ThresholdType, - ThresholdValue: oldAtr.ThresholdValue, - Recurrent: oldAtr.Recurrent, - MinSleep: oldAtr.MinSleep, - BalanceId: oldAtr.BalanceId, - BalanceType: oldAtr.BalanceType, - BalanceDirections: utils.ParseStringMap(oldAtr.BalanceDirection), - BalanceDestinationIds: utils.ParseStringMap(oldAtr.BalanceDestinationIds), - BalanceWeight: oldAtr.BalanceWeight, - BalanceExpirationDate: oldAtr.BalanceExpirationDate, - BalanceTimingTags: utils.ParseStringMap(oldAtr.BalanceTimingTags), - BalanceRatingSubject: oldAtr.BalanceRatingSubject, - BalanceCategories: utils.ParseStringMap(oldAtr.BalanceCategory), - BalanceSharedGroups: utils.ParseStringMap(oldAtr.BalanceSharedGroup), - BalanceDisabled: oldAtr.BalanceDisabled, - Weight: oldAtr.Weight, - ActionsId: oldAtr.ActionsId, - MinQueuedItems: oldAtr.MinQueuedItems, - Executed: oldAtr.Executed, + at := &engine.ActionTrigger{ + UniqueID: oldAtr.Id, + ThresholdType: oldAtr.ThresholdType, + ThresholdValue: oldAtr.ThresholdValue, + Recurrent: oldAtr.Recurrent, + MinSleep: oldAtr.MinSleep, + Weight: oldAtr.Weight, + ActionsID: oldAtr.ActionsId, + MinQueuedItems: oldAtr.MinQueuedItems, + Executed: oldAtr.Executed, } + bf := &engine.BalanceFilter{} + if oldAtr.BalanceId != "" { + bf.ID = utils.StringPointer(oldAtr.BalanceId) + } + if oldAtr.BalanceType != "" { + bf.Type = utils.StringPointer(oldAtr.BalanceType) + } + if oldAtr.BalanceRatingSubject != "" { + bf.RatingSubject = utils.StringPointer(oldAtr.BalanceRatingSubject) + } + if oldAtr.BalanceDirection != "" { + bf.Directions = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceDirection)) + } + if oldAtr.BalanceDestinationIds != "" { + bf.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceDestinationIds)) + } + if oldAtr.BalanceTimingTags != "" { + bf.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceTimingTags)) + } + if oldAtr.BalanceCategory != "" { + bf.Categories = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceCategory)) + } + if oldAtr.BalanceSharedGroup != "" { + bf.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceSharedGroup)) + } + if oldAtr.BalanceWeight != 0 { + bf.Weight = utils.Float64Pointer(oldAtr.BalanceWeight) + } + if oldAtr.BalanceDisabled != false { + bf.Disabled = utils.BoolPointer(oldAtr.BalanceDisabled) + } + if !oldAtr.BalanceExpirationDate.IsZero() { + bf.ExpirationDate = utils.TimePointer(oldAtr.BalanceExpirationDate) + } + at.Balance = bf + newAtrs[index] = at if newAtrs[index].ThresholdType == "*min_counter" || newAtrs[index].ThresholdType == "*max_counter" { newAtrs[index].ThresholdType = strings.Replace(newAtrs[index].ThresholdType, "_", "_event_", 1) @@ -381,29 +463,53 @@ func (mig MigratorRC8) migrateActions() error { } newAcs := make(engine.Actions, len(oldAcs)) for index, oldAc := range oldAcs { - newAcs[index] = &engine.Action{ + a := &engine.Action{ Id: oldAc.Id, ActionType: oldAc.ActionType, - BalanceType: oldAc.BalanceType, ExtraParameters: oldAc.ExtraParameters, ExpirationString: oldAc.ExpirationString, Weight: oldAc.Weight, - Balance: &engine.Balance{ - Uuid: oldAc.Balance.Uuid, - Id: oldAc.Balance.Id, - Value: oldAc.Balance.Value, - Directions: utils.ParseStringMap(oldAc.Direction), - ExpirationDate: oldAc.Balance.ExpirationDate, - Weight: oldAc.Balance.Weight, - DestinationIds: utils.ParseStringMap(oldAc.Balance.DestinationIds), - RatingSubject: oldAc.Balance.RatingSubject, - Categories: utils.ParseStringMap(oldAc.Balance.Category), - SharedGroups: utils.ParseStringMap(oldAc.Balance.SharedGroup), - Timings: oldAc.Balance.Timings, - TimingIDs: utils.ParseStringMap(oldAc.Balance.TimingIDs), - Disabled: oldAc.Balance.Disabled, - }, + Balance: &engine.BalanceFilter{}, } + bf := a.Balance + if oldAc.Balance.Uuid != "" { + bf.Uuid = utils.StringPointer(oldAc.Balance.Uuid) + } + if oldAc.Balance.Id != "" { + bf.ID = utils.StringPointer(oldAc.Balance.Id) + } + if oldAc.BalanceType != "" { + bf.Type = utils.StringPointer(oldAc.BalanceType) + } + if oldAc.Balance.Value != 0 { + bf.Value = utils.Float64Pointer(oldAc.Balance.Value) + } + if oldAc.Balance.RatingSubject != "" { + bf.RatingSubject = utils.StringPointer(oldAc.Balance.RatingSubject) + } + if oldAc.Balance.DestinationIds != "" { + bf.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(oldAc.Balance.DestinationIds)) + } + if oldAc.Balance.TimingIDs != "" { + bf.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(oldAc.Balance.TimingIDs)) + } + if oldAc.Balance.Category != "" { + bf.Categories = utils.StringMapPointer(utils.ParseStringMap(oldAc.Balance.Category)) + } + if oldAc.Balance.SharedGroup != "" { + bf.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(oldAc.Balance.SharedGroup)) + } + if oldAc.Balance.Weight != 0 { + bf.Weight = utils.Float64Pointer(oldAc.Balance.Weight) + } + if oldAc.Balance.Disabled != false { + bf.Disabled = utils.BoolPointer(oldAc.Balance.Disabled) + } + if !oldAc.Balance.ExpirationDate.IsZero() { + bf.ExpirationDate = utils.TimePointer(oldAc.Balance.ExpirationDate) + } + bf.Timings = oldAc.Balance.Timings + newAcs[index] = a } newAcsMap[key] = newAcs } @@ -475,7 +581,7 @@ func (mig MigratorRC8) migrateActionPlans() error { // fix id idElements := strings.Split(actionId, utils.CONCATENATED_KEY_SEP) if len(idElements) != 3 { - log.Printf("Malformed account ID %s", actionId) + //log.Printf("Malformed account ID %s", actionId) continue } apl.AccountIds[idx] = fmt.Sprintf("%s:%s", idElements[1], idElements[2]) @@ -491,14 +597,14 @@ func (mig MigratorRC8) migrateActionPlans() error { if !exists { newApl = &engine.ActionPlan{ Id: apl.Id, - AccountIDs: make(map[string]struct{}), + AccountIDs: make(utils.StringMap), } newAplMap[key] = newApl } if !apl.IsASAP() { for _, accID := range apl.AccountIds { if _, exists := newApl.AccountIDs[accID]; !exists { - newApl.AccountIDs[accID] = struct{}{} + newApl.AccountIDs[accID] = true } } } diff --git a/cmd/cgr-loader/migrator_rc8int.go b/cmd/cgr-loader/migrator_rc8int.go new file mode 100644 index 000000000..41f55afb1 --- /dev/null +++ b/cmd/cgr-loader/migrator_rc8int.go @@ -0,0 +1,409 @@ +package main + +import ( + "log" + "time" + + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + +type Account1 struct { + Id string + BalanceMap map[string]BalanceChain1 + UnitCounters UnitCounters1 + ActionTriggers ActionTriggers1 + AllowNegative bool + Disabled bool +} + +type Balance1 struct { + Uuid string //system wide unique + Id string // account wide unique + Value float64 + Directions utils.StringMap + ExpirationDate time.Time + Weight float64 + DestinationIds utils.StringMap + RatingSubject string + Categories utils.StringMap + SharedGroups utils.StringMap + Timings []*engine.RITiming + TimingIDs utils.StringMap + Disabled bool + Factor engine.ValueFactor + Blocker bool + precision int + account *Account // used to store ub reference for shared balances + dirty bool +} + +type BalanceChain1 []*Balance1 + +type UnitCounter1 struct { + BalanceType string // *monetary/*voice/*sms/etc + CounterType string // *event or *balance + Balances BalanceChain1 // first balance is the general one (no destination) +} + +type UnitCounters1 []*UnitCounter1 + +type Action1 struct { + Id string + ActionType string + BalanceType string + ExtraParameters string + Filter string + ExpirationString string // must stay as string because it can have relative values like 1month + Weight float64 + Balance *Balance1 +} + +type Actions1 []*Action1 + +type ActionTrigger1 struct { + ID string // original csv tag + UniqueID string // individual id + ThresholdType string + ThresholdValue float64 + Recurrent bool // reset eexcuted flag each run + MinSleep time.Duration // Minimum duration between two executions in case of recurrent triggers + BalanceId string + BalanceType string // *monetary/*voice etc + BalanceDirections utils.StringMap // filter for balance + BalanceDestinationIds utils.StringMap // filter for balance + BalanceWeight float64 // filter for balance + BalanceExpirationDate time.Time // filter for balance + BalanceTimingTags utils.StringMap // filter for balance + BalanceRatingSubject string // filter for balance + BalanceCategories utils.StringMap // filter for balance + BalanceSharedGroups utils.StringMap // filter for balance + BalanceBlocker bool + BalanceDisabled bool // filter for balance + Weight float64 + ActionsId string + MinQueuedItems int // Trigger actions only if this number is hit (stats only) + Executed bool +} +type ActionTriggers1 []*ActionTrigger1 + +func (mig MigratorRC8) migrateAccountsInt() error { + keys, err := mig.db.Cmd("KEYS", utils.ACCOUNT_PREFIX+"*").List() + if err != nil { + return err + } + newAccounts := make([]*engine.Account, 0) + var migratedKeys []string + // get existing accounts + for _, key := range keys { + log.Printf("Migrating account: %s...", key) + values, err := mig.db.Cmd("GET", key).Bytes() + if err != nil { + continue + } + var oldAcc Account1 + if err = mig.ms.Unmarshal(values, &oldAcc); err != nil { + return err + } + // transfer data into new structurse + newAcc := &engine.Account{ + ID: oldAcc.Id, + BalanceMap: make(map[string]engine.Balances, len(oldAcc.BalanceMap)), + UnitCounters: make(engine.UnitCounters), + ActionTriggers: make(engine.ActionTriggers, len(oldAcc.ActionTriggers)), + AllowNegative: oldAcc.AllowNegative, + Disabled: oldAcc.Disabled, + } + // balances + balanceErr := false + for key, oldBalChain := range oldAcc.BalanceMap { + newAcc.BalanceMap[key] = make(engine.Balances, len(oldBalChain)) + for index, oldBal := range oldBalChain { + newAcc.BalanceMap[key][index] = &engine.Balance{ + Uuid: oldBal.Uuid, + ID: oldBal.Id, + Value: oldBal.Value, + Directions: oldBal.Directions, + ExpirationDate: oldBal.ExpirationDate, + Weight: oldBal.Weight, + DestinationIDs: oldBal.DestinationIds, + RatingSubject: oldBal.RatingSubject, + Categories: oldBal.Categories, + SharedGroups: oldBal.SharedGroups, + Timings: oldBal.Timings, + TimingIDs: oldBal.TimingIDs, + Disabled: oldBal.Disabled, + Factor: oldBal.Factor, + Blocker: oldBal.Blocker, + } + } + } + if balanceErr { + continue + } + // unit counters + for _, oldUc := range oldAcc.UnitCounters { + newUc := &engine.UnitCounter{ + Counters: make(engine.CounterFilters, len(oldUc.Balances)), + } + for index, oldUcBal := range oldUc.Balances { + b := &engine.Balance{ + Uuid: oldUcBal.Uuid, + ID: oldUcBal.Id, + Value: oldUcBal.Value, + Directions: oldUcBal.Directions, + ExpirationDate: oldUcBal.ExpirationDate, + Weight: oldUcBal.Weight, + DestinationIDs: oldUcBal.DestinationIds, + RatingSubject: oldUcBal.RatingSubject, + Categories: oldUcBal.Categories, + SharedGroups: oldUcBal.SharedGroups, + Timings: oldUcBal.Timings, + TimingIDs: oldUcBal.TimingIDs, + Disabled: oldUcBal.Disabled, + Factor: oldUcBal.Factor, + Blocker: oldUcBal.Blocker, + } + bf := &engine.BalanceFilter{} + bf.LoadFromBalance(b) + cf := &engine.CounterFilter{ + Value: oldUcBal.Value, + Filter: bf, + } + newUc.Counters[index] = cf + } + newAcc.UnitCounters[oldUc.BalanceType] = append(newAcc.UnitCounters[oldUc.BalanceType], newUc) + } + // action triggers + for index, oldAtr := range oldAcc.ActionTriggers { + at := &engine.ActionTrigger{ + ID: oldAtr.ID, + UniqueID: oldAtr.UniqueID, + ThresholdType: oldAtr.ThresholdType, + ThresholdValue: oldAtr.ThresholdValue, + Recurrent: oldAtr.Recurrent, + MinSleep: oldAtr.MinSleep, + Weight: oldAtr.Weight, + ActionsID: oldAtr.ActionsId, + MinQueuedItems: oldAtr.MinQueuedItems, + Executed: oldAtr.Executed, + } + bf := &engine.BalanceFilter{} + if oldAtr.BalanceId != "" { + bf.ID = utils.StringPointer(oldAtr.BalanceId) + } + if oldAtr.BalanceType != "" { + bf.Type = utils.StringPointer(oldAtr.BalanceType) + } + if oldAtr.BalanceRatingSubject != "" { + bf.RatingSubject = utils.StringPointer(oldAtr.BalanceRatingSubject) + } + if !oldAtr.BalanceDirections.IsEmpty() { + bf.Directions = utils.StringMapPointer(oldAtr.BalanceDirections) + } + if !oldAtr.BalanceDestinationIds.IsEmpty() { + bf.DestinationIDs = utils.StringMapPointer(oldAtr.BalanceDestinationIds) + } + if !oldAtr.BalanceTimingTags.IsEmpty() { + bf.TimingIDs = utils.StringMapPointer(oldAtr.BalanceTimingTags) + } + if !oldAtr.BalanceCategories.IsEmpty() { + bf.Categories = utils.StringMapPointer(oldAtr.BalanceCategories) + } + if !oldAtr.BalanceSharedGroups.IsEmpty() { + bf.SharedGroups = utils.StringMapPointer(oldAtr.BalanceSharedGroups) + } + if oldAtr.BalanceWeight != 0 { + bf.Weight = utils.Float64Pointer(oldAtr.BalanceWeight) + } + if oldAtr.BalanceDisabled != false { + bf.Disabled = utils.BoolPointer(oldAtr.BalanceDisabled) + } + if !oldAtr.BalanceExpirationDate.IsZero() { + bf.ExpirationDate = utils.TimePointer(oldAtr.BalanceExpirationDate) + } + at.Balance = bf + newAcc.ActionTriggers[index] = at + } + newAcc.InitCounters() + newAccounts = append(newAccounts, newAcc) + migratedKeys = append(migratedKeys, key) + } + // write data back + for _, newAcc := range newAccounts { + result, err := mig.ms.Marshal(newAcc) + if err != nil { + return err + } + if err := mig.db.Cmd("SET", utils.ACCOUNT_PREFIX+newAcc.ID, result).Err; err != nil { + return err + } + } + notMigrated := len(keys) - len(migratedKeys) + if notMigrated > 0 { + log.Printf("WARNING: there are %d accounts that failed migration!", notMigrated) + } + return err +} + +func (mig MigratorRC8) migrateActionTriggersInt() error { + keys, err := mig.db.Cmd("KEYS", utils.ACTION_TRIGGER_PREFIX+"*").List() + if err != nil { + return err + } + newAtrsMap := make(map[string]engine.ActionTriggers, len(keys)) + for _, key := range keys { + log.Printf("Migrating action trigger: %s...", key) + var oldAtrs ActionTriggers1 + var values []byte + if values, err = mig.db.Cmd("GET", key).Bytes(); err == nil { + if err := mig.ms.Unmarshal(values, &oldAtrs); err != nil { + return err + } + } + newAtrs := make(engine.ActionTriggers, len(oldAtrs)) + for index, oldAtr := range oldAtrs { + at := &engine.ActionTrigger{ + ID: oldAtr.ID, + UniqueID: oldAtr.UniqueID, + ThresholdType: oldAtr.ThresholdType, + ThresholdValue: oldAtr.ThresholdValue, + Recurrent: oldAtr.Recurrent, + MinSleep: oldAtr.MinSleep, + Weight: oldAtr.Weight, + ActionsID: oldAtr.ActionsId, + MinQueuedItems: oldAtr.MinQueuedItems, + Executed: oldAtr.Executed, + } + bf := &engine.BalanceFilter{} + if oldAtr.BalanceId != "" { + bf.ID = utils.StringPointer(oldAtr.BalanceId) + } + if oldAtr.BalanceType != "" { + bf.Type = utils.StringPointer(oldAtr.BalanceType) + } + if oldAtr.BalanceRatingSubject != "" { + bf.RatingSubject = utils.StringPointer(oldAtr.BalanceRatingSubject) + } + if !oldAtr.BalanceDirections.IsEmpty() { + bf.Directions = utils.StringMapPointer(oldAtr.BalanceDirections) + } + if !oldAtr.BalanceDestinationIds.IsEmpty() { + bf.DestinationIDs = utils.StringMapPointer(oldAtr.BalanceDestinationIds) + } + if !oldAtr.BalanceTimingTags.IsEmpty() { + bf.TimingIDs = utils.StringMapPointer(oldAtr.BalanceTimingTags) + } + if !oldAtr.BalanceCategories.IsEmpty() { + bf.Categories = utils.StringMapPointer(oldAtr.BalanceCategories) + } + if !oldAtr.BalanceSharedGroups.IsEmpty() { + bf.SharedGroups = utils.StringMapPointer(oldAtr.BalanceSharedGroups) + } + if oldAtr.BalanceWeight != 0 { + bf.Weight = utils.Float64Pointer(oldAtr.BalanceWeight) + } + if oldAtr.BalanceDisabled != false { + bf.Disabled = utils.BoolPointer(oldAtr.BalanceDisabled) + } + if !oldAtr.BalanceExpirationDate.IsZero() { + bf.ExpirationDate = utils.TimePointer(oldAtr.BalanceExpirationDate) + } + at.Balance = bf + newAtrs[index] = at + } + newAtrsMap[key] = newAtrs + } + // write data back + for key, atrs := range newAtrsMap { + result, err := mig.ms.Marshal(&atrs) + if err != nil { + return err + } + if err = mig.db.Cmd("SET", key, result).Err; err != nil { + return err + } + } + return nil +} + +func (mig MigratorRC8) migrateActionsInt() error { + keys, err := mig.db.Cmd("KEYS", utils.ACTION_PREFIX+"*").List() + if err != nil { + return err + } + newAcsMap := make(map[string]engine.Actions, len(keys)) + for _, key := range keys { + log.Printf("Migrating action: %s...", key) + var oldAcs Actions1 + var values []byte + if values, err = mig.db.Cmd("GET", key).Bytes(); err == nil { + if err := mig.ms.Unmarshal(values, &oldAcs); err != nil { + return err + } + } + newAcs := make(engine.Actions, len(oldAcs)) + for index, oldAc := range oldAcs { + a := &engine.Action{ + Id: oldAc.Id, + ActionType: oldAc.ActionType, + ExtraParameters: oldAc.ExtraParameters, + ExpirationString: oldAc.ExpirationString, + Filter: oldAc.Filter, + Weight: oldAc.Weight, + Balance: &engine.BalanceFilter{}, + } + bf := a.Balance + if oldAc.Balance.Uuid != "" { + bf.Uuid = utils.StringPointer(oldAc.Balance.Uuid) + } + if oldAc.Balance.Id != "" { + bf.ID = utils.StringPointer(oldAc.Balance.Id) + } + if oldAc.BalanceType != "" { + bf.Type = utils.StringPointer(oldAc.BalanceType) + } + if oldAc.Balance.Value != 0 { + bf.Value = utils.Float64Pointer(oldAc.Balance.Value) + } + if oldAc.Balance.RatingSubject != "" { + bf.RatingSubject = utils.StringPointer(oldAc.Balance.RatingSubject) + } + if !oldAc.Balance.DestinationIds.IsEmpty() { + bf.DestinationIDs = utils.StringMapPointer(oldAc.Balance.DestinationIds) + } + if !oldAc.Balance.TimingIDs.IsEmpty() { + bf.TimingIDs = utils.StringMapPointer(oldAc.Balance.TimingIDs) + } + if !oldAc.Balance.Categories.IsEmpty() { + bf.Categories = utils.StringMapPointer(oldAc.Balance.Categories) + } + if !oldAc.Balance.SharedGroups.IsEmpty() { + bf.SharedGroups = utils.StringMapPointer(oldAc.Balance.SharedGroups) + } + if oldAc.Balance.Weight != 0 { + bf.Weight = utils.Float64Pointer(oldAc.Balance.Weight) + } + if oldAc.Balance.Disabled != false { + bf.Disabled = utils.BoolPointer(oldAc.Balance.Disabled) + } + if !oldAc.Balance.ExpirationDate.IsZero() { + bf.ExpirationDate = utils.TimePointer(oldAc.Balance.ExpirationDate) + } + bf.Timings = oldAc.Balance.Timings + newAcs[index] = a + } + newAcsMap[key] = newAcs + } + // write data back + for key, acs := range newAcsMap { + result, err := mig.ms.Marshal(&acs) + if err != nil { + return err + } + if err = mig.db.Cmd("SET", key, result).Err; err != nil { + return err + } + } + return nil +} diff --git a/config/config.go b/config/config.go index 10e75e06f..e6c2283a4 100644 --- a/config/config.go +++ b/config/config.go @@ -167,88 +167,89 @@ func NewCGRConfigFromFolder(cfgDir string) (*CGRConfig, error) { // Holds system configuration, defaults are overwritten with values from config file if found type CGRConfig struct { - TpDbType string - TpDbHost string // The host to connect to. Values that start with / are for UNIX domain sockets. - TpDbPort string // The port to bind to. - TpDbName string // The name of the database to connect to. - TpDbUser string // The user to sign in as. - TpDbPass string // The user's password. - DataDbType string - DataDbHost string // The host to connect to. Values that start with / are for UNIX domain sockets. - DataDbPort string // The port to bind to. - DataDbName string // The name of the database to connect to. - DataDbUser string // The user to sign in as. - DataDbPass string // The user's password. - LoadHistorySize int // Maximum number of records to archive in load history - StorDBType string // Should reflect the database type used to store logs - StorDBHost string // The host to connect to. Values that start with / are for UNIX domain sockets. - StorDBPort string // Th e port to bind to. - StorDBName string // The name of the database to connect to. - StorDBUser string // The user to sign in as. - StorDBPass string // The user's password. - StorDBMaxOpenConns int // Maximum database connections opened - StorDBMaxIdleConns int // Maximum idle connections to keep opened - StorDBCDRSIndexes []string - DBDataEncoding string // The encoding used to store object data in strings: - RPCJSONListen string // RPC JSON listening address - RPCGOBListen string // RPC GOB listening address - HTTPListen string // HTTP listening address - DefaultReqType string // Use this request type if not defined on top - DefaultCategory string // set default type of record - DefaultTenant string // set default tenant - DefaultSubject string // set default rating subject, useful in case of fallback - DefaultTimezone string // default timezone for timestamps where not specified <""|UTC|Local|$IANA_TZ_DB> - Reconnects int // number of recconect attempts in case of connection lost <-1 for infinite | nb> - ConnectAttempts int // number of initial connection attempts before giving up - ResponseCacheTTL time.Duration // the life span of a cached response - InternalTtl time.Duration // maximum duration to wait for internal connections before giving up - RoundingDecimals int // Number of decimals to round end prices at - HttpSkipTlsVerify bool // If enabled Http Client will accept any TLS certificate - TpExportPath string // Path towards export folder for offline Tariff Plans - HttpFailedDir string // Directory path where we store failed http requests - MaxCallDuration time.Duration // The maximum call duration (used by responder when querying DerivedCharging) // ToDo: export it in configuration file - RaterEnabled bool // start standalone server (no balancer) - RaterBalancer string // balancer address host:port - RaterCdrStats string // address where to reach the cdrstats service. Empty to disable stats gathering <""|internal|x.y.z.y:1234> - RaterHistoryServer string - RaterPubSubServer string - RaterUserServer string - RaterAliasesServer string - BalancerEnabled bool - SchedulerEnabled bool - CDRSEnabled bool // Enable CDR Server service - CDRSExtraFields []*utils.RSRField // Extra fields to store in CDRs - CDRSStoreCdrs bool // store cdrs in storDb - CDRSRaterConns []*HaPoolConfig // address where to reach the Rater for cost calculation: <""|internal|x.y.z.y:1234> - CDRSPubSub string // address where to reach the pubsub service: <""|internal|x.y.z.y:1234> - CDRSUsers string // address where to reach the users service: <""|internal|x.y.z.y:1234> - CDRSAliases string // address where to reach the aliases service: <""|internal|x.y.z.y:1234> - CDRSStats string // address where to reach the cdrstats service. Empty to disable stats gathering <""|internal|x.y.z.y:1234> - CDRSCdrReplication []*CdrReplicationCfg // Replicate raw CDRs to a number of servers - CDRStatsEnabled bool // Enable CDR Stats service - CDRStatsSaveInterval time.Duration // Save interval duration - CdreProfiles map[string]*CdreConfig - CdrcProfiles map[string]map[string]*CdrcConfig // Number of CDRC instances running imports, format map[dirPath]map[instanceName]{Configs} - SmGenericConfig *SmGenericConfig - SmFsConfig *SmFsConfig // SM-FreeSWITCH configuration - SmKamConfig *SmKamConfig // SM-Kamailio Configuration - SmOsipsConfig *SmOsipsConfig // SM-OpenSIPS Configuration - diameterAgentCfg *DiameterAgentCfg // DiameterAgent configuration - HistoryServer string // Address where to reach the master history server: - HistoryServerEnabled bool // Starts History as server: . - HistoryDir string // Location on disk where to store history files. - HistorySaveInterval time.Duration // The timout duration between pubsub writes - PubSubServerEnabled bool // Starts PubSub as server: . - AliasesServerEnabled bool // Starts PubSub as server: . - UserServerEnabled bool // Starts User as server: - UserServerIndexes []string // List of user profile field indexes - MailerServer string // The server to use when sending emails out - MailerAuthUser string // Authenticate to email server using this user - MailerAuthPass string // Authenticate to email server with this password - MailerFromAddr string // From address used when sending emails out - DataFolderPath string // Path towards data folder, for tests internal usage, not loading out of .json options - sureTaxCfg *SureTaxCfg // Load here SureTax configuration, as pointer so we can have runtime reloads in the future - ConfigReloads map[string]chan struct{} // Signals to specific entities that a config reload should occur + TpDbType string + TpDbHost string // The host to connect to. Values that start with / are for UNIX domain sockets. + TpDbPort string // The port to bind to. + TpDbName string // The name of the database to connect to. + TpDbUser string // The user to sign in as. + TpDbPass string // The user's password. + DataDbType string + DataDbHost string // The host to connect to. Values that start with / are for UNIX domain sockets. + DataDbPort string // The port to bind to. + DataDbName string // The name of the database to connect to. + DataDbUser string // The user to sign in as. + DataDbPass string // The user's password. + LoadHistorySize int // Maximum number of records to archive in load history + StorDBType string // Should reflect the database type used to store logs + StorDBHost string // The host to connect to. Values that start with / are for UNIX domain sockets. + StorDBPort string // Th e port to bind to. + StorDBName string // The name of the database to connect to. + StorDBUser string // The user to sign in as. + StorDBPass string // The user's password. + StorDBMaxOpenConns int // Maximum database connections opened + StorDBMaxIdleConns int // Maximum idle connections to keep opened + StorDBCDRSIndexes []string + DBDataEncoding string // The encoding used to store object data in strings: + RPCJSONListen string // RPC JSON listening address + RPCGOBListen string // RPC GOB listening address + HTTPListen string // HTTP listening address + DefaultReqType string // Use this request type if not defined on top + DefaultCategory string // set default type of record + DefaultTenant string // set default tenant + DefaultSubject string // set default rating subject, useful in case of fallback + DefaultTimezone string // default timezone for timestamps where not specified <""|UTC|Local|$IANA_TZ_DB> + Reconnects int // number of recconect attempts in case of connection lost <-1 for infinite | nb> + ConnectAttempts int // number of initial connection attempts before giving up + ResponseCacheTTL time.Duration // the life span of a cached response + InternalTtl time.Duration // maximum duration to wait for internal connections before giving up + RoundingDecimals int // Number of decimals to round end prices at + HttpSkipTlsVerify bool // If enabled Http Client will accept any TLS certificate + TpExportPath string // Path towards export folder for offline Tariff Plans + HttpFailedDir string // Directory path where we store failed http requests + MaxCallDuration time.Duration // The maximum call duration (used by responder when querying DerivedCharging) // ToDo: export it in configuration file + RaterEnabled bool // start standalone server (no balancer) + RaterBalancer string // balancer address host:port + RaterCdrStats string // address where to reach the cdrstats service. Empty to disable stats gathering <""|internal|x.y.z.y:1234> + RaterHistoryServer string + RaterPubSubServer string + RaterUserServer string + RaterAliasesServer string + RpSubjectPrefixMatching bool // enables prefix matching for the rating profile subject + BalancerEnabled bool + SchedulerEnabled bool + CDRSEnabled bool // Enable CDR Server service + CDRSExtraFields []*utils.RSRField // Extra fields to store in CDRs + CDRSStoreCdrs bool // store cdrs in storDb + CDRSRaterConns []*HaPoolConfig // address where to reach the Rater for cost calculation: <""|internal|x.y.z.y:1234> + CDRSPubSubSConns []*HaPoolConfig // address where to reach the pubsub service: <""|internal|x.y.z.y:1234> + CDRSUserSConns []*HaPoolConfig // address where to reach the users service: <""|internal|x.y.z.y:1234> + CDRSAliaseSConns []*HaPoolConfig // address where to reach the aliases service: <""|internal|x.y.z.y:1234> + CDRSStatSConns []*HaPoolConfig // address where to reach the cdrstats service. Empty to disable stats gathering <""|internal|x.y.z.y:1234> + CDRSCdrReplication []*CdrReplicationCfg // Replicate raw CDRs to a number of servers + CDRStatsEnabled bool // Enable CDR Stats service + CDRStatsSaveInterval time.Duration // Save interval duration + CdreProfiles map[string]*CdreConfig + CdrcProfiles map[string]map[string]*CdrcConfig // Number of CDRC instances running imports, format map[dirPath]map[instanceName]{Configs} + SmGenericConfig *SmGenericConfig + SmFsConfig *SmFsConfig // SM-FreeSWITCH configuration + SmKamConfig *SmKamConfig // SM-Kamailio Configuration + SmOsipsConfig *SmOsipsConfig // SM-OpenSIPS Configuration + diameterAgentCfg *DiameterAgentCfg // DiameterAgent configuration + HistoryServer string // Address where to reach the master history server: + HistoryServerEnabled bool // Starts History as server: . + HistoryDir string // Location on disk where to store history files. + HistorySaveInterval time.Duration // The timout duration between pubsub writes + PubSubServerEnabled bool // Starts PubSub as server: . + AliasesServerEnabled bool // Starts PubSub as server: . + UserServerEnabled bool // Starts User as server: + UserServerIndexes []string // List of user profile field indexes + MailerServer string // The server to use when sending emails out + MailerAuthUser string // Authenticate to email server using this user + MailerAuthPass string // Authenticate to email server with this password + MailerFromAddr string // From address used when sending emails out + DataFolderPath string // Path towards data folder, for tests internal usage, not loading out of .json options + sureTaxCfg *SureTaxCfg // Load here SureTax configuration, as pointer so we can have runtime reloads in the future + ConfigReloads map[string]chan struct{} // Signals to specific entities that a config reload should occur // Cache defaults loaded from json and needing clones dfltCdreProfile *CdreConfig // Default cdreConfig profile dfltCdrcProfile *CdrcConfig // Default cdrcConfig profile @@ -283,17 +284,25 @@ func (self *CGRConfig) checkConfigSanity() error { return errors.New("Rater not enabled but requested by CDRS component.") } } - if self.CDRSPubSub == utils.INTERNAL && !self.PubSubServerEnabled { - return errors.New("PubSub service not enabled but requested by CDRS component.") + for _, connCfg := range self.CDRSPubSubSConns { + if connCfg.Server == utils.INTERNAL && !self.PubSubServerEnabled { + return errors.New("PubSubS not enabled but requested by CDRS component.") + } } - if self.CDRSUsers == utils.INTERNAL && !self.UserServerEnabled { - return errors.New("Users service not enabled but requested by CDRS component.") + for _, connCfg := range self.CDRSUserSConns { + if connCfg.Server == utils.INTERNAL && !self.UserServerEnabled { + return errors.New("UserS not enabled but requested by CDRS component.") + } } - if self.CDRSAliases == utils.INTERNAL && !self.AliasesServerEnabled { - return errors.New("Aliases service not enabled but requested by CDRS component.") + for _, connCfg := range self.CDRSAliaseSConns { + if connCfg.Server == utils.INTERNAL && !self.AliasesServerEnabled { + return errors.New("AliaseS not enabled but requested by CDRS component.") + } } - if self.CDRSStats == utils.INTERNAL && !self.CDRStatsEnabled { - return errors.New("CDRStats not enabled but requested by CDRS component.") + for _, connCfg := range self.CDRSStatSConns { + if connCfg.Server == utils.INTERNAL && !self.CDRStatsEnabled { + return errors.New("CDRStatS not enabled but requested by CDRS component.") + } } } // CDRC sanity checks @@ -686,6 +695,9 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { if jsnRaterCfg.Users != nil { self.RaterUserServer = *jsnRaterCfg.Users } + if jsnRaterCfg.Rp_subject_prefix_matching != nil { + self.RpSubjectPrefixMatching = *jsnRaterCfg.Rp_subject_prefix_matching + } } if jsnBalancerCfg != nil && jsnBalancerCfg.Enabled != nil { @@ -708,7 +720,6 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { if jsnCdrsCfg.Store_cdrs != nil { self.CDRSStoreCdrs = *jsnCdrsCfg.Store_cdrs } - if jsnCdrsCfg.Rater_conns != nil { self.CDRSRaterConns = make([]*HaPoolConfig, len(*jsnCdrsCfg.Rater_conns)) for idx, jsnHaCfg := range *jsnCdrsCfg.Rater_conns { @@ -716,17 +727,33 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { self.CDRSRaterConns[idx].loadFromJsonCfg(jsnHaCfg) } } - if jsnCdrsCfg.Pubsubs != nil { - self.CDRSPubSub = *jsnCdrsCfg.Pubsubs + if jsnCdrsCfg.Pubsubs_conns != nil { + self.CDRSPubSubSConns = make([]*HaPoolConfig, len(*jsnCdrsCfg.Pubsubs_conns)) + for idx, jsnHaCfg := range *jsnCdrsCfg.Pubsubs_conns { + self.CDRSPubSubSConns[idx] = NewDfltHaPoolConfig() + self.CDRSPubSubSConns[idx].loadFromJsonCfg(jsnHaCfg) + } } - if jsnCdrsCfg.Users != nil { - self.CDRSUsers = *jsnCdrsCfg.Users + if jsnCdrsCfg.Users_conns != nil { + self.CDRSUserSConns = make([]*HaPoolConfig, len(*jsnCdrsCfg.Users_conns)) + for idx, jsnHaCfg := range *jsnCdrsCfg.Users_conns { + self.CDRSUserSConns[idx] = NewDfltHaPoolConfig() + self.CDRSUserSConns[idx].loadFromJsonCfg(jsnHaCfg) + } } - if jsnCdrsCfg.Aliases != nil { - self.CDRSAliases = *jsnCdrsCfg.Aliases + if jsnCdrsCfg.Aliases_conns != nil { + self.CDRSAliaseSConns = make([]*HaPoolConfig, len(*jsnCdrsCfg.Aliases_conns)) + for idx, jsnHaCfg := range *jsnCdrsCfg.Aliases_conns { + self.CDRSAliaseSConns[idx] = NewDfltHaPoolConfig() + self.CDRSAliaseSConns[idx].loadFromJsonCfg(jsnHaCfg) + } } - if jsnCdrsCfg.Cdrstats != nil { - self.CDRSStats = *jsnCdrsCfg.Cdrstats + if jsnCdrsCfg.Cdrstats_conns != nil { + self.CDRSStatSConns = make([]*HaPoolConfig, len(*jsnCdrsCfg.Cdrstats_conns)) + for idx, jsnHaCfg := range *jsnCdrsCfg.Cdrstats_conns { + self.CDRSStatSConns[idx] = NewDfltHaPoolConfig() + self.CDRSStatSConns[idx].loadFromJsonCfg(jsnHaCfg) + } } if jsnCdrsCfg.Cdr_replication != nil { self.CDRSCdrReplication = make([]*CdrReplicationCfg, len(*jsnCdrsCfg.Cdr_replication)) diff --git a/config/config_defaults.go b/config/config_defaults.go index cc66ebe52..efa97ff29 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -99,6 +99,7 @@ const CGRATES_CFG_JSON = ` "pubsubs": "", // address where to reach the pubusb service, empty to disable pubsub functionality: <""|internal|x.y.z.y:1234> "users": "", // address where to reach the user service, empty to disable user profile functionality: <""|internal|x.y.z.y:1234> "aliases": "", // address where to reach the aliases service, empty to disable aliases functionality: <""|internal|x.y.z.y:1234> + "rp_subject_prefix_matching": false // enables prefix matching for the rating profile subject }, @@ -112,13 +113,13 @@ const CGRATES_CFG_JSON = ` "extra_fields": [], // extra fields to store in CDRs for non-generic CDRs "store_cdrs": true, // store cdrs in storDb "rater_conns": [ - {"server":"internal"} // address where to reach the Rater for cost calculation, empty to disable functionality: <""|internal|x.y.z.y:1234> - ], - "pubsubs": "", // address where to reach the pubusb service, empty to disable pubsub functionality: <""|internal|x.y.z.y:1234> - "users": "", // address where to reach the user service, empty to disable user profile functionality: <""|internal|x.y.z.y:1234> - "aliases": "", // address where to reach the aliases service, empty to disable aliases functionality: <""|internal|x.y.z.y:1234> - "cdrstats": "", // address where to reach the cdrstats service, empty to disable stats functionality<""|internal|x.y.z.y:1234> - "cdr_replication":[], // replicate the raw CDR to a number of servers + {"server": "internal"} // address where to reach the Rater for cost calculation, empty to disable functionality: <""|internal|x.y.z.y:1234> + ], + "pubsubs_conns": [], // address where to reach the pubusb service, empty to disable pubsub functionality: <""|internal|x.y.z.y:1234> + "users_conns": [], // address where to reach the user service, empty to disable user profile functionality: <""|internal|x.y.z.y:1234> + "aliases_conns": [], // address where to reach the aliases service, empty to disable aliases functionality: <""|internal|x.y.z.y:1234> + "cdrstats_conns": [], // address where to reach the cdrstats service, empty to disable stats functionality<""|internal|x.y.z.y:1234> + "cdr_replication":[] // replicate the raw CDR to a number of servers }, @@ -286,7 +287,8 @@ const CGRATES_CFG_JSON = ` "dictionaries_dir": "/usr/share/cgrates/diameter/dict/", // path towards directory holding additional dictionaries to load "sm_generic_conns": [ {"server": "internal"} // connection towards SMG component for session management - ], // connection towards SMG component for session management + ], + "pubsubs": [], // address where to reach the pubusb service, empty to disable pubsub functionality: <""|internal|x.y.z.y:1234> "create_cdr": true, // create CDR out of CCR terminate and send it to SMG component "debit_interval": "5m", // interval for CCR updates "timezone": "", // timezone for timestamps where not specified, empty for general defaults <""|UTC|Local|$IANA_TZ_DB> @@ -297,10 +299,13 @@ const CGRATES_CFG_JSON = ` "product_name": "CGRateS", // diameter Product-Name AVP used in replies "request_processors": [ { - "id": "*default", // formal identifier of this processor - "dry_run": false, // do not send the events to SMG, just log them - "request_filter": "Subscription-Id>Subscription-Id-Type(0)", // filter requests processed by this processor + "id": "*default", // formal identifier of this processor + "dry_run": false, // do not send the events to SMG, just log them + "publish_event": false, // if enabled, it will publish internal event to pubsub + "request_filter": "Subscription-Id>Subscription-Id-Type(0)", // filter requests processed by this processor + "flags": [], // flags to influence processing behavior "continue_on_success": false, // continue to the next template if executed + "append_cca": true, // when continuing will append cca fields to the previous ones "ccr_fields":[ // import content_fields template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value {"tag": "TOR", "field_id": "ToR", "type": "*composed", "value": "^*voice", "mandatory": true}, {"tag": "OriginID", "field_id": "OriginID", "type": "*composed", "value": "Session-Id", "mandatory": true}, diff --git a/config/config_json_test.go b/config/config_json_test.go index 36a779fff..e79f01aa0 100644 --- a/config/config_json_test.go +++ b/config/config_json_test.go @@ -56,7 +56,7 @@ func TestDfGeneralJsonCfg(t *testing.T) { if gCfg, err := dfCgrJsonCfg.GeneralJsonCfg(); err != nil { t.Error(err) } else if !reflect.DeepEqual(eCfg, gCfg) { - t.Error("Received: ", gCfg) + t.Error("Received: ", utils.ToIJSON(gCfg)) } } @@ -129,7 +129,7 @@ func TestDfBalancerJsonCfg(t *testing.T) { func TestDfRaterJsonCfg(t *testing.T) { eCfg := &RaterJsonCfg{Enabled: utils.BoolPointer(false), Balancer: utils.StringPointer(""), Cdrstats: utils.StringPointer(""), - Historys: utils.StringPointer(""), Pubsubs: utils.StringPointer(""), Users: utils.StringPointer(""), Aliases: utils.StringPointer("")} + Historys: utils.StringPointer(""), Pubsubs: utils.StringPointer(""), Users: utils.StringPointer(""), Aliases: utils.StringPointer(""), Rp_subject_prefix_matching: utils.BoolPointer(false)} if cfg, err := dfCgrJsonCfg.RaterJsonCfg(); err != nil { t.Error(err) } else if !reflect.DeepEqual(eCfg, cfg) { @@ -155,10 +155,10 @@ func TestDfCdrsJsonCfg(t *testing.T) { &HaPoolJsonCfg{ Server: utils.StringPointer("internal"), }}, - Pubsubs: utils.StringPointer(""), - Users: utils.StringPointer(""), - Aliases: utils.StringPointer(""), - Cdrstats: utils.StringPointer(""), + Pubsubs_conns: &[]*HaPoolJsonCfg{}, + Users_conns: &[]*HaPoolJsonCfg{}, + Aliases_conns: &[]*HaPoolJsonCfg{}, + Cdrstats_conns: &[]*HaPoolJsonCfg{}, Cdr_replication: &[]*CdrReplicationJsonCfg{}, } if cfg, err := dfCgrJsonCfg.CdrsJsonCfg(); err != nil { @@ -456,6 +456,7 @@ func TestDiameterAgentJsonCfg(t *testing.T) { &HaPoolJsonCfg{ Server: utils.StringPointer("internal"), }}, + Pubsub_conns: nil, Create_cdr: utils.BoolPointer(true), Debit_interval: utils.StringPointer("5m"), Timezone: utils.StringPointer(""), @@ -468,8 +469,11 @@ func TestDiameterAgentJsonCfg(t *testing.T) { &DARequestProcessorJsnCfg{ Id: utils.StringPointer("*default"), Dry_run: utils.BoolPointer(false), + Publish_event: utils.BoolPointer(false), Request_filter: utils.StringPointer("Subscription-Id>Subscription-Id-Type(0)"), + Flags: utils.StringSlicePointer([]string{}), Continue_on_success: utils.BoolPointer(false), + Append_cca: utils.BoolPointer(true), CCR_fields: &[]*CdrFieldJsonCfg{ &CdrFieldJsonCfg{Tag: utils.StringPointer("TOR"), Field_id: utils.StringPointer(utils.TOR), Type: utils.StringPointer(utils.META_COMPOSED), Value: utils.StringPointer("^*voice"), Mandatory: utils.BoolPointer(true)}, diff --git a/config/daconfig.go b/config/daconfig.go index 0a2588240..ee7adad15 100644 --- a/config/daconfig.go +++ b/config/daconfig.go @@ -29,6 +29,7 @@ type DiameterAgentCfg struct { Listen string // address where to listen for diameter requests DictionariesDir string SMGenericConns []*HaPoolConfig // connections towards SMG component + PubSubConns []*HaPoolConfig // connection towards pubsubs CreateCDR bool DebitInterval time.Duration Timezone string // timezone for timestamps where not specified <""|UTC|Local|$IANA_TZ_DB> @@ -60,6 +61,13 @@ func (self *DiameterAgentCfg) loadFromJsonCfg(jsnCfg *DiameterAgentJsonCfg) erro self.SMGenericConns[idx].loadFromJsonCfg(jsnHaCfg) } } + if jsnCfg.Pubsub_conns != nil { + self.PubSubConns = make([]*HaPoolConfig, len(*jsnCfg.Pubsub_conns)) + for idx, jsnHaCfg := range *jsnCfg.Pubsub_conns { + self.PubSubConns[idx] = NewDfltHaPoolConfig() + self.PubSubConns[idx].loadFromJsonCfg(jsnHaCfg) + } + } if jsnCfg.Create_cdr != nil { self.CreateCDR = *jsnCfg.Create_cdr } @@ -106,8 +114,11 @@ func (self *DiameterAgentCfg) loadFromJsonCfg(jsnCfg *DiameterAgentJsonCfg) erro type DARequestProcessor struct { Id string DryRun bool + PublishEvent bool RequestFilter utils.RSRFields + Flags utils.StringMap // Various flags to influence behavior ContinueOnSuccess bool + AppendCCA bool CCRFields []*CfgCdrField CCAFields []*CfgCdrField } @@ -122,12 +133,24 @@ func (self *DARequestProcessor) loadFromJsonCfg(jsnCfg *DARequestProcessorJsnCfg if jsnCfg.Dry_run != nil { self.DryRun = *jsnCfg.Dry_run } + if jsnCfg.Publish_event != nil { + self.PublishEvent = *jsnCfg.Publish_event + } var err error if jsnCfg.Request_filter != nil { if self.RequestFilter, err = utils.ParseRSRFields(*jsnCfg.Request_filter, utils.INFIELD_SEP); err != nil { return err } } + if jsnCfg.Flags != nil { + self.Flags = utils.StringMapFromSlice(*jsnCfg.Flags) + } + if jsnCfg.Continue_on_success != nil { + self.ContinueOnSuccess = *jsnCfg.Continue_on_success + } + if jsnCfg.Append_cca != nil { + self.AppendCCA = *jsnCfg.Append_cca + } if jsnCfg.CCR_fields != nil { if self.CCRFields, err = CfgCdrFieldsFromCdrFieldsJsonCfg(*jsnCfg.CCR_fields); err != nil { return err diff --git a/config/libconfig_json.go b/config/libconfig_json.go index f751cf83c..3c0e67a50 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -64,13 +64,14 @@ type BalancerJsonCfg struct { // Rater config section type RaterJsonCfg struct { - Enabled *bool - Balancer *string - Cdrstats *string - Historys *string - Pubsubs *string - Aliases *string - Users *string + Enabled *bool + Balancer *string + Cdrstats *string + Historys *string + Pubsubs *string + Aliases *string + Users *string + Rp_subject_prefix_matching *bool } // Scheduler config section @@ -84,10 +85,10 @@ type CdrsJsonCfg struct { Extra_fields *[]string Store_cdrs *bool Rater_conns *[]*HaPoolJsonCfg - Pubsubs *string - Users *string - Aliases *string - Cdrstats *string + Pubsubs_conns *[]*HaPoolJsonCfg + Users_conns *[]*HaPoolJsonCfg + Aliases_conns *[]*HaPoolJsonCfg + Cdrstats_conns *[]*HaPoolJsonCfg Cdr_replication *[]*CdrReplicationJsonCfg } @@ -250,6 +251,7 @@ type DiameterAgentJsonCfg struct { Listen *string // address where to listen for diameter requests Dictionaries_dir *string // path towards additional dictionaries Sm_generic_conns *[]*HaPoolJsonCfg // Connections towards generic SM + Pubsub_conns *[]*HaPoolJsonCfg // connection towards pubsubs Create_cdr *bool Debit_interval *string Timezone *string // timezone for timestamps where not specified <""|UTC|Local|$IANA_TZ_DB> @@ -265,8 +267,11 @@ type DiameterAgentJsonCfg struct { type DARequestProcessorJsnCfg struct { Id *string Dry_run *bool + Publish_event *bool Request_filter *string + Flags *[]string Continue_on_success *bool + Append_cca *bool CCR_fields *[]*CdrFieldJsonCfg CCA_fields *[]*CdrFieldJsonCfg } diff --git a/console/account_set.go b/console/account_set.go index d81b0c38e..5906d32db 100644 --- a/console/account_set.go +++ b/console/account_set.go @@ -18,12 +18,12 @@ along with this program. If not, see package console -import "github.com/cgrates/cgrates/utils" +import "github.com/cgrates/cgrates/apier/v2" func init() { c := &CmdAddAccount{ name: "account_set", - rpcMethod: "ApierV1.SetAccount", + rpcMethod: "ApierV2.SetAccount", } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} @@ -33,7 +33,7 @@ func init() { type CmdAddAccount struct { name string rpcMethod string - rpcParams *utils.AttrSetAccount + rpcParams *v2.AttrSetAccount *CommandExecuter } @@ -47,7 +47,7 @@ func (self *CmdAddAccount) RpcMethod() string { func (self *CmdAddAccount) RpcParams(reset bool) interface{} { if reset || self.rpcParams == nil { - self.rpcParams = &utils.AttrSetAccount{} + self.rpcParams = &v2.AttrSetAccount{} } return self.rpcParams } diff --git a/console/balance_enabledisable.go b/console/balance_add.go similarity index 72% rename from console/balance_enabledisable.go rename to console/balance_add.go index 975d19ae7..b60a59380 100644 --- a/console/balance_enabledisable.go +++ b/console/balance_add.go @@ -24,42 +24,42 @@ import ( ) func init() { - c := &CmdEnableDisableBalance{ - name: "balance_enabledisable", - rpcMethod: "ApierV1.EnableDisableBalance", + c := &CmdAddBalance{ + name: "balance_add", + rpcMethod: "ApierV1.AddBalance", } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} } // Commander implementation -type CmdEnableDisableBalance struct { +type CmdAddBalance struct { name string rpcMethod string rpcParams *v1.AttrAddBalance *CommandExecuter } -func (self *CmdEnableDisableBalance) Name() string { +func (self *CmdAddBalance) Name() string { return self.name } -func (self *CmdEnableDisableBalance) RpcMethod() string { +func (self *CmdAddBalance) RpcMethod() string { return self.rpcMethod } -func (self *CmdEnableDisableBalance) RpcParams(reset bool) interface{} { +func (self *CmdAddBalance) RpcParams(reset bool) interface{} { if reset || self.rpcParams == nil { self.rpcParams = &v1.AttrAddBalance{BalanceType: utils.MONETARY, Overwrite: false} } return self.rpcParams } -func (self *CmdEnableDisableBalance) PostprocessRpcParams() error { +func (self *CmdAddBalance) PostprocessRpcParams() error { return nil } -func (self *CmdEnableDisableBalance) RpcResult() interface{} { +func (self *CmdAddBalance) RpcResult() interface{} { var s string return &s } diff --git a/console/balance_debit.go b/console/balance_remove.go similarity index 51% rename from console/balance_debit.go rename to console/balance_remove.go index ab6f45ec8..b01324f6f 100644 --- a/console/balance_debit.go +++ b/console/balance_remove.go @@ -18,51 +18,48 @@ along with this program. If not, see package console -import "github.com/cgrates/cgrates/engine" +import ( + "github.com/cgrates/cgrates/apier/v1" + "github.com/cgrates/cgrates/utils" +) func init() { - c := &CmdDebitBalance{ - name: "balance_debit", - rpcMethod: "Responder.Debit", - rpcParams: &engine.CallDescriptor{Direction: "*out"}, - clientArgs: []string{"Direction", "Category", "TOR", "Tenant", "Subject", "Account", "Destination", "TimeStart", "TimeEnd", "CallDuration", "FallbackSubject"}, + c := &CmdRemoveBalance{ + name: "balance_remove", + rpcMethod: "ApierV1.RemoveBalances", } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} } // Commander implementation -type CmdDebitBalance struct { - name string - rpcMethod string - rpcParams *engine.CallDescriptor - clientArgs []string +type CmdRemoveBalance struct { + name string + rpcMethod string + rpcParams *v1.AttrAddBalance *CommandExecuter } -func (self *CmdDebitBalance) Name() string { +func (self *CmdRemoveBalance) Name() string { return self.name } -func (self *CmdDebitBalance) RpcMethod() string { +func (self *CmdRemoveBalance) RpcMethod() string { return self.rpcMethod } -func (self *CmdDebitBalance) RpcParams(reset bool) interface{} { +func (self *CmdRemoveBalance) RpcParams(reset bool) interface{} { if reset || self.rpcParams == nil { - self.rpcParams = &engine.CallDescriptor{Direction: "*out"} + self.rpcParams = &v1.AttrAddBalance{BalanceType: utils.MONETARY, Overwrite: false} } return self.rpcParams } -func (self *CmdDebitBalance) PostprocessRpcParams() error { +func (self *CmdRemoveBalance) PostprocessRpcParams() error { return nil } -func (self *CmdDebitBalance) RpcResult() interface{} { - return &engine.CallCost{} -} - -func (self *CmdDebitBalance) ClientArgs() []string { - return self.clientArgs +func (self *CmdRemoveBalance) RpcResult() interface{} { + var s string + return &s } diff --git a/console/balance_set.go b/console/balance_set.go index 89f6a70c5..bc52e8489 100644 --- a/console/balance_set.go +++ b/console/balance_set.go @@ -24,42 +24,42 @@ import ( ) func init() { - c := &CmdAddBalance{ + c := &CmdSetBalance{ name: "balance_set", - rpcMethod: "ApierV1.AddBalance", + rpcMethod: "ApierV1.SetBalance", } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} } // Commander implementation -type CmdAddBalance struct { +type CmdSetBalance struct { name string rpcMethod string - rpcParams *v1.AttrAddBalance + rpcParams *v1.AttrSetBalance *CommandExecuter } -func (self *CmdAddBalance) Name() string { +func (self *CmdSetBalance) Name() string { return self.name } -func (self *CmdAddBalance) RpcMethod() string { +func (self *CmdSetBalance) RpcMethod() string { return self.rpcMethod } -func (self *CmdAddBalance) RpcParams(reset bool) interface{} { +func (self *CmdSetBalance) RpcParams(reset bool) interface{} { if reset || self.rpcParams == nil { - self.rpcParams = &v1.AttrAddBalance{BalanceType: utils.MONETARY, Overwrite: false} + self.rpcParams = &v1.AttrSetBalance{BalanceType: utils.MONETARY} } return self.rpcParams } -func (self *CmdAddBalance) PostprocessRpcParams() error { +func (self *CmdSetBalance) PostprocessRpcParams() error { return nil } -func (self *CmdAddBalance) RpcResult() interface{} { +func (self *CmdSetBalance) RpcResult() interface{} { var s string return &s } diff --git a/console/command_executer.go b/console/command_executer.go index bf836da45..27d0ded67 100644 --- a/console/command_executer.go +++ b/console/command_executer.go @@ -67,7 +67,10 @@ func (ce *CommandExecuter) clientArgs(iface interface{}) (args []string) { for i := 0; i < typ.NumField(); i++ { valField := val.Field(i) typeField := typ.Field(i) - //log.Printf("%v (%v)", typeField.Name, valField.Kind()) + //log.Printf("%v (%v : %v)", typeField.Name, valField.Kind(), typeField.PkgPath) + if len(typeField.PkgPath) > 0 { //unexported field + continue + } switch valField.Kind() { case reflect.Ptr, reflect.Struct: if valField.Kind() == reflect.Ptr { @@ -79,6 +82,7 @@ func (ce *CommandExecuter) clientArgs(iface interface{}) (args []string) { } } args = append(args, ce.clientArgs(valField.Interface())...) + default: args = append(args, typeField.Name) } diff --git a/console/debit.go b/console/debit.go index bf5a2b185..19d8c308d 100644 --- a/console/debit.go +++ b/console/debit.go @@ -24,7 +24,7 @@ func init() { c := &CmdDebit{ name: "debit", rpcMethod: "Responder.Debit", - clientArgs: []string{"Direction", "Category", "TOR", "Tenant", "Subject", "Account", "Destination", "TimeStart", "TimeEnd", "CallDuration", "FallbackSubject"}, + clientArgs: []string{"Direction", "Category", "TOR", "Tenant", "Subject", "Account", "Destination", "TimeStart", "TimeEnd", "CallDuration", "FallbackSubject", "DryRun"}, } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} diff --git a/console/debit_fake.go b/console/debit_max.go similarity index 76% rename from console/debit_fake.go rename to console/debit_max.go index 60c9e4efc..291b8281d 100644 --- a/console/debit_fake.go +++ b/console/debit_max.go @@ -21,9 +21,9 @@ package console import "github.com/cgrates/cgrates/engine" func init() { - c := &CmdFakeDebit{ - name: "debit_fake", - rpcMethod: "Responder.FakeDebit", + c := &CmdMaxDebit{ + name: "debit_max", + rpcMethod: "Responder.MaxDebit", clientArgs: []string{"Direction", "Category", "TOR", "Tenant", "Subject", "Account", "Destination", "TimeStart", "TimeEnd", "CallDuration", "FallbackSubject"}, } commands[c.Name()] = c @@ -31,7 +31,7 @@ func init() { } // Commander implementation -type CmdFakeDebit struct { +type CmdMaxDebit struct { name string rpcMethod string rpcParams *engine.CallDescriptor @@ -39,29 +39,29 @@ type CmdFakeDebit struct { *CommandExecuter } -func (self *CmdFakeDebit) Name() string { +func (self *CmdMaxDebit) Name() string { return self.name } -func (self *CmdFakeDebit) RpcMethod() string { +func (self *CmdMaxDebit) RpcMethod() string { return self.rpcMethod } -func (self *CmdFakeDebit) RpcParams(reset bool) interface{} { +func (self *CmdMaxDebit) RpcParams(reset bool) interface{} { if reset || self.rpcParams == nil { self.rpcParams = &engine.CallDescriptor{Direction: "*out"} } return self.rpcParams } -func (self *CmdFakeDebit) PostprocessRpcParams() error { +func (self *CmdMaxDebit) PostprocessRpcParams() error { return nil } -func (self *CmdFakeDebit) RpcResult() interface{} { +func (self *CmdMaxDebit) RpcResult() interface{} { return &engine.CallCost{} } -func (self *CmdFakeDebit) ClientArgs() []string { +func (self *CmdMaxDebit) ClientArgs() []string { return self.clientArgs } diff --git a/console/scheduler_execute.go b/console/scheduler_execute.go new file mode 100644 index 000000000..172d3e833 --- /dev/null +++ b/console/scheduler_execute.go @@ -0,0 +1,63 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2012-2015 ITsysCOM + +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 console + +import "github.com/cgrates/cgrates/apier/v1" + +func init() { + c := &CmdExecuteScheduledActions{ + name: "scheduler_execute", + rpcMethod: "ApierV1.ExecuteScheduledActions", + rpcParams: &v1.AttrsExecuteScheduledActions{}, + } + commands[c.Name()] = c + c.CommandExecuter = &CommandExecuter{c} +} + +// Commander implementation +type CmdExecuteScheduledActions struct { + name string + rpcMethod string + rpcParams *v1.AttrsExecuteScheduledActions + *CommandExecuter +} + +func (self *CmdExecuteScheduledActions) Name() string { + return self.name +} + +func (self *CmdExecuteScheduledActions) RpcMethod() string { + return self.rpcMethod +} + +func (self *CmdExecuteScheduledActions) RpcParams(reset bool) interface{} { + if reset || self.rpcParams == nil { + self.rpcParams = &v1.AttrsExecuteScheduledActions{} + } + return self.rpcParams +} + +func (self *CmdExecuteScheduledActions) PostprocessRpcParams() error { + return nil +} + +func (self *CmdExecuteScheduledActions) RpcResult() interface{} { + var s string + return &s +} diff --git a/console/triggeredaction_add.go b/console/trigger_add.go similarity index 66% rename from console/triggeredaction_add.go rename to console/trigger_add.go index 298782b6c..bef41f910 100644 --- a/console/triggeredaction_add.go +++ b/console/trigger_add.go @@ -21,42 +21,43 @@ package console import "github.com/cgrates/cgrates/apier/v1" func init() { - c := &CmdAddTriggeredAction{ - name: "triggeredaction_add", - rpcMethod: "ApierV1.AddTriggeredAction", + c := &CmdAddTriggers{ + name: "triggers_add", + rpcMethod: "ApierV1.AddAccountActionTriggers", + rpcParams: &v1.AttrAddAccountActionTriggers{}, } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} } // Commander implementation -type CmdAddTriggeredAction struct { +type CmdAddTriggers struct { name string rpcMethod string - rpcParams *v1.AttrAddActionTrigger + rpcParams *v1.AttrAddAccountActionTriggers *CommandExecuter } -func (self *CmdAddTriggeredAction) Name() string { +func (self *CmdAddTriggers) Name() string { return self.name } -func (self *CmdAddTriggeredAction) RpcMethod() string { +func (self *CmdAddTriggers) RpcMethod() string { return self.rpcMethod } -func (self *CmdAddTriggeredAction) RpcParams(reset bool) interface{} { +func (self *CmdAddTriggers) RpcParams(reset bool) interface{} { if reset || self.rpcParams == nil { - self.rpcParams = &v1.AttrAddActionTrigger{BalanceDirection: "*out"} + self.rpcParams = &v1.AttrAddAccountActionTriggers{} } return self.rpcParams } -func (self *CmdAddTriggeredAction) PostprocessRpcParams() error { +func (self *CmdAddTriggers) PostprocessRpcParams() error { return nil } -func (self *CmdAddTriggeredAction) RpcResult() interface{} { +func (self *CmdAddTriggers) RpcResult() interface{} { var s string return &s } diff --git a/console/trigger_remove.go b/console/trigger_remove.go new file mode 100644 index 000000000..a460d8528 --- /dev/null +++ b/console/trigger_remove.go @@ -0,0 +1,63 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2012-2015 ITsysCOM + +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 console + +import "github.com/cgrates/cgrates/apier/v1" + +func init() { + c := &CmdRemoveTriggers{ + name: "triggers_remove", + rpcMethod: "ApierV1.RemoveAccountActionTriggers", + rpcParams: &v1.AttrRemoveAccountActionTriggers{}, + } + commands[c.Name()] = c + c.CommandExecuter = &CommandExecuter{c} +} + +// Commander implementation +type CmdRemoveTriggers struct { + name string + rpcMethod string + rpcParams *v1.AttrRemoveAccountActionTriggers + *CommandExecuter +} + +func (self *CmdRemoveTriggers) Name() string { + return self.name +} + +func (self *CmdRemoveTriggers) RpcMethod() string { + return self.rpcMethod +} + +func (self *CmdRemoveTriggers) RpcParams(reset bool) interface{} { + if reset || self.rpcParams == nil { + self.rpcParams = &v1.AttrRemoveAccountActionTriggers{} + } + return self.rpcParams +} + +func (self *CmdRemoveTriggers) PostprocessRpcParams() error { + return nil +} + +func (self *CmdRemoveTriggers) RpcResult() interface{} { + var s string + return &s +} diff --git a/console/trigger_reset.go b/console/trigger_reset.go new file mode 100644 index 000000000..0d5be9f24 --- /dev/null +++ b/console/trigger_reset.go @@ -0,0 +1,63 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2012-2015 ITsysCOM + +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 console + +import "github.com/cgrates/cgrates/apier/v1" + +func init() { + c := &CmdResetTriggers{ + name: "triggers_reset", + rpcMethod: "ApierV1.ResetAccountActionTriggers", + rpcParams: &v1.AttrRemoveAccountActionTriggers{}, + } + commands[c.Name()] = c + c.CommandExecuter = &CommandExecuter{c} +} + +// Commander implementation +type CmdResetTriggers struct { + name string + rpcMethod string + rpcParams *v1.AttrRemoveAccountActionTriggers + *CommandExecuter +} + +func (self *CmdResetTriggers) Name() string { + return self.name +} + +func (self *CmdResetTriggers) RpcMethod() string { + return self.rpcMethod +} + +func (self *CmdResetTriggers) RpcParams(reset bool) interface{} { + if reset || self.rpcParams == nil { + self.rpcParams = &v1.AttrRemoveAccountActionTriggers{} + } + return self.rpcParams +} + +func (self *CmdResetTriggers) PostprocessRpcParams() error { + return nil +} + +func (self *CmdResetTriggers) RpcResult() interface{} { + var s string + return &s +} diff --git a/console/trigger_set.go b/console/trigger_set.go new file mode 100644 index 000000000..2e9095d95 --- /dev/null +++ b/console/trigger_set.go @@ -0,0 +1,63 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2012-2015 ITsysCOM + +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 console + +import "github.com/cgrates/cgrates/apier/v1" + +func init() { + c := &CmdSetTriggers{ + name: "triggers_set", + rpcMethod: "ApierV1.SetAccountActionTriggers", + rpcParams: &v1.AttrSetAccountActionTriggers{}, + } + commands[c.Name()] = c + c.CommandExecuter = &CommandExecuter{c} +} + +// Commander implementation +type CmdSetTriggers struct { + name string + rpcMethod string + rpcParams *v1.AttrSetAccountActionTriggers + *CommandExecuter +} + +func (self *CmdSetTriggers) Name() string { + return self.name +} + +func (self *CmdSetTriggers) RpcMethod() string { + return self.rpcMethod +} + +func (self *CmdSetTriggers) RpcParams(reset bool) interface{} { + if reset || self.rpcParams == nil { + self.rpcParams = &v1.AttrSetAccountActionTriggers{} + } + return self.rpcParams +} + +func (self *CmdSetTriggers) PostprocessRpcParams() error { + return nil +} + +func (self *CmdSetTriggers) RpcResult() interface{} { + var s string + return &s +} diff --git a/data/conf/cgrates/cgrates.json b/data/conf/cgrates/cgrates.json index d33530d7c..e39be9061 100644 --- a/data/conf/cgrates/cgrates.json +++ b/data/conf/cgrates/cgrates.json @@ -77,6 +77,7 @@ // "pubsubs": "", // address where to reach the pubusb service, empty to disable pubsub functionality: <""|internal|x.y.z.y:1234> // "users": "", // address where to reach the user service, empty to disable user profile functionality: <""|internal|x.y.z.y:1234> // "aliases": "", // address where to reach the aliases service, empty to disable aliases functionality: <""|internal|x.y.z.y:1234> +// "rp_subject_prefix_matching": false, // enables prefix matching for the rating profile subject //}, diff --git a/data/conf/samples/cgradmin/cgradmin.json b/data/conf/samples/cgradmin/cgradmin.json index e09f94c20..2f6f9e4a1 100644 --- a/data/conf/samples/cgradmin/cgradmin.json +++ b/data/conf/samples/cgradmin/cgradmin.json @@ -10,11 +10,25 @@ "http": ":2080", // HTTP listening address }, +//"tariffplan_db": { // database used to store offline tariff plans and CDRs +// "db_type": "mongo", // stor database type to use: +// "db_host": "127.0.0.1", // the host to connect to +// "db_port": 27017, // the port to reach the stordb +// "db_name": "tpdb", +//}, +// +//"data_db": { // database used to store offline tariff plans and CDRs +// "db_type": "mongo", // stor database type to use: +// "db_host": "127.0.0.1", // the host to connect to +// "db_port": 27017, // the port to reach the stordb +// "db_name": "datadb", +//}, + "stor_db": { // database used to store offline tariff plans and CDRs "db_type": "mongo", // stor database type to use: "db_host": "127.0.0.1", // the host to connect to "db_port": 27017, // the port to reach the stordb - "db_name": "cgrates", + "db_name": "stordb", }, "rater": { diff --git a/data/conf/samples/dmtagent/cgrates.json b/data/conf/samples/dmtagent/cgrates.json index 676d330bc..90a610dae 100644 --- a/data/conf/samples/dmtagent/cgrates.json +++ b/data/conf/samples/dmtagent/cgrates.json @@ -59,6 +59,7 @@ "diameter_agent": { "enabled": true, + "pubsubs": "internal", }, } diff --git a/data/conf/samples/dmtagent/diameter_processors.json b/data/conf/samples/dmtagent/diameter_processors.json index 90d8b611a..df3d23737 100644 --- a/data/conf/samples/dmtagent/diameter_processors.json +++ b/data/conf/samples/dmtagent/diameter_processors.json @@ -4,26 +4,45 @@ "request_processors": [ { "id": "*default", // formal identifier of this processor - "dry_run": false, // do not send the events to SMG, just log them - "request_filter": "Service-Context-Id(^voice)", // filter requests processed by this processor + "request_filter": "Service-Context-Id(nonexistent)", // cancel matching of this processor + }, + { + "id": "dryrun1", // formal identifier of this processor + "dry_run": true, // do not send the events to SMG, just log them + "request_filter": "Service-Context-Id(dryrun1)", // filter requests processed by this processor "continue_on_success": false, // continue to the next template if executed "ccr_fields":[ // import content_fields template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value - {"tag": "TOR", "field_id": "ToR", "type": "*composed", "value": "^*voice", "mandatory": true}, + {"tag": "TOR", "field_id": "ToR", "type": "*composed", "value": "^*sms", "mandatory": true}, {"tag": "OriginID", "field_id": "OriginID", "type": "*composed", "value": "Session-Id", "mandatory": true}, - {"tag": "RequestType", "field_id": "RequestType", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "RequestType", "field_id": "RequestType", "type": "*composed", "value": "^*prepaid", "mandatory": true}, {"tag": "Direction", "field_id": "Direction", "type": "*composed", "value": "^*out", "mandatory": true}, - {"tag": "Tenant", "field_id": "Tenant", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Tenant", "field_id": "Tenant", "type": "*composed", "value": "^cgrates.org", "mandatory": true}, {"tag": "Category", "field_id": "Category", "type": "*composed", "value": "^call", "mandatory": true}, - {"tag": "Account", "field_id": "Account", "type": "*composed", "value": "^*users", "mandatory": true}, - {"tag": "Subject", "field_id": "Subject", "type": "*composed", "value": "^*users", "mandatory": true}, - {"tag": "Destination", "field_id": "Destination", "type": "*composed", "value": "Service-Information>IN-Information>Real-Called-Number", "mandatory": true}, {"tag": "SetupTime", "field_id": "SetupTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, {"tag": "AnswerTime", "field_id": "AnswerTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, - {"tag": "Usage", "field_id": "Usage", "type": "*handler", "handler_id": "*ccr_usage", "mandatory": true}, - {"tag": "SubscriberID", "field_id": "SubscriberId", "type": "*composed", "value": "Subscription-Id>Subscription-Id-Data", "mandatory": true}, ], "cca_fields":[ // fields returned in CCA - {"tag": "GrantedUnits", "field_id": "Granted-Service-Unit>CC-Time", "type": "*handler", "handler_id": "*cca_usage", "mandatory": true}, + {"tag": "ResultCode", "field_id": "Result-Code", "type": "*constant", "value": "300"}, + ], + }, + { + "id": "pubsub1", // formal identifier of this processor + "dry_run": true, // do not send the events to SMG, just log them + "publish_event": true, + "request_filter": "Service-Context-Id(pubsub1)", // filter requests processed by this processor + "continue_on_success": false, // continue to the next template if executed + "ccr_fields":[ // import content_fields template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value + {"tag": "TOR", "field_id": "ToR", "type": "*composed", "value": "^*sms", "mandatory": true}, + {"tag": "OriginID", "field_id": "OriginID", "type": "*composed", "value": "Session-Id", "mandatory": true}, + {"tag": "RequestType", "field_id": "RequestType", "type": "*composed", "value": "^*prepaid", "mandatory": true}, + {"tag": "Direction", "field_id": "Direction", "type": "*composed", "value": "^*out", "mandatory": true}, + {"tag": "Tenant", "field_id": "Tenant", "type": "*composed", "value": "^cgrates.org", "mandatory": true}, + {"tag": "Category", "field_id": "Category", "type": "*composed", "value": "^call", "mandatory": true}, + {"tag": "SetupTime", "field_id": "SetupTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "AnswerTime", "field_id": "AnswerTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + ], + "cca_fields":[ // fields returned in CCA + {"tag": "ResultCode", "field_id": "Result-Code", "type": "*constant", "value": "300"}, ], }, { @@ -45,7 +64,14 @@ {"tag": "AnswerTime", "field_id": "AnswerTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, {"tag": "Usage", "field_id": "Usage", "type": "*composed", "value": "Requested-Service-Unit>CC-Time", "mandatory": true}, ], + "cca_fields":[ + {"tag": "ResultCode", "field_filter":"CGRError(ACCOUNT_NOT_FOUND)", + "field_id": "Result-Code", "type": "*constant", "value": "5030"}, + {"tag": "ResultCode", "field_filter":"CGRError(USER_NOT_FOUND)", + "field_id": "Result-Code", "type": "*constant", "value": "5030"}, + ], }, + ], }, diff --git a/data/conf/samples/dmtagent/simpa.json b/data/conf/samples/dmtagent/simpa.json new file mode 100644 index 000000000..6a1fb9503 --- /dev/null +++ b/data/conf/samples/dmtagent/simpa.json @@ -0,0 +1,29 @@ + +{ + +"diameter_agent": { + "request_processors": [ + { + "id": "simpa_event", // formal identifier of this processor + "dry_run": false, // do not send the events to SMG, just log them + "request_filter": "Service-Context-Id(^simpa);CC-Request-Type(4)", // filter requests processed by this processor + "continue_on_success": false, // continue to the next template if executed + "ccr_fields":[ // import content_fields template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value + {"tag": "TOR", "field_id": "ToR", "type": "*composed", "value": "^*generic", "mandatory": true}, + {"tag": "OriginID", "field_id": "OriginID", "type": "*composed", "value": "Session-Id", "mandatory": true}, + {"tag": "RequestType", "field_id": "RequestType", "type": "*composed", "value": "^*prepaid", "mandatory": true}, + {"tag": "Direction", "field_id": "Direction", "type": "*composed", "value": "^*out", "mandatory": true}, + {"tag": "Tenant", "field_id": "Tenant", "type": "*composed", "value": "^cgrates.org", "mandatory": true}, + {"tag": "Category", "field_id": "Category", "type": "*composed", "value": "^generic", "mandatory": true}, + {"tag": "Account", "field_id": "Account", "type": "*composed", "value": "Subscription-Id>Subscription-Id-Data", "field_filter":"Subscription-Id>Subscription-Id-Type(0)", "mandatory": true}, + {"tag": "Subject", "field_id": "Subject", "type": "*composed", "value": "Subscription-Id>Subscription-Id-Data", "field_filter":"Subscription-Id>Subscription-Id-Type(0)", "mandatory": true}, + {"tag": "SetupTime", "field_id": "SetupTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "AnswerTime", "field_id": "AnswerTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "Usage", "field_id": "Usage", "type": "*handler", "handler_id": "*value_exponent", + "value": "Requested-Service-Unit>CC-Money>Unit-Value>Value-Digits;^|;Requested-Service-Unit>CC-Money>Unit-Value>Exponent", "mandatory": true}, + ], + }, + ], +}, + +} \ No newline at end of file diff --git a/data/conf/samples/dmtagent/voice.json b/data/conf/samples/dmtagent/voice.json new file mode 100644 index 000000000..a1b369ea4 --- /dev/null +++ b/data/conf/samples/dmtagent/voice.json @@ -0,0 +1,88 @@ + +{ + +"diameter_agent": { + "request_processors": [ + { + "id": "VoiceInit", // formal identifier of this processor + "dry_run": false, // do not send the events to SMG, just log them + "request_filter": "Service-Context-Id(^voice);CC-Request-Type(1)", // filter requests processed by this processor + "continue_on_success": false, // continue to the next template if executed + "ccr_fields":[ // import content_fields template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value + {"tag": "TOR", "field_id": "ToR", "type": "*composed", "value": "^*voice", "mandatory": true}, + {"tag": "OriginID", "field_id": "OriginID", "type": "*composed", "value": "Session-Id", "mandatory": true}, + {"tag": "RequestType", "field_id": "RequestType", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Direction", "field_id": "Direction", "type": "*composed", "value": "^*out", "mandatory": true}, + {"tag": "Tenant", "field_id": "Tenant", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Category", "field_id": "Category", "type": "*composed", "value": "^call", "mandatory": true}, + {"tag": "Account", "field_id": "Account", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Subject", "field_id": "Subject", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Destination", "field_id": "Destination", "type": "*composed", "value": "Service-Information>IN-Information>Real-Called-Number", "mandatory": true}, + {"tag": "SetupTime", "field_id": "SetupTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "AnswerTime", "field_id": "AnswerTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "Usage", "field_id": "Usage", "type": "*handler", "handler_id": "*ccr_usage", "mandatory": true}, + {"tag": "SubscriberID", "field_id": "SubscriberId", "type": "*composed", "value": "Subscription-Id>Subscription-Id-Data", "mandatory": true}, + ], + "cca_fields":[ + {"tag": "ResultCode", "field_filter":"CGRError(ACCOUNT_NOT_FOUND)", + "field_id": "Result-Code", "type": "*constant", "value": "5030"}, + {"tag": "ResultCode", "field_filter":"CGRError(USER_NOT_FOUND)", + "field_id": "Result-Code", "type": "*constant", "value": "5030"}, + {"tag": "GrantedUnits", "field_filter":"CGRError(^$)", + "field_id": "Granted-Service-Unit>CC-Time", "type": "*composed", "value": "CGRMaxUsage", "mandatory": true}, + ], + }, + { + "id": "VoiceUpdate", // formal identifier of this processor + "dry_run": false, // do not send the events to SMG, just log them + "request_filter": "Service-Context-Id(^voice);CC-Request-Type(2)", // filter requests processed by this processor + "continue_on_success": false, // continue to the next template if executed + "ccr_fields":[ // import content_fields template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value + {"tag": "TOR", "field_id": "ToR", "type": "*composed", "value": "^*voice", "mandatory": true}, + {"tag": "OriginID", "field_id": "OriginID", "type": "*composed", "value": "Session-Id", "mandatory": true}, + {"tag": "RequestType", "field_id": "RequestType", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Direction", "field_id": "Direction", "type": "*composed", "value": "^*out", "mandatory": true}, + {"tag": "Tenant", "field_id": "Tenant", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Category", "field_id": "Category", "type": "*composed", "value": "^call", "mandatory": true}, + {"tag": "Account", "field_id": "Account", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Subject", "field_id": "Subject", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Destination", "field_id": "Destination", "type": "*composed", "value": "Service-Information>IN-Information>Real-Called-Number", "mandatory": true}, + {"tag": "SetupTime", "field_id": "SetupTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "AnswerTime", "field_id": "AnswerTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "Usage", "field_id": "Usage", "type": "*handler", "handler_id": "*ccr_usage", "mandatory": true}, + {"tag": "SubscriberID", "field_id": "SubscriberId", "type": "*composed", "value": "Subscription-Id>Subscription-Id-Data", "mandatory": true}, + ], + "cca_fields":[ // fields returned in CCA + {"tag": "GrantedUnits", "field_filter":"CGRError(^$)", + "field_id": "Granted-Service-Unit>CC-Time", "type": "*composed", "value": "CGRMaxUsage", "mandatory": true}, + ], + }, + { + "id": "VoiceTerminate", // formal identifier of this processor + "dry_run": false, // do not send the events to SMG, just log them + "request_filter": "Service-Context-Id(^voice);CC-Request-Type(3)", // filter requests processed by this processor + "continue_on_success": false, // continue to the next template if executed + "ccr_fields":[ // import content_fields template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value + {"tag": "TOR", "field_id": "ToR", "type": "*composed", "value": "^*voice", "mandatory": true}, + {"tag": "OriginID", "field_id": "OriginID", "type": "*composed", "value": "Session-Id", "mandatory": true}, + {"tag": "RequestType", "field_id": "RequestType", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Direction", "field_id": "Direction", "type": "*composed", "value": "^*out", "mandatory": true}, + {"tag": "Tenant", "field_id": "Tenant", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Category", "field_id": "Category", "type": "*composed", "value": "^call", "mandatory": true}, + {"tag": "Account", "field_id": "Account", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Subject", "field_id": "Subject", "type": "*composed", "value": "^*users", "mandatory": true}, + {"tag": "Destination", "field_id": "Destination", "type": "*composed", "value": "Service-Information>IN-Information>Real-Called-Number", "mandatory": true}, + {"tag": "SetupTime", "field_id": "SetupTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "AnswerTime", "field_id": "AnswerTime", "type": "*composed", "value": "Event-Timestamp", "mandatory": true}, + {"tag": "Usage", "field_id": "Usage", "type": "*handler", "handler_id": "*ccr_usage", "mandatory": true}, + {"tag": "SubscriberID", "field_id": "SubscriberId", "type": "*composed", "value": "Subscription-Id>Subscription-Id-Data", "mandatory": true}, + ], + "cca_fields":[ // fields returned in CCA + {"tag": "GrantedUnits", "field_filter":"CGRError(^$)", + "field_id": "Granted-Service-Unit>CC-Time", "type": "*composed", "value": "CGRMaxUsage", "mandatory": true}, + ], + }, + ], +}, + +} \ No newline at end of file diff --git a/data/diameter/dict/huawei/nasreq.xml b/data/diameter/dict/huawei/nasreq.xml index 347bf7199..97784e691 100644 --- a/data/diameter/dict/huawei/nasreq.xml +++ b/data/diameter/dict/huawei/nasreq.xml @@ -1,6 +1,6 @@ - + diff --git a/data/docker/devel/Dockerfile b/data/docker/devel/Dockerfile index 915076ebb..47e251317 100644 --- a/data/docker/devel/Dockerfile +++ b/data/docker/devel/Dockerfile @@ -11,10 +11,10 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-key D76EDC7725E010CF && gpg - RUN echo 'deb http://files.freeswitch.org/repo/deb/debian/ jessie main' > /etc/apt/sources.list.d/freeswitch.list # add mongo repo keys -RUN apt-key adv --keyserver 'keyserver.ubuntu.com' --recv '7F0CEB10' +RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927 # add mongo repo -RUN echo 'deb http://repo.mongodb.org/apt/debian wheezy/mongodb-org/3.0 main' | tee '/etc/apt/sources.list.d/mongodb-org-3.0.list' +RUN echo 'deb http://repo.mongodb.org/apt/debian wheezy/mongodb-org/3.2 main' | tee '/etc/apt/sources.list.d/mongodb-org-3.2.list' # install dependencies RUN apt-get -y update && apt-get -y install git bzr mercurial redis-server mysql-server python-pycurl python-mysqldb postgresql postgresql-client sudo wget freeswitch-meta-vanilla vim zsh mongodb-org @@ -26,7 +26,7 @@ COPY mongod.conf /etc/mongod.conf RUN useradd -c CGRateS -d /var/run/cgrates -s /bin/false -r cgrates # install golang -RUN wget -qO- https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz | tar xzf - -C /root/ +RUN wget -qO- https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz | tar xzf - -C /root/ #install glide RUN GOROOT=/root/go GOPATH=/root/code /root/go/bin/go get github.com/Masterminds/glide diff --git a/data/docker/devel/start.sh b/data/docker/devel/start.sh index 75df8c736..fbacacd13 100755 --- a/data/docker/devel/start.sh +++ b/data/docker/devel/start.sh @@ -24,11 +24,10 @@ mongo --eval 'db.createUser({"user":"cgrates", "pwd":"CGRateS.org", "roles":[{ro #env vars export GOROOT=/root/go; export GOPATH=/root/code; export PATH=$GOROOT/bin:$GOPATH/bin:$PATH -export GO15VENDOREXPERIMENT=1 # build and install cgrates cd /root/cgr -#glide -y devel.yaml up +#glide -y devel.yaml install ./build.sh # create cgr-engine link diff --git a/data/storage/mysql/create_tariffplan_tables.sql b/data/storage/mysql/create_tariffplan_tables.sql index 15783c927..b6240cdc5 100644 --- a/data/storage/mysql/create_tariffplan_tables.sql +++ b/data/storage/mysql/create_tariffplan_tables.sql @@ -152,16 +152,16 @@ CREATE TABLE `tp_actions` ( `balance_tag` varchar(64) NOT NULL, `balance_type` varchar(24) NOT NULL, `directions` varchar(8) NOT NULL, - `units` DECIMAL(20,4) NOT NULL, + `units` varchar(24) NOT NULL, `expiry_time` varchar(24) NOT NULL, `timing_tags` varchar(128) NOT NULL, `destination_tags` varchar(64) NOT NULL, `rating_subject` varchar(64) NOT NULL, `categories` varchar(32) NOT NULL, `shared_groups` varchar(64) NOT NULL, - `balance_weight` DECIMAL(8,2) NOT NULL, - `balance_blocker` BOOLEAN NOT NULL, - `balance_disabled` BOOLEAN NOT NULL, + `balance_weight` varchar(10) NOT NULL, + `balance_blocker` varchar(5) NOT NULL, + `balance_disabled` varchar(24) NOT NULL, `extra_parameters` varchar(256) NOT NULL, `filter` varchar(256) NOT NULL, `weight` DECIMAL(8,2) NOT NULL, @@ -203,6 +203,8 @@ CREATE TABLE `tp_action_triggers` ( `threshold_value` DECIMAL(20,4) NOT NULL, `recurrent` BOOLEAN NOT NULL, `min_sleep` varchar(16) NOT NULL, + `expiry_time` varchar(24) NOT NULL, + `activation_time` varchar(24) NOT NULL, `balance_tag` varchar(64) NOT NULL, `balance_type` varchar(24) NOT NULL, `balance_directions` varchar(8) NOT NULL, @@ -212,9 +214,9 @@ CREATE TABLE `tp_action_triggers` ( `balance_shared_groups` varchar(64) NOT NULL, `balance_expiry_time` varchar(24) NOT NULL, `balance_timing_tags` varchar(128) NOT NULL, - `balance_weight` DECIMAL(8,2) NOT NULL, - `balance_blocker` BOOL NOT NULL, - `balance_disabled` BOOL NOT NULL, + `balance_weight` varchar(10) NOT NULL, + `balance_blocker` varchar(5) NOT NULL, + `balance_disabled` varchar(5) NOT NULL, `min_queued_items` int(11) NOT NULL, `actions_tag` varchar(64) NOT NULL, `weight` DECIMAL(8,2) NOT NULL, diff --git a/data/storage/postgres/create_cdrs_tables.sql b/data/storage/postgres/create_cdrs_tables.sql index 3d386ec7e..d4425fb89 100644 --- a/data/storage/postgres/create_cdrs_tables.sql +++ b/data/storage/postgres/create_cdrs_tables.sql @@ -36,7 +36,7 @@ CREATE TABLE cdrs ( ); ; DROP INDEX IF EXISTS deleted_at_cp_idx; -CREATE INDEX deleted_at_cp_idx ON cdrs_primary (deleted_at); +CREATE INDEX deleted_at_cp_idx ON cdrs (deleted_at); DROP TABLE IF EXISTS sm_costs; diff --git a/data/storage/postgres/create_tariffplan_tables.sql b/data/storage/postgres/create_tariffplan_tables.sql index 857fb956f..b5d8193c8 100644 --- a/data/storage/postgres/create_tariffplan_tables.sql +++ b/data/storage/postgres/create_tariffplan_tables.sql @@ -109,7 +109,7 @@ CREATE TABLE tp_rating_profiles ( activation_time VARCHAR(24) NOT NULL, rating_plan_tag VARCHAR(64) NOT NULL, fallback_subjects VARCHAR(64), - cdr_stat_queue_ids varchar(64), + cdr_stat_queue_ids VARCHAR(64), created_at TIMESTAMP, UNIQUE (tpid, loadid, tenant, category, direction, subject, activation_time) ); @@ -147,16 +147,16 @@ CREATE TABLE tp_actions ( balance_tag VARCHAR(64) NOT NULL, balance_type VARCHAR(24) NOT NULL, directions VARCHAR(8) NOT NULL, - units NUMERIC(20,4) NOT NULL, + units VARCHAR(10) NOT NULL, expiry_time VARCHAR(24) NOT NULL, timing_tags VARCHAR(128) NOT NULL, destination_tags VARCHAR(64) NOT NULL, rating_subject VARCHAR(64) NOT NULL, categories VARCHAR(32) NOT NULL, shared_groups VARCHAR(64) NOT NULL, - balance_weight NUMERIC(8,2) NOT NULL, - balance_blocker BOOLEAN NOT NULL, - balance_disabled BOOLEAN NOT NULL, + balance_weight VARCHAR(10) NOT NULL, + balance_blocker VARCHAR(5) NOT NULL, + balance_disabled VARCHAR(5) NOT NULL, extra_parameters VARCHAR(256) NOT NULL, filter VARCHAR(256) NOT NULL, weight NUMERIC(8,2) NOT NULL, @@ -198,6 +198,8 @@ CREATE TABLE tp_action_triggers ( threshold_value NUMERIC(20,4) NOT NULL, recurrent BOOLEAN NOT NULL, min_sleep VARCHAR(16) NOT NULL, + expiry_time VARCHAR(24) NOT NULL, + activation_time VARCHAR(24) NOT NULL, balance_tag VARCHAR(64) NOT NULL, balance_type VARCHAR(24) NOT NULL, balance_directions VARCHAR(8) NOT NULL, @@ -207,9 +209,9 @@ CREATE TABLE tp_action_triggers ( balance_shared_groups VARCHAR(64) NOT NULL, balance_expiry_time VARCHAR(24) NOT NULL, balance_timing_tags VARCHAR(128) NOT NULL, - balance_weight NUMERIC(8,2) NOT NULL, - balance_blocker BOOL NOT NULL, - balance_disabled BOOL NOT NULL, + balance_weight VARCHAR(10) NOT NULL, + balance_blocker VARCHAR(5) NOT NULL, + balance_disabled VARCHAR(5) NOT NULL, min_queued_items INTEGER NOT NULL, actions_tag VARCHAR(64) NOT NULL, weight NUMERIC(8,2) NOT NULL, diff --git a/data/tariffplans/cdrstats/ActionTriggers.csv b/data/tariffplans/cdrstats/ActionTriggers.csv index 6081b9d67..153da6ceb 100644 --- a/data/tariffplans/cdrstats/ActionTriggers.csv +++ b/data/tariffplans/cdrstats/ActionTriggers.csv @@ -1,6 +1,6 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsTag[19],Weight[20] -CDRST3_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_LOG,10 -CDRST3_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST3_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST4_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST4_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,2,CDRST_LOG,10 +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],ExpiryTime[6],ActivationTime[7],BalanceTag[8],BalanceType[9],BalanceDirections[10],BalanceCategories[11],BalanceDestinationIds[12],BalanceRatingSubject[13],BalanceSharedGroup[14],BalanceExpiryTime[15],BalanceTimingIds[16],BalanceWeight[17],BalanceBlocker[18],BalanceDisabled[19],StatsMinQueuedItems[20],ActionsId[21],Weight[22] +CDRST3_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,,,3,CDRST_LOG,10 +CDRST3_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST3_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST4_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST4_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,,,2,CDRST_LOG,10 diff --git a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv deleted file mode 100644 index c50ff9a7c..000000000 --- a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv +++ /dev/null @@ -1,9 +0,0 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsTag[19],Weight[20] -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 -STANDARD_TRIGGERS,,*max_event_counter,15,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_BALANCE,10 -CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,2,CDRST_LOG,10 diff --git a/data/tariffplans/prepaid1centpsec/AccountActions.csv b/data/tariffplans/testtp/AccountActions.csv similarity index 91% rename from data/tariffplans/prepaid1centpsec/AccountActions.csv rename to data/tariffplans/testtp/AccountActions.csv index 1e3d995b5..ca940949b 100644 --- a/data/tariffplans/prepaid1centpsec/AccountActions.csv +++ b/data/tariffplans/testtp/AccountActions.csv @@ -4,3 +4,4 @@ cgrates.org,1002,PREPAID_10,STANDARD_TRIGGERS,, cgrates.org,1003,PREPAID_10,STANDARD_TRIGGERS,, cgrates.org,1004,PREPAID_10,STANDARD_TRIGGERS,, cgrates.org,1005,PREPAID_10,STANDARD_TRIGGERS,, +cgrates.org,1009,TEST_EXE,,, \ No newline at end of file diff --git a/data/tariffplans/prepaid1centpsec/ActionPlans.csv b/data/tariffplans/testtp/ActionPlans.csv similarity index 76% rename from data/tariffplans/prepaid1centpsec/ActionPlans.csv rename to data/tariffplans/testtp/ActionPlans.csv index 756ee2ede..670cf8c93 100644 --- a/data/tariffplans/prepaid1centpsec/ActionPlans.csv +++ b/data/tariffplans/testtp/ActionPlans.csv @@ -1,3 +1,4 @@ #Tag,ActionsTag,TimingTag,Weight PREPAID_10,PREPAID_10,ASAP,10 PREPAID_10,BONUS_1,ASAP,10 +TEST_EXE,TOPUP_EXE,ALWAYS,10 \ No newline at end of file diff --git a/data/tariffplans/testtp/ActionTriggers.csv b/data/tariffplans/testtp/ActionTriggers.csv new file mode 100644 index 000000000..795ef6ab8 --- /dev/null +++ b/data/tariffplans/testtp/ActionTriggers.csv @@ -0,0 +1,9 @@ +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],ExpiryTime[6],ActivationTime[7],BalanceTag[8],BalanceType[9],BalanceDirections[10],BalanceCategories[11],BalanceDestinationIds[12],BalanceRatingSubject[13],BalanceSharedGroup[14],BalanceExpiryTime[15],BalanceTimingIds[16],BalanceWeight[17],BalanceBlocker[18],BalanceDisabled[19],StatsMinQueuedItems[20],ActionsId[21],Weight[22] +STANDARD_TRIGGERS,,*min_balance,2,false,0,,,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 +STANDARD_TRIGGERS,,*max_event_counter,15,false,0,,,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_BALANCE,10 +CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,,,2,CDRST_LOG,10 diff --git a/data/tariffplans/prepaid1centpsec/Actions.csv b/data/tariffplans/testtp/Actions.csv similarity index 88% rename from data/tariffplans/prepaid1centpsec/Actions.csv rename to data/tariffplans/testtp/Actions.csv index 2729b8c77..045fc0666 100644 --- a/data/tariffplans/prepaid1centpsec/Actions.csv +++ b/data/tariffplans/testtp/Actions.csv @@ -4,3 +4,4 @@ BONUS_1,*topup,,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 LOG_BALANCE,*log,,,,,,,,,,,,,,false,false,10 CDRST_WARN_HTTP,*call_url,http://localhost:8080,,,,,,,,,,,,,false,false,10 CDRST_LOG,*log,,,,,,,,,,,,,,false,false,10 +TOPUP_EXE,*topup,,,,*monetary,*out,,*any,,,*unlimited,,5,10,false,false,10 \ No newline at end of file diff --git a/data/tariffplans/prepaid1centpsec/Aliases.csv b/data/tariffplans/testtp/Aliases.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/Aliases.csv rename to data/tariffplans/testtp/Aliases.csv diff --git a/data/tariffplans/prepaid1centpsec/CdrStats.csv b/data/tariffplans/testtp/CdrStats.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/CdrStats.csv rename to data/tariffplans/testtp/CdrStats.csv diff --git a/data/tariffplans/prepaid1centpsec/DerivedChargers.csv b/data/tariffplans/testtp/DerivedChargers.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/DerivedChargers.csv rename to data/tariffplans/testtp/DerivedChargers.csv diff --git a/data/tariffplans/prepaid1centpsec/DestinationRates.csv b/data/tariffplans/testtp/DestinationRates.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/DestinationRates.csv rename to data/tariffplans/testtp/DestinationRates.csv diff --git a/data/tariffplans/prepaid1centpsec/Destinations.csv b/data/tariffplans/testtp/Destinations.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/Destinations.csv rename to data/tariffplans/testtp/Destinations.csv diff --git a/data/tariffplans/prepaid1centpsec/LcrRules.csv b/data/tariffplans/testtp/LcrRules.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/LcrRules.csv rename to data/tariffplans/testtp/LcrRules.csv diff --git a/data/tariffplans/prepaid1centpsec/README.md b/data/tariffplans/testtp/README.md similarity index 100% rename from data/tariffplans/prepaid1centpsec/README.md rename to data/tariffplans/testtp/README.md diff --git a/data/tariffplans/prepaid1centpsec/Rates.csv b/data/tariffplans/testtp/Rates.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/Rates.csv rename to data/tariffplans/testtp/Rates.csv diff --git a/data/tariffplans/prepaid1centpsec/RatingPlans.csv b/data/tariffplans/testtp/RatingPlans.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/RatingPlans.csv rename to data/tariffplans/testtp/RatingPlans.csv diff --git a/data/tariffplans/prepaid1centpsec/RatingProfiles.csv b/data/tariffplans/testtp/RatingProfiles.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/RatingProfiles.csv rename to data/tariffplans/testtp/RatingProfiles.csv diff --git a/data/tariffplans/prepaid1centpsec/SharedGroups.csv b/data/tariffplans/testtp/SharedGroups.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/SharedGroups.csv rename to data/tariffplans/testtp/SharedGroups.csv diff --git a/data/tariffplans/prepaid1centpsec/Timings.csv b/data/tariffplans/testtp/Timings.csv similarity index 71% rename from data/tariffplans/prepaid1centpsec/Timings.csv rename to data/tariffplans/testtp/Timings.csv index f6c4a6ffb..8e6f78589 100644 --- a/data/tariffplans/prepaid1centpsec/Timings.csv +++ b/data/tariffplans/testtp/Timings.csv @@ -1,3 +1,3 @@ #Tag,Years,Months,MonthDays,WeekDays,Time ALWAYS,*any,*any,*any,*any,00:00:00 -ASAP,*any,*any,*any,*any,*asap +ASAP,*any,*any,*any,*any,*asap \ No newline at end of file diff --git a/data/tariffplans/prepaid1centpsec/Users.csv b/data/tariffplans/testtp/Users.csv similarity index 100% rename from data/tariffplans/prepaid1centpsec/Users.csv rename to data/tariffplans/testtp/Users.csv diff --git a/data/tariffplans/tutorial/ActionTriggers.csv b/data/tariffplans/tutorial/ActionTriggers.csv index e68fbdf34..2036c0ad2 100644 --- a/data/tariffplans/tutorial/ActionTriggers.csv +++ b/data/tariffplans/tutorial/ActionTriggers.csv @@ -1,12 +1,12 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationIds[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingIds[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsId[19],Weight[20] -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,100,false,0,,*monetary,*out,,,,,,,,,,,DISABLE_AND_LOG,10 -CDRST1_WARN,,*min_asr,45,true,1m,,,,,,,,,,,,,3,LOG_WARNING,10 -CDRST1_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1_WARN,,*max_acc,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1001_WARN,,*min_asr,65,true,1m,,,,,,,,,,,,,3,LOG_WARNING,10 -CDRST1001_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1001_WARN,,*max_acc,5,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST3_WARN,,*min_acd,60,false,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],ExpiryTime[6],ActivationTime[7],BalanceTag[8],BalanceType[9],BalanceDirections[10],BalanceCategories[11],BalanceDestinationIds[12],BalanceRatingSubject[13],BalanceSharedGroup[14],BalanceExpiryTime[15],BalanceTimingIds[16],BalanceWeight[17],BalanceBlocker[18],BalanceDisabled[19],StatsMinQueuedItems[20],ActionsId[21],Weight[22] +STANDARD_TRIGGERS,,*min_balance,2,false,0,,,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,100,false,0,,,,*monetary,*out,,,,,,,,,,,DISABLE_AND_LOG,10 +CDRST1_WARN,,*min_asr,45,true,1m,,,,,,,,,,,,,,,3,LOG_WARNING,10 +CDRST1_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1_WARN,,*max_acc,10,true,1m,,,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1001_WARN,,*min_asr,65,true,1m,,,,,,,,,,,,,,,3,LOG_WARNING,10 +CDRST1001_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1001_WARN,,*max_acc,5,true,1m,,,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST3_WARN,,*min_acd,60,false,1m,,,,,,,,,,,,,,,5,LOG_WARNING,10 diff --git a/data/tariffplans/tutorial/DestinationRates.csv b/data/tariffplans/tutorial/DestinationRates.csv index 2602159d0..ee3352fc3 100644 --- a/data/tariffplans/tutorial/DestinationRates.csv +++ b/data/tariffplans/tutorial/DestinationRates.csv @@ -8,3 +8,4 @@ DR_FS_10CNT,DST_FS,RT_10CNT,*up,4,0, DR_SPECIAL_1002,DST_1002,RT_1CNT,*up,4,0, DR_1007_MAXCOST_DISC,DST_1007,RT_1CNT_PER_SEC,*up,4,0.62,*disconnect DR_1007_MAXCOST_FREE,DST_1007,RT_1CNT_PER_SEC,*up,4,0.62,*free +DR_GENERIC,*any,RT_GENERIC_1,*up,4,0, diff --git a/data/tariffplans/tutorial/Rates.csv b/data/tariffplans/tutorial/Rates.csv index c333bff3e..f4ca8a14f 100644 --- a/data/tariffplans/tutorial/Rates.csv +++ b/data/tariffplans/tutorial/Rates.csv @@ -7,3 +7,4 @@ RT_40CNT,0.8,0.4,60s,30s,0s RT_40CNT,0,0.2,60s,10s,60s RT_1CNT,0,0.01,60s,60s,0s RT_1CNT_PER_SEC,0,0.01,1s,1s,0s +RT_GENERIC_1,0,1,1,1,0 diff --git a/data/tariffplans/tutorial/RatingPlans.csv b/data/tariffplans/tutorial/RatingPlans.csv index 80bb48ee7..b983bf4ba 100644 --- a/data/tariffplans/tutorial/RatingPlans.csv +++ b/data/tariffplans/tutorial/RatingPlans.csv @@ -18,3 +18,4 @@ RP_RETAIL2,DR_FS_10CNT,OFFPEAK_EVENING,10 RP_RETAIL2,DR_FS_10CNT,OFFPEAK_WEEKEND,10 RP_RETAIL2,DR_1007_MAXCOST_FREE,ALWAYS,10 RP_SPECIAL_1002,DR_SPECIAL_1002,ALWAYS,10 +RP_GENERIC,DR_GENERIC,*any,10 \ No newline at end of file diff --git a/data/tariffplans/tutorial/RatingProfiles.csv b/data/tariffplans/tutorial/RatingProfiles.csv index db4434165..0d1d56064 100644 --- a/data/tariffplans/tutorial/RatingProfiles.csv +++ b/data/tariffplans/tutorial/RatingProfiles.csv @@ -7,3 +7,4 @@ *out,cgrates.org,lcr_profile2,suppl1,2014-01-14T00:00:00Z,RP_RETAIL2,,STATS_SUPPL1 *out,cgrates.org,lcr_profile2,suppl2,2014-01-14T00:00:00Z,RP_RETAIL1,,STATS_SUPPL2 *out,cgrates.org,lcr_profile2,suppl3,2014-01-14T00:00:00Z,RP_SPECIAL_1002,, +*out,cgrates.org,generic,*any,2014-01-14T00:00:00Z,RP_GENERIC,, diff --git a/data/tariffplans/tutorial/Timings.csv b/data/tariffplans/tutorial/Timings.csv index 59fbab0a9..d5018f529 100644 --- a/data/tariffplans/tutorial/Timings.csv +++ b/data/tariffplans/tutorial/Timings.csv @@ -4,4 +4,4 @@ ASAP,*any,*any,*any,*any,*asap PEAK,*any,*any,*any,1;2;3;4;5,08:00:00 OFFPEAK_MORNING,*any,*any,*any,1;2;3;4;5,00:00:00 OFFPEAK_EVENING,*any,*any,*any,1;2;3;4;5,19:00:00 -OFFPEAK_WEEKEND,*any,*any,*any,6;7,00:00:00 +OFFPEAK_WEEKEND,*any,*any,*any,6;7,00:00:00 \ No newline at end of file diff --git a/data/vagrant/Vagrantfile b/data/vagrant/Vagrantfile index 63bdddac2..680947427 100644 --- a/data/vagrant/Vagrantfile +++ b/data/vagrant/Vagrantfile @@ -1,37 +1,38 @@ # -*- mode: ruby -*- # vi: set ft=ruby : -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure(2) do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - # All Vagrant configuration is done here. The most common configuration - # options are documented and commented below. For a complete reference, - # please see the online documentation at vagrantup.com. - - # Every Vagrant virtual environment requires a box to build off of. - config.vm.box = "debian74_64" - config.vm.box_url = "https://s3-eu-west-1.amazonaws.com/ffuenf-vagrant-boxes/debian/debian-7.4.0-amd64_virtualbox.box" - config.vm.network :public_network + # Every Vagrant development environment requires a box. You can search for + # boxes at https://atlas.hashicorp.com/search. + config.vm.box = "debian83_64" + config.vm.box_url = "https://s3.eu-central-1.amazonaws.com/ffuenf-vagrantboxes/debian/debian-8.3.0-amd64_virtualbox.box" + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false # Create a forwarded port mapping which allows access to a specific port # within the machine from a port on the host machine. In the example below, # accessing "localhost:8080" will access port 80 on the guest machine. - # config.vm.network :forwarded_port, guest: 80, host: 8080 + # config.vm.network "forwarded_port", guest: 80, host: 8080 # Create a private network, which allows host-only access to the machine # using a specific IP. - # config.vm.network :private_network, ip: "192.168.33.10" + # config.vm.network "private_network", ip: "192.168.33.10" # Create a public network, which generally matched to bridged network. # Bridged networks make the machine appear as another physical device on # your network. - # config.vm.network :public_network - - # If true, then any SSH connections made will enable agent forwarding. - # Default value: false - # config.ssh.forward_agent = true + config.vm.network "public_network" # Share an additional folder to the guest VM. The first argument is # the path on the host to the actual folder. The second argument is @@ -43,16 +44,24 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # backing providers for Vagrant. These expose provider-specific options. # Example for VirtualBox: # - # config.vm.provider :virtualbox do |vb| - # # Don't boot with headless mode + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine # vb.gui = true # - # # Use VBoxManage to customize the VM. For example to change memory: - # vb.customize ["modifyvm", :id, "--memory", "1024"] + # # Customize the amount of memory on the VM: + # vb.memory = "1024" # end # - # View the documentation for the provider you're using for more + # View the documentation for the provider you are using for more # information on available options. + + # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies + # such as FTP and Heroku are also available. See the documentation at + # https://docs.vagrantup.com/v2/push/atlas.html for more information. + # config.push.define "atlas" do |push| + # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" + # end + config.vm.provision "ansible" do |ansible| ansible.playbook = "cgrates_devel.yml" #ansible.verbose = "vvvv" diff --git a/data/vagrant/cgrates_devel.retry b/data/vagrant/cgrates_devel.retry new file mode 100644 index 000000000..4ad96d515 --- /dev/null +++ b/data/vagrant/cgrates_devel.retry @@ -0,0 +1 @@ +default diff --git a/data/vagrant/cgrates_devel.yml b/data/vagrant/cgrates_devel.yml index d567537b6..4002907c0 100644 --- a/data/vagrant/cgrates_devel.yml +++ b/data/vagrant/cgrates_devel.yml @@ -1,76 +1,74 @@ --- - hosts: all - user: vagrant - sudo: yes + become: true vars: root_db_password: CGRateS.org tasks: - - name: install dependency - apt: pkg={{ item }} state=latest - with_items: - - git - - bzr - - redis-server - - mysql-server - - python-pycurl - - python-mysqldb - - mercurial - - name: add cgrates user user: name=cgrates comment=CGRateS home=/var/run/cgrates shell=/bin/false system=yes - name: add freeswitch gpg key - command: gpg --keyserver pool.sks-keyservers.net --recv-key D76EDC7725E010CF - - - name: add freeswitch apt key - shell: gpg -a --export D76EDC7725E010CF | sudo apt-key add - + apt_key: url=https://files.freeswitch.org/repo/deb/debian/freeswitch_archive_g0.pub state=present - name: add freeswitch apt repo - apt_repository: repo='deb http://files.freeswitch.org/repo/deb/debian/ wheezy main' state=present - - - name: install freeswitch + apt_repository: repo='deb http://files.freeswitch.org/repo/deb/freeswitch-1.6/ jessie main' state=present + + - name: add mongo gpg key + apt_key: keyserver=keyserver.ubuntu.com id=EA312927 state=present + + - name: add mongo apt repo + apt_repository: repo='deb http://repo.mongodb.org/apt/debian wheezy/mongodb-org/3.2 main' state=present + + - name: install dependency apt: pkg={{ item }} update_cache=yes state=latest with_items: + - git + - bzr + - mercurial + - redis-server + - mysql-server + - postgresql-9.4 + - mongodb-org - freeswitch-meta-vanilla - freeswitch-mod-json-cdr + - libyuv-dev - name: update mysql root password for root account - mysql_user: name=root host=localhost password={{ root_db_password }} + mysql_user: name=cgrates host=localhost password={{ root_db_password }} - name: copy .my.cnf template: src=my.cnf dest=/root/.my.cnf mode=0600 -- hosts: all - user: vagrant +- hosts: all vars: root_db_password: CGRateS.org - go_version: 1.3 + go_version: 1.6 tasks: - name: get golang - get_url: url=https://storage.googleapis.com/golang/go{{ go_version }}.linux-amd64.tar.gz dest=~/go{{ go_version }}.linux-amd64.tar.gz - - - name: unpack go - command: chdir=~/ tar xvf go{{ go_version }}.linux-amd64.tar.gz - - - name: delete golang archive - file: path=~/go{{ go_version }}.linux-amd64.tar.gz state=absent + unarchive: src=https://storage.googleapis.com/golang/go{{ go_version }}.linux-amd64.tar.gz dest=~/go creates=~/go copy=no - name: add variables to variables /etc/profile copy: src=golang.sh dest=/etc/profile.d/golang.sh - sudo: yes + become: yes - name: get cgrates - shell: GOROOT=~/go GOPATH=~/code ~/go/bin/go get -u -v github.com/cgrates/cgrates + git: repo=https://github.com/cgrates/cgrates.git dest=/home/vagrant/code/src/github.com/cgrates/cgrates + + - name: get glide + shell: GOROOT=/home/vagrant/go GOPATH=/home/vagrant/code ~/go/bin/go get -u -v github.com/Masterminds/glide + + - name: install cgrates + shell: cd /home/vagrant/code/src/github.com/cgrates/cgrates; ~/code/bin/glide install - name: create cgr-engine link file: src=/home/vagrant/code/bin/cgr-engine dest=/usr/bin/cgr-engine state=link - sudo: yes + become: yes - name: create a link to data dir - sudo: yes + become: yes file: src=/home/vagrant/code/src/github.com/cgrates/cgrates/data dest=/usr/share/cgrates state=link - name: expand freeswitch json conf diff --git a/docs/tut_opensips_installs.rst b/docs/tut_opensips_installs.rst index 3b83a9aba..2c68ad126 100644 --- a/docs/tut_opensips_installs.rst +++ b/docs/tut_opensips_installs.rst @@ -9,11 +9,11 @@ OpenSIPS_ We got OpenSIPS_ installed via following commands: :: - wget -O - http://apt.opensips.org/key.asc | apt-key add - - echo "deb http://apt.opensips.org/debian/stable-2.1/jessie opensips-2.1-jessie main" > /etc/apt/sources.list.d/opensips.list + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5F2FBB7C + echo "deb http://apt.opensips.org jessie 2.1-releases" >>/etc/apt/sources.list apt-get update apt-get install opensips opensips-json-module opensips-restclient-module Once installed we proceed with loading the configuration out of specific tutorial cases bellow. -.. _OpenSIPS: http://www.opensips.org/ \ No newline at end of file +.. _OpenSIPS: http://www.opensips.org/ diff --git a/engine/account.go b/engine/account.go index 7ae6b8083..e231875a2 100644 --- a/engine/account.go +++ b/engine/account.go @@ -25,8 +25,8 @@ import ( "time" "github.com/cgrates/cgrates/cache2go" + "github.com/cgrates/cgrates/structmatcher" "github.com/cgrates/cgrates/utils" - "github.com/cgrates/structmatcher" "strings" ) @@ -36,21 +36,22 @@ Structure containing information about user's credit (minutes, cents, sms...).' This can represent a user or a shared group. */ type Account struct { - Id string - BalanceMap map[string]BalanceChain - UnitCounters UnitCounters - ActionTriggers ActionTriggers - AllowNegative bool - Disabled bool + ID string + BalanceMap map[string]Balances + UnitCounters UnitCounters + ActionTriggers ActionTriggers + AllowNegative bool + Disabled bool + executingTriggers bool } // User's available minutes for the specified destination -func (ub *Account) getCreditForPrefix(cd *CallDescriptor) (duration time.Duration, credit float64, balances BalanceChain) { +func (ub *Account) getCreditForPrefix(cd *CallDescriptor) (duration time.Duration, credit float64, balances Balances) { creditBalances := ub.getBalancesForPrefix(cd.Destination, cd.Category, cd.Direction, utils.MONETARY, "") unitBalances := ub.getBalancesForPrefix(cd.Destination, cd.Category, cd.Direction, cd.TOR, "") // gather all balances from shared groups - var extendedCreditBalances BalanceChain + var extendedCreditBalances Balances for _, cb := range creditBalances { if len(cb.SharedGroups) > 0 { for sg := range cb.SharedGroups { @@ -64,7 +65,7 @@ func (ub *Account) getCreditForPrefix(cd *CallDescriptor) (duration time.Duratio extendedCreditBalances = append(extendedCreditBalances, cb) } } - var extendedMinuteBalances BalanceChain + var extendedMinuteBalances Balances for _, mb := range unitBalances { if len(mb.SharedGroups) > 0 { for sg := range mb.SharedGroups { @@ -88,22 +89,96 @@ func (ub *Account) getCreditForPrefix(cd *CallDescriptor) (duration time.Duratio return } +// sets all the fields of the balance +func (acc *Account) setBalanceAction(a *Action) error { + if a == nil { + return errors.New("nil action") + } + if a.Balance.Type == nil { + return errors.New("missing balance type") + } + balanceType := *a.Balance.Type + if acc.BalanceMap == nil { + acc.BalanceMap = make(map[string]Balances, 1) + } + var previousSharedGroups utils.StringMap // kept for comparison + var balance *Balance + var found bool + for _, b := range acc.BalanceMap[balanceType] { + if b.IsExpired() { + continue + } + if (a.Balance.Uuid != nil && b.Uuid == *a.Balance.Uuid) || + (a.Balance.ID != nil && b.ID == *a.Balance.ID) { + previousSharedGroups = b.SharedGroups + balance = b + found = true + break // only set one balance + } + } + + // if it is not found then we add it to the list + if balance == nil { + balance = &Balance{} + balance.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency + acc.BalanceMap[balanceType] = append(acc.BalanceMap[balanceType], balance) + } + + if a.Balance.ID != nil && *a.Balance.ID == utils.META_DEFAULT { + balance.ID = utils.META_DEFAULT + if a.Balance.Value != nil { + balance.Value = *a.Balance.Value + } + } else { + a.Balance.ModifyBalance(balance) + } + + if !found || !previousSharedGroups.Equal(balance.SharedGroups) { + _, err := Guardian.Guard(func() (interface{}, error) { + for sgID := range balance.SharedGroups { + // add shared group member + sg, err := ratingStorage.GetSharedGroup(sgID, false) + if err != nil || sg == nil { + //than is problem + utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID)) + } else { + if _, found := sg.MemberIds[acc.ID]; !found { + // add member and save + if sg.MemberIds == nil { + sg.MemberIds = make(utils.StringMap) + } + sg.MemberIds[acc.ID] = true + ratingStorage.SetSharedGroup(sg) + } + } + } + return 0, nil + }, 0, balance.SharedGroups.Slice()...) + if err != nil { + return err + } + } + acc.InitCounters() + acc.ExecuteActionTriggers(nil) + return nil +} + // Debits some amount of user's specified balance adding the balance if it does not exists. // Returns the remaining credit in user's balance. func (ub *Account) debitBalanceAction(a *Action, reset bool) error { if a == nil { return errors.New("nil action") } - bClone := a.Balance.Clone() + bClone := a.Balance.CreateBalance() if bClone == nil { return errors.New("nil balance") } if ub.BalanceMap == nil { - ub.BalanceMap = make(map[string]BalanceChain, 1) + ub.BalanceMap = make(map[string]Balances, 1) } found := false - id := a.BalanceType - for _, b := range ub.BalanceMap[id] { + balanceType := a.Balance.GetType() + for _, b := range ub.BalanceMap[balanceType] { if b.IsExpired() { continue // just to be safe (cleaned expired balances above) } @@ -113,6 +188,7 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { b.SetValue(0) } b.SubstractValue(bClone.GetValue()) + b.dirty = true found = true } } @@ -120,9 +196,9 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { if !found { // check if the Id is *default (user trying to create the default balance) // use only it's value value - if bClone.Id == utils.META_DEFAULT { + if bClone.ID == utils.META_DEFAULT { bClone = &Balance{ - Id: utils.META_DEFAULT, + ID: utils.META_DEFAULT, Value: -bClone.GetValue(), } } else { @@ -131,9 +207,8 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { } } bClone.dirty = true // Mark the balance as dirty since we have modified and it should be checked by action triggers - if bClone.Uuid == "" { - bClone.Uuid = utils.GenUUID() - } + + bClone.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency // load ValueFactor if defined in extra parametrs if a.ExtraParameters != "" { vf := ValueFactor{} @@ -144,36 +219,44 @@ func (ub *Account) debitBalanceAction(a *Action, reset bool) error { utils.Logger.Warning(fmt.Sprintf("Could load value factor from actions: extra parametrs: %s", a.ExtraParameters)) } } - ub.BalanceMap[id] = append(ub.BalanceMap[id], bClone) - } - for sgId := range a.Balance.SharedGroups { - // add shared group member - sg, err := ratingStorage.GetSharedGroup(sgId, false) - if err != nil || sg == nil { - //than is problem - utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgId)) - } else { - if _, found := sg.MemberIds[ub.Id]; !found { - // add member and save - if sg.MemberIds == nil { - sg.MemberIds = make(utils.StringMap) + ub.BalanceMap[balanceType] = append(ub.BalanceMap[balanceType], bClone) + _, err := Guardian.Guard(func() (interface{}, error) { + for sgId := range bClone.SharedGroups { + // add shared group member + sg, err := ratingStorage.GetSharedGroup(sgId, false) + if err != nil || sg == nil { + //than is problem + utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgId)) + } else { + if _, found := sg.MemberIds[ub.ID]; !found { + // add member and save + if sg.MemberIds == nil { + sg.MemberIds = make(utils.StringMap) + } + sg.MemberIds[ub.ID] = true + ratingStorage.SetSharedGroup(sg) + } } - sg.MemberIds[ub.Id] = true - ratingStorage.SetSharedGroup(sg) } + return 0, nil + }, 0, bClone.SharedGroups.Slice()...) + if err != nil { + return err } } - ub.executeActionTriggers(nil) - return nil //ub.BalanceMap[id].GetTotalValue() + ub.InitCounters() + ub.ExecuteActionTriggers(nil) + return nil } +/* func (ub *Account) enableDisableBalanceAction(a *Action) error { if a == nil { return errors.New("nil action") } if ub.BalanceMap == nil { - ub.BalanceMap = make(map[string]BalanceChain) + ub.BalanceMap = make(map[string]Balances) } found := false id := a.BalanceType @@ -186,19 +269,20 @@ func (ub *Account) enableDisableBalanceAction(a *Action) error { found = true } } + a.Balance.Disabled = disabled // restore balance aaction as it is cached if !found { return utils.ErrNotFound } return nil } - -func (ub *Account) getBalancesForPrefix(prefix, category, direction, tor string, sharedGroup string) BalanceChain { - var balances BalanceChain +*/ +func (ub *Account) getBalancesForPrefix(prefix, category, direction, tor string, sharedGroup string) Balances { + var balances Balances balances = append(balances, ub.BalanceMap[tor]...) if tor != utils.MONETARY && tor != utils.GENERIC { balances = append(balances, ub.BalanceMap[utils.GENERIC]...) } - var usefulBalances BalanceChain + var usefulBalances Balances for _, b := range balances { if b.Disabled { continue @@ -216,12 +300,12 @@ func (ub *Account) getBalancesForPrefix(prefix, category, direction, tor string, continue } b.account = ub - if len(b.DestinationIds) > 0 && b.DestinationIds[utils.ANY] == false { + if len(b.DestinationIDs) > 0 && b.DestinationIDs[utils.ANY] == false { for _, p := range utils.SplitPrefix(prefix, MIN_PREFIX_MATCH) { if x, err := cache2go.Get(utils.DESTINATION_PREFIX + p); err == nil { destIds := x.(map[interface{}]struct{}) for dId, _ := range destIds { - if b.DestinationIds[dId.(string)] == true { + if b.DestinationIDs[dId.(string)] == true { b.precision = len(p) usefulBalances = append(usefulBalances, b) break @@ -249,7 +333,7 @@ func (ub *Account) getBalancesForPrefix(prefix, category, direction, tor string, } // like getBalancesForPrefix but expanding shared balances -func (account *Account) getAlldBalancesForPrefix(destination, category, direction, balanceType string) (bc BalanceChain) { +func (account *Account) getAlldBalancesForPrefix(destination, category, direction, balanceType string) (bc Balances) { balances := account.getBalancesForPrefix(destination, category, direction, balanceType, "") for _, b := range balances { if len(b.SharedGroups) > 0 { @@ -402,8 +486,12 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo cost := increment.Cost defaultBalance := ub.GetDefaultMoneyBalance() defaultBalance.SubstractValue(cost) - increment.BalanceInfo.MoneyBalanceUuid = defaultBalance.Uuid - increment.BalanceInfo.AccountId = ub.Id + increment.BalanceInfo.Monetary = &MonetaryInfo{ + UUID: defaultBalance.Uuid, + ID: defaultBalance.ID, + Value: defaultBalance.Value, + } + increment.BalanceInfo.AccountID = ub.ID increment.paid = true if count { ub.countUnits( @@ -413,7 +501,7 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo &Balance{ Directions: utils.StringMap{leftCC.Direction: true}, Value: cost, - DestinationIds: utils.NewStringMap(leftCC.Destination), + DestinationIDs: utils.NewStringMap(leftCC.Destination), }) } } @@ -439,44 +527,32 @@ func (ub *Account) GetDefaultMoneyBalance() *Balance { // create default balance defaultBalance := &Balance{ Uuid: utils.GenUUID(), - Id: utils.META_DEFAULT, + ID: utils.META_DEFAULT, } // minimum weight if ub.BalanceMap == nil { - ub.BalanceMap = make(map[string]BalanceChain) + ub.BalanceMap = make(map[string]Balances) } ub.BalanceMap[utils.MONETARY] = append(ub.BalanceMap[utils.MONETARY], defaultBalance) return defaultBalance } -func (ub *Account) refundIncrement(increment *Increment, cd *CallDescriptor, count bool) { - var balance *Balance - unitType := cd.TOR - cc := cd.CreateCallCost() - if increment.BalanceInfo.UnitBalanceUuid != "" { - if balance = ub.BalanceMap[unitType].GetBalance(increment.BalanceInfo.UnitBalanceUuid); balance == nil { - return - } - balance.AddValue(increment.Duration.Seconds()) - if count { - ub.countUnits(-increment.Duration.Seconds(), unitType, cc, balance) - } - } - // check money too - if increment.BalanceInfo.MoneyBalanceUuid != "" { - if balance = ub.BalanceMap[utils.MONETARY].GetBalance(increment.BalanceInfo.MoneyBalanceUuid); balance == nil { - return - } - balance.AddValue(increment.Cost) - if count { - ub.countUnits(-increment.Cost, utils.MONETARY, cc, balance) - } - } -} - // Scans the action trigers and execute the actions for which trigger is met -func (ub *Account) executeActionTriggers(a *Action) { - ub.ActionTriggers.Sort() - for _, at := range ub.ActionTriggers { +func (acc *Account) ExecuteActionTriggers(a *Action) { + if acc.executingTriggers { + return + } + acc.executingTriggers = true + defer func() { + acc.executingTriggers = false + }() + + acc.ActionTriggers.Sort() + for _, at := range acc.ActionTriggers { + // check is effective + if at.IsExpired(time.Now()) || !at.IsActive(time.Now()) { + continue + } + // sanity check if !strings.Contains(at.ThresholdType, "counter") && !strings.Contains(at.ThresholdType, "balance") { continue @@ -490,45 +566,51 @@ func (ub *Account) executeActionTriggers(a *Action) { continue } if strings.Contains(at.ThresholdType, "counter") { - for _, uc := range ub.UnitCounters { - if uc.BalanceType == at.BalanceType && - strings.Contains(at.ThresholdType, uc.CounterType[1:]) { - for _, mb := range uc.Balances { - if strings.HasPrefix(at.ThresholdType, "*max") { - if mb.MatchActionTrigger(at) && mb.GetValue() >= at.ThresholdValue { - at.Execute(ub, nil) - } - } else { //MIN - if mb.MatchActionTrigger(at) && mb.GetValue() <= at.ThresholdValue { - at.Execute(ub, nil) + if (at.Balance.ID == nil || *at.Balance.ID != "") && at.UniqueID != "" { + at.Balance.ID = utils.StringPointer(at.UniqueID) + } + for _, counters := range acc.UnitCounters { + for _, uc := range counters { + if strings.Contains(at.ThresholdType, uc.CounterType[1:]) { + for _, c := range uc.Counters { + //log.Print("C: ", utils.ToJSON(c)) + if strings.HasPrefix(at.ThresholdType, "*max") { + if c.Filter.Equal(at.Balance) && c.Value >= at.ThresholdValue { + //log.Print("HERE") + at.Execute(acc, nil) + } + } else { //MIN + if c.Filter.Equal(at.Balance) && c.Value <= at.ThresholdValue { + at.Execute(acc, nil) + } } } } } } } else { // BALANCE - for _, b := range ub.BalanceMap[at.BalanceType] { + for _, b := range acc.BalanceMap[at.Balance.GetType()] { if !b.dirty && at.ThresholdType != utils.TRIGGER_BALANCE_EXPIRED { // do not check clean balances continue } switch at.ThresholdType { case utils.TRIGGER_MAX_BALANCE: if b.MatchActionTrigger(at) && b.GetValue() >= at.ThresholdValue { - at.Execute(ub, nil) + at.Execute(acc, nil) } case utils.TRIGGER_MIN_BALANCE: if b.MatchActionTrigger(at) && b.GetValue() <= at.ThresholdValue { - at.Execute(ub, nil) + at.Execute(acc, nil) } case utils.TRIGGER_BALANCE_EXPIRED: if b.MatchActionTrigger(at) && b.IsExpired() { - at.Execute(ub, nil) + at.Execute(acc, nil) } } } } } - ub.CleanExpiredBalances() + acc.CleanExpiredStuff() } // Mark all action trigers as ready for execution @@ -540,7 +622,7 @@ func (acc *Account) ResetActionTriggers(a *Action) { } at.Executed = false } - acc.executeActionTriggers(a) + acc.ExecuteActionTriggers(a) } // Sets/Unsets recurrent flag for action triggers @@ -556,14 +638,16 @@ func (acc *Account) SetRecurrent(a *Action, recurrent bool) { // Increments the counter for the type func (acc *Account) countUnits(amount float64, kind string, cc *CallCost, b *Balance) { acc.UnitCounters.addUnits(amount, kind, cc, b) - acc.executeActionTriggers(nil) + acc.ExecuteActionTriggers(nil) } // Create counters for all triggered actions func (acc *Account) InitCounters() { - acc.UnitCounters = nil + oldUcs := acc.UnitCounters + acc.UnitCounters = make(UnitCounters) ucTempMap := make(map[string]*UnitCounter) for _, at := range acc.ActionTriggers { + //log.Print("AT: ", utils.ToJSON(at)) if !strings.Contains(at.ThresholdType, "counter") { continue } @@ -571,25 +655,46 @@ func (acc *Account) InitCounters() { if strings.Contains(at.ThresholdType, "balance") { ct = utils.COUNTER_BALANCE } - - uc, exists := ucTempMap[at.BalanceType+ct] + uc, exists := ucTempMap[at.Balance.GetType()+ct] + //log.Print("CT: ", at.Balance.GetType()+ct) if !exists { uc = &UnitCounter{ - BalanceType: at.BalanceType, CounterType: ct, } - ucTempMap[at.BalanceType+ct] = uc - uc.Balances = BalanceChain{} - acc.UnitCounters = append(acc.UnitCounters, uc) + ucTempMap[at.Balance.GetType()+ct] = uc + uc.Counters = make(CounterFilters, 0) + acc.UnitCounters[at.Balance.GetType()] = append(acc.UnitCounters[at.Balance.GetType()], uc) } - b := at.CreateBalance() - if !uc.Balances.HasBalance(b) { - uc.Balances = append(uc.Balances, b) + + c := &CounterFilter{Filter: at.Balance.Clone()} + if (c.Filter.ID == nil || *c.Filter.ID == "") && at.UniqueID != "" { + c.Filter.ID = utils.StringPointer(at.UniqueID) } + //log.Print("C: ", utils.ToJSON(c)) + if !uc.Counters.HasCounter(c) { + uc.Counters = append(uc.Counters, c) + } + } + // copy old counter values + for key, counters := range acc.UnitCounters { + oldCounters, found := oldUcs[key] + if !found { + continue + } + for _, uc := range counters { + for _, oldUc := range oldCounters { + if uc.CopyCounterValues(oldUc) { + break + } + } + } + } + if len(acc.UnitCounters) == 0 { + acc.UnitCounters = nil // leave it nil if empty } } -func (acc *Account) CleanExpiredBalances() { +func (acc *Account) CleanExpiredStuff() { for key, bm := range acc.BalanceMap { for i := 0; i < len(bm); i++ { if bm[i].IsExpired() { @@ -599,6 +704,12 @@ func (acc *Account) CleanExpiredBalances() { } acc.BalanceMap[key] = bm } + + for i := 0; i < len(acc.ActionTriggers); i++ { + if acc.ActionTriggers[i].IsExpired(time.Now()) { + acc.ActionTriggers = append(acc.ActionTriggers[:i], acc.ActionTriggers[i+1:]...) + } + } } func (acc *Account) allBalancesExpired() bool { @@ -655,8 +766,8 @@ type TenantAccount struct { func (acc *Account) Clone() *Account { newAcc := &Account{ - Id: acc.Id, - BalanceMap: make(map[string]BalanceChain, len(acc.BalanceMap)), + ID: acc.ID, + BalanceMap: make(map[string]Balances, len(acc.BalanceMap)), UnitCounters: nil, // not used when cloned (dryRun) ActionTriggers: nil, // not used when cloned (dryRun) AllowNegative: acc.AllowNegative, @@ -668,7 +779,7 @@ func (acc *Account) Clone() *Account { return newAcc } -func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances BalanceChain, count bool) { +func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balances, count bool) { if cc.deductConnectFee { connectFee := cc.GetConnectFee() //log.Print("CONNECT FEE: %f", connectFee) @@ -742,12 +853,12 @@ func (acc *Account) AsOldStructure() interface{} { account *Account dirty bool } - type BalanceChain []*Balance + type Balances []*Balance type UnitsCounter struct { Direction string BalanceType string // Units float64 - Balances BalanceChain // first balance is the general one (no destination) + Balances Balances // first balance is the general one (no destination) } type ActionTrigger struct { Id string @@ -774,7 +885,7 @@ func (acc *Account) AsOldStructure() interface{} { type ActionTriggers []*ActionTrigger type Account struct { Id string - BalanceMap map[string]BalanceChain + BalanceMap map[string]Balances UnitCounters []*UnitsCounter ActionTriggers ActionTriggers AllowNegative bool @@ -782,61 +893,64 @@ func (acc *Account) AsOldStructure() interface{} { } result := &Account{ - Id: utils.OUT + ":" + acc.Id, - BalanceMap: make(map[string]BalanceChain, len(acc.BalanceMap)), + Id: utils.OUT + ":" + acc.ID, + BalanceMap: make(map[string]Balances, len(acc.BalanceMap)), UnitCounters: make([]*UnitsCounter, len(acc.UnitCounters)), ActionTriggers: make(ActionTriggers, len(acc.ActionTriggers)), AllowNegative: acc.AllowNegative, Disabled: acc.Disabled, } - for i, uc := range acc.UnitCounters { - if uc == nil { - continue - } - result.UnitCounters[i] = &UnitsCounter{ - BalanceType: uc.BalanceType, - Balances: make(BalanceChain, len(uc.Balances)), - } - if len(uc.Balances) > 0 { - result.UnitCounters[i].Direction = uc.Balances[0].Directions.String() - for j, b := range uc.Balances { - result.UnitCounters[i].Balances[j] = &Balance{ - Uuid: b.Uuid, - Id: b.Id, - Value: b.Value, - ExpirationDate: b.ExpirationDate, - Weight: b.Weight, - DestinationIds: b.DestinationIds.String(), - RatingSubject: b.RatingSubject, - Category: b.Categories.String(), - SharedGroup: b.SharedGroups.String(), - Timings: b.Timings, - TimingIDs: b.TimingIDs.String(), - Disabled: b.Disabled, + for balanceType, counters := range acc.UnitCounters { + for i, uc := range counters { + if uc == nil { + continue + } + result.UnitCounters[i] = &UnitsCounter{ + BalanceType: balanceType, + Balances: make(Balances, len(uc.Counters)), + } + if len(uc.Counters) > 0 { + result.UnitCounters[i].Direction = (*uc.Counters[0].Filter.Directions).String() + for j, c := range uc.Counters { + result.UnitCounters[i].Balances[j] = &Balance{ + Uuid: c.Filter.GetUuid(), + Id: c.Filter.GetID(), + Value: c.Filter.GetValue(), + ExpirationDate: c.Filter.GetExpirationDate(), + Weight: c.Filter.GetWeight(), + DestinationIds: c.Filter.GetDestinationIDs().String(), + RatingSubject: c.Filter.GetRatingSubject(), + Category: c.Filter.GetCategories().String(), + SharedGroup: c.Filter.GetSharedGroups().String(), + Timings: c.Filter.Timings, + TimingIDs: c.Filter.GetTimingIDs().String(), + Disabled: c.Filter.GetDisabled(), + } } } } } for i, at := range acc.ActionTriggers { + b := at.Balance.CreateBalance() result.ActionTriggers[i] = &ActionTrigger{ Id: at.ID, ThresholdType: at.ThresholdType, ThresholdValue: at.ThresholdValue, Recurrent: at.Recurrent, MinSleep: at.MinSleep, - BalanceId: at.BalanceId, - BalanceType: at.BalanceType, - BalanceDirection: at.BalanceDirections.String(), - BalanceDestinationIds: at.BalanceDestinationIds.String(), - BalanceWeight: at.BalanceWeight, - BalanceExpirationDate: at.BalanceExpirationDate, - BalanceTimingTags: at.BalanceTimingTags.String(), - BalanceRatingSubject: at.BalanceRatingSubject, - BalanceCategory: at.BalanceCategories.String(), - BalanceSharedGroup: at.BalanceSharedGroups.String(), - BalanceDisabled: at.BalanceDisabled, + BalanceType: at.Balance.GetType(), + BalanceId: b.ID, + BalanceDirection: b.Directions.String(), + BalanceDestinationIds: b.DestinationIDs.String(), + BalanceWeight: b.Weight, + BalanceExpirationDate: b.ExpirationDate, + BalanceTimingTags: b.TimingIDs.String(), + BalanceRatingSubject: b.RatingSubject, + BalanceCategory: b.Categories.String(), + BalanceSharedGroup: b.SharedGroups.String(), + BalanceDisabled: b.Disabled, Weight: at.Weight, - ActionsId: at.ActionsId, + ActionsId: at.ActionsID, MinQueuedItems: at.MinQueuedItems, Executed: at.Executed, } @@ -844,15 +958,15 @@ func (acc *Account) AsOldStructure() interface{} { for key, values := range acc.BalanceMap { if len(values) > 0 { key += utils.OUT - result.BalanceMap[key] = make(BalanceChain, len(values)) + result.BalanceMap[key] = make(Balances, len(values)) for i, b := range values { result.BalanceMap[key][i] = &Balance{ Uuid: b.Uuid, - Id: b.Id, + Id: b.ID, Value: b.Value, ExpirationDate: b.ExpirationDate, Weight: b.Weight, - DestinationIds: b.DestinationIds.String(), + DestinationIds: b.DestinationIDs.String(), RatingSubject: b.RatingSubject, Category: b.Categories.String(), SharedGroup: b.SharedGroups.String(), diff --git a/engine/account_test.go b/engine/account_test.go index 334836999..90be903d2 100644 --- a/engine/account_test.go +++ b/engine/account_test.go @@ -19,7 +19,6 @@ along with this program. If not, see package engine import ( - "log" "testing" "time" @@ -67,13 +66,13 @@ func TestBalanceStoreRestoreZero(t *testing.T) { } } -func TestBalanceChainStoreRestore(t *testing.T) { - bc := BalanceChain{&Balance{Value: 14, ExpirationDate: time.Date(2013, time.July, 15, 17, 48, 0, 0, time.UTC)}, &Balance{Value: 1024}} +func TestBalancesStoreRestore(t *testing.T) { + bc := Balances{&Balance{Value: 14, ExpirationDate: time.Date(2013, time.July, 15, 17, 48, 0, 0, time.UTC)}, &Balance{Value: 1024}} output, err := marsh.Marshal(bc) if err != nil { t.Error("Error storing balance chain: ", err) } - bc1 := BalanceChain{} + bc1 := Balances{} err = marsh.Unmarshal(output, &bc1) if err != nil { t.Error("Error restoring balance chain: ", err) @@ -84,9 +83,9 @@ func TestBalanceChainStoreRestore(t *testing.T) { } func TestAccountStorageStoreRestore(t *testing.T) { - b1 := &Balance{Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}} - b2 := &Balance{Value: 100, Weight: 20, DestinationIds: utils.StringMap{"RET": true}} - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b1, b2}, utils.MONETARY: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}} + b2 := &Balance{Value: 100, Weight: 20, DestinationIDs: utils.StringMap{"RET": true}} + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{utils.VOICE: Balances{b1, b2}, utils.MONETARY: Balances{&Balance{Value: 21}}}} accountingStorage.SetAccount(rifsBalance) ub1, err := accountingStorage.GetAccount("other") if err != nil || !ub1.BalanceMap[utils.MONETARY].Equal(rifsBalance.BalanceMap[utils.MONETARY]) { @@ -96,9 +95,9 @@ func TestAccountStorageStoreRestore(t *testing.T) { } func TestGetSecondsForPrefix(t *testing.T) { - b1 := &Balance{Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}} - b2 := &Balance{Value: 100, Weight: 20, DestinationIds: utils.StringMap{"RET": true}} - ub1 := &Account{Id: "CUSTOMER_1:rif", BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b1, b2}, utils.MONETARY: BalanceChain{&Balance{Value: 200}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}} + b2 := &Balance{Value: 100, Weight: 20, DestinationIDs: utils.StringMap{"RET": true}} + ub1 := &Account{ID: "CUSTOMER_1:rif", BalanceMap: map[string]Balances{utils.VOICE: Balances{b1, b2}, utils.MONETARY: Balances{&Balance{Value: 200}}}} cd := &CallDescriptor{ Category: "0", Tenant: "vdf", @@ -119,14 +118,14 @@ func TestGetSecondsForPrefix(t *testing.T) { } func TestGetSpecialPricedSeconds(t *testing.T) { - b1 := &Balance{Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "minu"} - b2 := &Balance{Value: 100, Weight: 20, DestinationIds: utils.StringMap{"RET": true}, RatingSubject: "minu"} + b1 := &Balance{Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "minu"} + b2 := &Balance{Value: 100, Weight: 20, DestinationIDs: utils.StringMap{"RET": true}, RatingSubject: "minu"} ub1 := &Account{ - Id: "OUT:CUSTOMER_1:rif", - BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1, b2}, - utils.MONETARY: BalanceChain{&Balance{Value: 21}}, + ID: "OUT:CUSTOMER_1:rif", + BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1, b2}, + utils.MONETARY: Balances{&Balance{Value: 21}}, }, } cd := &CallDescriptor{ @@ -151,12 +150,12 @@ func TestGetSpecialPricedSeconds(t *testing.T) { } func TestAccountStorageStore(t *testing.T) { - b1 := &Balance{Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}} - b2 := &Balance{Value: 100, Weight: 20, DestinationIds: utils.StringMap{"RET": true}} - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b1, b2}, utils.MONETARY: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}} + b2 := &Balance{Value: 100, Weight: 20, DestinationIDs: utils.StringMap{"RET": true}} + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{utils.VOICE: Balances{b1, b2}, utils.MONETARY: Balances{&Balance{Value: 21}}}} accountingStorage.SetAccount(rifsBalance) - result, err := accountingStorage.GetAccount(rifsBalance.Id) - if err != nil || rifsBalance.Id != result.Id || + result, err := accountingStorage.GetAccount(rifsBalance.ID) + if err != nil || rifsBalance.ID != result.ID || len(rifsBalance.BalanceMap[utils.VOICE]) < 2 || len(result.BalanceMap[utils.VOICE]) < 2 || !(rifsBalance.BalanceMap[utils.VOICE][0].Equal(result.BalanceMap[utils.VOICE][0])) || !(rifsBalance.BalanceMap[utils.VOICE][1].Equal(result.BalanceMap[utils.VOICE][1])) || @@ -166,7 +165,7 @@ func TestAccountStorageStore(t *testing.T) { } func TestDebitCreditZeroSecond(t *testing.T) { - b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1s"} + b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1s"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -189,13 +188,13 @@ func TestDebitCreditZeroSecond(t *testing.T) { TOR: utils.VOICE, testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b1}, utils.MONETARY: BalanceChain{&Balance{Categories: utils.NewStringMap("0"), Value: 21}}}} + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{utils.VOICE: Balances{b1}, utils.MONETARY: Balances{&Balance{Categories: utils.NewStringMap("0"), Value: 21}}}} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" { + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" { t.Logf("%+v", cc.Timespans[0]) t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -206,7 +205,7 @@ func TestDebitCreditZeroSecond(t *testing.T) { } func TestDebitCreditZeroMinute(t *testing.T) { - b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -229,9 +228,9 @@ func TestDebitCreditZeroMinute(t *testing.T) { TOR: utils.VOICE, testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1}, - utils.MONETARY: BalanceChain{&Balance{Value: 21}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1}, + utils.MONETARY: Balances{&Balance{Value: 21}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) @@ -239,7 +238,7 @@ func TestDebitCreditZeroMinute(t *testing.T) { t.Error("Error debiting balance: ", err) } t.Logf("%+v", cc.Timespans) - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" || + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" || cc.Timespans[0].Increments[0].Duration != time.Minute { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -251,8 +250,8 @@ func TestDebitCreditZeroMinute(t *testing.T) { } func TestDebitCreditZeroMixedMinute(t *testing.T) { - b1 := &Balance{Uuid: "testm", Value: 70, Weight: 5, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} - b2 := &Balance{Uuid: "tests", Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1s"} + b1 := &Balance{Uuid: "testm", Value: 70, Weight: 5, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} + b2 := &Balance{Uuid: "tests", Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1s"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -276,17 +275,17 @@ func TestDebitCreditZeroMixedMinute(t *testing.T) { DurationIndex: cc.Timespans[0].GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1, b2}, - utils.MONETARY: BalanceChain{&Balance{Value: 21}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1, b2}, + utils.MONETARY: Balances{&Balance{Value: 21}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "tests" || - cc.Timespans[1].Increments[0].BalanceInfo.UnitBalanceUuid != "testm" { + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "tests" || + cc.Timespans[1].Increments[0].BalanceInfo.Unit.UUID != "testm" { t.Error("Error setting balance id to increment: ", cc.Timespans) } if rifsBalance.BalanceMap[utils.VOICE][1].GetValue() != 0 || @@ -299,7 +298,7 @@ func TestDebitCreditZeroMixedMinute(t *testing.T) { } func TestDebitCreditNoCredit(t *testing.T) { - b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -328,15 +327,15 @@ func TestDebitCreditNoCredit(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err == nil { t.Error("Showing no enough credit error ") } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" || + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" || cc.Timespans[0].Increments[0].Duration != time.Minute { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -350,7 +349,7 @@ func TestDebitCreditNoCredit(t *testing.T) { } func TestDebitCreditHasCredit(t *testing.T) { - b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -379,16 +378,16 @@ func TestDebitCreditHasCredit(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1}, - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneya", Value: 110}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1}, + utils.MONETARY: Balances{&Balance{Uuid: "moneya", Value: 110}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" || + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" || cc.Timespans[0].Increments[0].Duration != time.Minute { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -403,7 +402,7 @@ func TestDebitCreditHasCredit(t *testing.T) { } func TestDebitCreditSplitMinutesMoney(t *testing.T) { - b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1s"} + b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1s"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -427,16 +426,16 @@ func TestDebitCreditSplitMinutesMoney(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1}, - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneya", Value: 50}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1}, + utils.MONETARY: Balances{&Balance{Uuid: "moneya", Value: 50}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" || + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" || cc.Timespans[0].Increments[0].Duration != 1*time.Second { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0].Duration) } @@ -451,7 +450,7 @@ func TestDebitCreditSplitMinutesMoney(t *testing.T) { } func TestDebitCreditMoreTimespans(t *testing.T) { - b1 := &Balance{Uuid: "testb", Value: 150, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} + b1 := &Balance{Uuid: "testb", Value: 150, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -480,15 +479,15 @@ func TestDebitCreditMoreTimespans(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" || + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" || cc.Timespans[0].Increments[0].Duration != time.Minute { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -499,8 +498,8 @@ func TestDebitCreditMoreTimespans(t *testing.T) { } func TestDebitCreditMoreTimespansMixed(t *testing.T) { - b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} - b2 := &Balance{Uuid: "testa", Value: 150, Weight: 5, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1s"} + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} + b2 := &Balance{Uuid: "testa", Value: 150, Weight: 5, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1s"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -529,15 +528,15 @@ func TestDebitCreditMoreTimespansMixed(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1, b2}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1, b2}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" || + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" || cc.Timespans[0].Increments[0].Duration != time.Minute { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -549,7 +548,7 @@ func TestDebitCreditMoreTimespansMixed(t *testing.T) { } func TestDebitCreditNoConectFeeCredit(t *testing.T) { - b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "*zero1m"} cc := &CallCost{ Direction: utils.OUT, Destination: "0723045326", @@ -579,8 +578,8 @@ func TestDebitCreditNoConectFeeCredit(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) @@ -623,15 +622,15 @@ func TestDebitCreditMoneyOnly(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Uuid: "money", Value: 50}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Uuid: "money", Value: 50}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err == nil { t.Error("Missing noy enough credit error ") } - if cc.Timespans[0].Increments[0].BalanceInfo.MoneyBalanceUuid != "money" || + if cc.Timespans[0].Increments[0].BalanceInfo.Monetary.UUID != "money" || cc.Timespans[0].Increments[0].Duration != 10*time.Second { t.Logf("%+v", cc.Timespans[0].Increments) t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0].BalanceInfo) @@ -648,7 +647,7 @@ func TestDebitCreditMoneyOnly(t *testing.T) { } func TestDebitCreditSubjectMinutes(t *testing.T) { - b1 := &Balance{Uuid: "testb", Categories: utils.NewStringMap("0"), Value: 250, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "minu"} + b1 := &Balance{Uuid: "testb", Categories: utils.NewStringMap("0"), Value: 250, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "minu"} cc := &CallCost{ Tenant: "vdf", Category: "0", @@ -676,17 +675,17 @@ func TestDebitCreditSubjectMinutes(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1}, - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneya", Value: 350}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1}, + utils.MONETARY: Balances{&Balance{Uuid: "moneya", Value: 350}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" || - cc.Timespans[0].Increments[0].BalanceInfo.MoneyBalanceUuid != "moneya" || + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" || + cc.Timespans[0].Increments[0].BalanceInfo.Monetary.UUID != "moneya" || cc.Timespans[0].Increments[0].Duration != 10*time.Second { t.Errorf("Error setting balance id to increment: %+v", cc.Timespans[0].Increments[0]) } @@ -731,15 +730,15 @@ func TestDebitCreditSubjectMoney(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneya", Value: 75, DestinationIds: utils.StringMap{"NAT": true}, RatingSubject: "minu"}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Uuid: "moneya", Value: 75, DestinationIDs: utils.StringMap{"NAT": true}, RatingSubject: "minu"}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.MoneyBalanceUuid != "moneya" || + if cc.Timespans[0].Increments[0].BalanceInfo.Monetary.UUID != "moneya" || cc.Timespans[0].Increments[0].Duration != 10*time.Second { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -781,17 +780,17 @@ func TestDebitCreditSubjectMoney(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{b1}, - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneya", Value: 19500, RatingSubject: "minu"}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.VOICE: Balances{b1}, + utils.MONETARY: Balances{&Balance{Uuid: "moneya", Value: 19500, RatingSubject: "minu"}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testb" || - cc.Timespans[0].Increments[0].BalanceInfo.MoneyBalanceUuid != "moneya" || + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testb" || + cc.Timespans[0].Increments[0].BalanceInfo.Money.UUID != "moneya" || cc.Timespans[0].Increments[0].Duration != 10*time.Second { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } @@ -810,26 +809,36 @@ func TestDebitCreditSubjectMoney(t *testing.T) { func TestAccountdebitBalance(t *testing.T) { ub := &Account{ - Id: "rif", + ID: "rif", AllowNegative: true, - BalanceMap: map[string]BalanceChain{utils.SMS: BalanceChain{&Balance{Value: 14}}, utils.DATA: BalanceChain{&Balance{Value: 1204}}, utils.VOICE: BalanceChain{&Balance{Weight: 20, DestinationIds: utils.StringMap{"NAT": true}}, &Balance{Weight: 10, DestinationIds: utils.StringMap{"RET": true}}}}, + BalanceMap: map[string]Balances{utils.SMS: Balances{&Balance{Value: 14}}, utils.DATA: Balances{&Balance{Value: 1204}}, utils.VOICE: Balances{&Balance{Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, } - newMb := &Balance{Weight: 20, DestinationIds: utils.StringMap{"NEW": true}, Directions: utils.NewStringMap(utils.OUT)} - a := &Action{BalanceType: utils.VOICE, Balance: newMb} + newMb := &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Weight: utils.Float64Pointer(20), + DestinationIDs: utils.StringMapPointer(utils.StringMap{"NEW": true}), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + } + a := &Action{Balance: newMb} ub.debitBalanceAction(a, false) - if len(ub.BalanceMap[utils.VOICE]) != 3 || !ub.BalanceMap[utils.VOICE][2].DestinationIds.Equal(newMb.DestinationIds) { + if len(ub.BalanceMap[utils.VOICE]) != 3 || !ub.BalanceMap[utils.VOICE][2].DestinationIDs.Equal(*newMb.DestinationIDs) { t.Errorf("Error adding minute bucket! %d %+v %+v", len(ub.BalanceMap[utils.VOICE]), ub.BalanceMap[utils.VOICE][2], newMb) } } -func TestAccountDisableBalance(t *testing.T) { +/*func TestAccountDisableBalance(t *testing.T) { ub := &Account{ Id: "rif", AllowNegative: true, - BalanceMap: map[string]BalanceChain{utils.SMS: BalanceChain{&Balance{Value: 14}}, utils.DATA: BalanceChain{&Balance{Value: 1204}}, utils.VOICE: BalanceChain{&Balance{Weight: 20, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIds: utils.StringMap{"RET": true}}}}, + BalanceMap: map[string]Balances{utils.SMS: Balances{&Balance{Value: 14}}, utils.DATA: Balances{&Balance{Value: 1204}}, utils.VOICE: Balances{&Balance{Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}, Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, } - newMb := &Balance{Weight: 20, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.NewStringMap(utils.OUT), Disabled: true} - a := &Action{BalanceType: utils.VOICE, Balance: newMb} + newMb := &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Weight: utils.Float64Pointer(20), + DestinationIDs: utils.StringMapPointer(utils.StringMap{"NAT": true}), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + } + a := &Action{Balance: newMb} ub.enableDisableBalanceAction(a) if len(ub.BalanceMap[utils.VOICE]) != 2 || ub.BalanceMap[utils.VOICE][0].Disabled != true { for _, b := range ub.BalanceMap[utils.VOICE] { @@ -837,17 +846,23 @@ func TestAccountDisableBalance(t *testing.T) { } t.Errorf("Error disabling balance! %d %+v %+v", len(ub.BalanceMap[utils.VOICE]), ub.BalanceMap[utils.VOICE][0], newMb) } -} +}*/ func TestAccountdebitBalanceExists(t *testing.T) { ub := &Account{ - Id: "rif", + ID: "rif", AllowNegative: true, - BalanceMap: map[string]BalanceChain{utils.SMS: BalanceChain{&Balance{Value: 14}}, utils.DATA: BalanceChain{&Balance{Value: 1024}}, utils.VOICE: BalanceChain{&Balance{Value: 15, Weight: 20, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIds: utils.StringMap{"RET": true}}}}, + BalanceMap: map[string]Balances{utils.SMS: Balances{&Balance{Value: 14}}, utils.DATA: Balances{&Balance{Value: 1024}}, utils.VOICE: Balances{&Balance{Value: 15, Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}, Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, } - newMb := &Balance{Value: -10, Weight: 20, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.NewStringMap(utils.OUT)} - a := &Action{BalanceType: utils.VOICE, Balance: newMb} + newMb := &BalanceFilter{ + Value: utils.Float64Pointer(-10), + Type: utils.StringPointer(utils.VOICE), + Weight: utils.Float64Pointer(20), + DestinationIDs: utils.StringMapPointer(utils.StringMap{"NAT": true}), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + } + a := &Action{Balance: newMb} ub.debitBalanceAction(a, false) if len(ub.BalanceMap[utils.VOICE]) != 2 || ub.BalanceMap[utils.VOICE][0].GetValue() != 25 { t.Error("Error adding minute bucket!") @@ -856,9 +871,9 @@ func TestAccountdebitBalanceExists(t *testing.T) { func TestAccountAddMinuteNil(t *testing.T) { ub := &Account{ - Id: "rif", + ID: "rif", AllowNegative: true, - BalanceMap: map[string]BalanceChain{utils.SMS: BalanceChain{&Balance{Value: 14}}, utils.DATA: BalanceChain{&Balance{Value: 1024}}, utils.VOICE: BalanceChain{&Balance{Weight: 20, DestinationIds: utils.StringMap{"NAT": true}}, &Balance{Weight: 10, DestinationIds: utils.StringMap{"RET": true}}}}, + BalanceMap: map[string]Balances{utils.SMS: Balances{&Balance{Value: 14}}, utils.DATA: Balances{&Balance{Value: 1024}}, utils.VOICE: Balances{&Balance{Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, } ub.debitBalanceAction(nil, false) if len(ub.BalanceMap[utils.VOICE]) != 2 { @@ -867,21 +882,36 @@ func TestAccountAddMinuteNil(t *testing.T) { } func TestAccountAddMinutBucketEmpty(t *testing.T) { - mb1 := &Balance{Value: -10, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.NewStringMap(utils.OUT)} - mb2 := &Balance{Value: -10, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.NewStringMap(utils.OUT)} - mb3 := &Balance{Value: -10, DestinationIds: utils.StringMap{"OTHER": true}, Directions: utils.NewStringMap(utils.OUT)} + mb1 := &BalanceFilter{ + Value: utils.Float64Pointer(-10), + Type: utils.StringPointer(utils.VOICE), + DestinationIDs: utils.StringMapPointer(utils.StringMap{"NAT": true}), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + } + mb2 := &BalanceFilter{ + Value: utils.Float64Pointer(-10), + Type: utils.StringPointer(utils.VOICE), + DestinationIDs: utils.StringMapPointer(utils.StringMap{"NAT": true}), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + } + mb3 := &BalanceFilter{ + Value: utils.Float64Pointer(-10), + Type: utils.StringPointer(utils.VOICE), + DestinationIDs: utils.StringMapPointer(utils.StringMap{"OTHER": true}), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + } ub := &Account{} - a := &Action{BalanceType: utils.VOICE, Balance: mb1} + a := &Action{Balance: mb1} ub.debitBalanceAction(a, false) if len(ub.BalanceMap[utils.VOICE]) != 1 { t.Error("Error adding minute bucket: ", ub.BalanceMap[utils.VOICE]) } - a = &Action{BalanceType: utils.VOICE, Balance: mb2} + a = &Action{Balance: mb2} ub.debitBalanceAction(a, false) if len(ub.BalanceMap[utils.VOICE]) != 1 || ub.BalanceMap[utils.VOICE][0].GetValue() != 20 { t.Error("Error adding minute bucket: ", ub.BalanceMap[utils.VOICE]) } - a = &Action{BalanceType: utils.VOICE, Balance: mb3} + a = &Action{Balance: mb3} ub.debitBalanceAction(a, false) if len(ub.BalanceMap[utils.VOICE]) != 2 { t.Error("Error adding minute bucket: ", ub.BalanceMap[utils.VOICE]) @@ -890,10 +920,10 @@ func TestAccountAddMinutBucketEmpty(t *testing.T) { func TestAccountExecuteTriggeredActions(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.StringMap{utils.OUT: true}}, &Balance{Weight: 10, DestinationIds: utils.StringMap{"RET": true}}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1, Directions: utils.StringMap{utils.OUT: true}}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.StringMap{utils.OUT: true}, ThresholdValue: 2, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ActionsId: "TEST_ACTIONS"}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}, Directions: utils.StringMap{utils.OUT: true}}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1, Filter: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.StringMap{utils.OUT: true})}}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ActionsID: "TEST_ACTIONS"}}, } ub.countUnits(1, utils.MONETARY, &CallCost{Direction: utils.OUT}, nil) if ub.BalanceMap[utils.MONETARY][0].GetValue() != 110 || ub.BalanceMap[utils.VOICE][0].GetValue() != 20 { @@ -914,10 +944,10 @@ func TestAccountExecuteTriggeredActions(t *testing.T) { func TestAccountExecuteTriggeredActionsBalance(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: BalanceChain{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 10, Weight: 20, DestinationIds: utils.StringMap{"NAT": true}}, &Balance{Directions: utils.NewStringMap(utils.OUT), Weight: 10, DestinationIds: utils.StringMap{"RET": true}}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 100, ThresholdType: utils.TRIGGER_MIN_EVENT_COUNTER, ActionsId: "TEST_ACTIONS"}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 10, Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}}, &Balance{Directions: utils.NewStringMap(utils.OUT), Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Filter: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, Value: 1.0}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 100, ThresholdType: utils.TRIGGER_MIN_EVENT_COUNTER, ActionsID: "TEST_ACTIONS"}}, } ub.countUnits(1, utils.MONETARY, nil, nil) if ub.BalanceMap[utils.MONETARY][0].GetValue() != 110 || ub.BalanceMap[utils.VOICE][0].GetValue() != 20 { @@ -927,56 +957,56 @@ func TestAccountExecuteTriggeredActionsBalance(t *testing.T) { func TestAccountExecuteTriggeredActionsOrder(t *testing.T) { ub := &Account{ - Id: "TEST_UB_OREDER", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1, Directions: utils.NewStringMap(utils.OUT)}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ActionsId: "TEST_ACTIONS_ORDER"}}, + ID: "TEST_UB_OREDER", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), Type: utils.StringPointer(utils.MONETARY)}}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ActionsID: "TEST_ACTIONS_ORDER"}}, } ub.countUnits(1, utils.MONETARY, &CallCost{Direction: utils.OUT}, nil) if len(ub.BalanceMap[utils.MONETARY]) != 1 || ub.BalanceMap[utils.MONETARY][0].GetValue() != 10 { - t.Errorf("Error executing triggered actions in order %v BAL: %+v", ub.BalanceMap[utils.MONETARY][0].GetValue(), ub.BalanceMap[utils.MONETARY][1]) + t.Errorf("Error executing triggered actions in order %v", ub.BalanceMap[utils.MONETARY][0].GetValue()) } } func TestAccountExecuteTriggeredDayWeek(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.StringMap{utils.OUT: true}}, &Balance{Weight: 10, DestinationIds: utils.StringMap{"RET": true}}}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}, Directions: utils.StringMap{utils.OUT: true}}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, ActionTriggers: ActionTriggers{ - &ActionTrigger{UniqueID: "day_trigger", BalanceType: utils.MONETARY, BalanceDirections: utils.StringMap{utils.OUT: true}, ThresholdValue: 10, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ActionsId: "TEST_ACTIONS"}, - &ActionTrigger{UniqueID: "week_trigger", BalanceType: utils.MONETARY, BalanceDirections: utils.StringMap{utils.OUT: true}, ThresholdValue: 100, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ActionsId: "TEST_ACTIONS"}, + &ActionTrigger{UniqueID: "day_trigger", Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 10, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ActionsID: "TEST_ACTIONS"}, + &ActionTrigger{UniqueID: "week_trigger", Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 100, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ActionsID: "TEST_ACTIONS"}, }, } ub.InitCounters() - if len(ub.UnitCounters) != 1 || len(ub.UnitCounters[0].Balances) != 2 { - log.Print("Error initializing counters: ", ub.UnitCounters[0].Balances[0]) + if len(ub.UnitCounters) != 1 || len(ub.UnitCounters[utils.MONETARY][0].Counters) != 2 { + t.Error("Error initializing counters: ", ub.UnitCounters[utils.MONETARY][0].Counters[0]) } ub.countUnits(1, utils.MONETARY, &CallCost{Direction: utils.OUT}, nil) - if ub.UnitCounters[0].Balances[0].Value != 1 || - ub.UnitCounters[0].Balances[1].Value != 1 { - t.Error("Error incrementing both counters", ub.UnitCounters[0].Balances[0].Value, ub.UnitCounters[0].Balances[1].Value) + if ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 1 || + ub.UnitCounters[utils.MONETARY][0].Counters[1].Value != 1 { + t.Error("Error incrementing both counters", ub.UnitCounters[utils.MONETARY][0].Counters[0].Value, ub.UnitCounters[utils.MONETARY][0].Counters[1].Value) } // we can reset them - resetCountersAction(ub, nil, &Action{BalanceType: utils.MONETARY, Balance: &Balance{Id: "day_trigger"}}, nil) - if ub.UnitCounters[0].Balances[0].Value != 0 || - ub.UnitCounters[0].Balances[1].Value != 1 { - t.Error("Error reseting both counters", ub.UnitCounters[0].Balances[0].Value, ub.UnitCounters[0].Balances[1].Value) + resetCountersAction(ub, nil, &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), ID: utils.StringPointer("day_trigger")}}, nil) + if ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 0 || + ub.UnitCounters[utils.MONETARY][0].Counters[1].Value != 1 { + t.Error("Error reseting both counters", ub.UnitCounters[utils.MONETARY][0].Counters[0].Value, ub.UnitCounters[utils.MONETARY][0].Counters[1].Value) } } func TestAccountExpActionTrigger(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100, ExpirationDate: time.Date(2015, time.November, 9, 9, 48, 0, 0, time.UTC)}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.StringMap{"NAT": true}, Directions: utils.StringMap{utils.OUT: true}}, &Balance{Weight: 10, DestinationIds: utils.StringMap{"RET": true}}}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100, ExpirationDate: time.Date(2015, time.November, 9, 9, 48, 0, 0, time.UTC)}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}, Directions: utils.StringMap{utils.OUT: true}}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, ActionTriggers: ActionTriggers{ - &ActionTrigger{ID: "check expired balances", BalanceType: utils.MONETARY, BalanceDirections: utils.StringMap{utils.OUT: true}, ThresholdValue: 10, ThresholdType: utils.TRIGGER_BALANCE_EXPIRED, ActionsId: "TEST_ACTIONS"}, + &ActionTrigger{ID: "check expired balances", Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 10, ThresholdType: utils.TRIGGER_BALANCE_EXPIRED, ActionsID: "TEST_ACTIONS"}, }, } - ub.executeActionTriggers(nil) + ub.ExecuteActionTriggers(nil) if ub.BalanceMap[utils.MONETARY][0].IsExpired() || ub.BalanceMap[utils.MONETARY][0].GetValue() != 10 || // expired was cleaned ub.BalanceMap[utils.VOICE][0].GetValue() != 20 || @@ -986,97 +1016,117 @@ func TestAccountExpActionTrigger(t *testing.T) { } } +func TestAccountExpActionTriggerNotActivated(t *testing.T) { + ub := &Account{ + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}, Directions: utils.StringMap{utils.OUT: true}}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, + ActionTriggers: ActionTriggers{ + &ActionTrigger{ID: "check expired balances", ActivationDate: time.Date(2116, 2, 5, 18, 0, 0, 0, time.UTC), Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 10, ThresholdType: utils.TRIGGER_BALANCE_EXPIRED, ActionsID: "TEST_ACTIONS"}, + }, + } + ub.ExecuteActionTriggers(nil) + if ub.BalanceMap[utils.MONETARY][0].IsExpired() || + ub.BalanceMap[utils.MONETARY][0].GetValue() != 100 || + ub.BalanceMap[utils.VOICE][0].GetValue() != 10 || + ub.ActionTriggers[0].Executed != false { + t.Log(ub.BalanceMap[utils.MONETARY][0].IsExpired()) + t.Error("Error executing triggered actions", ub.BalanceMap[utils.MONETARY][0].GetValue(), ub.BalanceMap[utils.VOICE][0].GetValue(), len(ub.BalanceMap[utils.MONETARY])) + } +} + +func TestAccountExpActionTriggerExpired(t *testing.T) { + ub := &Account{ + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}, Directions: utils.StringMap{utils.OUT: true}}, &Balance{Weight: 10, DestinationIDs: utils.StringMap{"RET": true}}}}, + ActionTriggers: ActionTriggers{ + &ActionTrigger{ID: "check expired balances", ExpirationDate: time.Date(2016, 2, 4, 18, 0, 0, 0, time.UTC), Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 10, ThresholdType: utils.TRIGGER_BALANCE_EXPIRED, ActionsID: "TEST_ACTIONS"}, + }, + } + ub.ExecuteActionTriggers(nil) + if ub.BalanceMap[utils.MONETARY][0].IsExpired() || + ub.BalanceMap[utils.MONETARY][0].GetValue() != 100 || + ub.BalanceMap[utils.VOICE][0].GetValue() != 10 || + len(ub.ActionTriggers) != 0 { + t.Log(ub.BalanceMap[utils.MONETARY][0].IsExpired()) + t.Error("Error executing triggered actions", ub.BalanceMap[utils.MONETARY][0].GetValue(), ub.BalanceMap[utils.VOICE][0].GetValue(), len(ub.BalanceMap[utils.MONETARY])) + } +} + func TestCleanExpired(t *testing.T) { ub := &Account{ - Id: "TEST_UB_OREDER", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{ + ID: "TEST_UB_OREDER", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{ &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}, &Balance{ExpirationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC)}, - &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}}, utils.VOICE: BalanceChain{ + &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}}, utils.VOICE: Balances{ &Balance{ExpirationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC)}, &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}, }}, + ActionTriggers: ActionTriggers{ + &ActionTrigger{ + ExpirationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC), + }, + &ActionTrigger{ + ActivationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC), + }, + }, } - ub.CleanExpiredBalances() + ub.CleanExpiredStuff() if len(ub.BalanceMap[utils.MONETARY]) != 2 { t.Error("Error cleaning expired balances!") } if len(ub.BalanceMap[utils.VOICE]) != 1 { t.Error("Error cleaning expired minute buckets!") } + if len(ub.ActionTriggers) != 1 { + t.Error("Error cleaning expired action triggers!") + } } func TestAccountUnitCounting(t *testing.T) { - ub := &Account{UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 0}}}}} + ub := &Account{UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 0}}}}}} ub.countUnits(10, utils.MONETARY, &CallCost{}, nil) - if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceType != utils.MONETARY || ub.UnitCounters[0].Balances[0].GetValue() != 10 { + if len(ub.UnitCounters[utils.MONETARY]) != 1 || ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 10 { t.Error("Error counting units") } ub.countUnits(10, utils.MONETARY, &CallCost{}, nil) - if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceType != utils.MONETARY || ub.UnitCounters[0].Balances[0].GetValue() != 20 { + if len(ub.UnitCounters[utils.MONETARY]) != 1 || ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 20 { t.Error("Error counting units") } } func TestAccountUnitCountingOutbound(t *testing.T) { - ub := &Account{UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 0, Directions: utils.NewStringMap(utils.OUT)}}}}} + ub := &Account{UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 0, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}}}}}}} ub.countUnits(10, utils.MONETARY, &CallCost{Direction: utils.OUT}, nil) - if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceType != utils.MONETARY || ub.UnitCounters[0].Balances[0].GetValue() != 10 { + if len(ub.UnitCounters[utils.MONETARY]) != 1 || ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 10 { t.Error("Error counting units") } ub.countUnits(10, utils.MONETARY, &CallCost{Direction: utils.OUT}, nil) - if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceType != utils.MONETARY || ub.UnitCounters[0].Balances[0].GetValue() != 20 { + if len(ub.UnitCounters[utils.MONETARY]) != 1 || ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 20 { t.Error("Error counting units") } ub.countUnits(10, utils.MONETARY, &CallCost{Direction: utils.OUT}, nil) - if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceType != utils.MONETARY || ub.UnitCounters[0].Balances[0].GetValue() != 30 { + if len(ub.UnitCounters[utils.MONETARY]) != 1 || ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 30 { t.Error("Error counting units") } } func TestAccountUnitCountingOutboundInbound(t *testing.T) { - ub := &Account{UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 0, Directions: utils.NewStringMap(utils.OUT)}}}}} + ub := &Account{UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 0, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}}}}}}} ub.countUnits(10, utils.MONETARY, &CallCost{Direction: utils.OUT}, nil) - if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceType != utils.MONETARY || ub.UnitCounters[0].Balances[0].GetValue() != 10 { - t.Errorf("Error counting units: %+v", ub.UnitCounters[0].Balances[0]) + if len(ub.UnitCounters[utils.MONETARY]) != 1 || ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 10 { + t.Errorf("Error counting units: %+v", ub.UnitCounters[utils.MONETARY][0].Counters[0]) } ub.countUnits(10, utils.MONETARY, &CallCost{Direction: utils.OUT}, nil) - if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceType != utils.MONETARY || ub.UnitCounters[0].Balances[0].GetValue() != 20 { + if len(ub.UnitCounters[utils.MONETARY]) != 1 || ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 20 { t.Error("Error counting units") } ub.countUnits(10, utils.MONETARY, &CallCost{Direction: utils.IN}, nil) - if len(ub.UnitCounters) != 1 || (ub.UnitCounters[0].BalanceType != utils.MONETARY || ub.UnitCounters[0].Balances[0].GetValue() != 20) { + if len(ub.UnitCounters[utils.MONETARY]) != 1 || ub.UnitCounters[utils.MONETARY][0].Counters[0].Value != 20 { t.Error("Error counting units") } } -func TestAccountRefund(t *testing.T) { - ub := &Account{ - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ - &Balance{Uuid: "moneya", Value: 100}, - }, - utils.VOICE: BalanceChain{ - &Balance{Uuid: "minutea", Value: 10, Weight: 20, DestinationIds: utils.StringMap{"NAT": true}}, - &Balance{Uuid: "minuteb", Value: 10, DestinationIds: utils.StringMap{"RET": true}}, - }, - }, - } - increments := Increments{ - &Increment{Cost: 2, BalanceInfo: &BalanceInfo{UnitBalanceUuid: "", MoneyBalanceUuid: "moneya"}}, - &Increment{Cost: 2, Duration: 3 * time.Second, BalanceInfo: &BalanceInfo{UnitBalanceUuid: "minutea", MoneyBalanceUuid: "moneya"}}, - &Increment{Duration: 4 * time.Second, BalanceInfo: &BalanceInfo{UnitBalanceUuid: "minuteb", MoneyBalanceUuid: ""}}, - } - for _, increment := range increments { - ub.refundIncrement(increment, &CallDescriptor{TOR: utils.VOICE}, false) - } - if ub.BalanceMap[utils.MONETARY][0].GetValue() != 104 || - ub.BalanceMap[utils.VOICE][0].GetValue() != 13 || - ub.BalanceMap[utils.VOICE][1].GetValue() != 14 { - t.Error("Error refunding money: ", ub.BalanceMap[utils.VOICE][1].GetValue()) - } -} - func TestDebitShared(t *testing.T) { cc := &CallCost{ Tenant: "vdf", @@ -1104,14 +1154,14 @@ func TestDebitShared(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rif := &Account{Id: "rif", BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneya", Value: 0, SharedGroups: utils.NewStringMap("SG_TEST")}}, + rif := &Account{ID: "rif", BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Uuid: "moneya", Value: 0, SharedGroups: utils.NewStringMap("SG_TEST")}}, }} - groupie := &Account{Id: "groupie", BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneyc", Value: 130, SharedGroups: utils.NewStringMap("SG_TEST")}}, + groupie := &Account{ID: "groupie", BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Uuid: "moneyc", Value: 130, SharedGroups: utils.NewStringMap("SG_TEST")}}, }} - sg := &SharedGroup{Id: "SG_TEST", MemberIds: utils.NewStringMap(rif.Id, groupie.Id), AccountParameters: map[string]*SharingParameters{"*any": &SharingParameters{Strategy: STRATEGY_MINE_RANDOM}}} + sg := &SharedGroup{Id: "SG_TEST", MemberIds: utils.NewStringMap(rif.ID, groupie.ID), AccountParameters: map[string]*SharingParameters{"*any": &SharingParameters{Strategy: STRATEGY_MINE_RANDOM}}} accountingStorage.SetAccount(groupie) ratingStorage.SetSharedGroup(sg) @@ -1137,12 +1187,12 @@ func TestDebitShared(t *testing.T) { t.Errorf("I%d: %+v (%+v)", index, incr, incr.BalanceInfo) } } - if cc.Timespans[0].Increments[0].BalanceInfo.AccountId != "groupie" || - cc.Timespans[0].Increments[1].BalanceInfo.AccountId != "groupie" || - cc.Timespans[0].Increments[2].BalanceInfo.AccountId != "groupie" || - cc.Timespans[0].Increments[3].BalanceInfo.AccountId != "groupie" || - cc.Timespans[0].Increments[4].BalanceInfo.AccountId != "groupie" || - cc.Timespans[0].Increments[5].BalanceInfo.AccountId != "groupie" { + if cc.Timespans[0].Increments[0].BalanceInfo.AccountID != "groupie" || + cc.Timespans[0].Increments[1].BalanceInfo.AccountID != "groupie" || + cc.Timespans[0].Increments[2].BalanceInfo.AccountID != "groupie" || + cc.Timespans[0].Increments[3].BalanceInfo.AccountID != "groupie" || + cc.Timespans[0].Increments[4].BalanceInfo.AccountID != "groupie" || + cc.Timespans[0].Increments[5].BalanceInfo.AccountID != "groupie" { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } } @@ -1174,14 +1224,14 @@ func TestMaxDurationShared(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rif := &Account{Id: "rif", BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneya", Value: 0, SharedGroups: utils.NewStringMap("SG_TEST")}}, + rif := &Account{ID: "rif", BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Uuid: "moneya", Value: 0, SharedGroups: utils.NewStringMap("SG_TEST")}}, }} - groupie := &Account{Id: "groupie", BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneyc", Value: 130, SharedGroups: utils.NewStringMap("SG_TEST")}}, + groupie := &Account{ID: "groupie", BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Uuid: "moneyc", Value: 130, SharedGroups: utils.NewStringMap("SG_TEST")}}, }} - sg := &SharedGroup{Id: "SG_TEST", MemberIds: utils.NewStringMap(rif.Id, groupie.Id), AccountParameters: map[string]*SharingParameters{"*any": &SharingParameters{Strategy: STRATEGY_MINE_RANDOM}}} + sg := &SharedGroup{Id: "SG_TEST", MemberIds: utils.NewStringMap(rif.ID, groupie.ID), AccountParameters: map[string]*SharingParameters{"*any": &SharingParameters{Strategy: STRATEGY_MINE_RANDOM}}} accountingStorage.SetAccount(groupie) ratingStorage.SetSharedGroup(sg) @@ -1209,8 +1259,8 @@ func TestMaxDurationConnectFeeOnly(t *testing.T) { TOR: utils.VOICE, DurationIndex: 600, } - rif := &Account{Id: "rif", BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Uuid: "moneya", Value: 0.2}}, + rif := &Account{ID: "rif", BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Uuid: "moneya", Value: 0.2}}, }} duration, err := cd.getMaxSessionDuration(rif) @@ -1247,16 +1297,16 @@ func TestDebitSMS(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.SMS: BalanceChain{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIds: utils.StringMap{"NAT": true}}}, - utils.MONETARY: BalanceChain{&Balance{Value: 21}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.SMS: Balances{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIDs: utils.StringMap{"NAT": true}}}, + utils.MONETARY: Balances{&Balance{Value: 21}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testm" { + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testm" { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } if rifsBalance.BalanceMap[utils.SMS][0].GetValue() != 99 || @@ -1290,16 +1340,16 @@ func TestDebitGeneric(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.GENERIC: BalanceChain{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIds: utils.StringMap{"NAT": true}}}, - utils.MONETARY: BalanceChain{&Balance{Value: 21}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.GENERIC: Balances{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIDs: utils.StringMap{"NAT": true}}}, + utils.MONETARY: Balances{&Balance{Value: 21}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testm" { + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testm" { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } if rifsBalance.BalanceMap[utils.GENERIC][0].GetValue() != 99 || @@ -1333,19 +1383,19 @@ func TestDebitGenericBalance(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.GENERIC: BalanceChain{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIds: utils.StringMap{"NAT": true}, Factor: ValueFactor{utils.VOICE: 60.0}}}, - utils.MONETARY: BalanceChain{&Balance{Value: 21}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.GENERIC: Balances{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIDs: utils.StringMap{"NAT": true}, Factor: ValueFactor{utils.VOICE: 60.0}}}, + utils.MONETARY: Balances{&Balance{Value: 21}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testm" { + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testm" { t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) } - if rifsBalance.BalanceMap[utils.GENERIC][0].GetValue() != 99.4999 || + if rifsBalance.BalanceMap[utils.GENERIC][0].GetValue() != 99.49999 || rifsBalance.BalanceMap[utils.MONETARY][0].GetValue() != 21 { t.Logf("%+v", cc.Timespans[0].Increments[0]) t.Error("Error extracting minutes from balance: ", rifsBalance.BalanceMap[utils.GENERIC][0].GetValue(), rifsBalance.BalanceMap[utils.MONETARY][0].GetValue()) @@ -1376,19 +1426,19 @@ func TestDebitGenericBalanceWithRatingSubject(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.GENERIC: BalanceChain{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIds: utils.StringMap{"NAT": true}, Factor: ValueFactor{utils.VOICE: 60.0}, RatingSubject: "free"}}, - utils.MONETARY: BalanceChain{&Balance{Value: 21}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.GENERIC: Balances{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIDs: utils.StringMap{"NAT": true}, Factor: ValueFactor{utils.VOICE: 60.0}, RatingSubject: "free"}}, + utils.MONETARY: Balances{&Balance{Value: 21}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) if err != nil { t.Error("Error debiting balance: ", err) } - if cc.Timespans[0].Increments[0].BalanceInfo.UnitBalanceUuid != "testm" { + if cc.Timespans[0].Increments[0].BalanceInfo.Unit.UUID != "testm" { t.Error("Error setting balance id to increment: ", cc.Timespans[0]) } - if rifsBalance.BalanceMap[utils.GENERIC][0].GetValue() != 99.4999 || + if rifsBalance.BalanceMap[utils.GENERIC][0].GetValue() != 99.49999 || rifsBalance.BalanceMap[utils.MONETARY][0].GetValue() != 21 { t.Logf("%+v", cc.Timespans[0].Increments[0]) t.Error("Error extracting minutes from balance: ", rifsBalance.BalanceMap[utils.GENERIC][0].GetValue(), rifsBalance.BalanceMap[utils.MONETARY][0].GetValue()) @@ -1426,9 +1476,9 @@ func TestDebitDataUnits(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.DATA: BalanceChain{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIds: utils.StringMap{"NAT": true}}}, - utils.MONETARY: BalanceChain{&Balance{Value: 21}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.DATA: Balances{&Balance{Uuid: "testm", Value: 100, Weight: 5, DestinationIDs: utils.StringMap{"NAT": true}}}, + utils.MONETARY: Balances{&Balance{Value: 21}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) @@ -1440,7 +1490,7 @@ func TestDebitDataUnits(t *testing.T) { if err != nil { t.Error("Error debiting balance: ", err) } - if ts.Increments[0].BalanceInfo.UnitBalanceUuid != "testm" { + if ts.Increments[0].BalanceInfo.Unit.UUID != "testm" { t.Error("Error setting balance id to increment: ", ts.Increments[0]) } if rifsBalance.BalanceMap[utils.DATA][0].GetValue() != 20 || @@ -1480,9 +1530,9 @@ func TestDebitDataMoney(t *testing.T) { DurationIndex: cc.GetDuration(), testCallcost: cc, } - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{ - utils.DATA: BalanceChain{&Balance{Uuid: "testm", Value: 0, Weight: 5, DestinationIds: utils.StringMap{"NAT": true}}}, - utils.MONETARY: BalanceChain{&Balance{Value: 160}}, + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{ + utils.DATA: Balances{&Balance{Uuid: "testm", Value: 0, Weight: 5, DestinationIDs: utils.StringMap{"NAT": true}}}, + utils.MONETARY: Balances{&Balance{Value: 160}}, }} var err error cc, err = rifsBalance.debitCreditBalance(cd, false, false, true) @@ -1505,7 +1555,7 @@ func TestAccountGetDefaultMoneyBalanceEmpty(t *testing.T) { func TestAccountGetDefaultMoneyBalance(t *testing.T) { acc := &Account{} - acc.BalanceMap = make(map[string]BalanceChain) + acc.BalanceMap = make(map[string]Balances) tag := utils.MONETARY acc.BalanceMap[tag] = append(acc.BalanceMap[tag], &Balance{Weight: 10}) defBal := acc.GetDefaultMoneyBalance() @@ -1518,59 +1568,74 @@ func TestAccountInitCounters(t *testing.T) { a := &Account{ ActionTriggers: ActionTriggers{ &ActionTrigger{ - UniqueID: "TestTR1", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR1", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR11", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR11", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR2", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR2", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR3", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR3", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR4", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR4", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR5", - ThresholdType: utils.TRIGGER_MAX_BALANCE, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR5", + ThresholdType: utils.TRIGGER_MAX_BALANCE, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, }, } a.InitCounters() - if len(a.UnitCounters) != 4 || - len(a.UnitCounters[0].Balances) != 2 || - len(a.UnitCounters[1].Balances) != 1 || - len(a.UnitCounters[2].Balances) != 1 || - len(a.UnitCounters[3].Balances) != 1 { - for _, uc := range a.UnitCounters { - t.Logf("UC: %+v", uc) - for _, b := range uc.Balances { - t.Logf("B: %+v", b) + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.MONETARY][0].Counters) != 2 || + len(a.UnitCounters[utils.VOICE][0].Counters) != 1 || + len(a.UnitCounters[utils.VOICE][1].Counters) != 1 || + len(a.UnitCounters[utils.SMS][0].Counters) != 1 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, c := range uc.Counters { + t.Logf("B: %+v", c) + } } } t.Errorf("Error Initializing unit counters: %v", len(a.UnitCounters)) @@ -1581,60 +1646,75 @@ func TestAccountDoubleInitCounters(t *testing.T) { a := &Account{ ActionTriggers: ActionTriggers{ &ActionTrigger{ - UniqueID: "TestTR1", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR1", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR11", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR11", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR2", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR2", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR3", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR3", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR4", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR4", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR5", - ThresholdType: utils.TRIGGER_MAX_BALANCE, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR5", + ThresholdType: utils.TRIGGER_MAX_BALANCE, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, }, } a.InitCounters() a.InitCounters() - if len(a.UnitCounters) != 4 || - len(a.UnitCounters[0].Balances) != 2 || - len(a.UnitCounters[1].Balances) != 1 || - len(a.UnitCounters[2].Balances) != 1 || - len(a.UnitCounters[3].Balances) != 1 { - for _, uc := range a.UnitCounters { - t.Logf("UC: %+v", uc) - for _, b := range uc.Balances { - t.Logf("B: %+v", b) + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.MONETARY][0].Counters) != 2 || + len(a.UnitCounters[utils.VOICE][0].Counters) != 1 || + len(a.UnitCounters[utils.VOICE][1].Counters) != 1 || + len(a.UnitCounters[utils.SMS][0].Counters) != 1 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, c := range uc.Counters { + t.Logf("B: %+v", c) + } } } t.Errorf("Error Initializing unit counters: %v", len(a.UnitCounters)) @@ -1645,10 +1725,10 @@ func TestAccountDoubleInitCounters(t *testing.T) { func BenchmarkGetSecondForPrefix(b *testing.B) { b.StopTimer() - b1 := &Balance{Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}} - b2 := &Balance{Value: 100, Weight: 20, DestinationIds: utils.StringMap{"RET": true}} + b1 := &Balance{Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}} + b2 := &Balance{Value: 100, Weight: 20, DestinationIDs: utils.StringMap{"RET": true}} - ub1 := &Account{Id: "other", BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b1, b2}, utils.MONETARY: BalanceChain{&Balance{Value: 21}}}} + ub1 := &Account{ID: "other", BalanceMap: map[string]Balances{utils.VOICE: Balances{b1, b2}, utils.MONETARY: Balances{&Balance{Value: 21}}}} cd := &CallDescriptor{ Destination: "0723", } @@ -1659,19 +1739,19 @@ func BenchmarkGetSecondForPrefix(b *testing.B) { } func BenchmarkAccountStorageStoreRestore(b *testing.B) { - b1 := &Balance{Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}} - b2 := &Balance{Value: 100, Weight: 20, DestinationIds: utils.StringMap{"RET": true}} - rifsBalance := &Account{Id: "other", BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b1, b2}, utils.MONETARY: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}} + b2 := &Balance{Value: 100, Weight: 20, DestinationIDs: utils.StringMap{"RET": true}} + rifsBalance := &Account{ID: "other", BalanceMap: map[string]Balances{utils.VOICE: Balances{b1, b2}, utils.MONETARY: Balances{&Balance{Value: 21}}}} for i := 0; i < b.N; i++ { accountingStorage.SetAccount(rifsBalance) - accountingStorage.GetAccount(rifsBalance.Id) + accountingStorage.GetAccount(rifsBalance.ID) } } func BenchmarkGetSecondsForPrefix(b *testing.B) { - b1 := &Balance{Value: 10, Weight: 10, DestinationIds: utils.StringMap{"NAT": true}} - b2 := &Balance{Value: 100, Weight: 20, DestinationIds: utils.StringMap{"RET": true}} - ub1 := &Account{Id: "OUT:CUSTOMER_1:rif", BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b1, b2}, utils.MONETARY: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationIDs: utils.StringMap{"NAT": true}} + b2 := &Balance{Value: 100, Weight: 20, DestinationIDs: utils.StringMap{"RET": true}} + ub1 := &Account{ID: "OUT:CUSTOMER_1:rif", BalanceMap: map[string]Balances{utils.VOICE: Balances{b1, b2}, utils.MONETARY: Balances{&Balance{Value: 21}}}} cd := &CallDescriptor{ Destination: "0723", } diff --git a/engine/action.go b/engine/action.go index 147b176a7..fff13356c 100644 --- a/engine/action.go +++ b/engine/action.go @@ -40,32 +40,32 @@ Structure to be filled for each tariff plan with the bonus value for received ca type Action struct { Id string ActionType string - BalanceType string ExtraParameters string Filter string ExpirationString string // must stay as string because it can have relative values like 1month Weight float64 - Balance *Balance + Balance *BalanceFilter } const ( - LOG = "*log" - RESET_TRIGGERS = "*reset_triggers" - SET_RECURRENT = "*set_recurrent" - UNSET_RECURRENT = "*unset_recurrent" - ALLOW_NEGATIVE = "*allow_negative" - DENY_NEGATIVE = "*deny_negative" - RESET_ACCOUNT = "*reset_account" - REMOVE_ACCOUNT = "*remove_account" - REMOVE_BALANCE = "*remove_balance" - TOPUP_RESET = "*topup_reset" - TOPUP = "*topup" - DEBIT_RESET = "*debit_reset" - DEBIT = "*debit" - RESET_COUNTERS = "*reset_counters" - ENABLE_ACCOUNT = "*enable_account" - DISABLE_ACCOUNT = "*disable_account" - ENABLE_DISABLE_BALANCE = "*enable_disable_balance" + LOG = "*log" + RESET_TRIGGERS = "*reset_triggers" + SET_RECURRENT = "*set_recurrent" + UNSET_RECURRENT = "*unset_recurrent" + ALLOW_NEGATIVE = "*allow_negative" + DENY_NEGATIVE = "*deny_negative" + RESET_ACCOUNT = "*reset_account" + REMOVE_ACCOUNT = "*remove_account" + SET_BALANCE = "*set_balance" + REMOVE_BALANCE = "*remove_balance" + TOPUP_RESET = "*topup_reset" + TOPUP = "*topup" + DEBIT_RESET = "*debit_reset" + DEBIT = "*debit" + RESET_COUNTERS = "*reset_counters" + ENABLE_ACCOUNT = "*enable_account" + DISABLE_ACCOUNT = "*disable_account" + //ENABLE_DISABLE_BALANCE = "*enable_disable_balance" CALL_URL = "*call_url" CALL_URL_ASYNC = "*call_url_async" MAIL_ASYNC = "*mail_async" @@ -77,13 +77,13 @@ const ( func (a *Action) Clone() *Action { return &Action{ - Id: a.Id, - ActionType: a.ActionType, - BalanceType: a.BalanceType, + Id: a.Id, + ActionType: a.ActionType, + //BalanceType: a.BalanceType, ExtraParameters: a.ExtraParameters, ExpirationString: a.ExpirationString, Weight: a.Weight, - Balance: a.Balance.Clone(), + Balance: a.Balance, } } @@ -121,8 +121,8 @@ func getActionFunc(typ string) (actionTypeFunc, bool) { return enableUserAction, true case DISABLE_ACCOUNT: return disableUserAction, true - case ENABLE_DISABLE_BALANCE: - return enableDisableBalanceAction, true + //case ENABLE_DISABLE_BALANCE: + // return enableDisableBalanceAction, true case CALL_URL: return callUrl, true case CALL_URL_ASYNC: @@ -135,8 +135,10 @@ func getActionFunc(typ string) (actionTypeFunc, bool) { return removeAccountAction, true case REMOVE_BALANCE: return removeBalanceAction, true + case SET_BALANCE: + return setBalanceAction, true case TRANSFER_MONETARY_DEFAULT: - return transferMonetaryDefault, true + return transferMonetaryDefaultAction, true } return nil, false } @@ -155,17 +157,22 @@ func logAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (er // Used by cdrLogAction to dynamically parse values out of account and action func parseTemplateValue(rsrFlds utils.RSRFields, acnt *Account, action *Action) string { - dta, err := utils.NewTAFromAccountKey(acnt.Id) // Account information should be valid - if err != nil { + var err error + var dta *utils.TenantAccount + if acnt != nil { + dta, err = utils.NewTAFromAccountKey(acnt.ID) // Account information should be valid + } + if err != nil || acnt == nil { dta = new(utils.TenantAccount) // Init with empty values } var parsedValue string // Template values + b := action.Balance.CreateBalance() for _, rsrFld := range rsrFlds { switch rsrFld.Id { case "AccountID": - parsedValue += rsrFld.ParseValue(acnt.Id) + parsedValue += rsrFld.ParseValue(acnt.ID) case "Directions": - parsedValue += rsrFld.ParseValue(action.Balance.Directions.String()) + parsedValue += rsrFld.ParseValue(b.Directions.String()) case utils.TENANT: parsedValue += rsrFld.ParseValue(dta.Tenant) case utils.ACCOUNT: @@ -175,19 +182,19 @@ func parseTemplateValue(rsrFlds utils.RSRFields, acnt *Account, action *Action) case "ActionType": parsedValue += rsrFld.ParseValue(action.ActionType) case "BalanceType": - parsedValue += rsrFld.ParseValue(action.BalanceType) + parsedValue += rsrFld.ParseValue(action.Balance.GetType()) case "BalanceUUID": - parsedValue += rsrFld.ParseValue(action.Balance.Uuid) + parsedValue += rsrFld.ParseValue(b.Uuid) case "BalanceID": - parsedValue += rsrFld.ParseValue(action.Balance.Id) + parsedValue += rsrFld.ParseValue(b.ID) case "BalanceValue": - parsedValue += rsrFld.ParseValue(strconv.FormatFloat(action.Balance.GetValue(), 'f', -1, 64)) + parsedValue += rsrFld.ParseValue(strconv.FormatFloat(b.GetValue(), 'f', -1, 64)) case "DestinationIDs": - parsedValue += rsrFld.ParseValue(action.Balance.DestinationIds.String()) + parsedValue += rsrFld.ParseValue(b.DestinationIDs.String()) case "ExtraParameters": parsedValue += rsrFld.ParseValue(action.ExtraParameters) case "RatingSubject": - parsedValue += rsrFld.ParseValue(action.Balance.RatingSubject) + parsedValue += rsrFld.ParseValue(b.RatingSubject) case utils.CATEGORY: parsedValue += rsrFld.ParseValue(action.Balance.Categories.String()) case "SharedGroups": @@ -268,7 +275,7 @@ func cdrLogAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) func resetTriggersAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } ub.ResetActionTriggers(a) return @@ -276,7 +283,7 @@ func resetTriggersAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Ac func setRecurrentAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } ub.SetRecurrent(a, true) return @@ -284,7 +291,7 @@ func setRecurrentAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Act func unsetRecurrentAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } ub.SetRecurrent(a, false) return @@ -292,7 +299,7 @@ func unsetRecurrentAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs A func allowNegativeAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } ub.AllowNegative = true return @@ -300,7 +307,7 @@ func allowNegativeAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Ac func denyNegativeAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } ub.AllowNegative = false return @@ -308,17 +315,17 @@ func denyNegativeAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Act func resetAccountAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } return genericReset(ub) } func topupResetAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } if ub.BalanceMap == nil { // Init the map since otherwise will get error if nil - ub.BalanceMap = make(map[string]BalanceChain, 0) + ub.BalanceMap = make(map[string]Balances, 0) } c := a.Clone() genericMakeNegative(c) @@ -327,7 +334,7 @@ func topupResetAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actio func topupAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } c := a.Clone() genericMakeNegative(c) @@ -336,17 +343,17 @@ func topupAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) ( func debitResetAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } if ub.BalanceMap == nil { // Init the map since otherwise will get error if nil - ub.BalanceMap = make(map[string]BalanceChain, 0) + ub.BalanceMap = make(map[string]Balances, 0) } return genericDebit(ub, a, true) } func debitAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } err = genericDebit(ub, a, false) return @@ -354,7 +361,7 @@ func debitAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) ( func resetCountersAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } if ub.UnitCounters != nil { ub.UnitCounters.resetCounters(a) @@ -370,17 +377,17 @@ func genericMakeNegative(a *Action) { func genericDebit(ub *Account, a *Action, reset bool) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } if ub.BalanceMap == nil { - ub.BalanceMap = make(map[string]BalanceChain) + ub.BalanceMap = make(map[string]Balances) } return ub.debitBalanceAction(a, reset) } func enableUserAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } ub.Disabled = false return @@ -388,23 +395,23 @@ func enableUserAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actio func disableUserAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } ub.Disabled = true return } -func enableDisableBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { +/*func enableDisableBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) (err error) { if ub == nil { - return errors.New("nil user balance") + return errors.New("nil account") } ub.enableDisableBalanceAction(a) return -} +}*/ func genericReset(ub *Account) error { for k, _ := range ub.BalanceMap { - ub.BalanceMap[k] = BalanceChain{&Balance{Value: 0}} + ub.BalanceMap[k] = Balances{&Balance{Value: 0}} } ub.InitCounters() ub.ResetActionTriggers(nil) @@ -461,7 +468,7 @@ func mailAsync(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) err if err != nil { return err } - message = []byte(fmt.Sprintf("To: %s\r\nSubject: [CGR Notification] Threshold hit on Balance: %s\r\n\r\nTime: \r\n\t%s\r\n\r\nBalance:\r\n\t%s\r\n\r\nYours faithfully,\r\nCGR Balance Monitor\r\n", toAddrStr, ub.Id, time.Now(), balJsn)) + message = []byte(fmt.Sprintf("To: %s\r\nSubject: [CGR Notification] Threshold hit on Balance: %s\r\n\r\nTime: \r\n\t%s\r\n\r\nBalance:\r\n\t%s\r\n\r\nYours faithfully,\r\nCGR Balance Monitor\r\n", toAddrStr, ub.ID, time.Now(), balJsn)) } else if sq != nil { message = []byte(fmt.Sprintf("To: %s\r\nSubject: [CGR Notification] Threshold hit on StatsQueueId: %s\r\n\r\nTime: \r\n\t%s\r\n\r\nStatsQueueId:\r\n\t%s\r\n\r\nMetrics:\r\n\t%+v\r\n\r\nTrigger:\r\n\t%+v\r\n\r\nYours faithfully,\r\nCGR CDR Stats Monitor\r\n", toAddrStr, sq.Id, time.Now(), sq.Id, sq.Metrics, sq.Trigger)) @@ -473,7 +480,7 @@ func mailAsync(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) err break } else if i == 4 { if ub != nil { - utils.Logger.Warning(fmt.Sprintf(" WARNING: Failed emailing, params: [%s], error: [%s], BalanceId: %s", a.ExtraParameters, err.Error(), ub.Id)) + utils.Logger.Warning(fmt.Sprintf(" WARNING: Failed emailing, params: [%s], error: [%s], BalanceId: %s", a.ExtraParameters, err.Error(), ub.ID)) } else if sq != nil { utils.Logger.Warning(fmt.Sprintf(" WARNING: Failed emailing, params: [%s], error: [%s], StatsQueueTriggeredId: %s", a.ExtraParameters, err.Error(), sq.Id)) } @@ -489,7 +496,7 @@ func setddestinations(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actio var ddcDestId string for _, bchain := range ub.BalanceMap { for _, b := range bchain { - for destId := range b.DestinationIds { + for destId := range b.DestinationIDs { if strings.HasPrefix(destId, "*ddc") { ddcDestId = destId break @@ -526,7 +533,7 @@ func setddestinations(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actio func removeAccountAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { var accID string if ub != nil { - accID = ub.Id + accID = ub.ID } else { accountInfo := struct { Tenant string @@ -546,34 +553,41 @@ func removeAccountAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Ac utils.Logger.Err(fmt.Sprintf("Could not remove account Id: %s: %v", accID, err)) return err } - // clean the account id from all action plans - allAPs, err := ratingStorage.GetAllActionPlans() - if err != nil && err != utils.ErrNotFound { - utils.Logger.Err(fmt.Sprintf("Could not get action plans: %s: %v", accID, err)) - return err - } - for key, ap := range allAPs { - if _, exists := ap.AccountIDs[accID]; !exists { - _, err := Guardian.Guard(func() (interface{}, error) { + _, err := Guardian.Guard(func() (interface{}, error) { + // clean the account id from all action plans + allAPs, err := ratingStorage.GetAllActionPlans() + if err != nil && err != utils.ErrNotFound { + utils.Logger.Err(fmt.Sprintf("Could not get action plans: %s: %v", accID, err)) + return 0, err + } + var dirtyAps []string + for key, ap := range allAPs { + if _, exists := ap.AccountIDs[accID]; !exists { // save action plan - ratingStorage.SetActionPlan(key, ap) - // cache - ratingStorage.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: []string{utils.ACTION_PLAN_PREFIX + key}}) - return 0, nil - }, 0, utils.ACTION_PLAN_PREFIX) - if err != nil { - return err + delete(ap.AccountIDs, key) + ratingStorage.SetActionPlan(key, ap, true) + dirtyAps = append(dirtyAps, utils.ACTION_PLAN_PREFIX+key) } } + if len(dirtyAps) > 0 { + // cache + ratingStorage.CacheRatingPrefixValues(map[string][]string{ + utils.ACTION_PLAN_PREFIX: dirtyAps}) + } + return 0, nil + + }, 0, utils.ACTION_PLAN_PREFIX) + if err != nil { + return err } return nil } func removeBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if _, exists := ub.BalanceMap[a.BalanceType]; !exists { + if _, exists := ub.BalanceMap[a.Balance.GetType()]; !exists { return utils.ErrNotFound } - bChain := ub.BalanceMap[a.BalanceType] + bChain := ub.BalanceMap[a.Balance.GetType()] found := false for i := 0; i < len(bChain); i++ { if bChain[i].MatchFilter(a.Balance, false) { @@ -584,15 +598,18 @@ func removeBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Ac found = true } } - ub.BalanceMap[a.BalanceType] = bChain + ub.BalanceMap[a.Balance.GetType()] = bChain if !found { return utils.ErrNotFound } - // update account in storage - return accountingStorage.SetAccount(ub) + return nil } -func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { +func setBalanceAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + return acc.setBalanceAction(a) +} + +func transferMonetaryDefaultAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { if acc == nil { utils.Logger.Err("*transfer_monetary_default called without account") return utils.ErrAccountNotFound @@ -604,7 +621,7 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a bChain := acc.BalanceMap[utils.MONETARY] for _, balance := range bChain { if balance.Uuid != defaultBalance.Uuid && - balance.Id != defaultBalance.Id && // extra caution + balance.ID != defaultBalance.ID && // extra caution balance.MatchFilter(a.Balance, false) { if balance.Value > 0 { defaultBalance.Value += balance.Value @@ -612,8 +629,7 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a } } } - // update account in storage - return accountingStorage.SetAccount(acc) + return nil } // Structure to store actions according to weight diff --git a/engine/action_plan.go b/engine/action_plan.go index 163ca4595..79095227c 100644 --- a/engine/action_plan.go +++ b/engine/action_plan.go @@ -37,9 +37,9 @@ type ActionTiming struct { ActionsID string Weight float64 actions Actions - accountIDs map[string]struct{} // copy of action plans accounts - actionPlanID string // the id of the belonging action plan (info only) - stCache time.Time // cached time of the next start + accountIDs utils.StringMap // copy of action plans accounts + actionPlanID string // the id of the belonging action plan (info only) + stCache time.Time // cached time of the next start } type Task struct { @@ -50,7 +50,7 @@ type Task struct { type ActionPlan struct { Id string // informative purpose only - AccountIDs map[string]struct{} + AccountIDs utils.StringMap ActionTimings []*ActionTiming } @@ -65,7 +65,7 @@ func (t *Task) Execute() error { return (&ActionTiming{ Uuid: t.Uuid, ActionsID: t.ActionsID, - accountIDs: map[string]struct{}{t.AccountID: struct{}{}}, + accountIDs: utils.StringMap{t.AccountID: true}, }).Execute() } @@ -250,11 +250,11 @@ func (at *ActionTiming) SetActions(as Actions) { at.actions = as } -func (at *ActionTiming) SetAccountIDs(accIDs map[string]struct{}) { +func (at *ActionTiming) SetAccountIDs(accIDs utils.StringMap) { at.accountIDs = accIDs } -func (at *ActionTiming) GetAccountIDs() map[string]struct{} { +func (at *ActionTiming) GetAccountIDs() utils.StringMap { return at.accountIDs } @@ -283,7 +283,7 @@ func (at *ActionTiming) Execute() (err error) { } for accID, _ := range at.accountIDs { _, err = Guardian.Guard(func() (interface{}, error) { - ub, err := accountingStorage.GetAccount(accID) + acc, err := accountingStorage.GetAccount(accID) if err != nil { utils.Logger.Warning(fmt.Sprintf("Could not get account id: %s. Skipping!", accID)) return 0, err @@ -291,9 +291,11 @@ func (at *ActionTiming) Execute() (err error) { transactionFailed := false removeAccountActionFound := false for _, a := range aac { + //log.Print("A: ", utils.ToJSON(a)) // check action filter if len(a.Filter) > 0 { - matched, err := ub.matchActionFilter(a.Filter) + matched, err := acc.matchActionFilter(a.Filter) + //log.Print("Checkng: ", a.Filter, matched) if err != nil { return 0, err } @@ -301,12 +303,14 @@ func (at *ActionTiming) Execute() (err error) { continue } } - if ub.Disabled && a.ActionType != ENABLE_ACCOUNT { - continue // disabled acocunts are not removed from action plan - //return 0, fmt.Errorf("Account %s is disabled", accID) + if a.Balance == nil { + a.Balance = &BalanceFilter{} } - if expDate, parseErr := utils.ParseDate(a.ExpirationString); (a.Balance == nil || a.Balance.ExpirationDate.IsZero()) && parseErr == nil && !expDate.IsZero() { - a.Balance.ExpirationDate = expDate + if a.ExpirationString != "" { // if it's *unlimited then it has to be zero time + if expDate, parseErr := utils.ParseDate(a.ExpirationString); parseErr == nil { + a.Balance.ExpirationDate = &time.Time{} + *a.Balance.ExpirationDate = expDate + } } actionFunction, exists := getActionFunc(a.ActionType) @@ -317,7 +321,7 @@ func (at *ActionTiming) Execute() (err error) { transactionFailed = true break } - if err := actionFunction(ub, nil, a, aac); err != nil { + if err := actionFunction(acc, nil, a, aac); err != nil { utils.Logger.Err(fmt.Sprintf("Error executing action %s: %v!", a.ActionType, err)) transactionFailed = true break @@ -327,17 +331,17 @@ func (at *ActionTiming) Execute() (err error) { } } if !transactionFailed && !removeAccountActionFound { - accountingStorage.SetAccount(ub) + accountingStorage.SetAccount(acc) } return 0, nil }, 0, accID) } if len(at.accountIDs) == 0 { // action timing executing without accounts for _, a := range aac { - - if expDate, parseErr := utils.ParseDate(a.ExpirationString); (a.Balance == nil || a.Balance.ExpirationDate.IsZero()) && + if expDate, parseErr := utils.ParseDate(a.ExpirationString); (a.Balance == nil || a.Balance.EmptyExpirationDate()) && parseErr == nil && !expDate.IsZero() { - a.Balance.ExpirationDate = expDate + a.Balance.ExpirationDate = &time.Time{} + *a.Balance.ExpirationDate = expDate } actionFunction, exists := getActionFunc(a.ActionType) @@ -348,7 +352,7 @@ func (at *ActionTiming) Execute() (err error) { break } if err := actionFunction(nil, nil, a, aac); err != nil { - utils.Logger.Err(fmt.Sprintf("Error executing action %s: %v!", a.ActionType, err)) + utils.Logger.Err(fmt.Sprintf("Error executing accountless action %s: %v!", a.ActionType, err)) break } } @@ -368,7 +372,7 @@ func (at *ActionTiming) IsASAP() bool { return at.Timing.Timing.StartTime == utils.ASAP } -// Structure to store actions according to weight +// Structure to store actions according to execution time and weight type ActionTimingPriorityList []*ActionTiming func (atpl ActionTimingPriorityList) Len() int { @@ -390,3 +394,22 @@ func (atpl ActionTimingPriorityList) Less(i, j int) bool { func (atpl ActionTimingPriorityList) Sort() { sort.Sort(atpl) } + +// Structure to store actions according to weight +type ActionTimingWeightOnlyPriorityList []*ActionTiming + +func (atpl ActionTimingWeightOnlyPriorityList) Len() int { + return len(atpl) +} + +func (atpl ActionTimingWeightOnlyPriorityList) Swap(i, j int) { + atpl[i], atpl[j] = atpl[j], atpl[i] +} + +func (atpl ActionTimingWeightOnlyPriorityList) Less(i, j int) bool { + return atpl[i].Weight > atpl[j].Weight +} + +func (atpl ActionTimingWeightOnlyPriorityList) Sort() { + sort.Sort(atpl) +} diff --git a/engine/action_trigger.go b/engine/action_trigger.go index 9fa9ace1d..0b4d4e03a 100644 --- a/engine/action_trigger.go +++ b/engine/action_trigger.go @@ -21,7 +21,6 @@ package engine import ( "encoding/json" "fmt" - "regexp" "sort" "time" @@ -33,26 +32,18 @@ type ActionTrigger struct { UniqueID string // individual id ThresholdType string //*min_event_counter, *max_event_counter, *min_balance_counter, *max_balance_counter, *min_balance, *max_balance, *exp_balance // stats: *min_asr, *max_asr, *min_acd, *max_acd, *min_tcd, *max_tcd, *min_acc, *max_acc, *min_tcc, *max_tcc, *min_ddc, *max_ddc - ThresholdValue float64 - Recurrent bool // reset eexcuted flag each run - MinSleep time.Duration // Minimum duration between two executions in case of recurrent triggers - BalanceId string - BalanceType string // *monetary/*voice etc - BalanceDirections utils.StringMap // filter for balance - BalanceDestinationIds utils.StringMap // filter for balance - BalanceWeight float64 // filter for balance - BalanceExpirationDate time.Time // filter for balance - BalanceTimingTags utils.StringMap // filter for balance - BalanceRatingSubject string // filter for balance - BalanceCategories utils.StringMap // filter for balance - BalanceSharedGroups utils.StringMap // filter for balance - BalanceBlocker bool - BalanceDisabled bool // filter for balance - Weight float64 - ActionsId string - MinQueuedItems int // Trigger actions only if this number is hit (stats only) - Executed bool - lastExecutionTime time.Time + ThresholdValue float64 + Recurrent bool // reset excuted flag each run + MinSleep time.Duration // Minimum duration between two executions in case of recurrent triggers + ExpirationDate time.Time + ActivationDate time.Time + //BalanceType string // *monetary/*voice etc + Balance *BalanceFilter + Weight float64 + ActionsID string + MinQueuedItems int // Trigger actions only if this number is hit (stats only) + Executed bool + lastExecutionTime time.Time } func (at *ActionTrigger) Execute(ub *Account, sq *StatsQueueTriggered) (err error) { @@ -62,11 +53,11 @@ func (at *ActionTrigger) Execute(ub *Account, sq *StatsQueueTriggered) (err erro } at.lastExecutionTime = time.Now() if ub != nil && ub.Disabled { - return fmt.Errorf("User %s is disabled and there are triggers in action!", ub.Id) + return fmt.Errorf("User %s is disabled and there are triggers in action!", ub.ID) } // does NOT need to Lock() because it is triggered from a method that took the Lock var aac Actions - aac, err = ratingStorage.GetActions(at.ActionsId, false) + aac, err = ratingStorage.GetActions(at.ActionsID, false) aac.Sort() if err != nil { utils.Logger.Err(fmt.Sprintf("Failed to get actions: %v", err)) @@ -86,11 +77,16 @@ func (at *ActionTrigger) Execute(ub *Account, sq *StatsQueueTriggered) (err erro continue } } - if a.Balance == nil { - a.Balance = &Balance{} + a.Balance = &BalanceFilter{} } - a.Balance.ExpirationDate, _ = utils.ParseDate(a.ExpirationString) + if a.ExpirationString != "" { // if it's *unlimited then it has to be zero time' + if expDate, parseErr := utils.ParseDate(a.ExpirationString); parseErr == nil { + a.Balance.ExpirationDate = &time.Time{} + *a.Balance.ExpirationDate = expDate + } + } + actionFunction, exists := getActionFunc(a.ActionType) if !exists { utils.Logger.Err(fmt.Sprintf("Function type %v not available, aborting execution!", a.ActionType)) @@ -111,7 +107,7 @@ func (at *ActionTrigger) Execute(ub *Account, sq *StatsQueueTriggered) (err erro at.Executed = false } if !transactionFailed && ub != nil && !removeAccountActionFound { - storageLogger.LogActionTrigger(ub.Id, utils.RATER_SOURCE, at, aac) + storageLogger.LogActionTrigger(ub.ID, utils.RATER_SOURCE, at, aac) accountingStorage.SetAccount(ub) } return @@ -120,44 +116,37 @@ func (at *ActionTrigger) Execute(ub *Account, sq *StatsQueueTriggered) (err erro // returns true if the field of the action timing are equeal to the non empty // fields of the action func (at *ActionTrigger) Match(a *Action) bool { - if a == nil { + if a == nil || a.Balance == nil { return true } - // if we have Id than we can draw an early conclusion - if a.Id != "" { - match, _ := regexp.MatchString(a.Id, at.ID) - return match + if a.Balance.Type != nil && a.Balance.GetType() != at.Balance.GetType() { + return false } - id := a.BalanceType == "" || at.BalanceType == a.BalanceType - thresholdType, thresholdValue, direction, destinationId, weight, ratingSubject, categories, sharedGroup, timings, blocker, disabled := true, true, true, true, true, true, true, true, true, true, true + var thresholdType bool if a.ExtraParameters != "" { t := struct { - ThresholdType string - ThresholdValue float64 - DestinationIds string - BalanceDirections string - BalanceWeight float64 - BalanceRatingSubject string - BalanceCategories string - BalanceSharedGroups string - BalanceTimingTags string - BalanceBlocker bool - BalanceDisabled bool + GroupID string + UniqueID string + ThresholdType string }{} json.Unmarshal([]byte(a.ExtraParameters), &t) + // check Ids first + if t.GroupID != "" { + return at.ID == t.GroupID + } + if t.UniqueID != "" { + return at.UniqueID == t.UniqueID + } thresholdType = t.ThresholdType == "" || at.ThresholdType == t.ThresholdType - thresholdValue = t.ThresholdValue == 0 || at.ThresholdValue == t.ThresholdValue - direction = len(t.BalanceDirections) == 0 || at.BalanceDirections.Equal(utils.ParseStringMap(t.BalanceDirections)) - destinationId = len(t.DestinationIds) == 0 || at.BalanceDestinationIds.Equal(utils.ParseStringMap(t.DestinationIds)) - categories = len(t.BalanceCategories) == 0 || at.BalanceCategories.Equal(utils.ParseStringMap(t.BalanceCategories)) - timings = len(t.BalanceTimingTags) == 0 || at.BalanceTimingTags.Equal(utils.ParseStringMap(t.BalanceTimingTags)) - sharedGroup = len(t.BalanceSharedGroups) == 0 || at.BalanceSharedGroups.Equal(utils.ParseStringMap(t.BalanceSharedGroups)) - weight = t.BalanceWeight == 0 || at.BalanceWeight == t.BalanceWeight - ratingSubject = t.BalanceRatingSubject == "" || at.BalanceRatingSubject == t.BalanceRatingSubject - blocker = at.BalanceBlocker == t.BalanceBlocker - disabled = at.BalanceDisabled == t.BalanceDisabled } - return id && direction && thresholdType && thresholdValue && destinationId && weight && ratingSubject && categories && sharedGroup && timings && blocker && disabled + + return thresholdType && at.Balance.CreateBalance().MatchFilter(a.Balance, false) +} + +func (at *ActionTrigger) CreateBalance() *Balance { + b := at.Balance.CreateBalance() + b.ID = at.UniqueID + return b } // makes a shallow copy of the receiver @@ -167,20 +156,17 @@ func (at *ActionTrigger) Clone() *ActionTrigger { return clone } -func (at *ActionTrigger) CreateBalance() *Balance { - return &Balance{ - Id: at.UniqueID, - Directions: at.BalanceDirections, - ExpirationDate: at.BalanceExpirationDate, - DestinationIds: at.BalanceDestinationIds, - RatingSubject: at.BalanceRatingSubject, - Categories: at.BalanceCategories, - SharedGroups: at.BalanceSharedGroups, - TimingIDs: at.BalanceTimingTags, - Blocker: at.BalanceBlocker, - Disabled: at.BalanceDisabled, - Weight: at.BalanceWeight, - } +func (at *ActionTrigger) Equals(oat *ActionTrigger) bool { + // ids only + return at.ID == oat.ID && at.UniqueID == oat.UniqueID +} + +func (at *ActionTrigger) IsActive(t time.Time) bool { + return at.ActivationDate.IsZero() || t.After(at.ActivationDate) +} + +func (at *ActionTrigger) IsExpired(t time.Time) bool { + return !at.ExpirationDate.IsZero() && t.After(at.ExpirationDate) } // Structure to store actions according to weight diff --git a/engine/actions_local_test.go b/engine/actions_local_test.go index 177fe19b0..b074fc630 100644 --- a/engine/actions_local_test.go +++ b/engine/actions_local_test.go @@ -23,6 +23,7 @@ import ( "net/rpc" "net/rpc/jsonrpc" "path" + "strconv" "testing" "time" @@ -94,7 +95,7 @@ func TestActionsLocalSetCdrlogDebit(t *testing.T) { t.Errorf("Calling ApierV1.SetAccount received: %s", reply) } attrsAA := &utils.AttrSetActions{ActionsId: "ACTS_1", Actions: []*utils.TPAction{ - &utils.TPAction{Identifier: DEBIT, BalanceType: utils.MONETARY, Units: 5.0, ExpiryTime: UNLIMITED, Weight: 20.0}, + &utils.TPAction{Identifier: DEBIT, BalanceType: utils.MONETARY, Units: "5", ExpiryTime: UNLIMITED, Weight: 20.0}, &utils.TPAction{Identifier: CDRLOG}, }} if err := actsLclRpc.Call("ApierV1.SetActions", attrsAA, &reply); err != nil && err.Error() != utils.ErrExists.Error() { @@ -122,7 +123,7 @@ func TestActionsLocalSetCdrlogDebit(t *testing.T) { rcvedCdrs[0].Subject != "dan2904" || rcvedCdrs[0].Usage != "1" || rcvedCdrs[0].RunID != DEBIT || - rcvedCdrs[0].Cost != attrsAA.Actions[0].Units { + strconv.FormatFloat(rcvedCdrs[0].Cost, 'f', -1, 64) != attrsAA.Actions[0].Units { t.Errorf("Received: %+v", rcvedCdrs[0]) } } @@ -139,7 +140,7 @@ func TestActionsLocalSetCdrlogTopup(t *testing.T) { t.Errorf("Calling ApierV1.SetAccount received: %s", reply) } attrsAA := &utils.AttrSetActions{ActionsId: "ACTS_2", Actions: []*utils.TPAction{ - &utils.TPAction{Identifier: TOPUP, BalanceType: utils.MONETARY, Units: 5.0, ExpiryTime: UNLIMITED, Weight: 20.0}, + &utils.TPAction{Identifier: TOPUP, BalanceType: utils.MONETARY, Units: "5", ExpiryTime: UNLIMITED, Weight: 20.0}, &utils.TPAction{Identifier: CDRLOG}, }} if err := actsLclRpc.Call("ApierV1.SetActions", attrsAA, &reply); err != nil && err.Error() != utils.ErrExists.Error() { @@ -167,7 +168,7 @@ func TestActionsLocalSetCdrlogTopup(t *testing.T) { rcvedCdrs[0].Subject != "dan2905" || rcvedCdrs[0].Usage != "1" || rcvedCdrs[0].RunID != TOPUP || - rcvedCdrs[0].Cost != attrsAA.Actions[0].Units { + strconv.FormatFloat(-rcvedCdrs[0].Cost, 'f', -1, 64) != attrsAA.Actions[0].Units { t.Errorf("Received: %+v", rcvedCdrs[0]) } } diff --git a/engine/actions_test.go b/engine/actions_test.go index 604b85447..1fea3f47d 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -409,9 +409,11 @@ func TestActionPlanCheckForASAP(t *testing.T) { func TestActionPlanLogFunction(t *testing.T) { a := &Action{ - ActionType: "*log", - BalanceType: "test", - Balance: &Balance{Value: 1.1}, + ActionType: "*log", + Balance: &BalanceFilter{ + Type: utils.StringPointer("test"), + Value: utils.Float64Pointer(1.1), + }, } at := &ActionTiming{ actions: []*Action{a}, @@ -424,12 +426,14 @@ func TestActionPlanLogFunction(t *testing.T) { func TestActionPlanFunctionNotAvailable(t *testing.T) { a := &Action{ - ActionType: "VALID_FUNCTION_TYPE", - BalanceType: "test", - Balance: &Balance{Value: 1.1}, + ActionType: "VALID_FUNCTION_TYPE", + Balance: &BalanceFilter{ + Type: utils.StringPointer("test"), + Value: utils.Float64Pointer(1.1), + }, } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:dy": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:dy": true}, Timing: &RateInterval{}, actions: []*Action{a}, } @@ -501,13 +505,13 @@ func TestActionPlansRemoveMember(t *testing.T) { Uuid: "some uuid", Id: "test", AccountIDs: []string{"one", "two", "three"}, - ActionsId: "TEST_ACTIONS", + ActionsID: "TEST_ACTIONS", } at2 := &ActionPlan{ Uuid: "some uuid22", Id: "test2", AccountIDs: []string{"three", "four"}, - ActionsId: "TEST_ACTIONS2", + ActionsID: "TEST_ACTIONS2", } ats := ActionPlans{at1, at2} if outAts := RemActionPlan(ats, "", "four"); len(outAts[1].AccountIds) != 1 { @@ -527,10 +531,12 @@ func TestActionPlansRemoveMember(t *testing.T) { func TestActionTriggerMatchNil(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + }, + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 2, } var a *Action if !at.Match(a) { @@ -540,10 +546,12 @@ func TestActionTriggerMatchNil(t *testing.T) { func TestActionTriggerMatchAllBlank(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + }, + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 2, } a := &Action{} if !at.Match(a) { @@ -553,12 +561,14 @@ func TestActionTriggerMatchAllBlank(t *testing.T) { func TestActionTriggerMatchMinuteBucketBlank(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + }, + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 2, } - a := &Action{BalanceType: utils.MONETARY, ExtraParameters: `{"BalanceDirections":"*out"}`} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ExtraParameters: `{"BalanceDirections":"*out"}`} if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } @@ -566,10 +576,12 @@ func TestActionTriggerMatchMinuteBucketBlank(t *testing.T) { func TestActionTriggerMatchMinuteBucketFull(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + }, + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 2, } a := &Action{ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v}`, utils.TRIGGER_MAX_BALANCE, 2)} if !at.Match(a) { @@ -579,12 +591,14 @@ func TestActionTriggerMatchMinuteBucketFull(t *testing.T) { func TestActionTriggerMatchAllFull(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + }, + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 2, } - a := &Action{BalanceType: utils.MONETARY, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v, "BalanceDirections":"*out"}`, utils.TRIGGER_MAX_BALANCE, 2)} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v, "BalanceDirections":"*out"}`, utils.TRIGGER_MAX_BALANCE, 2)} if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } @@ -592,12 +606,14 @@ func TestActionTriggerMatchAllFull(t *testing.T) { func TestActionTriggerMatchSomeFalse(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + }, + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 2, } - a := &Action{BalanceType: utils.MONETARY, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v, "BalanceDirections":"*in"}`, utils.TRIGGER_MAX_BALANCE, 2)} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%s"}`, utils.TRIGGER_MAX_BALANCE_COUNTER)} if at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } @@ -605,12 +621,14 @@ func TestActionTriggerMatchSomeFalse(t *testing.T) { func TestActionTriggerMatcBalanceFalse(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + }, + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 2, } - a := &Action{BalanceType: utils.MONETARY, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v, "BalanceDirections":"*out"}`, utils.TRIGGER_MAX_BALANCE, 3.0)} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ExtraParameters: fmt.Sprintf(`{"GroupID":"%s", "ThresholdType":"%s"}`, "TEST", utils.TRIGGER_MAX_BALANCE)} if at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } @@ -618,12 +636,14 @@ func TestActionTriggerMatcBalanceFalse(t *testing.T) { func TestActionTriggerMatcAllFalse(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + }, + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 2, } - a := &Action{BalanceType: utils.VOICE, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v, "BalanceDirections":"*in"}`, utils.TRIGGER_MAX_EVENT_COUNTER, 3)} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ExtraParameters: fmt.Sprintf(`{"UniqueID":"ZIP", "GroupID":"%s", "ThresholdType":"%s"}`, "TEST", utils.TRIGGER_MAX_BALANCE)} if at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } @@ -631,16 +651,28 @@ func TestActionTriggerMatcAllFalse(t *testing.T) { func TestActionTriggerMatchAll(t *testing.T) { at := &ActionTrigger{ - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceType: utils.MONETARY, - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 2, - BalanceDestinationIds: utils.NewStringMap("NAT"), - BalanceWeight: 1.0, - BalanceRatingSubject: "test1", - BalanceSharedGroups: utils.NewStringMap("test2"), + ID: "TEST", + UniqueID: "ZIP", + ThresholdType: "TT", + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + RatingSubject: utils.StringPointer("test1"), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Value: utils.Float64Pointer(2), + Weight: utils.Float64Pointer(1.0), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), + SharedGroups: utils.StringMapPointer(utils.NewStringMap("test2")), + }, } - a := &Action{BalanceType: utils.MONETARY, ExtraParameters: fmt.Sprintf(`{"BalanceDirections":"*out", "ThresholdType":"%v", "ThresholdValue": %v, "DestinationIds": "%v", "BalanceWeight": %v, "BalanceRatingSubject": "%v", "BalanceSharedGroup": "%v"}`, utils.TRIGGER_MAX_BALANCE, 2, "NAT", 1.0, "test1", "test2")} + a := &Action{Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + RatingSubject: utils.StringPointer("test1"), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Value: utils.Float64Pointer(2), + Weight: utils.Float64Pointer(1.0), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), + SharedGroups: utils.StringMapPointer(utils.NewStringMap("test2")), + }, ExtraParameters: fmt.Sprintf(`{"UniqueID":"ZIP", "GroupID":"TEST", "ThresholdType":"TT"}`)} if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } @@ -660,10 +692,10 @@ func TestActionTriggers(t *testing.T) { func TestActionResetTriggres(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 10}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 10}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } resetTriggersAction(ub, nil, nil, nil) if ub.ActionTriggers[0].Executed == true || ub.ActionTriggers[1].Executed == true { @@ -673,10 +705,10 @@ func TestActionResetTriggres(t *testing.T) { func TestActionResetTriggresExecutesThem(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 10}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 10}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } resetTriggersAction(ub, nil, nil, nil) if ub.ActionTriggers[0].Executed == true || ub.BalanceMap[utils.MONETARY][0].GetValue() == 12 { @@ -686,12 +718,12 @@ func TestActionResetTriggresExecutesThem(t *testing.T) { func TestActionResetTriggresActionFilter(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 10}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 10}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - resetTriggersAction(ub, nil, &Action{BalanceType: utils.SMS}, nil) + resetTriggersAction(ub, nil, &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.SMS)}}, nil) if ub.ActionTriggers[0].Executed == false || ub.ActionTriggers[1].Executed == false { t.Error("Reset triggers action failed!") } @@ -699,10 +731,10 @@ func TestActionResetTriggresActionFilter(t *testing.T) { func TestActionSetPostpaid(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } allowNegativeAction(ub, nil, nil, nil) if !ub.AllowNegative { @@ -712,11 +744,11 @@ func TestActionSetPostpaid(t *testing.T) { func TestActionSetPrepaid(t *testing.T) { ub := &Account{ - Id: "TEST_UB", + ID: "TEST_UB", AllowNegative: true, - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } denyNegativeAction(ub, nil, nil, nil) if ub.AllowNegative { @@ -726,11 +758,11 @@ func TestActionSetPrepaid(t *testing.T) { func TestActionResetPrepaid(t *testing.T) { ub := &Account{ - Id: "TEST_UB", + ID: "TEST_UB", AllowNegative: true, - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.SMS, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.SMS, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.SMS)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.SMS)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } resetAccountAction(ub, nil, nil, nil) if !ub.AllowNegative || @@ -739,54 +771,54 @@ func TestActionResetPrepaid(t *testing.T) { ub.BalanceMap[utils.VOICE][0].GetValue() != 0 || ub.ActionTriggers[0].Executed == true || ub.ActionTriggers[1].Executed == true { t.Log(ub.BalanceMap) - t.Error("Reset prepaid action failed!") + t.Error("Reset account action failed!") } } func TestActionResetPostpaid(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.SMS, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.SMS, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.SMS)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.SMS)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } resetAccountAction(ub, nil, nil, nil) if ub.BalanceMap[utils.MONETARY].GetTotalValue() != 0 || len(ub.UnitCounters) != 0 || ub.BalanceMap[utils.VOICE][0].GetValue() != 0 || ub.ActionTriggers[0].Executed == true || ub.ActionTriggers[1].Executed == true { - t.Error("Reset postpaid action failed!") + t.Error("Reset account action failed!") } } func TestActionTopupResetCredit(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1, Directions: utils.NewStringMap(utils.OUT)}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Directions: utils.NewStringMap(utils.OUT), Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceType: utils.MONETARY, Balance: &Balance{Value: 10, Directions: utils.NewStringMap(utils.OUT)}} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Value: utils.Float64Pointer(10), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}} topupResetAction(ub, nil, a, nil) if ub.AllowNegative || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 10 || - len(ub.UnitCounters) != 1 || + len(ub.UnitCounters) != 0 || // InitCounters finds no counters len(ub.BalanceMap[utils.VOICE]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Errorf("Topup reset action failed: %+v", ub.BalanceMap[utils.MONETARY][0]) + t.Errorf("Topup reset action failed: %+s", utils.ToIJSON(ub)) } } func TestActionTopupValueFactor(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{}, } a := &Action{ - BalanceType: utils.MONETARY, - Balance: &Balance{ - Value: 10, - Directions: utils.NewStringMap(utils.OUT), + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Value: utils.Float64Pointer(10), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), }, ExtraParameters: `{"*monetary":2.0}`, } @@ -798,15 +830,15 @@ func TestActionTopupValueFactor(t *testing.T) { func TestActionTopupResetCreditId(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "TEST_UB", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{Value: 100}, - &Balance{Id: "TEST_B", Value: 15}, + &Balance{ID: "TEST_B", Value: 15}, }, }, } - a := &Action{BalanceType: utils.MONETARY, Balance: &Balance{Id: "TEST_B", Value: 10, Directions: utils.NewStringMap(utils.OUT)}} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), ID: utils.StringPointer("TEST_B"), Value: utils.Float64Pointer(10), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}} topupResetAction(ub, nil, a, nil) if ub.AllowNegative || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 110 || @@ -817,15 +849,15 @@ func TestActionTopupResetCreditId(t *testing.T) { func TestActionTopupResetCreditNoId(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "TEST_UB", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{Value: 100, Directions: utils.NewStringMap(utils.OUT)}, - &Balance{Id: "TEST_B", Value: 15, Directions: utils.NewStringMap(utils.OUT)}, + &Balance{ID: "TEST_B", Value: 15, Directions: utils.NewStringMap(utils.OUT)}, }, }, } - a := &Action{BalanceType: utils.MONETARY, Balance: &Balance{Value: 10, Directions: utils.NewStringMap(utils.OUT)}} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Value: utils.Float64Pointer(10), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}} topupResetAction(ub, nil, a, nil) if ub.AllowNegative || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 20 || @@ -836,19 +868,19 @@ func TestActionTopupResetCreditNoId(t *testing.T) { func TestActionTopupResetMinutes(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Value: 100}}, - utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1, Directions: utils.NewStringMap(utils.OUT)}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Value: 100}}, + utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceType: utils.VOICE, Balance: &Balance{Value: 5, Weight: 20, DestinationIds: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.VOICE), Value: utils.Float64Pointer(5), Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}} topupResetAction(ub, nil, a, nil) if ub.AllowNegative || ub.BalanceMap[utils.VOICE].GetTotalValue() != 5 || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 100 || - len(ub.UnitCounters) != 1 || + len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.VOICE]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Errorf("Topup reset minutes action failed: %+v", ub.BalanceMap[utils.VOICE][0]) @@ -857,16 +889,16 @@ func TestActionTopupResetMinutes(t *testing.T) { func TestActionTopupCredit(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1, Directions: utils.NewStringMap(utils.OUT)}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceType: utils.MONETARY, Balance: &Balance{Value: 10, Directions: utils.NewStringMap(utils.OUT)}} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Value: utils.Float64Pointer(10), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}} topupAction(ub, nil, a, nil) if ub.AllowNegative || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 110 || - len(ub.UnitCounters) != 1 || + len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.VOICE]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Topup action failed!", ub.BalanceMap[utils.MONETARY].GetTotalValue()) @@ -875,17 +907,17 @@ func TestActionTopupCredit(t *testing.T) { func TestActionTopupMinutes(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceType: utils.VOICE, Balance: &Balance{Value: 5, Weight: 20, DestinationIds: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.VOICE), Value: utils.Float64Pointer(5), Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}} topupAction(ub, nil, a, nil) if ub.AllowNegative || ub.BalanceMap[utils.VOICE].GetTotalValue() != 15 || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 100 || - len(ub.UnitCounters) != 1 || + len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.VOICE]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Topup minutes action failed!", ub.BalanceMap[utils.VOICE]) @@ -894,35 +926,35 @@ func TestActionTopupMinutes(t *testing.T) { func TestActionDebitCredit(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1, Directions: utils.NewStringMap(utils.OUT)}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceType: utils.MONETARY, Balance: &Balance{Value: 10, Directions: utils.NewStringMap(utils.OUT)}} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Value: utils.Float64Pointer(10), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}} debitAction(ub, nil, a, nil) if ub.AllowNegative || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 90 || - len(ub.UnitCounters) != 1 || + len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.VOICE]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Error("Debit action failed!", ub.BalanceMap[utils.MONETARY].GetTotalValue()) + t.Error("Debit action failed!", utils.ToIJSON(ub)) } } func TestActionDebitMinutes(t *testing.T) { ub := &Account{ - Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceType: utils.MONETARY, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + ID: "TEST_UB", + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceType: utils.VOICE, Balance: &Balance{Value: 5, Weight: 20, DestinationIds: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.VOICE), Value: utils.Float64Pointer(5), Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}} debitAction(ub, nil, a, nil) if ub.AllowNegative || ub.BalanceMap[utils.VOICE][0].GetValue() != 5 || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 100 || - len(ub.UnitCounters) != 1 || + len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.VOICE]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Debit minutes action failed!", ub.BalanceMap[utils.VOICE][0]) @@ -931,76 +963,76 @@ func TestActionDebitMinutes(t *testing.T) { func TestActionResetAllCounters(t *testing.T) { ub := &Account{ - Id: "TEST_UB", + ID: "TEST_UB", AllowNegative: true, - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Value: 100}}, - utils.VOICE: BalanceChain{ - &Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, - &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET"), Directions: utils.NewStringMap(utils.OUT)}}}, + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Value: 100}}, + utils.VOICE: Balances{ + &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT"), Directions: utils.NewStringMap(utils.OUT)}, + &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET"), Directions: utils.NewStringMap(utils.OUT)}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, BalanceType: utils.MONETARY, ThresholdValue: 2, BalanceDestinationIds: utils.NewStringMap("NAT"), BalanceWeight: 20, ActionsId: "TEST_ACTIONS", Executed: true}}, + ActionTriggers: ActionTriggers{&ActionTrigger{ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ThresholdValue: 2, Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), Weight: utils.Float64Pointer(20)}, ActionsID: "TEST_ACTIONS", Executed: true}}, } ub.InitCounters() resetCountersAction(ub, nil, nil, nil) if !ub.AllowNegative || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 100 || len(ub.UnitCounters) != 1 || - len(ub.UnitCounters[0].Balances) != 1 || + len(ub.UnitCounters[utils.MONETARY][0].Counters) != 1 || len(ub.BalanceMap[utils.MONETARY]) != 1 || ub.ActionTriggers[0].Executed != true { - t.Errorf("Reset counters action failed: %+v %+v %+v", ub.UnitCounters, ub.UnitCounters[0], ub.UnitCounters[0].Balances[0]) + t.Errorf("Reset counters action failed: %+v %+v %+v", ub.UnitCounters, ub.UnitCounters[utils.MONETARY][0], ub.UnitCounters[utils.MONETARY][0].Counters[0]) } if len(ub.UnitCounters) < 1 { t.FailNow() } - mb := ub.UnitCounters[0].Balances[0] - if mb.Weight != 20 || mb.GetValue() != 0 || mb.DestinationIds["NAT"] == false { - t.Errorf("Balance cloned incorrectly: %+v", mb) + c := ub.UnitCounters[utils.MONETARY][0].Counters[0] + if c.Filter.GetWeight() != 20 || c.Value != 0 || c.Filter.GetDestinationIDs()["NAT"] == false { + t.Errorf("Balance cloned incorrectly: %+v", c) } } func TestActionResetCounterOnlyDefault(t *testing.T) { ub := &Account{ - Id: "TEST_UB", + ID: "TEST_UB", AllowNegative: true, - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Value: 100}}, - utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Value: 100}}, + utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}, ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceType: utils.MONETARY} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}} ub.InitCounters() resetCountersAction(ub, nil, a, nil) if !ub.AllowNegative || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 100 || len(ub.UnitCounters) != 1 || - len(ub.UnitCounters[0].Balances) != 1 || + len(ub.UnitCounters[utils.MONETARY][0].Counters) != 1 || len(ub.BalanceMap[utils.VOICE]) != 2 || ub.ActionTriggers[0].Executed != true { - for _, b := range ub.UnitCounters[0].Balances { + for _, b := range ub.UnitCounters[utils.MONETARY][0].Counters { t.Logf("B: %+v", b) } t.Errorf("Reset counters action failed: %+v", ub.UnitCounters) } - if len(ub.UnitCounters) < 1 || len(ub.UnitCounters[0].Balances) < 1 { + if len(ub.UnitCounters) < 1 || len(ub.UnitCounters[utils.MONETARY][0].Counters) < 1 { t.FailNow() } - mb := ub.UnitCounters[0].Balances[0] - if mb.Weight != 0 || mb.GetValue() != 0 || len(mb.DestinationIds) != 0 { - t.Errorf("Balance cloned incorrectly: %+v!", mb) + c := ub.UnitCounters[utils.MONETARY][0].Counters[0] + if c.Filter.GetWeight() != 0 || c.Value != 0 || len(c.Filter.GetDestinationIDs()) != 0 { + t.Errorf("Balance cloned incorrectly: %+v!", c) } } func TestActionResetCounterCredit(t *testing.T) { ub := &Account{ - Id: "TEST_UB", + ID: "TEST_UB", AllowNegative: true, - BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{Value: 100}}, utils.VOICE: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{&UnitCounter{BalanceType: utils.MONETARY, Balances: BalanceChain{&Balance{Value: 1, Directions: utils.NewStringMap(utils.OUT)}}}, &UnitCounter{BalanceType: utils.SMS, Balances: BalanceChain{&Balance{Value: 1, Directions: utils.NewStringMap(utils.OUT)}}}}, - ActionTriggers: ActionTriggers{&ActionTrigger{BalanceType: utils.MONETARY, BalanceDirections: utils.NewStringMap(utils.OUT), ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, + BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{Value: 100}}, utils.VOICE: Balances{&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.MONETARY: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}}}}}, utils.SMS: []*UnitCounter{&UnitCounter{Counters: CounterFilters{&CounterFilter{Value: 1, Filter: &BalanceFilter{Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}}}}}}, + ActionTriggers: ActionTriggers{&ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT))}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceType: utils.MONETARY} + a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY)}} resetCountersAction(ub, nil, a, nil) if !ub.AllowNegative || ub.BalanceMap[utils.MONETARY].GetTotalValue() != 100 || @@ -1013,15 +1045,17 @@ func TestActionResetCounterCredit(t *testing.T) { func TestActionTriggerLogging(t *testing.T) { at := &ActionTrigger{ - ID: "some_uuid", - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT), - ThresholdValue: 100.0, - BalanceDestinationIds: utils.NewStringMap("NAT"), - Weight: 10.0, - ActionsId: "TEST_ACTIONS", + ID: "some_uuid", + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), + }, + ThresholdValue: 100.0, + Weight: 10.0, + ActionsID: "TEST_ACTIONS", } - as, err := ratingStorage.GetActions(at.ActionsId, false) + as, err := ratingStorage.GetActions(at.ActionsID, false) if err != nil { t.Error("Error getting actions for the action timing: ", as, err) } @@ -1058,7 +1092,7 @@ func TestActionPlanLogging(t *testing.T) { }, } at := &ActionTiming{ - accountIDs: map[string]struct{}{"one": struct{}{}, "two": struct{}{}, "three": struct{}{}}, + accountIDs: utils.StringMap{"one": true, "two": true, "three": true}, Timing: i, Weight: 10.0, ActionsID: "TEST_ACTIONS", @@ -1084,7 +1118,7 @@ func TestActionPlanLogging(t *testing.T) { } func TestActionMakeNegative(t *testing.T) { - a := &Action{Balance: &Balance{Value: 10}} + a := &Action{Balance: &BalanceFilter{Value: utils.Float64Pointer(10)}} genericMakeNegative(a) if a.Balance.GetValue() > 0 { t.Error("Failed to make negative: ", a) @@ -1104,7 +1138,7 @@ func TestRemoveAction(t *testing.T) { } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:remo": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:remo": true}, actions: Actions{a}, } at.Execute() @@ -1117,13 +1151,12 @@ func TestRemoveAction(t *testing.T) { func TestTopupAction(t *testing.T) { initialUb, _ := accountingStorage.GetAccount("vdf:minu") a := &Action{ - ActionType: TOPUP, - BalanceType: utils.MONETARY, - Balance: &Balance{Value: 25, DestinationIds: utils.NewStringMap("RET"), Directions: utils.NewStringMap(utils.OUT), Weight: 20}, + ActionType: TOPUP, + Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Value: utils.Float64Pointer(25), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), Weight: utils.Float64Pointer(20)}, } at := &ActionTiming{ - accountIDs: map[string]struct{}{"vdf:minu": struct{}{}}, + accountIDs: utils.StringMap{"vdf:minu": true}, actions: Actions{a}, } @@ -1139,13 +1172,12 @@ func TestTopupAction(t *testing.T) { func TestTopupActionLoaded(t *testing.T) { initialUb, _ := accountingStorage.GetAccount("vdf:minitsboy") a := &Action{ - ActionType: TOPUP, - BalanceType: utils.MONETARY, - Balance: &Balance{Value: 25, DestinationIds: utils.NewStringMap("RET"), Directions: utils.NewStringMap(utils.OUT), Weight: 20}, + ActionType: TOPUP, + Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Value: utils.Float64Pointer(25), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), Weight: utils.Float64Pointer(20)}, } at := &ActionTiming{ - accountIDs: map[string]struct{}{"vdf:minitsboy": struct{}{}}, + accountIDs: utils.StringMap{"vdf:minitsboy": true}, actions: Actions{a}, } @@ -1161,14 +1193,14 @@ func TestTopupActionLoaded(t *testing.T) { } func TestActionCdrlogEmpty(t *testing.T) { - acnt := &Account{Id: "cgrates.org:dan2904"} + acnt := &Account{ID: "cgrates.org:dan2904"} cdrlog := &Action{ ActionType: CDRLOG, } err := cdrLogAction(acnt, nil, cdrlog, Actions{ &Action{ ActionType: DEBIT, - Balance: &Balance{Value: 25, DestinationIds: utils.NewStringMap("RET"), Weight: 20}, + Balance: &BalanceFilter{Value: utils.Float64Pointer(25), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Weight: utils.Float64Pointer(20)}, }, }) if err != nil { @@ -1182,7 +1214,7 @@ func TestActionCdrlogEmpty(t *testing.T) { } func TestActionCdrlogWithParams(t *testing.T) { - acnt := &Account{Id: "cgrates.org:dan2904"} + acnt := &Account{ID: "cgrates.org:dan2904"} cdrlog := &Action{ ActionType: CDRLOG, ExtraParameters: `{"ReqType":"^*pseudoprepaid","Subject":"^rif", "TOR":"~action_type:s/^\\*(.*)$/did_$1/"}`, @@ -1190,11 +1222,11 @@ func TestActionCdrlogWithParams(t *testing.T) { err := cdrLogAction(acnt, nil, cdrlog, Actions{ &Action{ ActionType: DEBIT, - Balance: &Balance{Value: 25, DestinationIds: utils.NewStringMap("RET"), Weight: 20}, + Balance: &BalanceFilter{Value: utils.Float64Pointer(25), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Weight: utils.Float64Pointer(20)}, }, &Action{ ActionType: DEBIT_RESET, - Balance: &Balance{Value: 25, DestinationIds: utils.NewStringMap("RET"), Weight: 20}, + Balance: &BalanceFilter{Value: utils.Float64Pointer(25), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Weight: utils.Float64Pointer(20)}, }, }) if err != nil { @@ -1209,7 +1241,7 @@ func TestActionCdrlogWithParams(t *testing.T) { } func TestActionCdrLogParamsWithOverload(t *testing.T) { - acnt := &Account{Id: "cgrates.org:dan2904"} + acnt := &Account{ID: "cgrates.org:dan2904"} cdrlog := &Action{ ActionType: CDRLOG, ExtraParameters: `{"Subject":"^rif","Destination":"^1234","ToR":"~ActionTag:s/^at(.)$/0$1/","AccountID":"~AccountID:s/^\\*(.*)$/$1/"}`, @@ -1217,11 +1249,11 @@ func TestActionCdrLogParamsWithOverload(t *testing.T) { err := cdrLogAction(acnt, nil, cdrlog, Actions{ &Action{ ActionType: DEBIT, - Balance: &Balance{Value: 25, DestinationIds: utils.NewStringMap("RET"), Weight: 20}, + Balance: &BalanceFilter{Value: utils.Float64Pointer(25), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Weight: utils.Float64Pointer(20)}, }, &Action{ ActionType: DEBIT_RESET, - Balance: &Balance{Value: 25, DestinationIds: utils.NewStringMap("RET"), Weight: 20}, + Balance: &BalanceFilter{Value: utils.Float64Pointer(25), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Weight: utils.Float64Pointer(20)}, }, }) if err != nil { @@ -1242,7 +1274,7 @@ func TestActionCdrLogParamsWithOverload(t *testing.T) { } func TestActionSetDDestination(t *testing.T) { - acc := &Account{BalanceMap: map[string]BalanceChain{utils.MONETARY: BalanceChain{&Balance{DestinationIds: utils.NewStringMap("*ddc_test")}}}} + acc := &Account{BalanceMap: map[string]Balances{utils.MONETARY: Balances{&Balance{DestinationIDs: utils.NewStringMap("*ddc_test")}}}} origD := &Destination{Id: "*ddc_test", Prefixes: []string{"111", "222"}} ratingStorage.SetDestination(origD) ratingStorage.CacheRatingPrefixValues(map[string][]string{utils.DESTINATION_PREFIX: []string{utils.DESTINATION_PREFIX + "*ddc_test"}}) @@ -1286,9 +1318,9 @@ func TestActionSetDDestination(t *testing.T) { func TestActionTransactionFuncType(t *testing.T) { err := accountingStorage.SetAccount(&Account{ - Id: "cgrates.org:trans", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{ + ID: "cgrates.org:trans", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{ Value: 10, }}, }, @@ -1297,18 +1329,16 @@ func TestActionTransactionFuncType(t *testing.T) { t.Error("Error setting account: ", err) } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:trans": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:trans": true}, Timing: &RateInterval{}, actions: []*Action{ &Action{ - ActionType: TOPUP, - BalanceType: utils.MONETARY, - Balance: &Balance{Value: 1.1}, + ActionType: TOPUP, + Balance: &BalanceFilter{Value: utils.Float64Pointer(1.1), Type: utils.StringPointer(utils.MONETARY)}, }, &Action{ - ActionType: "VALID_FUNCTION_TYPE", - BalanceType: "test", - Balance: &Balance{Value: 1.1}, + ActionType: "VALID_FUNCTION_TYPE", + Balance: &BalanceFilter{Value: utils.Float64Pointer(1.1), Type: utils.StringPointer("test")}, }, }, } @@ -1324,9 +1354,9 @@ func TestActionTransactionFuncType(t *testing.T) { func TestActionTransactionBalanceType(t *testing.T) { err := accountingStorage.SetAccount(&Account{ - Id: "cgrates.org:trans", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{ + ID: "cgrates.org:trans", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{ Value: 10, }}, }, @@ -1335,18 +1365,16 @@ func TestActionTransactionBalanceType(t *testing.T) { t.Error("Error setting account: ", err) } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:trans": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:trans": true}, Timing: &RateInterval{}, actions: []*Action{ &Action{ - ActionType: TOPUP, - BalanceType: utils.MONETARY, - Balance: &Balance{Value: 1.1}, + ActionType: TOPUP, + Balance: &BalanceFilter{Value: utils.Float64Pointer(1.1), Type: utils.StringPointer(utils.MONETARY)}, }, &Action{ - ActionType: TOPUP, - BalanceType: "test", - Balance: nil, + ActionType: TOPUP, + Balance: &BalanceFilter{Type: utils.StringPointer("test")}, }, }, } @@ -1355,16 +1383,16 @@ func TestActionTransactionBalanceType(t *testing.T) { if err != nil || acc == nil { t.Error("Error getting account: ", acc, err) } - if acc.BalanceMap[utils.MONETARY][0].Value != 10 { + if acc.BalanceMap[utils.MONETARY][0].Value != 11.1 { t.Errorf("Transaction didn't work: %v", acc.BalanceMap[utils.MONETARY][0].Value) } } -func TestActionWithExpireWithoutExpire(t *testing.T) { +func TestActionTransactionBalanceNotType(t *testing.T) { err := accountingStorage.SetAccount(&Account{ - Id: "cgrates.org:exp", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{ + ID: "cgrates.org:trans", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{ Value: 10, }}, }, @@ -1373,22 +1401,58 @@ func TestActionWithExpireWithoutExpire(t *testing.T) { t.Error("Error setting account: ", err) } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:exp": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:trans": true}, Timing: &RateInterval{}, actions: []*Action{ &Action{ - ActionType: TOPUP, - BalanceType: utils.VOICE, - Balance: &Balance{ - Value: 15, + ActionType: TOPUP, + Balance: &BalanceFilter{Value: utils.Float64Pointer(1.1), Type: utils.StringPointer(utils.VOICE)}, + }, + &Action{ + ActionType: TOPUP, + Balance: &BalanceFilter{Type: utils.StringPointer("test")}, + }, + }, + } + err = at.Execute() + acc, err := accountingStorage.GetAccount("cgrates.org:trans") + if err != nil || acc == nil { + t.Error("Error getting account: ", acc, err) + } + if acc.BalanceMap[utils.MONETARY][0].Value != 10.0 { + t.Errorf("Transaction didn't work: %v", acc.BalanceMap[utils.MONETARY][0].Value) + } +} + +func TestActionWithExpireWithoutExpire(t *testing.T) { + err := accountingStorage.SetAccount(&Account{ + ID: "cgrates.org:exp", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{ + Value: 10, + }}, + }, + }) + if err != nil { + t.Error("Error setting account: ", err) + } + at := &ActionTiming{ + accountIDs: utils.StringMap{"cgrates.org:exp": true}, + Timing: &RateInterval{}, + actions: []*Action{ + &Action{ + ActionType: TOPUP, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Value: utils.Float64Pointer(15), }, }, &Action{ - ActionType: TOPUP, - BalanceType: utils.VOICE, - Balance: &Balance{ - Value: 30, - ExpirationDate: time.Date(2025, time.November, 11, 22, 39, 0, 0, time.UTC), + ActionType: TOPUP, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Value: utils.Float64Pointer(30), + ExpirationDate: utils.TimePointer(time.Date(2025, time.November, 11, 22, 39, 0, 0, time.UTC)), }, }, }, @@ -1406,20 +1470,20 @@ func TestActionWithExpireWithoutExpire(t *testing.T) { func TestActionRemoveBalance(t *testing.T) { err := accountingStorage.SetAccount(&Account{ - Id: "cgrates.org:rembal", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "cgrates.org:rembal", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{ Value: 10, }, &Balance{ Value: 10, - DestinationIds: utils.NewStringMap("NAT", "RET"), + DestinationIDs: utils.NewStringMap("NAT", "RET"), ExpirationDate: time.Date(2025, time.November, 11, 22, 39, 0, 0, time.UTC), }, &Balance{ Value: 10, - DestinationIds: utils.NewStringMap("NAT", "RET"), + DestinationIDs: utils.NewStringMap("NAT", "RET"), }, }, }, @@ -1428,14 +1492,14 @@ func TestActionRemoveBalance(t *testing.T) { t.Error("Error setting account: ", err) } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:rembal": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:rembal": true}, Timing: &RateInterval{}, actions: []*Action{ &Action{ - ActionType: REMOVE_BALANCE, - BalanceType: utils.MONETARY, - Balance: &Balance{ - DestinationIds: utils.NewStringMap("NAT", "RET"), + ActionType: REMOVE_BALANCE, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT", "RET")), }, }, }, @@ -1454,12 +1518,12 @@ func TestActionRemoveBalance(t *testing.T) { func TestActionTransferMonetaryDefault(t *testing.T) { err := accountingStorage.SetAccount( &Account{ - Id: "cgrates.org:trans", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "cgrates.org:trans", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{ Uuid: utils.GenUUID(), - Id: utils.META_DEFAULT, + ID: utils.META_DEFAULT, Value: 10, }, &Balance{ @@ -1486,7 +1550,7 @@ func TestActionTransferMonetaryDefault(t *testing.T) { } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:trans": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:trans": true}, actions: Actions{a}, } at.Execute() @@ -1510,12 +1574,12 @@ func TestActionTransferMonetaryDefault(t *testing.T) { func TestActionTransferMonetaryDefaultFilter(t *testing.T) { err := accountingStorage.SetAccount( &Account{ - Id: "cgrates.org:trans", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "cgrates.org:trans", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{ Uuid: utils.GenUUID(), - Id: utils.META_DEFAULT, + ID: utils.META_DEFAULT, Value: 10, Weight: 20, }, @@ -1543,11 +1607,11 @@ func TestActionTransferMonetaryDefaultFilter(t *testing.T) { a := &Action{ ActionType: TRANSFER_MONETARY_DEFAULT, - Balance: &Balance{Weight: 20}, + Balance: &BalanceFilter{Weight: utils.Float64Pointer(20)}, } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:trans": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:trans": true}, actions: Actions{a}, } at.Execute() @@ -1571,12 +1635,12 @@ func TestActionTransferMonetaryDefaultFilter(t *testing.T) { func TestActionConditionalTopup(t *testing.T) { err := accountingStorage.SetAccount( &Account{ - Id: "cgrates.org:cond", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "cgrates.org:cond", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{ Uuid: utils.GenUUID(), - Id: utils.META_DEFAULT, + ID: utils.META_DEFAULT, Value: 10, Weight: 20, }, @@ -1603,17 +1667,17 @@ func TestActionConditionalTopup(t *testing.T) { } a := &Action{ - ActionType: TOPUP, - BalanceType: utils.MONETARY, - Filter: `{"Type":"*monetary","Value":1,"Weight":10}`, - Balance: &Balance{ - Value: 11, - Weight: 30, + ActionType: TOPUP, + Filter: `{"Type":"*monetary","Value":1,"Weight":10}`, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Value: utils.Float64Pointer(11), + Weight: utils.Float64Pointer(30), }, } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:cond": true}, actions: Actions{a}, } at.Execute() @@ -1635,12 +1699,12 @@ func TestActionConditionalTopup(t *testing.T) { func TestActionConditionalTopupNoMatch(t *testing.T) { err := accountingStorage.SetAccount( &Account{ - Id: "cgrates.org:cond", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "cgrates.org:cond", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{ Uuid: utils.GenUUID(), - Id: utils.META_DEFAULT, + ID: utils.META_DEFAULT, Value: 10, Weight: 20, }, @@ -1667,17 +1731,17 @@ func TestActionConditionalTopupNoMatch(t *testing.T) { } a := &Action{ - ActionType: TOPUP, - BalanceType: utils.MONETARY, - Filter: `{"Type":"*monetary","Value":2,"Weight":10}`, - Balance: &Balance{ - Value: 11, - Weight: 30, + ActionType: TOPUP, + Filter: `{"Type":"*monetary","Value":2,"Weight":10}`, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Value: utils.Float64Pointer(11), + Weight: utils.Float64Pointer(30), }, } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:cond": true}, actions: Actions{a}, } at.Execute() @@ -1698,9 +1762,9 @@ func TestActionConditionalTopupNoMatch(t *testing.T) { func TestActionConditionalTopupExistingBalance(t *testing.T) { err := accountingStorage.SetAccount( &Account{ - Id: "cgrates.org:cond", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "cgrates.org:cond", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{ Uuid: utils.GenUUID(), Value: 1, @@ -1712,7 +1776,7 @@ func TestActionConditionalTopupExistingBalance(t *testing.T) { Weight: 20, }, }, - utils.VOICE: BalanceChain{ + utils.VOICE: Balances{ &Balance{ Uuid: utils.GenUUID(), Value: 10, @@ -1731,17 +1795,17 @@ func TestActionConditionalTopupExistingBalance(t *testing.T) { } a := &Action{ - ActionType: TOPUP, - BalanceType: utils.MONETARY, - Filter: `{"Type":"*voice","Value":{"*gte":100}}`, - Balance: &Balance{ - Value: 11, - Weight: 10, + ActionType: TOPUP, + Filter: `{"Type":"*voice","Value":{"*gte":100}}`, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Value: utils.Float64Pointer(11), + Weight: utils.Float64Pointer(10), }, } at := &ActionTiming{ - accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}}, + accountIDs: utils.StringMap{"cgrates.org:cond": true}, actions: Actions{a}, } at.Execute() @@ -1759,6 +1823,280 @@ func TestActionConditionalTopupExistingBalance(t *testing.T) { } } +func TestActionConditionalDisabledIfNegative(t *testing.T) { + err := accountingStorage.SetAccount( + &Account{ + ID: "cgrates.org:af", + BalanceMap: map[string]Balances{ + "*data": Balances{ + &Balance{ + Uuid: "fc927edb-1bd6-425e-a2a3-9fd8bafaa524", + ID: "for_v3hsillmilld500m_data_500_m", + Value: 5.242, + Weight: 10, + RatingSubject: "for_v3hsillmilld500m_data_forfait", + Categories: utils.StringMap{ + "data_france": true, + }, + }, + }, + "*monetary": Balances{ + &Balance{ + Uuid: "9fa1847a-f36a-41a7-8ec0-dfaab370141e", + ID: "*default", + Value: -1.95001, + }, + }, + "*sms": Balances{ + &Balance{ + Uuid: "d348d15d-2988-4ee4-b847-6a552f94e2ec", + ID: "for_v3hsillmilld500m_mms_ill", + Value: 20000, + Weight: 10, + DestinationIDs: utils.StringMap{ + "FRANCE_NATIONAL": true, + }, + Categories: utils.StringMap{ + "mms_france": true, + "tmms_france": true, + "vmms_france": true, + }, + }, + &Balance{ + Uuid: "f4643517-31f6-4199-980f-04cf535471ed", + ID: "for_v3hsillmilld500m_sms_ill", + Value: 20000, + Weight: 10, + DestinationIDs: utils.StringMap{ + "FRANCE_NATIONAL": true, + }, + Categories: utils.StringMap{ + "ms_france": true, + }, + }, + }, + "*voice": Balances{ + &Balance{ + Uuid: "079ab190-77f4-44f3-9c6f-3a0dd1a59dfd", + ID: "for_v3hsillmilld500m_voice_3_h", + Value: 10800, + Weight: 10, + DestinationIDs: utils.StringMap{ + "FRANCE_NATIONAL": true, + }, + Categories: utils.StringMap{ + "call_france": true, + }, + }, + }, + }, + }) + if err != nil { + t.Errorf("error setting account: %v", err) + } + + a1 := &Action{ + ActionType: "*set_balance", + Filter: "{\"*and\":[{\"Value\":{\"*lt\":0}},{\"ID\":{\"*eq\":\"*default\"}}]}", + Balance: &BalanceFilter{ + Type: utils.StringPointer("*sms"), + ID: utils.StringPointer("for_v3hsillmilld500m_sms_ill"), + Disabled: utils.BoolPointer(true), + }, + Weight: 9, + } + a2 := &Action{ + ActionType: "*set_balance", + Filter: "{\"*and\":[{\"Value\":{\"*lt\":0}},{\"ID\":{\"*eq\":\"*default\"}}]}", + Balance: &BalanceFilter{ + Type: utils.StringPointer("*sms"), + ID: utils.StringPointer("for_v3hsillmilld500m_mms_ill"), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("FRANCE_NATIONAL")), + Weight: utils.Float64Pointer(10), + Disabled: utils.BoolPointer(true), + }, + Weight: 8, + } + a3 := &Action{ + ActionType: "*set_balance", + Filter: "{\"*and\":[{\"Value\":{\"*lt\":0}},{\"ID\":{\"*eq\":\"*default\"}}]}", + Balance: &BalanceFilter{ + Type: utils.StringPointer("*sms"), + ID: utils.StringPointer("for_v3hsillmilld500m_sms_ill"), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("FRANCE_NATIONAL")), + Weight: utils.Float64Pointer(10), + Disabled: utils.BoolPointer(true), + }, + Weight: 8, + } + a4 := &Action{ + ActionType: "*set_balance", + Filter: "{\"*and\":[{\"Value\":{\"*lt\":0}},{\"ID\":{\"*eq\":\"*default\"}}]}", + Balance: &BalanceFilter{ + Type: utils.StringPointer("*data"), + Uuid: utils.StringPointer("fc927edb-1bd6-425e-a2a3-9fd8bafaa524"), + RatingSubject: utils.StringPointer("for_v3hsillmilld500m_data_forfait"), + Weight: utils.Float64Pointer(10), + Disabled: utils.BoolPointer(true), + }, + Weight: 7, + } + a5 := &Action{ + ActionType: "*set_balance", + Filter: "{\"*and\":[{\"Value\":{\"*lt\":0}},{\"ID\":{\"*eq\":\"*default\"}}]}", + Balance: &BalanceFilter{ + Type: utils.StringPointer("*voice"), + ID: utils.StringPointer("for_v3hsillmilld500m_voice_3_h"), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("FRANCE_NATIONAL")), + Weight: utils.Float64Pointer(10), + Disabled: utils.BoolPointer(true), + }, + Weight: 6, + } + + at := &ActionTiming{ + accountIDs: utils.StringMap{"cgrates.org:af": true}, + actions: Actions{a1, a2, a3, a4, a5}, + } + at.Execute() + + afterUb, err := accountingStorage.GetAccount("cgrates.org:af") + if err != nil { + t.Error("account not found: ", err, afterUb) + } + + for btype, chain := range afterUb.BalanceMap { + if btype != utils.MONETARY { + for _, b := range chain { + if b.Disabled != true { + t.Errorf("Failed to disabled balance (%s): %+v", btype, b) + } + } + } + } +} + +func TestActionSetBalance(t *testing.T) { + err := accountingStorage.SetAccount( + &Account{ + ID: "cgrates.org:setb", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ + &Balance{ + ID: "m1", + Uuid: utils.GenUUID(), + Value: 1, + Weight: 10, + }, + &Balance{ + ID: "m2", + Uuid: utils.GenUUID(), + Value: 6, + Weight: 20, + }, + }, + utils.VOICE: Balances{ + &Balance{ + ID: "v1", + Uuid: utils.GenUUID(), + Value: 10, + Weight: 10, + }, + &Balance{ + ID: "v2", + Uuid: utils.GenUUID(), + Value: 100, + Weight: 20, + }, + }, + }, + }) + if err != nil { + t.Errorf("error setting account: %v", err) + } + + a := &Action{ + ActionType: SET_BALANCE, + Balance: &BalanceFilter{ + ID: utils.StringPointer("m2"), + Type: utils.StringPointer(utils.MONETARY), + Value: utils.Float64Pointer(11), + Weight: utils.Float64Pointer(10), + }, + } + + at := &ActionTiming{ + accountIDs: utils.StringMap{"cgrates.org:setb": true}, + actions: Actions{a}, + } + at.Execute() + + afterUb, err := accountingStorage.GetAccount("cgrates.org:setb") + if err != nil { + t.Error("account not found: ", err, afterUb) + } + if len(afterUb.BalanceMap[utils.MONETARY]) != 2 || + afterUb.BalanceMap[utils.MONETARY][1].Value != 11 || + afterUb.BalanceMap[utils.MONETARY][1].Weight != 10 { + for _, b := range afterUb.BalanceMap[utils.MONETARY] { + t.Logf("B: %+v", b) + } + t.Errorf("Balance: %+v", afterUb.BalanceMap[utils.MONETARY][1]) + } +} + +func TestActionCSVFilter(t *testing.T) { + act, err := ratingStorage.GetActions("FILTER", false) + if err != nil { + t.Error("error getting actions: ", err) + } + if len(act) != 1 || act[0].Filter != `{"*and":[{"Value":{"*lt":0}},{"Id":{"*eq":"*default"}}]}` { + t.Error("Error loading actions: ", act[0].Filter) + } +} + +func TestActionExpirationTime(t *testing.T) { + a, err := ratingStorage.GetActions("EXP", false) + if err != nil || a == nil { + t.Error("Error getting actions: ", err) + } + + at := &ActionTiming{ + accountIDs: utils.StringMap{"cgrates.org:expo": true}, + actions: a, + } + for rep := 0; rep < 5; rep++ { + at.Execute() + afterUb, err := accountingStorage.GetAccount("cgrates.org:expo") + if err != nil || + len(afterUb.BalanceMap[utils.VOICE]) != rep+1 { + t.Error("error topuping expiration balance: ", utils.ToIJSON(afterUb)) + } + } +} + +func TestActionExpNoExp(t *testing.T) { + exp, err := ratingStorage.GetActions("EXP", false) + if err != nil || exp == nil { + t.Error("Error getting actions: ", err) + } + noexp, err := ratingStorage.GetActions("NOEXP", false) + if err != nil || noexp == nil { + t.Error("Error getting actions: ", err) + } + exp = append(exp, noexp...) + at := &ActionTiming{ + accountIDs: utils.StringMap{"cgrates.org:expnoexp": true}, + actions: exp, + } + at.Execute() + afterUb, err := accountingStorage.GetAccount("cgrates.org:expnoexp") + if err != nil || + len(afterUb.BalanceMap[utils.VOICE]) != 2 { + t.Error("error topuping expiration balance: ", utils.ToIJSON(afterUb)) + } +} + /**************** Benchmarks ********************************/ func BenchmarkUUID(b *testing.B) { diff --git a/engine/aliases.go b/engine/aliases.go index 3621ad2f7..a927cc9b8 100644 --- a/engine/aliases.go +++ b/engine/aliases.go @@ -156,6 +156,16 @@ type AttrReverseAlias struct { Context string } +type AliasService interface { + SetAlias(Alias, *string) error + UpdateAlias(Alias, *string) error + RemoveAlias(Alias, *string) error + GetAlias(Alias, *Alias) error + GetMatchingAlias(AttrMatchingAlias, *string) error + GetReverseAlias(AttrReverseAlias, *map[string][]*Alias) error + RemoveReverseAlias(AttrReverseAlias, *string) error +} + type AliasHandler struct { accountingDb AccountingStorage mu sync.RWMutex diff --git a/engine/balance_filter.go b/engine/balance_filter.go new file mode 100644 index 000000000..f9e408d39 --- /dev/null +++ b/engine/balance_filter.go @@ -0,0 +1,325 @@ +package engine + +import ( + "reflect" + "time" + + "github.com/cgrates/cgrates/utils" +) + +type BalanceFilter struct { + Uuid *string + ID *string + Type *string + Value *float64 + Directions *utils.StringMap + ExpirationDate *time.Time + Weight *float64 + DestinationIDs *utils.StringMap + RatingSubject *string + Categories *utils.StringMap + SharedGroups *utils.StringMap + TimingIDs *utils.StringMap + Timings []*RITiming + Disabled *bool + Factor *ValueFactor + Blocker *bool +} + +func (bp *BalanceFilter) CreateBalance() *Balance { + b := &Balance{ + Uuid: bp.GetUuid(), + ID: bp.GetID(), + Value: bp.GetValue(), + Directions: bp.GetDirections(), + ExpirationDate: bp.GetExpirationDate(), + Weight: bp.GetWeight(), + DestinationIDs: bp.GetDestinationIDs(), + RatingSubject: bp.GetRatingSubject(), + Categories: bp.GetCategories(), + SharedGroups: bp.GetSharedGroups(), + Timings: bp.Timings, + TimingIDs: bp.GetTimingIDs(), + Disabled: bp.GetDisabled(), + Factor: bp.GetFactor(), + Blocker: bp.GetBlocker(), + } + return b.Clone() +} + +func (bf *BalanceFilter) Clone() *BalanceFilter { + result := &BalanceFilter{} + if bf.Uuid != nil { + result.Uuid = new(string) + *result.Uuid = *bf.Uuid + } + if bf.ID != nil { + result.ID = new(string) + *result.ID = *bf.ID + } + if bf.Value != nil { + result.Value = new(float64) + *result.Value = *bf.Value + } + if bf.RatingSubject != nil { + result.RatingSubject = new(string) + *result.RatingSubject = *bf.RatingSubject + } + if bf.Type != nil { + result.Type = new(string) + *result.Type = *bf.Type + } + if bf.ExpirationDate != nil { + result.ExpirationDate = new(time.Time) + *result.ExpirationDate = *bf.ExpirationDate + } + if bf.Weight != nil { + result.Weight = new(float64) + *result.Weight = *bf.Weight + } + if bf.Disabled != nil { + result.Disabled = new(bool) + *result.Disabled = *bf.Disabled + } + if bf.Blocker != nil { + result.Blocker = new(bool) + *result.Blocker = *bf.Blocker + } + if bf.Factor != nil { + result.Factor = new(ValueFactor) + *result.Factor = *bf.Factor + } + if bf.Directions != nil { + result.Directions = utils.StringMapPointer(bf.Directions.Clone()) + } + if bf.DestinationIDs != nil { + result.DestinationIDs = utils.StringMapPointer(bf.DestinationIDs.Clone()) + } + if bf.Categories != nil { + result.Categories = utils.StringMapPointer(bf.Categories.Clone()) + } + if bf.SharedGroups != nil { + result.SharedGroups = utils.StringMapPointer(bf.SharedGroups.Clone()) + } + if bf.TimingIDs != nil { + result.TimingIDs = utils.StringMapPointer(bf.TimingIDs.Clone()) + } + + return result +} + +func (bf *BalanceFilter) LoadFromBalance(b *Balance) *BalanceFilter { + if b.Uuid != "" { + bf.Uuid = &b.Uuid + } + if b.ID != "" { + bf.ID = &b.ID + } + if b.Value != 0 { + bf.Value = &b.Value + } + if !b.Directions.IsEmpty() { + bf.Directions = &b.Directions + } + if !b.ExpirationDate.IsZero() { + bf.ExpirationDate = &b.ExpirationDate + } + if b.Weight != 0 { + bf.Weight = &b.Weight + } + if !b.DestinationIDs.IsEmpty() { + bf.DestinationIDs = &b.DestinationIDs + } + if b.RatingSubject != "" { + bf.RatingSubject = &b.RatingSubject + } + if !b.Categories.IsEmpty() { + bf.Categories = &b.Categories + } + if !b.SharedGroups.IsEmpty() { + bf.SharedGroups = &b.SharedGroups + } + if !b.TimingIDs.IsEmpty() { + bf.TimingIDs = &b.TimingIDs + } + if len(b.Factor) != 0 { + bf.Factor = &b.Factor + } + if b.Disabled { + bf.Disabled = &b.Disabled + } + if b.Blocker { + bf.Blocker = &b.Blocker + } + bf.Timings = b.Timings + return bf +} + +func (bp *BalanceFilter) Equal(o *BalanceFilter) bool { + if bp.ID != nil && o.ID != nil { + return *bp.ID == *o.ID + } + return reflect.DeepEqual(bp, o) +} + +func (bp *BalanceFilter) GetType() string { + if bp == nil || bp.Type == nil { + return "" + } + return *bp.Type +} + +func (bp *BalanceFilter) GetValue() float64 { + if bp == nil || bp.Value == nil { + return 0.0 + } + return *bp.Value +} + +func (bp *BalanceFilter) SetValue(v float64) { + if bp.Value == nil { + bp.Value = new(float64) + } + *bp.Value = v +} + +func (bp *BalanceFilter) GetUuid() string { + if bp == nil || bp.Uuid == nil { + return "" + } + return *bp.Uuid +} + +func (bp *BalanceFilter) GetID() string { + if bp == nil || bp.ID == nil { + return "" + } + return *bp.ID +} + +func (bp *BalanceFilter) GetDirections() utils.StringMap { + if bp == nil || bp.Directions == nil { + return utils.StringMap{} + } + return *bp.Directions +} + +func (bp *BalanceFilter) GetDestinationIDs() utils.StringMap { + if bp == nil || bp.DestinationIDs == nil { + return utils.StringMap{} + } + return *bp.DestinationIDs +} + +func (bp *BalanceFilter) GetCategories() utils.StringMap { + if bp == nil || bp.Categories == nil { + return utils.StringMap{} + } + return *bp.Categories +} + +func (bp *BalanceFilter) GetTimingIDs() utils.StringMap { + if bp == nil || bp.TimingIDs == nil { + return utils.StringMap{} + } + return *bp.TimingIDs +} + +func (bp *BalanceFilter) GetSharedGroups() utils.StringMap { + if bp == nil || bp.SharedGroups == nil { + return utils.StringMap{} + } + return *bp.SharedGroups +} + +func (bp *BalanceFilter) GetWeight() float64 { + if bp == nil || bp.Weight == nil { + return 0.0 + } + return *bp.Weight +} + +func (bp *BalanceFilter) GetRatingSubject() string { + if bp == nil || bp.RatingSubject == nil { + return "" + } + return *bp.RatingSubject +} + +func (bp *BalanceFilter) GetDisabled() bool { + if bp == nil || bp.Disabled == nil { + return false + } + return *bp.Disabled +} + +func (bp *BalanceFilter) GetBlocker() bool { + if bp == nil || bp.Blocker == nil { + return false + } + return *bp.Blocker +} + +func (bp *BalanceFilter) GetExpirationDate() time.Time { + if bp == nil || bp.ExpirationDate == nil { + return time.Time{} + } + return *bp.ExpirationDate +} + +func (bp *BalanceFilter) GetFactor() ValueFactor { + if bp == nil || bp.Factor == nil { + return ValueFactor{} + } + return *bp.Factor +} + +func (bp *BalanceFilter) EmptyExpirationDate() bool { + if bp.ExpirationDate == nil { + return true + } + return (*bp.ExpirationDate).IsZero() +} + +func (bf *BalanceFilter) ModifyBalance(b *Balance) { + if b == nil { + return + } + if bf.ID != nil { + b.ID = *bf.ID + } + if bf.Directions != nil { + b.Directions = *bf.Directions + } + if bf.Value != nil { + b.Value = *bf.Value + } + if bf.ExpirationDate != nil { + b.ExpirationDate = *bf.ExpirationDate + } + if bf.RatingSubject != nil { + b.RatingSubject = *bf.RatingSubject + } + if bf.Categories != nil { + b.Categories = *bf.Categories + } + if bf.DestinationIDs != nil { + b.DestinationIDs = *bf.DestinationIDs + } + if bf.SharedGroups != nil { + b.SharedGroups = *bf.SharedGroups + } + if bf.TimingIDs != nil { + b.TimingIDs = *bf.TimingIDs + } + if bf.Weight != nil { + b.Weight = *bf.Weight + } + if bf.Blocker != nil { + b.Blocker = *bf.Blocker + } + if bf.Disabled != nil { + b.Disabled = *bf.Disabled + } + b.SetDirty() // Mark the balance as dirty since we have modified and it should be checked by action triggers +} diff --git a/engine/balances.go b/engine/balances.go index bc0ad33f8..e44fa6033 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -33,12 +33,12 @@ import ( // Can hold different units as seconds or monetary type Balance struct { Uuid string //system wide unique - Id string // account wide unique + ID string // account wide unique Value float64 Directions utils.StringMap ExpirationDate time.Time Weight float64 - DestinationIds utils.StringMap + DestinationIDs utils.StringMap RatingSubject string Categories utils.StringMap SharedGroups utils.StringMap @@ -53,17 +53,17 @@ type Balance struct { } func (b *Balance) Equal(o *Balance) bool { - if len(b.DestinationIds) == 0 { - b.DestinationIds = utils.StringMap{utils.ANY: true} + if len(b.DestinationIDs) == 0 { + b.DestinationIDs = utils.StringMap{utils.ANY: true} } - if len(o.DestinationIds) == 0 { - o.DestinationIds = utils.StringMap{utils.ANY: true} + if len(o.DestinationIDs) == 0 { + o.DestinationIDs = utils.StringMap{utils.ANY: true} } return b.Uuid == o.Uuid && - b.Id == o.Id && + b.ID == o.ID && b.ExpirationDate.Equal(o.ExpirationDate) && b.Weight == o.Weight && - b.DestinationIds.Equal(o.DestinationIds) && + b.DestinationIDs.Equal(o.DestinationIDs) && b.Directions.Equal(o.Directions) && b.RatingSubject == o.RatingSubject && b.Categories.Equal(o.Categories) && @@ -72,71 +72,53 @@ func (b *Balance) Equal(o *Balance) bool { b.Blocker == o.Blocker } -func (b *Balance) MatchFilter(o *Balance, skipIds bool) bool { +func (b *Balance) MatchFilter(o *BalanceFilter, skipIds bool) bool { if o == nil { return true } - if !skipIds && o.Uuid != "" { - return b.Uuid == o.Uuid + if !skipIds && o.Uuid != nil && *o.Uuid != "" { + return b.Uuid == *o.Uuid } - if !skipIds && o.Id != "" { - return b.Id == o.Id + if !skipIds && o.ID != nil && *o.ID != "" { + return b.ID == *o.ID } - if len(b.DestinationIds) == 0 { - b.DestinationIds = utils.StringMap{utils.ANY: true} - } - if len(o.DestinationIds) == 0 { - o.DestinationIds = utils.StringMap{utils.ANY: true} - } - return (o.ExpirationDate.IsZero() || b.ExpirationDate.Equal(o.ExpirationDate)) && - (o.Weight == 0 || b.Weight == o.Weight) && - (b.Blocker == o.Blocker) && - (b.Disabled == o.Disabled) && - (len(o.DestinationIds) == 0 || b.DestinationIds.Includes(o.DestinationIds)) && - (len(o.Directions) == 0 || b.Directions.Includes(o.Directions)) && - (len(o.Categories) == 0 || b.Categories.Includes(o.Categories)) && - (len(o.TimingIDs) == 0 || b.TimingIDs.Includes(o.TimingIDs)) && - (len(o.SharedGroups) == 0 || b.SharedGroups.Includes(o.SharedGroups)) && - (o.RatingSubject == "" || b.RatingSubject == o.RatingSubject) + return (o.ExpirationDate == nil || b.ExpirationDate.Equal(*o.ExpirationDate)) && + (o.Weight == nil || b.Weight == *o.Weight) && + (o.Blocker == nil || b.Blocker == *o.Blocker) && + (o.Disabled == nil || b.Disabled == *o.Disabled) && + (o.DestinationIDs == nil || b.DestinationIDs.Includes(*o.DestinationIDs)) && + (o.Directions == nil || b.Directions.Includes(*o.Directions)) && + (o.Categories == nil || b.Categories.Includes(*o.Categories)) && + (o.TimingIDs == nil || b.TimingIDs.Includes(*o.TimingIDs)) && + (o.SharedGroups == nil || b.SharedGroups.Includes(*o.SharedGroups)) && + (o.RatingSubject == nil || b.RatingSubject == *o.RatingSubject) } -func (b *Balance) MatchCCFilter(cc *CallCost) bool { - if len(b.Categories) > 0 && cc.Category != "" && b.Categories[cc.Category] == false { - return false +func (b *Balance) HardMatchFilter(o *BalanceFilter, skipIds bool) bool { + if o == nil { + return true } - if len(b.Directions) > 0 && cc.Direction != "" && b.Directions[cc.Direction] == false { - return false + if !skipIds && o.Uuid != nil && *o.Uuid != "" { + return b.Uuid == *o.Uuid } - - // match destination ids - foundMatchingDestId := false - if len(b.DestinationIds) > 0 && cc.Destination != "" { - for _, p := range utils.SplitPrefix(cc.Destination, MIN_PREFIX_MATCH) { - if x, err := cache2go.Get(utils.DESTINATION_PREFIX + p); err == nil { - destIds := x.(map[interface{}]struct{}) - for filterDestId := range b.DestinationIds { - if _, ok := destIds[filterDestId]; ok { - foundMatchingDestId = true - break - } - } - } - if foundMatchingDestId { - break - } - } - } else { - foundMatchingDestId = true + if !skipIds && o.ID != nil && *o.ID != "" { + return b.ID == *o.ID } - if !foundMatchingDestId { - return false - } - return true + return (o.ExpirationDate == nil || b.ExpirationDate.Equal(*o.ExpirationDate)) && + (o.Weight == nil || b.Weight == *o.Weight) && + (o.Blocker == nil || b.Blocker == *o.Blocker) && + (o.Disabled == nil || b.Disabled == *o.Disabled) && + (o.DestinationIDs == nil || b.DestinationIDs.Equal(*o.DestinationIDs)) && + (o.Directions == nil || b.Directions.Equal(*o.Directions)) && + (o.Categories == nil || b.Categories.Equal(*o.Categories)) && + (o.TimingIDs == nil || b.TimingIDs.Equal(*o.TimingIDs)) && + (o.SharedGroups == nil || b.SharedGroups.Equal(*o.SharedGroups)) && + (o.RatingSubject == nil || b.RatingSubject == *o.RatingSubject) } // the default balance has standard Id func (b *Balance) IsDefault() bool { - return b.Id == utils.META_DEFAULT + return b.ID == utils.META_DEFAULT } func (b *Balance) IsExpired() bool { @@ -168,59 +150,19 @@ func (b *Balance) MatchCategory(category string) bool { } func (b *Balance) HasDestination() bool { - return len(b.DestinationIds) > 0 && b.DestinationIds[utils.ANY] == false + return len(b.DestinationIDs) > 0 && b.DestinationIDs[utils.ANY] == false } func (b *Balance) HasDirection() bool { return len(b.Directions) > 0 } -func (b *Balance) MatchDestination(destinationId string) bool { - return !b.HasDestination() || b.DestinationIds[destinationId] == true +func (b *Balance) MatchDestination(destinationID string) bool { + return !b.HasDestination() || b.DestinationIDs[destinationID] == true } func (b *Balance) MatchActionTrigger(at *ActionTrigger) bool { - if at.BalanceId != "" { - return b.Id == at.BalanceId - } - matchesDestination := true - if len(at.BalanceDestinationIds) != 0 { - matchesDestination = (b.DestinationIds.Equal(at.BalanceDestinationIds)) - } - matchesDirection := true - if len(at.BalanceDirections) != 0 { - matchesDirection = (b.Directions.Equal(at.BalanceDirections)) - } - matchesExpirationDate := true - if !at.BalanceExpirationDate.IsZero() { - matchesExpirationDate = (at.BalanceExpirationDate.Equal(b.ExpirationDate)) - } - matchesWeight := true - if at.BalanceWeight > 0 { - matchesWeight = (at.BalanceWeight == b.Weight) - } - matchesRatingSubject := true - if at.BalanceRatingSubject != "" { - matchesRatingSubject = (at.BalanceRatingSubject == b.RatingSubject) - } - - matchesSharedGroup := true - if len(at.BalanceSharedGroups) != 0 { - matchesSharedGroup = at.BalanceSharedGroups.Equal(b.SharedGroups) - } - - matchesTiming := true - if len(at.BalanceTimingTags) != 0 { - matchesTiming = at.BalanceTimingTags.Equal(b.TimingIDs) - } - - return matchesDestination && - matchesDirection && - matchesExpirationDate && - matchesWeight && - matchesRatingSubject && - matchesSharedGroup && - matchesTiming + return b.HardMatchFilter(at.Balance, false) } func (b *Balance) Clone() *Balance { @@ -229,7 +171,7 @@ func (b *Balance) Clone() *Balance { } n := &Balance{ Uuid: b.Uuid, - Id: b.Id, + ID: b.ID, Value: b.Value, // this value is in seconds ExpirationDate: b.ExpirationDate, Weight: b.Weight, @@ -242,8 +184,8 @@ func (b *Balance) Clone() *Balance { Disabled: b.Disabled, dirty: b.dirty, } - if b.DestinationIds != nil { - n.DestinationIds = b.DestinationIds.Clone() + if b.DestinationIDs != nil { + n.DestinationIDs = b.DestinationIDs.Clone() } if b.Directions != nil { n.Directions = b.Directions.Clone() @@ -251,14 +193,14 @@ func (b *Balance) Clone() *Balance { return n } -func (b *Balance) getMatchingPrefixAndDestId(dest string) (prefix, destId string) { - if len(b.DestinationIds) != 0 && b.DestinationIds[utils.ANY] == false { +func (b *Balance) getMatchingPrefixAndDestID(dest string) (prefix, destId string) { + if len(b.DestinationIDs) != 0 && b.DestinationIDs[utils.ANY] == false { for _, p := range utils.SplitPrefix(dest, MIN_PREFIX_MATCH) { if x, err := cache2go.Get(utils.DESTINATION_PREFIX + p); err == nil { - destIds := x.(map[interface{}]struct{}) - for dId, _ := range destIds { - if b.DestinationIds[dId.(string)] == true { - return p, dId.(string) + destIDs := x.(map[interface{}]struct{}) + for dID := range destIDs { + if b.DestinationIDs[dID.(string)] == true { + return p, dID.(string) } } } @@ -359,7 +301,11 @@ func (b *Balance) SetValue(amount float64) { b.dirty = true } -func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances BalanceChain, count bool, dryRun, debitConnectFee bool) (cc *CallCost, err error) { +func (b *Balance) SetDirty() { + b.dirty = true +} + +func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Balances, count bool, dryRun, debitConnectFee bool) (cc *CallCost, err error) { if !b.IsActiveAt(cd.TimeStart) || b.GetValue() <= 0 { return } @@ -385,7 +331,7 @@ func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Bala }, }, } - prefix, destid := b.getMatchingPrefixAndDestId(cd.Destination) + prefix, destid := b.getMatchingPrefixAndDestID(cd.Destination) if prefix == "" { prefix = cd.Destination } @@ -409,9 +355,16 @@ func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Bala } if b.GetValue() >= amount { b.SubstractValue(amount) - inc.BalanceInfo.UnitBalanceUuid = b.Uuid - inc.BalanceInfo.AccountId = ub.Id - inc.UnitInfo = &UnitInfo{cc.Destination, amount, cc.TOR} + inc.BalanceInfo.Unit = &UnitInfo{ + UUID: b.Uuid, + ID: b.ID, + Value: b.Value, + DestinationID: cc.Destination, + Consumed: amount, + TOR: cc.TOR, + RateInterval: nil, + } + inc.BalanceInfo.AccountID = ub.ID inc.Cost = 0 inc.paid = true if count { @@ -480,8 +433,13 @@ func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Bala } if strategy == utils.MAX_COST_FREE && cd.MaxCostSoFar >= maxCost { cost, inc.Cost = 0.0, 0.0 - inc.BalanceInfo.MoneyBalanceUuid = b.Uuid - inc.BalanceInfo.AccountId = ub.Id + inc.BalanceInfo.Monetary = &MonetaryInfo{ + UUID: b.Uuid, + ID: b.ID, + Value: b.Value, + RateInterval: ts.RateInterval, + } + inc.BalanceInfo.AccountID = ub.ID inc.paid = true if count { ub.countUnits(cost, utils.MONETARY, cc, b) @@ -498,11 +456,22 @@ func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Bala } if (cost == 0 || moneyBal != nil) && b.GetValue() >= amount { b.SubstractValue(amount) - inc.BalanceInfo.UnitBalanceUuid = b.Uuid - inc.BalanceInfo.AccountId = ub.Id - inc.UnitInfo = &UnitInfo{cc.Destination, amount, cc.TOR} + inc.BalanceInfo.Unit = &UnitInfo{ + UUID: b.Uuid, + ID: b.ID, + Value: b.Value, + DestinationID: cc.Destination, + Consumed: amount, + TOR: cc.TOR, + RateInterval: ts.RateInterval, + } + inc.BalanceInfo.AccountID = ub.ID if cost != 0 { - inc.BalanceInfo.MoneyBalanceUuid = moneyBal.Uuid + inc.BalanceInfo.Monetary = &MonetaryInfo{ + UUID: moneyBal.Uuid, + ID: moneyBal.ID, + Value: moneyBal.Value, + } moneyBal.SubstractValue(cost) cd.MaxCostSoFar += cost } @@ -534,7 +503,7 @@ func (b *Balance) debitUnits(cd *CallDescriptor, ub *Account, moneyBalances Bala return } -func (b *Balance) debitMoney(cd *CallDescriptor, ub *Account, moneyBalances BalanceChain, count bool, dryRun, debitConnectFee bool) (cc *CallCost, err error) { +func (b *Balance) debitMoney(cd *CallDescriptor, ub *Account, moneyBalances Balances, count bool, dryRun, debitConnectFee bool) (cc *CallCost, err error) { if !b.IsActiveAt(cd.TimeStart) || b.GetValue() <= 0 { return } @@ -587,8 +556,15 @@ func (b *Balance) debitMoney(cd *CallDescriptor, ub *Account, moneyBalances Bala } if strategy == utils.MAX_COST_FREE && cd.MaxCostSoFar >= maxCost { amount, inc.Cost = 0.0, 0.0 - inc.BalanceInfo.MoneyBalanceUuid = b.Uuid - inc.BalanceInfo.AccountId = ub.Id + inc.BalanceInfo.Monetary = &MonetaryInfo{ + UUID: b.Uuid, + ID: b.ID, + Value: b.Value, + } + inc.BalanceInfo.AccountID = ub.ID + if b.RatingSubject != "" { + inc.BalanceInfo.Monetary.RateInterval = ts.RateInterval + } inc.paid = true if count { ub.countUnits(amount, utils.MONETARY, cc, b) @@ -602,8 +578,15 @@ func (b *Balance) debitMoney(cd *CallDescriptor, ub *Account, moneyBalances Bala if b.GetValue() >= amount { b.SubstractValue(amount) cd.MaxCostSoFar += amount - inc.BalanceInfo.MoneyBalanceUuid = b.Uuid - inc.BalanceInfo.AccountId = ub.Id + inc.BalanceInfo.Monetary = &MonetaryInfo{ + UUID: b.Uuid, + ID: b.ID, + Value: b.Value, + } + inc.BalanceInfo.AccountID = ub.ID + if b.RatingSubject != "" { + inc.BalanceInfo.Monetary.RateInterval = ts.RateInterval + } inc.paid = true if count { ub.countUnits(amount, utils.MONETARY, cc, b) @@ -635,37 +618,38 @@ func (b *Balance) debitMoney(cd *CallDescriptor, ub *Account, moneyBalances Bala /* Structure to store minute buckets according to weight, precision or price. */ -type BalanceChain []*Balance +type Balances []*Balance -func (bc BalanceChain) Len() int { +func (bc Balances) Len() int { return len(bc) } -func (bc BalanceChain) Swap(i, j int) { +func (bc Balances) Swap(i, j int) { bc[i], bc[j] = bc[j], bc[i] } // we need the better ones at the beginning -func (bc BalanceChain) Less(j, i int) bool { +func (bc Balances) Less(j, i int) bool { return bc[i].precision < bc[j].precision || (bc[i].precision == bc[j].precision && bc[i].Weight < bc[j].Weight) } -func (bc BalanceChain) Sort() { +func (bc Balances) Sort() { sort.Sort(bc) } -func (bc BalanceChain) GetTotalValue() (total float64) { +func (bc Balances) GetTotalValue() (total float64) { for _, b := range bc { if !b.IsExpired() && b.IsActive() { total += b.GetValue() } } + total = utils.Round(total, globalRoundingDecimals, utils.ROUNDING_MIDDLE) return } -func (bc BalanceChain) Equal(o BalanceChain) bool { +func (bc Balances) Equal(o Balances) bool { if len(bc) != len(o) { return false } @@ -679,15 +663,15 @@ func (bc BalanceChain) Equal(o BalanceChain) bool { return true } -func (bc BalanceChain) Clone() BalanceChain { - var newChain BalanceChain +func (bc Balances) Clone() Balances { + var newChain Balances for _, b := range bc { newChain = append(newChain, b.Clone()) } return newChain } -func (bc BalanceChain) GetBalance(uuid string) *Balance { +func (bc Balances) GetBalance(uuid string) *Balance { for _, balance := range bc { if balance.Uuid == uuid { return balance @@ -696,7 +680,7 @@ func (bc BalanceChain) GetBalance(uuid string) *Balance { return nil } -func (bc BalanceChain) HasBalance(balance *Balance) bool { +func (bc Balances) HasBalance(balance *Balance) bool { for _, b := range bc { if b.Equal(balance) { return true @@ -705,7 +689,7 @@ func (bc BalanceChain) HasBalance(balance *Balance) bool { return false } -func (bc BalanceChain) SaveDirtyBalances(acc *Account) { +func (bc Balances) SaveDirtyBalances(acc *Account) { savedAccounts := make(map[string]bool) for _, b := range bc { if b.dirty { @@ -715,17 +699,17 @@ func (bc BalanceChain) SaveDirtyBalances(acc *Account) { disabled := "" if b.account != nil { // only publish modifications for balances with account set //utils.LogStack() - accountId = b.account.Id + accountId = b.account.ID allowNegative = strconv.FormatBool(b.account.AllowNegative) disabled = strconv.FormatBool(b.account.Disabled) Publish(CgrEvent{ "EventName": utils.EVT_ACCOUNT_BALANCE_MODIFIED, "Uuid": b.Uuid, - "Id": b.Id, + "Id": b.ID, "Value": strconv.FormatFloat(b.Value, 'f', -1, 64), "ExpirationDate": b.ExpirationDate.String(), "Weight": strconv.FormatFloat(b.Weight, 'f', -1, 64), - "DestinationIds": b.DestinationIds.String(), + "DestinationIDs": b.DestinationIDs.String(), "Directions": b.Directions.String(), "RatingSubject": b.RatingSubject, "Categories": b.Categories.String(), @@ -737,9 +721,9 @@ func (bc BalanceChain) SaveDirtyBalances(acc *Account) { }) } } - if b.account != nil && b.account != acc && b.dirty && savedAccounts[b.account.Id] == false { + if b.account != nil && b.account != acc && b.dirty && savedAccounts[b.account.ID] == false { accountingStorage.SetAccount(b.account) - savedAccounts[b.account.Id] = true + savedAccounts[b.account.ID] = true } } } diff --git a/engine/balances_test.go b/engine/balances_test.go index 163ab79ec..83c1abb1d 100644 --- a/engine/balances_test.go +++ b/engine/balances_test.go @@ -27,7 +27,7 @@ import ( func TestBalanceSortPrecision(t *testing.T) { mb1 := &Balance{Weight: 1, precision: 2} mb2 := &Balance{Weight: 2, precision: 1} - var bs BalanceChain + var bs Balances bs = append(bs, mb2, mb1) bs.Sort() if bs[0] != mb1 || bs[1] != mb2 { @@ -38,7 +38,7 @@ func TestBalanceSortPrecision(t *testing.T) { func TestBalanceSortPrecisionWeightEqual(t *testing.T) { mb1 := &Balance{Weight: 1, precision: 2} mb2 := &Balance{Weight: 1, precision: 1} - var bs BalanceChain + var bs Balances bs = append(bs, mb2, mb1) bs.Sort() if bs[0] != mb1 || bs[1] != mb2 { @@ -49,7 +49,7 @@ func TestBalanceSortPrecisionWeightEqual(t *testing.T) { func TestBalanceSortPrecisionWeightGreater(t *testing.T) { mb1 := &Balance{Weight: 2, precision: 2} mb2 := &Balance{Weight: 1, precision: 1} - var bs BalanceChain + var bs Balances bs = append(bs, mb2, mb1) bs.Sort() if bs[0] != mb1 || bs[1] != mb2 { @@ -60,7 +60,7 @@ func TestBalanceSortPrecisionWeightGreater(t *testing.T) { func TestBalanceSortWeight(t *testing.T) { mb1 := &Balance{Weight: 2, precision: 1} mb2 := &Balance{Weight: 1, precision: 1} - var bs BalanceChain + var bs Balances bs = append(bs, mb2, mb1) bs.Sort() if bs[0] != mb1 || bs[1] != mb2 { @@ -71,7 +71,7 @@ func TestBalanceSortWeight(t *testing.T) { func TestBalanceSortWeightLess(t *testing.T) { mb1 := &Balance{Weight: 1, precision: 1} mb2 := &Balance{Weight: 2, precision: 1} - var bs BalanceChain + var bs Balances bs = append(bs, mb2, mb1) bs.Sort() if bs[0] != mb2 || bs[1] != mb1 { @@ -80,48 +80,48 @@ func TestBalanceSortWeightLess(t *testing.T) { } func TestBalanceEqual(t *testing.T) { - mb1 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIds: utils.StringMap{}} - mb2 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIds: utils.StringMap{}} - mb3 := &Balance{Weight: 1, precision: 1, RatingSubject: "2", DestinationIds: utils.StringMap{}} + mb1 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIDs: utils.StringMap{}} + mb2 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIDs: utils.StringMap{}} + mb3 := &Balance{Weight: 1, precision: 1, RatingSubject: "2", DestinationIDs: utils.StringMap{}} if !mb1.Equal(mb2) || mb2.Equal(mb3) { t.Error("Equal failure!", mb1 == mb2, mb3) } } func TestBalanceMatchFilter(t *testing.T) { - mb1 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIds: utils.StringMap{}} - mb2 := &Balance{Weight: 1, precision: 1, RatingSubject: "", DestinationIds: utils.StringMap{}} + mb1 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIDs: utils.StringMap{}} + mb2 := &BalanceFilter{Weight: utils.Float64Pointer(1), RatingSubject: nil, DestinationIDs: nil} if !mb1.MatchFilter(mb2, false) { t.Errorf("Match filter failure: %+v == %+v", mb1, mb2) } } func TestBalanceMatchFilterEmpty(t *testing.T) { - mb1 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIds: utils.StringMap{}} - mb2 := &Balance{} + mb1 := &Balance{Weight: 1, precision: 1, RatingSubject: "1", DestinationIDs: utils.StringMap{}} + mb2 := &BalanceFilter{} if !mb1.MatchFilter(mb2, false) { t.Errorf("Match filter failure: %+v == %+v", mb1, mb2) } } func TestBalanceMatchFilterId(t *testing.T) { - mb1 := &Balance{Id: "T1", Weight: 2, precision: 2, RatingSubject: "2", DestinationIds: utils.NewStringMap("NAT")} - mb2 := &Balance{Id: "T1", Weight: 1, precision: 1, RatingSubject: "1", DestinationIds: utils.StringMap{}} + mb1 := &Balance{ID: "T1", Weight: 2, precision: 2, RatingSubject: "2", DestinationIDs: utils.NewStringMap("NAT")} + mb2 := &BalanceFilter{ID: utils.StringPointer("T1"), Weight: utils.Float64Pointer(1), RatingSubject: utils.StringPointer("1"), DestinationIDs: nil} if !mb1.MatchFilter(mb2, false) { t.Errorf("Match filter failure: %+v == %+v", mb1, mb2) } } func TestBalanceMatchFilterDiffId(t *testing.T) { - mb1 := &Balance{Id: "T1", Weight: 1, precision: 1, RatingSubject: "1", DestinationIds: utils.StringMap{}} - mb2 := &Balance{Id: "T2", Weight: 1, precision: 1, RatingSubject: "1", DestinationIds: utils.StringMap{}} + mb1 := &Balance{ID: "T1", Weight: 1, precision: 1, RatingSubject: "1", DestinationIDs: utils.StringMap{}} + mb2 := &BalanceFilter{ID: utils.StringPointer("T2"), Weight: utils.Float64Pointer(1), RatingSubject: utils.StringPointer("1"), DestinationIDs: nil} if mb1.MatchFilter(mb2, false) { t.Errorf("Match filter failure: %+v != %+v", mb1, mb2) } } func TestBalanceClone(t *testing.T) { - mb1 := &Balance{Value: 1, Weight: 2, RatingSubject: "test", DestinationIds: utils.NewStringMap("5")} + mb1 := &Balance{Value: 1, Weight: 2, RatingSubject: "test", DestinationIDs: utils.NewStringMap("5")} mb2 := mb1.Clone() if mb1 == mb2 || !mb1.Equal(mb2) { t.Errorf("Cloning failure: \n%+v\n%+v", mb1, mb2) @@ -129,49 +129,49 @@ func TestBalanceClone(t *testing.T) { } func TestBalanceMatchActionTriggerId(t *testing.T) { - at := &ActionTrigger{BalanceId: "test"} - b := &Balance{Id: "test"} + at := &ActionTrigger{Balance: &BalanceFilter{ID: utils.StringPointer("test")}} + b := &Balance{ID: "test"} if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } - b.Id = "test1" + b.ID = "test1" if b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } - b.Id = "" + b.ID = "" if b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } - b.Id = "test" - at.BalanceId = "" + b.ID = "test" + at.Balance.ID = nil if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } } func TestBalanceMatchActionTriggerDestination(t *testing.T) { - at := &ActionTrigger{BalanceDestinationIds: utils.NewStringMap("test")} - b := &Balance{DestinationIds: utils.NewStringMap("test")} + at := &ActionTrigger{Balance: &BalanceFilter{DestinationIDs: utils.StringMapPointer(utils.NewStringMap("test"))}} + b := &Balance{DestinationIDs: utils.NewStringMap("test")} if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } - b.DestinationIds = utils.NewStringMap("test1") + b.DestinationIDs = utils.NewStringMap("test1") if b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } - b.DestinationIds = utils.NewStringMap("") + b.DestinationIDs = utils.NewStringMap("") if b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } - b.DestinationIds = utils.NewStringMap("test") - at.BalanceDestinationIds = utils.NewStringMap("") + b.DestinationIDs = utils.NewStringMap("test") + at.Balance.DestinationIDs = nil if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } } func TestBalanceMatchActionTriggerWeight(t *testing.T) { - at := &ActionTrigger{BalanceWeight: 1} + at := &ActionTrigger{Balance: &BalanceFilter{Weight: utils.Float64Pointer(1)}} b := &Balance{Weight: 1} if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) @@ -185,14 +185,14 @@ func TestBalanceMatchActionTriggerWeight(t *testing.T) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } b.Weight = 1 - at.BalanceWeight = 0 + at.Balance.Weight = nil if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } } func TestBalanceMatchActionTriggerRatingSubject(t *testing.T) { - at := &ActionTrigger{BalanceRatingSubject: "test"} + at := &ActionTrigger{Balance: &BalanceFilter{RatingSubject: utils.StringPointer("test")}} b := &Balance{RatingSubject: "test"} if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) @@ -206,14 +206,14 @@ func TestBalanceMatchActionTriggerRatingSubject(t *testing.T) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } b.RatingSubject = "test" - at.BalanceRatingSubject = "" + at.Balance.RatingSubject = nil if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } } func TestBalanceMatchActionTriggerSharedGroup(t *testing.T) { - at := &ActionTrigger{BalanceSharedGroups: utils.NewStringMap("test")} + at := &ActionTrigger{Balance: &BalanceFilter{SharedGroups: utils.StringMapPointer(utils.NewStringMap("test"))}} b := &Balance{SharedGroups: utils.NewStringMap("test")} if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) @@ -227,7 +227,7 @@ func TestBalanceMatchActionTriggerSharedGroup(t *testing.T) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } b.SharedGroups = utils.NewStringMap("test") - at.BalanceSharedGroups = utils.NewStringMap("") + at.Balance.SharedGroups = nil if !b.MatchActionTrigger(at) { t.Errorf("Error matching action trigger: %+v %+v", b, at) } @@ -238,7 +238,7 @@ func TestBalanceIsDefault(t *testing.T) { if b.IsDefault() { t.Errorf("Balance should not be default: %+v", b) } - b = &Balance{Id: utils.META_DEFAULT} + b = &Balance{ID: utils.META_DEFAULT} if !b.IsDefault() { t.Errorf("Balance should be default: %+v", b) } diff --git a/engine/callcost.go b/engine/callcost.go index e786318a7..71e8609f2 100644 --- a/engine/callcost.go +++ b/engine/callcost.go @@ -21,6 +21,7 @@ import ( "errors" "time" + "github.com/cgrates/cgrates/cache2go" "github.com/cgrates/cgrates/utils" ) @@ -89,6 +90,7 @@ func (cc *CallCost) CreateCallDescriptor() *CallDescriptor { Subject: cc.Subject, Account: cc.Account, Destination: cc.Destination, + TOR: cc.TOR, } } @@ -135,13 +137,11 @@ func (cc *CallCost) ToDataCost() (*DataCost, error) { dc.DataSpans[i].Increments = make([]*DataIncrement, len(ts.Increments)) for j, incr := range ts.Increments { dc.DataSpans[i].Increments[j] = &DataIncrement{ - Amount: incr.Duration.Seconds(), - Cost: incr.Cost, - BalanceInfo: incr.BalanceInfo, - BalanceRateInterval: incr.BalanceRateInterval, - UnitInfo: incr.UnitInfo, - CompressFactor: incr.CompressFactor, - paid: incr.paid, + Amount: incr.Duration.Seconds(), + Cost: incr.Cost, + BalanceInfo: incr.BalanceInfo, + CompressFactor: incr.CompressFactor, + paid: incr.paid, } } } @@ -162,15 +162,98 @@ func (cc *CallCost) AsJSON() string { return utils.ToJSON(cc) } +// public function to update final (merged) callcost +func (cc *CallCost) UpdateCost() { + cc.deductConnectFee = true + cc.updateCost() +} + func (cc *CallCost) updateCost() { cost := 0.0 if cc.deductConnectFee { // add back the connectFee cost += cc.GetConnectFee() } for _, ts := range cc.Timespans { - ts.Cost = ts.calculateCost() + ts.Cost = ts.CalculateCost() cost += ts.Cost cost = utils.Round(cost, globalRoundingDecimals, utils.ROUNDING_MIDDLE) // just get rid of the extra decimals } cc.Cost = cost } + +func (cc *CallCost) Round() { + if len(cc.Timespans) == 0 || cc.Timespans[0] == nil { + return + } + var totalCorrectionCost float64 + for _, ts := range cc.Timespans { + if len(ts.Increments) == 0 { + continue // safe check + } + inc := ts.Increments[0] + if inc.BalanceInfo.Monetary == nil || inc.Cost == 0 { + // this is a unit payied timespan, nothing to round + continue + } + cost := ts.CalculateCost() + roundedCost := utils.Round(cost, ts.RateInterval.Rating.RoundingDecimals, + ts.RateInterval.Rating.RoundingMethod) + correctionCost := roundedCost - cost + //log.Print(cost, roundedCost, correctionCost) + if correctionCost != 0 { + ts.RoundIncrement = &Increment{ + Cost: correctionCost, + BalanceInfo: inc.BalanceInfo, + } + totalCorrectionCost += correctionCost + ts.Cost += correctionCost + } + } + cc.Cost += totalCorrectionCost +} + +func (cc *CallCost) GetRoundIncrements() (roundIncrements Increments) { + for _, ts := range cc.Timespans { + if ts.RoundIncrement != nil && ts.RoundIncrement.Cost != 0 { + roundIncrements = append(roundIncrements, ts.RoundIncrement) + } + } + return +} + +func (cc *CallCost) MatchCCFilter(bf *BalanceFilter) bool { + if bf == nil { + return true + } + if bf.Categories != nil && cc.Category != "" && (*bf.Categories)[cc.Category] == false { + return false + } + if bf.Directions != nil && cc.Direction != "" && (*bf.Directions)[cc.Direction] == false { + return false + } + + // match destination ids + foundMatchingDestID := false + if bf.DestinationIDs != nil && cc.Destination != "" { + for _, p := range utils.SplitPrefix(cc.Destination, MIN_PREFIX_MATCH) { + if x, err := cache2go.Get(utils.DESTINATION_PREFIX + p); err == nil { + destIds := x.(map[interface{}]struct{}) + for filterDestID := range *bf.DestinationIDs { + if _, ok := destIds[filterDestID]; ok { + foundMatchingDestID = true + break + } + } + } + if foundMatchingDestID { + break + } + } + } else { + foundMatchingDestID = true + } + if !foundMatchingDestID { + return false + } + return true +} diff --git a/engine/calldesc.go b/engine/calldesc.go index abbb684bc..63b5719f8 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -69,16 +69,17 @@ func init() { } var ( - ratingStorage RatingStorage - accountingStorage AccountingStorage - storageLogger LogStorage - cdrStorage CdrStorage - debitPeriod = 10 * time.Second - globalRoundingDecimals = 5 - historyScribe rpcclient.RpcClientConnection - pubSubServer rpcclient.RpcClientConnection - userService rpcclient.RpcClientConnection - aliasService rpcclient.RpcClientConnection + ratingStorage RatingStorage + accountingStorage AccountingStorage + storageLogger LogStorage + cdrStorage CdrStorage + debitPeriod = 10 * time.Second + globalRoundingDecimals = 5 + historyScribe rpcclient.RpcClientConnection + pubSubServer rpcclient.RpcClientConnection + userService rpcclient.RpcClientConnection + aliasService rpcclient.RpcClientConnection + rpSubjectPrefixMatching bool ) // Exported method to set the storage getter. @@ -95,6 +96,10 @@ func SetRoundingDecimals(rd int) { globalRoundingDecimals = rd } +func SetRpSubjectPrefixMatching(flag bool) { + rpSubjectPrefixMatching = flag +} + /* Sets the database for logging (can be de same as storage getter or different db) */ @@ -149,12 +154,16 @@ type CallDescriptor struct { TOR string // used unit balances selector ExtraFields map[string]string // Extra fields, mostly used for user profile matching // session limits - MaxRate float64 - MaxRateUnit time.Duration - MaxCostSoFar float64 - CgrId string - account *Account - testCallcost *CallCost // testing purpose only! + MaxRate float64 + MaxRateUnit time.Duration + MaxCostSoFar float64 + CgrID string + RunID string + ForceDuration bool // for Max debit if less than duration return err + PerformRounding bool // flag for rating info rounding + DryRun bool + account *Account + testCallcost *CallCost // testing purpose only! } func (cd *CallDescriptor) ValidateCallData() error { @@ -178,7 +187,7 @@ func (cd *CallDescriptor) getAccount() (ub *Account, err error) { cd.account, err = accountingStorage.GetAccount(cd.GetAccountKey()) } if cd.account != nil && cd.account.Disabled { - return nil, fmt.Errorf("User %s is disabled", cd.account.Id) + return nil, fmt.Errorf("User %s is disabled", cd.account.ID) } return cd.account, err } @@ -192,16 +201,18 @@ func (cd *CallDescriptor) LoadRatingPlans() (err error) { if err == utils.ErrNotFound && rec == 1 { //if err != nil || !cd.continousRatingInfos() { // use the default subject only if the initial one was not found + //utils.Logger.Debug(fmt.Sprintf("Try the default subject for %s and account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject))) err, _ = cd.getRatingPlansForPrefix(cd.GetKey(FALLBACK_SUBJECT), 1) } //load the rating plans if err != nil { utils.Logger.Err(fmt.Sprintf("Rating plan not found for destination %s and account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject))) - err = utils.ErrRatingPlanNotFound + return utils.ErrRatingPlanNotFound + } if !cd.continousRatingInfos() { utils.Logger.Err(fmt.Sprintf("Destination %s not authorized for account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject))) - err = utils.ErrUnauthorizedDestination + return utils.ErrUnauthorizedDestination } return } @@ -212,7 +223,7 @@ func (cd *CallDescriptor) getRatingPlansForPrefix(key string, recursionDepth int if recursionDepth > RECURSION_MAX_DEPTH { return utils.ErrMaxRecursionDepth, recursionDepth } - rpf, err := ratingStorage.GetRatingProfile(key, false) + rpf, err := RatingProfileSubjectPrefixMatching(key) if err != nil || rpf == nil { return utils.ErrNotFound, recursionDepth } @@ -454,7 +465,7 @@ func (cd *CallDescriptor) GetCost() (*CallCost, error) { // handle max cost maxCost, strategy := ts.RateInterval.GetMaxCost() - ts.Cost = ts.calculateCost() + ts.Cost = ts.CalculateCost() cost += ts.Cost cd.MaxCostSoFar += cost //log.Print("Before: ", cost) @@ -513,7 +524,7 @@ func (cd *CallDescriptor) getCost() (*CallCost, error) { if cd.LoopIndex == 0 && i == 0 && ts.RateInterval != nil { cost += ts.RateInterval.Rating.ConnectFee } - cost += ts.calculateCost() + cost += ts.CalculateCost() } //startIndex := len(fmt.Sprintf("%s:%s:%s:", cd.Direction, cd.Tenant, cd.Category)) @@ -542,6 +553,7 @@ func (origCD *CallDescriptor) getMaxSessionDuration(origAcc *Account) (time.Dura if account.AllowNegative { return -1, nil } + // for zero duration index if origCD.DurationIndex < origCD.TimeEnd.Sub(origCD.TimeStart) { origCD.DurationIndex = origCD.TimeEnd.Sub(origCD.TimeStart) } @@ -596,7 +608,7 @@ func (origCD *CallDescriptor) getMaxSessionDuration(origAcc *Account) (time.Dura for _, incr := range ts.Increments { //utils.Logger.Debug("INCR: " + utils.ToJSON(incr)) totalCost += incr.Cost - if incr.BalanceInfo.MoneyBalanceUuid == defaultBalance.Uuid { + if incr.BalanceInfo.Monetary != nil && incr.BalanceInfo.Monetary.UUID == defaultBalance.Uuid { initialDefaultBalanceValue -= incr.Cost if initialDefaultBalanceValue < 0 { // this increment was payed with debt @@ -607,6 +619,7 @@ func (origCD *CallDescriptor) getMaxSessionDuration(origAcc *Account) (time.Dura } } totalDuration += incr.Duration + //log.Print("INC: ", utils.ToJSON(incr)) if totalDuration >= initialDuration { // we have enough, return //utils.Logger.Debug(fmt.Sprintf("2_INIT DUR %v, TOTAL DUR: %v", initialDuration, totalDuration)) @@ -615,6 +628,7 @@ func (origCD *CallDescriptor) getMaxSessionDuration(origAcc *Account) (time.Dura } } //utils.Logger.Debug(fmt.Sprintf("3_INIT DUR %v, TOTAL DUR: %v", initialDuration, totalDuration)) + return utils.MinDuration(initialDuration, totalDuration), nil } @@ -624,6 +638,9 @@ func (cd *CallDescriptor) GetMaxSessionDuration() (duration time.Duration, err e utils.Logger.Err(fmt.Sprintf("Account: %s, not found", cd.GetAccountKey())) return 0, utils.ErrAccountNotFound } else { + if account.Disabled { + return 0, utils.ErrAccountDisabled + } if memberIds, err := account.GetUniqueSharedGroupMembers(cd); err == nil { if _, err := Guardian.Guard(func() (interface{}, error) { duration, err = cd.getMaxSessionDuration(account) @@ -655,9 +672,6 @@ func (cd *CallDescriptor) debit(account *Account, dryRun bool, goNegative bool) } return cc, nil } - if !dryRun { - defer accountingStorage.SetAccount(account) - } if cd.TOR == "" { cd.TOR = utils.VOICE } @@ -671,6 +685,18 @@ func (cd *CallDescriptor) debit(account *Account, dryRun bool, goNegative bool) cc.updateCost() cc.UpdateRatedUsage() cc.Timespans.Compress() + if !dryRun { + accountingStorage.SetAccount(account) + } + if cd.PerformRounding { + cc.Round() + roundIncrements := cc.GetRoundIncrements() + if len(roundIncrements) != 0 { + rcd := cc.CreateCallDescriptor() + rcd.Increments = roundIncrements + rcd.RefundRounding() + } + } //log.Printf("OUT CC: ", cc) return } @@ -682,28 +708,12 @@ func (cd *CallDescriptor) Debit() (cc *CallCost, err error) { utils.Logger.Err(fmt.Sprintf("Account: %s, not found", cd.GetAccountKey())) return nil, utils.ErrAccountNotFound } else { - if memberIds, sgerr := account.GetUniqueSharedGroupMembers(cd); sgerr == nil { - _, err = Guardian.Guard(func() (interface{}, error) { - cc, err = cd.debit(account, false, true) - return 0, err - }, 0, memberIds.Slice()...) - } else { - return nil, sgerr + if account.Disabled { + return nil, utils.ErrAccountDisabled } - return cc, err - } -} - -func (cd *CallDescriptor) FakeDebit() (cc *CallCost, err error) { - cd.account = nil // make sure it's not cached - // lock all group members - if account, err := cd.getAccount(); err != nil || account == nil { - utils.Logger.Err(fmt.Sprintf("Account: %s, not found", cd.GetAccountKey())) - return nil, utils.ErrAccountNotFound - } else { if memberIds, sgerr := account.GetUniqueSharedGroupMembers(cd); sgerr == nil { _, err = Guardian.Guard(func() (interface{}, error) { - cc, err = cd.debit(account, true, true) + cc, err = cd.debit(account, cd.DryRun, true) return 0, err }, 0, memberIds.Slice()...) } else { @@ -723,10 +733,20 @@ func (cd *CallDescriptor) MaxDebit() (cc *CallCost, err error) { utils.Logger.Err(fmt.Sprintf("Account: %s, not found", cd.GetAccountKey())) return nil, utils.ErrAccountNotFound } else { + if account.Disabled { + return nil, utils.ErrAccountDisabled + } //log.Printf("ACC: %+v", account) - if memberIds, err := account.GetUniqueSharedGroupMembers(cd); err == nil { + if memberIDs, err := account.GetUniqueSharedGroupMembers(cd); err == nil { _, err = Guardian.Guard(func() (interface{}, error) { remainingDuration, err := cd.getMaxSessionDuration(account) + if err != nil && cd.GetDuration() > 0 { + return 0, err + } + // check ForceDuartion + if cd.ForceDuration && !account.AllowNegative && remainingDuration < cd.GetDuration() { + return 0, utils.ErrInsufficientCredit + } //log.Print("AFTER MAX SESSION: ", cd) if err != nil || remainingDuration == 0 { cc = cd.CreateCallCost() @@ -751,10 +771,11 @@ func (cd *CallDescriptor) MaxDebit() (cc *CallCost, err error) { cd.TimeEnd = cd.TimeStart.Add(remainingDuration) cd.DurationIndex -= initialDuration - remainingDuration } - cc, err = cd.debit(account, false, true) + //log.Print("Remaining duration: ", remainingDuration) + cc, err = cd.debit(account, cd.DryRun, true) //log.Print(balanceMap[0].Value, balanceMap[1].Value) return 0, err - }, 0, memberIds.Slice()...) + }, 0, memberIDs.Slice()...) if err != nil { return cc, err } @@ -765,42 +786,93 @@ func (cd *CallDescriptor) MaxDebit() (cc *CallCost, err error) { return cc, err } -func (cd *CallDescriptor) RefundIncrements() (left float64, err error) { - cd.account = nil // make sure it's not cached +func (cd *CallDescriptor) RefundIncrements() error { // get account list for locking // all must be locked in order to use cache - accMap := make(map[string]struct{}) - var accountIDs []string + accMap := make(utils.StringMap) cd.Increments.Decompress() for _, increment := range cd.Increments { - accMap[increment.BalanceInfo.AccountId] = struct{}{} - } - for key := range accMap { - accountIDs = append(accountIDs, key) + accMap[increment.BalanceInfo.AccountID] = true } // start increment refunding loop - Guardian.Guard(func() (interface{}, error) { + _, err := Guardian.Guard(func() (interface{}, error) { accountsCache := make(map[string]*Account) for _, increment := range cd.Increments { - account, found := accountsCache[increment.BalanceInfo.AccountId] + account, found := accountsCache[increment.BalanceInfo.AccountID] if !found { - if acc, err := accountingStorage.GetAccount(increment.BalanceInfo.AccountId); err == nil && acc != nil { + if acc, err := accountingStorage.GetAccount(increment.BalanceInfo.AccountID); err == nil && acc != nil { account = acc - accountsCache[increment.BalanceInfo.AccountId] = account + accountsCache[increment.BalanceInfo.AccountID] = account // will save the account only once at the end of the function defer accountingStorage.SetAccount(account) } } if account == nil { - utils.Logger.Warning(fmt.Sprintf("Could not get the account to be refunded: %s", increment.BalanceInfo.AccountId)) + utils.Logger.Warning(fmt.Sprintf("Could not get the account to be refunded: %s", increment.BalanceInfo.AccountID)) continue } //utils.Logger.Info(fmt.Sprintf("Refunding increment %+v", increment)) - account.refundIncrement(increment, cd, true) + var balance *Balance + unitType := cd.TOR + cc := cd.CreateCallCost() + if increment.BalanceInfo.Unit != nil && increment.BalanceInfo.Unit.UUID != "" { + if balance = account.BalanceMap[unitType].GetBalance(increment.BalanceInfo.Unit.UUID); balance == nil { + return 0, nil + } + balance.AddValue(increment.Duration.Seconds()) + account.countUnits(-increment.Duration.Seconds(), unitType, cc, balance) + } + // check money too + if increment.BalanceInfo.Monetary != nil && increment.BalanceInfo.Monetary.UUID != "" { + if balance = account.BalanceMap[utils.MONETARY].GetBalance(increment.BalanceInfo.Monetary.UUID); balance == nil { + return 0, nil + } + balance.AddValue(increment.Cost) + account.countUnits(-increment.Cost, utils.MONETARY, cc, balance) + } } - return 0, err - }, 0, accountIDs...) - return 0, err + return 0, nil + }, 0, accMap.Slice()...) + return err +} + +func (cd *CallDescriptor) RefundRounding() error { + // get account list for locking + // all must be locked in order to use cache + accMap := make(utils.StringMap) + for _, inc := range cd.Increments { + accMap[inc.BalanceInfo.AccountID] = true + } + // start increment refunding loop + _, err := Guardian.Guard(func() (interface{}, error) { + accountsCache := make(map[string]*Account) + for _, increment := range cd.Increments { + account, found := accountsCache[increment.BalanceInfo.AccountID] + if !found { + if acc, err := accountingStorage.GetAccount(increment.BalanceInfo.AccountID); err == nil && acc != nil { + account = acc + accountsCache[increment.BalanceInfo.AccountID] = account + // will save the account only once at the end of the function + defer accountingStorage.SetAccount(account) + } + } + if account == nil { + utils.Logger.Warning(fmt.Sprintf("Could not get the account to be refunded: %s", increment.BalanceInfo.AccountID)) + continue + } + cc := cd.CreateCallCost() + if increment.BalanceInfo.Monetary != nil { + var balance *Balance + if balance = account.BalanceMap[utils.MONETARY].GetBalance(increment.BalanceInfo.Monetary.UUID); balance == nil { + return 0, nil + } + balance.AddValue(-increment.Cost) + account.countUnits(increment.Cost, utils.MONETARY, cc, balance) + } + } + return 0, nil + }, 0, accMap.Slice()...) + return err } func (cd *CallDescriptor) FlushCache() (err error) { @@ -843,7 +915,10 @@ func (cd *CallDescriptor) Clone() *CallDescriptor { FallbackSubject: cd.FallbackSubject, //RatingInfos: cd.RatingInfos, //Increments: cd.Increments, - TOR: cd.TOR, + TOR: cd.TOR, + ForceDuration: cd.ForceDuration, + PerformRounding: cd.PerformRounding, + DryRun: cd.DryRun, } } @@ -973,7 +1048,7 @@ func (cd *CallDescriptor) GetLCR(stats rpcclient.RpcClientConnection, p *utils.P continue } rpfKey := utils.ConcatenatedKey(ratingProfileSearchKey, supplier) - if rpf, err := ratingStorage.GetRatingProfile(rpfKey, false); err != nil { + if rpf, err := RatingProfileSubjectPrefixMatching(rpfKey); err != nil { lcrCost.SupplierCosts = append(lcrCost.SupplierCosts, &LCRSupplierCost{ Supplier: fullSupplier, Error: fmt.Sprintf("Rating plan error: %s", err.Error()), diff --git a/engine/calldesc_test.go b/engine/calldesc_test.go index f470910ae..540bc8dad 100644 --- a/engine/calldesc_test.go +++ b/engine/calldesc_test.go @@ -41,63 +41,63 @@ func init() { func populateDB() { ats := []*Action{ - &Action{ActionType: "*topup", BalanceType: utils.MONETARY, Balance: &Balance{Value: 10}}, - &Action{ActionType: "*topup", BalanceType: utils.VOICE, Balance: &Balance{Weight: 20, Value: 10, DestinationIds: utils.NewStringMap("NAT")}}, + &Action{ActionType: "*topup", Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Value: utils.Float64Pointer(10)}}, + &Action{ActionType: "*topup", Balance: &BalanceFilter{Type: utils.StringPointer(utils.VOICE), Weight: utils.Float64Pointer(20), Value: utils.Float64Pointer(10), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT"))}}, } ats1 := []*Action{ - &Action{ActionType: "*topup", BalanceType: utils.MONETARY, Balance: &Balance{Value: 10}, Weight: 10}, + &Action{ActionType: "*topup", Balance: &BalanceFilter{Type: utils.StringPointer(utils.MONETARY), Value: utils.Float64Pointer(10)}, Weight: 10}, &Action{ActionType: "*reset_account", Weight: 20}, } minu := &Account{ - Id: "vdf:minu", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{&Balance{Value: 50}}, - utils.VOICE: BalanceChain{ - &Balance{Value: 200, DestinationIds: utils.NewStringMap("NAT"), Weight: 10}, - &Balance{Value: 100, DestinationIds: utils.NewStringMap("RET"), Weight: 20}, + ID: "vdf:minu", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{&Balance{Value: 50}}, + utils.VOICE: Balances{ + &Balance{Value: 200, DestinationIDs: utils.NewStringMap("NAT"), Weight: 10}, + &Balance{Value: 100, DestinationIDs: utils.NewStringMap("RET"), Weight: 20}, }}, } broker := &Account{ - Id: "vdf:broker", - BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{ - &Balance{Value: 20, DestinationIds: utils.NewStringMap("NAT"), Weight: 10, RatingSubject: "rif"}, - &Balance{Value: 100, DestinationIds: utils.NewStringMap("RET"), Weight: 20}, + ID: "vdf:broker", + BalanceMap: map[string]Balances{ + utils.VOICE: Balances{ + &Balance{Value: 20, DestinationIDs: utils.NewStringMap("NAT"), Weight: 10, RatingSubject: "rif"}, + &Balance{Value: 100, DestinationIDs: utils.NewStringMap("RET"), Weight: 20}, }}, } luna := &Account{ - Id: "vdf:luna", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "vdf:luna", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{Value: 0, Weight: 20}, }}, } // this is added to test if csv load tests account will not overwrite balances minitsboy := &Account{ - Id: "vdf:minitsboy", - BalanceMap: map[string]BalanceChain{ - utils.VOICE: BalanceChain{ - &Balance{Value: 20, DestinationIds: utils.NewStringMap("NAT"), Weight: 10, RatingSubject: "rif"}, - &Balance{Value: 100, DestinationIds: utils.NewStringMap("RET"), Weight: 20}, + ID: "vdf:minitsboy", + BalanceMap: map[string]Balances{ + utils.VOICE: Balances{ + &Balance{Value: 20, DestinationIDs: utils.NewStringMap("NAT"), Weight: 10, RatingSubject: "rif"}, + &Balance{Value: 100, DestinationIDs: utils.NewStringMap("RET"), Weight: 20}, }, - utils.MONETARY: BalanceChain{ + utils.MONETARY: Balances{ &Balance{Value: 100, Weight: 10}, }, }, } max := &Account{ - Id: "cgrates.org:max", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "cgrates.org:max", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{Value: 11, Weight: 20}, }}, } money := &Account{ - Id: "cgrates.org:money", - BalanceMap: map[string]BalanceChain{ - utils.MONETARY: BalanceChain{ + ID: "cgrates.org:money", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ &Balance{Value: 10000, Weight: 10}, }}, } @@ -566,7 +566,7 @@ func TestGetMaxSessiontWithBlocker(t *testing.T) { MaxCostSoFar: 0, } result, err := cd.GetMaxSessionDuration() - expected := 985 * time.Second + expected := 30 * time.Minute if result != expected || err != nil { t.Errorf("Expected %v was %v (%v)", expected, result, err) } @@ -630,7 +630,7 @@ func TestGetCostRoundingIssue(t *testing.T) { MaxCostSoFar: 0, } cc, err := cd.GetCost() - expected := 0.39 + expected := 0.17 if cc.Cost != expected || err != nil { t.Log(utils.ToIJSON(cc)) t.Errorf("Expected %v was %+v", expected, cc) @@ -745,32 +745,69 @@ func TestMaxDebitUnknowDest(t *testing.T) { } } -func TestGetCostMaxDebitRoundingIssue(t *testing.T) { +func TestMaxDebitRoundingIssue(t *testing.T) { ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false) for _, at := range ap.ActionTimings { at.accountIDs = ap.AccountIDs at.Execute() } cd := &CallDescriptor{ - Direction: "*out", - Category: "call", - Tenant: "cgrates.org", - Subject: "dy", - Account: "dy", - Destination: "0723123113", - TimeStart: time.Date(2015, 10, 26, 13, 29, 27, 0, time.UTC), - TimeEnd: time.Date(2015, 10, 26, 13, 29, 51, 0, time.UTC), - MaxCostSoFar: 0, + Direction: "*out", + Category: "call", + Tenant: "cgrates.org", + Subject: "dy", + Account: "dy", + Destination: "0723123113", + TimeStart: time.Date(2015, 10, 26, 13, 29, 27, 0, time.UTC), + TimeEnd: time.Date(2015, 10, 26, 13, 29, 51, 0, time.UTC), + MaxCostSoFar: 0, + PerformRounding: true, } acc, err := accountingStorage.GetAccount("cgrates.org:dy") if err != nil || acc.BalanceMap[utils.MONETARY][0].Value != 1 { t.Errorf("Error getting account: %+v (%v)", utils.ToIJSON(acc), err) } + cc, err := cd.MaxDebit() - expected := 0.39 + expected := 0.17 if cc.Cost != expected || err != nil { t.Log(utils.ToIJSON(cc)) - t.Errorf("Expected %v was %+v", expected, cc) + t.Errorf("Expected %v was %+v (%v)", expected, cc, err) + } + acc, err = accountingStorage.GetAccount("cgrates.org:dy") + if err != nil || acc.BalanceMap[utils.MONETARY][0].Value != 1-expected { + t.Errorf("Error getting account: %+v (%v)", utils.ToIJSON(acc), err) + } +} + +func TestDebitRoundingRefund(t *testing.T) { + ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false) + for _, at := range ap.ActionTimings { + at.accountIDs = ap.AccountIDs + at.Execute() + } + cd := &CallDescriptor{ + Direction: "*out", + Category: "call", + Tenant: "cgrates.org", + Subject: "dy", + Account: "dy", + Destination: "0723123113", + TimeStart: time.Date(2016, 3, 4, 13, 50, 00, 0, time.UTC), + TimeEnd: time.Date(2016, 3, 4, 13, 53, 00, 0, time.UTC), + MaxCostSoFar: 0, + PerformRounding: true, + } + acc, err := accountingStorage.GetAccount("cgrates.org:dy") + if err != nil || acc.BalanceMap[utils.MONETARY][0].Value != 1 { + t.Errorf("Error getting account: %+v (%v)", utils.ToIJSON(acc), err) + } + + cc, err := cd.Debit() + expected := 0.3 + if cc.Cost != expected || err != nil { + t.Log(utils.ToIJSON(cc)) + t.Errorf("Expected %v was %+v (%v)", expected, cc, err) } acc, err = accountingStorage.GetAccount("cgrates.org:dy") if err != nil || acc.BalanceMap[utils.MONETARY][0].Value != 1-expected { @@ -1059,15 +1096,16 @@ func TestDebitAndMaxDebit(t *testing.T) { if err1 != nil || err2 != nil { t.Error("Error debiting and/or maxdebiting: ", err1, err2) } + if cc1.Timespans[0].Increments[0].BalanceInfo.Unit.Value != 90 || + cc2.Timespans[0].Increments[0].BalanceInfo.Unit.Value != 80 { + t.Error("Error setting the Unit.Value: ", cc1.Timespans[0].Increments[0].BalanceInfo.Unit.Value, cc2.Timespans[0].Increments[0].BalanceInfo.Unit.Value) + } + // make Unit.Values have the same value + cc1.Timespans[0].Increments[0].BalanceInfo.Unit.Value = 0 + cc2.Timespans[0].Increments[0].BalanceInfo.Unit.Value = 0 if !reflect.DeepEqual(cc1, cc2) { - t.Logf("CC1: %+v", cc1) - for _, ts := range cc1.Timespans { - t.Logf("TS: %+v", ts) - } - t.Logf("CC2: %+v", cc2) - for _, ts := range cc2.Timespans { - t.Logf("TS: %+v", ts) - } + t.Log("CC1: ", utils.ToIJSON(cc1)) + t.Log("CC2: ", utils.ToIJSON(cc2)) t.Error("Debit and MaxDebit differ") } } @@ -1271,6 +1309,31 @@ func TestMaxDebitZeroDefinedRate(t *testing.T) { } } +func TestMaxDebitForceDuration(t *testing.T) { + ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false) + for _, at := range ap.ActionTimings { + at.accountIDs = ap.AccountIDs + at.Execute() + } + cd1 := &CallDescriptor{ + Direction: "*out", + Category: "call", + Tenant: "cgrates.org", + Subject: "12345", + Account: "12345", + Destination: "447956", + TimeStart: time.Date(2014, 3, 4, 6, 0, 0, 0, time.UTC), + TimeEnd: time.Date(2014, 3, 4, 6, 1, 40, 0, time.UTC), + LoopIndex: 0, + DurationIndex: 0, + ForceDuration: true, + } + _, err := cd1.MaxDebit() + if err != utils.ErrInsufficientCredit { + t.Fatal("Error forcing duration: ", err) + } +} + func TestMaxDebitZeroDefinedRateOnlyMinutes(t *testing.T) { ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false) for _, at := range ap.ActionTimings { @@ -1377,6 +1440,35 @@ func TestCDDataGetCost(t *testing.T) { } } +func TestCDRefundIncrements(t *testing.T) { + ub := &Account{ + ID: "test:ref", + BalanceMap: map[string]Balances{ + utils.MONETARY: Balances{ + &Balance{Uuid: "moneya", Value: 100}, + }, + utils.VOICE: Balances{ + &Balance{Uuid: "minutea", Value: 10, Weight: 20, DestinationIDs: utils.StringMap{"NAT": true}}, + &Balance{Uuid: "minuteb", Value: 10, DestinationIDs: utils.StringMap{"RET": true}}, + }, + }, + } + accountingStorage.SetAccount(ub) + increments := Increments{ + &Increment{Cost: 2, BalanceInfo: &DebitInfo{Monetary: &MonetaryInfo{UUID: "moneya"}, AccountID: ub.ID}}, + &Increment{Cost: 2, Duration: 3 * time.Second, BalanceInfo: &DebitInfo{Unit: &UnitInfo{UUID: "minutea"}, Monetary: &MonetaryInfo{UUID: "moneya"}, AccountID: ub.ID}}, + &Increment{Duration: 4 * time.Second, BalanceInfo: &DebitInfo{Unit: &UnitInfo{UUID: "minuteb"}, AccountID: ub.ID}}, + } + cd := &CallDescriptor{TOR: utils.VOICE, Increments: increments} + cd.RefundIncrements() + ub, _ = accountingStorage.GetAccount(ub.ID) + if ub.BalanceMap[utils.MONETARY][0].GetValue() != 104 || + ub.BalanceMap[utils.VOICE][0].GetValue() != 13 || + ub.BalanceMap[utils.VOICE][1].GetValue() != 14 { + t.Error("Error refunding money: ", utils.ToIJSON(ub.BalanceMap)) + } +} + /*************** BENCHMARKS ********************/ func BenchmarkStorageGetting(b *testing.B) { b.StopTimer() diff --git a/engine/cdr.go b/engine/cdr.go index 24164959f..bbdb56f84 100644 --- a/engine/cdr.go +++ b/engine/cdr.go @@ -767,7 +767,7 @@ func (self *UsageRecord) AsStoredCdr(timezone string) (*CDR, error) { func (self *UsageRecord) AsCallDescriptor(timezone string) (*CallDescriptor, error) { var err error cd := &CallDescriptor{ - CgrId: self.GetId(), + CgrID: self.GetId(), TOR: self.ToR, Direction: self.Direction, Tenant: self.Tenant, diff --git a/engine/cdrs.go b/engine/cdrs.go index d9dd39870..7fda4abc1 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -70,7 +70,7 @@ func fsCdrHandler(w http.ResponseWriter, r *http.Request) { } } -func NewCdrServer(cgrCfg *config.CGRConfig, cdrDb CdrStorage, rater rpcclient.RpcClientConnection, pubsub rpcclient.RpcClientConnection, users rpcclient.RpcClientConnection, aliases rpcclient.RpcClientConnection, stats rpcclient.RpcClientConnection) (*CdrServer, error) { +func NewCdrServer(cgrCfg *config.CGRConfig, cdrDb CdrStorage, rater, pubsub, users, aliases, stats rpcclient.RpcClientConnection) (*CdrServer, error) { return &CdrServer{cgrCfg: cgrCfg, cdrDb: cdrDb, client: rater, pubsub: pubsub, users: users, aliases: aliases, stats: stats, guard: &GuardianLock{locksMap: make(map[string]chan bool)}}, nil } @@ -136,23 +136,9 @@ func (self *CdrServer) ProcessExternalCdr(eCDR *ExternalCDR) error { return self.processCdr(cdr) } -func (self *CdrServer) LogCallCost(ccl *CallCostLog, reply *string) error { - cacheKey := "LogCallCost" + ccl.CgrId - if item, err := self.getCache().Get(cacheKey); err == nil && item != nil { - *reply = item.Value.(string) - return item.Err - } - if err := self.LocalLogCallCost(ccl); err != nil { - self.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) - return utils.NewErrServerError(err) - } - *reply = utils.OK - self.getCache().Cache(cacheKey, &cache2go.CacheItem{Value: utils.OK}) - return nil -} - // RPC method, used to log callcosts to db -func (self *CdrServer) LocalLogCallCost(ccl *CallCostLog) error { +func (self *CdrServer) LogCallCost(ccl *CallCostLog) error { + ccl.CallCost.UpdateCost() // make sure the total cost reflect the increments ccl.CallCost.UpdateRatedUsage() // make sure rated usage is updated if ccl.CheckDuplicate { _, err := self.guard.Guard(func() (interface{}, error) { @@ -177,6 +163,10 @@ func (self *CdrServer) RateCDRs(cdrFltr *utils.CDRsFilter, sendToStats bool) err return err } for _, cdr := range cdrs { + // replace user profile fields + if err := LoadUserProfile(cdr, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases for cases they were loaded after CDR received if err := LoadAlias(&AttrMatchingAlias{ Destination: cdr.Destination, @@ -189,10 +179,6 @@ func (self *CdrServer) RateCDRs(cdrFltr *utils.CDRsFilter, sendToStats bool) err }, cdr, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { return err } - // replace user profile fields - if err := LoadUserProfile(cdr, utils.EXTRA_FIELDS); err != nil { - return err - } if err := self.rateStoreStatsReplicate(cdr, sendToStats); err != nil { utils.Logger.Err(fmt.Sprintf(" Processing CDR %+v, got error: %s", cdr, err.Error())) } @@ -222,6 +208,7 @@ func (self *CdrServer) processCdr(cdr *CDR) (err error) { } if self.cgrCfg.CDRSStoreCdrs { // Store RawCDRs, this we do sync so we can reply with the status if cdr.CostDetails != nil { + cdr.CostDetails.UpdateCost() cdr.CostDetails.UpdateRatedUsage() } if err := self.cdrDb.SetCDR(cdr, false); err != nil { // Only original CDR stored in primary table, no derived @@ -251,6 +238,9 @@ func (self *CdrServer) rateStoreStatsReplicate(cdr *CDR, sendToStats bool) error if cdr.RunID == utils.MetaRaw { // Overwrite *raw with *default for rating cdr.RunID = utils.META_DEFAULT } + if err := LoadUserProfile(cdr, utils.EXTRA_FIELDS); err != nil { + return err + } if err := LoadAlias(&AttrMatchingAlias{ Destination: cdr.Destination, Direction: cdr.Direction, @@ -262,9 +252,7 @@ func (self *CdrServer) rateStoreStatsReplicate(cdr *CDR, sendToStats bool) error }, cdr, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { return err } - if err := LoadUserProfile(cdr, utils.EXTRA_FIELDS); err != nil { - return err - } + // Rate CDR if self.client != nil && !cdr.Rated { if err := self.rateCDR(cdr); err != nil { @@ -281,6 +269,7 @@ func (self *CdrServer) rateStoreStatsReplicate(cdr *CDR, sendToStats bool) error if self.cgrCfg.CDRSStoreCdrs { // Store CDRs // Store RatedCDR if cdr.CostDetails != nil { + cdr.CostDetails.UpdateCost() cdr.CostDetails.UpdateRatedUsage() } if err := self.cdrDb.SetCDR(cdr, true); err != nil { @@ -305,6 +294,21 @@ func (self *CdrServer) deriveCdrs(cdr *CDR) ([]*CDR, error) { if cdr.RunID != utils.MetaRaw { // Only derive *raw CDRs return cdrRuns, nil } + if err := LoadUserProfile(cdr, utils.EXTRA_FIELDS); err != nil { + return nil, err + } + if err := LoadAlias(&AttrMatchingAlias{ + Destination: cdr.Destination, + Direction: cdr.Direction, + Tenant: cdr.Tenant, + Category: cdr.Category, + Account: cdr.Account, + Subject: cdr.Subject, + Context: utils.ALIAS_CONTEXT_RATING, + }, cdr, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { + return nil, err + } + attrsDC := &utils.AttrDerivedChargers{Tenant: cdr.Tenant, Category: cdr.Category, Direction: cdr.Direction, Account: cdr.Account, Subject: cdr.Subject, Destination: cdr.Destination} var dcs utils.DerivedChargers @@ -395,16 +399,17 @@ func (self *CdrServer) getCostFromRater(cdr *CDR) (*CallCost, error) { timeStart = cdr.SetupTime } cd := &CallDescriptor{ - TOR: cdr.ToR, - Direction: cdr.Direction, - Tenant: cdr.Tenant, - Category: cdr.Category, - Subject: cdr.Subject, - Account: cdr.Account, - Destination: cdr.Destination, - TimeStart: timeStart, - TimeEnd: timeStart.Add(cdr.Usage), - DurationIndex: cdr.Usage, + TOR: cdr.ToR, + Direction: cdr.Direction, + Tenant: cdr.Tenant, + Category: cdr.Category, + Subject: cdr.Subject, + Account: cdr.Account, + Destination: cdr.Destination, + TimeStart: timeStart, + TimeEnd: timeStart.Add(cdr.Usage), + DurationIndex: cdr.Usage, + PerformRounding: true, } if utils.IsSliceMember([]string{utils.META_PSEUDOPREPAID, utils.META_POSTPAID, utils.META_PREPAID, utils.PSEUDOPREPAID, utils.POSTPAID, utils.PREPAID}, cdr.RequestType) { // Prepaid - Cost can be recalculated in case of missing records from SM err = self.client.Call("Responder.Debit", cd, cc) diff --git a/engine/datacost.go b/engine/datacost.go index 36698295a..722efdaa2 100644 --- a/engine/datacost.go +++ b/engine/datacost.go @@ -35,11 +35,9 @@ type DataSpan struct { } type DataIncrement struct { - Amount float64 - Cost float64 - BalanceInfo *BalanceInfo // need more than one for units with cost - BalanceRateInterval *RateInterval - UnitInfo *UnitInfo - CompressFactor int - paid bool + Amount float64 + Cost float64 + BalanceInfo *DebitInfo // need more than one for units with cost + CompressFactor int + paid bool } diff --git a/engine/libengine.go b/engine/libengine.go new file mode 100644 index 000000000..e694f599c --- /dev/null +++ b/engine/libengine.go @@ -0,0 +1,49 @@ +/* +Real-time Charging System for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +This program is free software: you can Storagetribute 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 WITH*out 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 engine + +import ( + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/utils" + "github.com/cgrates/rpcclient" +) + +func NewRPCPool(dispatchStrategy string, connAttempts, reconnects int, codec string, + rpcConnCfgs []*config.HaPoolConfig, internalConnChan chan rpcclient.RpcClientConnection) (*rpcclient.RpcClientPool, error) { + var rpcClient *rpcclient.RpcClient + var err error + rpcPool := rpcclient.NewRpcClientPool(dispatchStrategy) + for _, rpcConnCfg := range rpcConnCfgs { + if rpcConnCfg.Server == utils.INTERNAL { + internalConn := <-internalConnChan + internalConnChan <- internalConn + rpcClient, err = rpcclient.NewRpcClient("", "", 0, 0, rpcclient.INTERNAL_RPC, internalConn) + } else { + rpcClient, err = rpcclient.NewRpcClient("tcp", rpcConnCfg.Server, connAttempts, reconnects, codec, nil) + } + if err != nil { + break + } + rpcPool.AddClient(rpcClient) + } + if err != nil { + return nil, err + } + return rpcPool, nil +} diff --git a/engine/loader_csv_test.go b/engine/loader_csv_test.go index c13bac58d..7c17f02fe 100644 --- a/engine/loader_csv_test.go +++ b/engine/loader_csv_test.go @@ -174,6 +174,9 @@ DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,, NEG,*allow_negative,,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 BLOCK,*topup,,,bblocker,*monetary,*out,,NAT,,,*unlimited,,10,20,true,false,20 BLOCK,*topup,,,bfree,*monetary,*out,,,,,*unlimited,,20,10,false,false,10 +FILTER,*topup,,"{""*and"":[{""Value"":{""*lt"":0}},{""Id"":{""*eq"":""*default""}}]}",bfree,*monetary,*out,,,,,*unlimited,,20,10,false,false,10 +EXP,*topup,,,,*voice,*out,,,,,*monthly,*any,300,10,false,false,10 +NOEXP,*topup,,,,*voice,*out,,,,,*unlimited,*any,50,10,false,false,10 ` actionPlans = ` MORE_MINUTES,MINI,ONE_TIME_RUN,10 @@ -185,19 +188,20 @@ TOPUP_SHARED10_AT,SE10,*asap,10 TOPUP_EMPTY_AT,EE0,*asap,10 POST_AT,NEG,*asap,10 BLOCK_AT,BLOCK,*asap,10 +EXP_AT,EXP,*asap,10 ` actionTriggers = ` -STANDARD_TRIGGER,st0,*min_event_counter,10,false,0,,*voice,*out,,GERMANY_O2,,,,,,,,,SOME_1,10 -STANDARD_TRIGGER,st1,*max_balance,200,false,0,,*voice,*out,,GERMANY,,,,,,,,,SOME_2,10 -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10 -CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +STANDARD_TRIGGER,st0,*min_event_counter,10,false,0,,,,*voice,*out,,GERMANY_O2,,,,,,,,,SOME_1,10 +STANDARD_TRIGGER,st1,*max_balance,200,false,0,,,,*voice,*out,,GERMANY,,,,,,,,,SOME_2,10 +STANDARD_TRIGGERS,,*min_balance,2,false,0,,,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10 +CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 ` accountActions = ` vdf,minitsboy,MORE_MINUTES,STANDARD_TRIGGER,, @@ -212,6 +216,8 @@ vdf,emptyY,TOPUP_EMPTY_AT,,, vdf,post,POST_AT,,, cgrates.org,alodis,TOPUP_EMPTY_AT,,true,true cgrates.org,block,BLOCK_AT,,false,false +cgrates.org,expo,EXP_AT,,false,false +cgrates.org,expnoexp,,,false,false ` derivedCharges = ` @@ -814,7 +820,7 @@ func TestLoadRatingProfiles(t *testing.T) { } func TestLoadActions(t *testing.T) { - if len(csvr.actions) != 10 { + if len(csvr.actions) != 13 { t.Error("Failed to load actions: ", len(csvr.actions)) } as1 := csvr.actions["MINI"] @@ -822,38 +828,42 @@ func TestLoadActions(t *testing.T) { &Action{ Id: "MINI0", ActionType: TOPUP_RESET, - BalanceType: utils.MONETARY, ExpirationString: UNLIMITED, ExtraParameters: "", Weight: 10, - Balance: &Balance{ + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), Uuid: as1[0].Balance.Uuid, - Directions: utils.NewStringMap(utils.OUT), - Value: 10, - Weight: 10, - DestinationIds: utils.StringMap{}, - TimingIDs: utils.StringMap{}, - SharedGroups: utils.StringMap{}, - Categories: utils.StringMap{}, + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Value: utils.Float64Pointer(10), + Weight: utils.Float64Pointer(10), + DestinationIDs: nil, + TimingIDs: nil, + SharedGroups: nil, + Categories: nil, + Disabled: utils.BoolPointer(false), + Blocker: utils.BoolPointer(false), }, }, &Action{ Id: "MINI1", ActionType: TOPUP, - BalanceType: utils.VOICE, ExpirationString: UNLIMITED, ExtraParameters: "", Weight: 10, - Balance: &Balance{ + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), Uuid: as1[1].Balance.Uuid, - Directions: utils.NewStringMap(utils.OUT), - Value: 100, - Weight: 10, - RatingSubject: "test", - DestinationIds: utils.NewStringMap("NAT"), - TimingIDs: utils.StringMap{}, - SharedGroups: utils.StringMap{}, - Categories: utils.StringMap{}, + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Value: utils.Float64Pointer(100), + Weight: utils.Float64Pointer(10), + RatingSubject: utils.StringPointer("test"), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), + TimingIDs: nil, + SharedGroups: nil, + Categories: nil, + Disabled: utils.BoolPointer(false), + Blocker: utils.BoolPointer(false), }, }, } @@ -865,18 +875,20 @@ func TestLoadActions(t *testing.T) { &Action{ Id: "SHARED0", ActionType: TOPUP, - BalanceType: utils.MONETARY, ExpirationString: UNLIMITED, Weight: 10, - Balance: &Balance{ - Directions: utils.NewStringMap(utils.OUT), - DestinationIds: utils.StringMap{}, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + DestinationIDs: nil, Uuid: as2[0].Balance.Uuid, - Value: 100, - Weight: 10, - SharedGroups: utils.NewStringMap("SG1"), - TimingIDs: utils.StringMap{}, - Categories: utils.StringMap{}, + Value: utils.Float64Pointer(100), + Weight: utils.Float64Pointer(10), + SharedGroups: utils.StringMapPointer(utils.NewStringMap("SG1")), + TimingIDs: nil, + Categories: nil, + Disabled: utils.BoolPointer(false), + Blocker: utils.BoolPointer(false), }, }, } @@ -890,14 +902,15 @@ func TestLoadActions(t *testing.T) { ActionType: CDRLOG, ExtraParameters: `{"Category":"^ddi","MediationRunId":"^did_run"}`, Weight: 10, - Balance: &Balance{ + Balance: &BalanceFilter{ Uuid: as3[0].Balance.Uuid, - Directions: utils.StringMap{}, - DestinationIds: utils.StringMap{}, - TimingIDs: utils.StringMap{}, - Categories: utils.StringMap{}, - SharedGroups: utils.StringMap{}, - Blocker: false, + Directions: nil, + DestinationIDs: nil, + TimingIDs: nil, + Categories: nil, + SharedGroups: nil, + Blocker: utils.BoolPointer(false), + Disabled: utils.BoolPointer(false), }, }, } @@ -993,13 +1006,13 @@ func TestLoadLCRs(t *testing.T) { } func TestLoadActionTimings(t *testing.T) { - if len(csvr.actionPlans) != 7 { + if len(csvr.actionPlans) != 8 { t.Error("Failed to load action timings: ", len(csvr.actionPlans)) } atm := csvr.actionPlans["MORE_MINUTES"] expected := &ActionPlan{ Id: "MORE_MINUTES", - AccountIDs: map[string]struct{}{"vdf:minitsboy": struct{}{}}, + AccountIDs: utils.StringMap{"vdf:minitsboy": true}, ActionTimings: []*ActionTiming{ &ActionTiming{ Uuid: atm.ActionTimings[0].Uuid, @@ -1042,38 +1055,45 @@ func TestLoadActionTriggers(t *testing.T) { } atr := csvr.actionsTriggers["STANDARD_TRIGGER"][0] expected := &ActionTrigger{ - ID: "STANDARD_TRIGGER", - UniqueID: "st0", - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT), - ThresholdType: utils.TRIGGER_MIN_EVENT_COUNTER, - ThresholdValue: 10, - BalanceDestinationIds: utils.NewStringMap("GERMANY_O2"), - BalanceCategories: utils.StringMap{}, - BalanceTimingTags: utils.StringMap{}, - BalanceSharedGroups: utils.StringMap{}, - Weight: 10, - ActionsId: "SOME_1", - Executed: false, + ID: "STANDARD_TRIGGER", + UniqueID: "st0", + ThresholdType: utils.TRIGGER_MIN_EVENT_COUNTER, + ThresholdValue: 10, + Balance: &BalanceFilter{ + ID: nil, + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("GERMANY_O2")), + Categories: nil, + TimingIDs: nil, + SharedGroups: nil, + Disabled: nil, + Blocker: nil, + }, + Weight: 10, + ActionsID: "SOME_1", + Executed: false, } if !reflect.DeepEqual(atr, expected) { - t.Errorf("Error loading action trigger: %+v", atr) + t.Errorf("Error loading action trigger: %+v", utils.ToIJSON(atr.Balance)) } atr = csvr.actionsTriggers["STANDARD_TRIGGER"][1] expected = &ActionTrigger{ - ID: "STANDARD_TRIGGER", - UniqueID: "st1", - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT), - ThresholdType: utils.TRIGGER_MAX_BALANCE, - ThresholdValue: 200, - BalanceDestinationIds: utils.NewStringMap("GERMANY"), - BalanceCategories: utils.StringMap{}, - BalanceTimingTags: utils.StringMap{}, - BalanceSharedGroups: utils.StringMap{}, - Weight: 10, - ActionsId: "SOME_2", - Executed: false, + ID: "STANDARD_TRIGGER", + UniqueID: "st1", + ThresholdType: utils.TRIGGER_MAX_BALANCE, + ThresholdValue: 200, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("GERMANY")), + Categories: nil, + TimingIDs: nil, + SharedGroups: nil, + }, + Weight: 10, + ActionsID: "SOME_2", + Executed: false, } if !reflect.DeepEqual(atr, expected) { t.Errorf("Error loading action trigger: %+v", atr) @@ -1081,25 +1101,29 @@ func TestLoadActionTriggers(t *testing.T) { } func TestLoadAccountActions(t *testing.T) { - if len(csvr.accountActions) != 12 { + if len(csvr.accountActions) != 14 { t.Error("Failed to load account actions: ", len(csvr.accountActions)) } aa := csvr.accountActions["vdf:minitsboy"] expected := &Account{ - Id: "vdf:minitsboy", + ID: "vdf:minitsboy", UnitCounters: UnitCounters{ - &UnitCounter{ - BalanceType: "*voice", - CounterType: "*event", - Balances: BalanceChain{ - &Balance{ - Id: "2c2ce3c9-d62b-49dc-82a5-2a17bdc6eb4e", - Value: 0, - Directions: utils.NewStringMap("*out"), - DestinationIds: utils.NewStringMap("GERMANY_O2"), - SharedGroups: utils.StringMap{}, - Categories: utils.StringMap{}, - TimingIDs: utils.StringMap{}, + utils.VOICE: []*UnitCounter{ + &UnitCounter{ + CounterType: "*event", + Counters: CounterFilters{ + &CounterFilter{ + Value: 0, + Filter: &BalanceFilter{ + ID: utils.StringPointer("st0"), + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap("*out")), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("GERMANY_O2")), + SharedGroups: nil, + Categories: nil, + TimingIDs: nil, + }, + }, }, }, }, @@ -1110,19 +1134,19 @@ func TestLoadAccountActions(t *testing.T) { for i, atr := range aa.ActionTriggers { csvr.actionsTriggers["STANDARD_TRIGGER"][i].ID = atr.ID } - for i, b := range aa.UnitCounters[0].Balances { - expected.UnitCounters[0].Balances[i].Id = b.Id + for i, b := range aa.UnitCounters[utils.VOICE][0].Counters { + expected.UnitCounters[utils.VOICE][0].Counters[i].Filter.ID = b.Filter.ID } - if !reflect.DeepEqual(aa.UnitCounters[0].Balances[0], expected.UnitCounters[0].Balances[0]) { - t.Errorf("Error loading account action: %+v \n %+v", aa.UnitCounters[0].Balances[0], expected.UnitCounters[0].Balances[0]) + if !reflect.DeepEqual(aa.UnitCounters[utils.VOICE][0].Counters[0], expected.UnitCounters[utils.VOICE][0].Counters[0]) { + t.Errorf("Error loading account action: %+v", utils.ToIJSON(aa.UnitCounters[utils.VOICE][0].Counters[0].Filter)) } // test that it does not overwrite balances - existing, err := accountingStorage.GetAccount(aa.Id) + existing, err := accountingStorage.GetAccount(aa.ID) if err != nil || len(existing.BalanceMap) != 2 { t.Errorf("The account was not set before load: %+v", existing) } accountingStorage.SetAccount(aa) - existing, err = accountingStorage.GetAccount(aa.Id) + existing, err = accountingStorage.GetAccount(aa.ID) if err != nil || len(existing.BalanceMap) != 2 { t.Errorf("The set account altered the balances: %+v", existing) } @@ -1133,7 +1157,7 @@ func TestLoadDerivedChargers(t *testing.T) { t.Error("Failed to load derivedChargers: ", csvr.derivedChargers) } expCharger1 := &utils.DerivedChargers{ - DestinationIDs: utils.StringMap{}, + DestinationIDs: nil, Chargers: []*utils.DerivedCharger{ &utils.DerivedCharger{RunID: "extra1", RunFilters: "^filteredHeader1/filterValue1/", RequestTypeField: "^prepaid", DirectionField: utils.META_DEFAULT, TenantField: utils.META_DEFAULT, CategoryField: utils.META_DEFAULT, AccountField: "rif", SubjectField: "rif", DestinationField: utils.META_DEFAULT, diff --git a/engine/loader_local_test.go b/engine/loader_local_test.go index 0320f6840..e03f8e485 100644 --- a/engine/loader_local_test.go +++ b/engine/loader_local_test.go @@ -47,7 +47,7 @@ var accountDbCsv, accountDbStor, accountDbApier AccountingStorage // Each rating var storDb LoadStorage var lCfg *config.CGRConfig -var tpCsvScenario = flag.String("tp_scenario", "prepaid1centpsec", "Use this scenario folder to import tp csv data from") +var tpCsvScenario = flag.String("tp_scenario", "testtp", "Use this scenario folder to import tp csv data from") // Create connection to ratingDb // Will use 3 different datadbs in order to be able to see differences in data loaded diff --git a/engine/model_converters.go b/engine/model_converters.go index d3035772e..f15e99abd 100644 --- a/engine/model_converters.go +++ b/engine/model_converters.go @@ -224,6 +224,8 @@ func APItoModelActionTrigger(ats *utils.TPActionTriggers) (result []TpActionTrig ThresholdValue: at.ThresholdValue, Recurrent: at.Recurrent, MinSleep: at.MinSleep, + ExpiryTime: at.ExpirationDate, + ActivationTime: at.ActivationDate, BalanceTag: at.BalanceId, BalanceType: at.BalanceType, BalanceDirections: at.BalanceDirections, diff --git a/engine/model_helpers.go b/engine/model_helpers.go index 0d2d0a648..3d29e2d47 100644 --- a/engine/model_helpers.go +++ b/engine/model_helpers.go @@ -430,6 +430,8 @@ func (tps TpActionTriggers) GetActionTriggers() (map[string][]*utils.TPActionTri ThresholdValue: tpAt.ThresholdValue, Recurrent: tpAt.Recurrent, MinSleep: tpAt.MinSleep, + ExpirationDate: tpAt.ExpiryTime, + ActivationDate: tpAt.ActivationTime, BalanceId: tpAt.BalanceTag, BalanceType: tpAt.BalanceType, BalanceDirections: tpAt.BalanceDirections, diff --git a/engine/model_helpers_test.go b/engine/model_helpers_test.go index 7d0413c9a..94147a6a9 100644 --- a/engine/model_helpers_test.go +++ b/engine/model_helpers_test.go @@ -240,33 +240,33 @@ func TestTPActionsAsExportSlice(t *testing.T) { Identifier: "*topup_reset", BalanceType: "*monetary", Directions: utils.OUT, - Units: 5.0, + Units: "5.0", ExpiryTime: "*never", DestinationIds: "*any", RatingSubject: "special1", Categories: "call", SharedGroups: "GROUP1", - BalanceWeight: 10.0, + BalanceWeight: "10.0", ExtraParameters: "", Weight: 10.0}, &utils.TPAction{ Identifier: "*http_post", BalanceType: "", Directions: "", - Units: 0.0, + Units: "0.0", ExpiryTime: "", DestinationIds: "", RatingSubject: "", Categories: "", SharedGroups: "", - BalanceWeight: 0.0, + BalanceWeight: "0.0", ExtraParameters: "http://localhost/¶m1=value1", Weight: 20.0}, }, } expectedSlc := [][]string{ - []string{"TEST_ACTIONS", "*topup_reset", "", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "false", "10"}, - []string{"TEST_ACTIONS", "*http_post", "http://localhost/¶m1=value1", "", "", "", "", "", "", "", "", "", "", "0", "0", "false", "false", "20"}, + []string{"TEST_ACTIONS", "*topup_reset", "", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5.0", "10.0", "", "", "10"}, + []string{"TEST_ACTIONS", "*http_post", "http://localhost/¶m1=value1", "", "", "", "", "", "", "", "", "", "", "0.0", "0.0", "", "", "20"}, } ms := APItoModelAction(tpActs) @@ -561,14 +561,14 @@ func TestTPActionPlanAsExportSlice(t *testing.T) { BalanceType: "*monetary", BalanceDirections: "*out", BalanceDestinationIds: "", - BalanceWeight: 0.0, + BalanceWeight: "0.0", BalanceExpirationDate: "*never", BalanceTimingTags: "T1", BalanceRatingSubject: "special1", BalanceCategories: "call", BalanceSharedGroups: "SHARED_1", - BalanceBlocker: false, - BalanceDisabled: false, + BalanceBlocker: "false", + BalanceDisabled: "false", MinQueuedItems: 0, ActionsId: "LOG_WARNING", Weight: 10}, @@ -583,22 +583,22 @@ func TestTPActionPlanAsExportSlice(t *testing.T) { BalanceType: "*monetary", BalanceDirections: "*out", BalanceDestinationIds: "FS_USERS", - BalanceWeight: 0.0, + BalanceWeight: "0.0", BalanceExpirationDate: "*never", BalanceTimingTags: "T1", BalanceRatingSubject: "special1", BalanceCategories: "call", BalanceSharedGroups: "SHARED_1", - BalanceBlocker: false, - BalanceDisabled: false, + BalanceBlocker: "false", + BalanceDisabled: "false", MinQueuedItems: 0, ActionsId: "LOG_WARNING", Weight: 10}, }, } expectedSlc := [][]string{ - []string{"STANDARD_TRIGGERS", "1", "*min_balance", "2", "false", "0", "b1", "*monetary", "*out", "call", "", "special1", "SHARED_1", "*never", "T1", "0", "false", "false", "0", "LOG_WARNING", "10"}, - []string{"STANDARD_TRIGGERS", "2", "*max_event_counter", "5", "false", "0", "b2", "*monetary", "*out", "call", "FS_USERS", "special1", "SHARED_1", "*never", "T1", "0", "false", "false", "0", "LOG_WARNING", "10"}, + []string{"STANDARD_TRIGGERS", "1", "*min_balance", "2", "false", "0", "", "", "b1", "*monetary", "*out", "call", "", "special1", "SHARED_1", "*never", "T1", "0.0", "false", "false", "0", "LOG_WARNING", "10"}, + []string{"STANDARD_TRIGGERS", "2", "*max_event_counter", "5", "false", "0", "", "", "b2", "*monetary", "*out", "call", "FS_USERS", "special1", "SHARED_1", "*never", "T1", "0.0", "false", "false", "0", "LOG_WARNING", "10"}, } ms := APItoModelActionTrigger(at) var slc [][]string diff --git a/engine/models.go b/engine/models.go index 2fb2ffc56..85ce94362 100644 --- a/engine/models.go +++ b/engine/models.go @@ -166,10 +166,10 @@ type TpAction struct { SharedGroups string `index:"10" re:"[0-9A-Za-z_;]*"` ExpiryTime string `index:"11" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` TimingTags string `index:"12" re:"[0-9A-Za-z_;]*|\*any"` - Units float64 `index:"13" re:"\d+\s*"` - BalanceWeight float64 `index:"14" re:"\d+\.?\d*\s*"` - BalanceBlocker bool `index:"15" re:""` - BalanceDisabled bool `index:"16" re:""` + Units string `index:"13" re:"\d+\s*"` + BalanceWeight string `index:"14" re:"\d+\.?\d*\s*"` + BalanceBlocker string `index:"15" re:""` + BalanceDisabled string `index:"16" re:""` Weight float64 `index:"17" re:"\d+\.?\d*\s*"` CreatedAt time.Time } @@ -193,21 +193,23 @@ type TpActionTrigger struct { ThresholdValue float64 `index:"3" re:"\d+\.?\d*"` Recurrent bool `index:"4" re:"true|false"` MinSleep string `index:"5" re:"\d+[smh]?"` - BalanceTag string `index:"6" re:"\w+\s*"` - BalanceType string `index:"7" re:"\*\w+"` - BalanceDirections string `index:"8" re:"\*out"` - BalanceCategories string `index:"9" re:""` - BalanceDestinationTags string `index:"10" re:"\w+|\*any"` - BalanceRatingSubject string `index:"11" re:"\w+|\*any"` - BalanceSharedGroups string `index:"12" re:"\w+|\*any"` - BalanceExpiryTime string `index:"13" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` - BalanceTimingTags string `index:"14" re:"[0-9A-Za-z_;]*|\*any"` - BalanceWeight float64 `index:"15" re:"\d+\.?\d*"` - BalanceBlocker bool `index:"16" re:""` - BalanceDisabled bool `index:"17" re:""` - MinQueuedItems int `index:"18" re:"\d+"` - ActionsTag string `index:"19" re:"\w+"` - Weight float64 `index:"20" re:"\d+\.?\d*"` + ExpiryTime string `index:"6" re:""` + ActivationTime string `index:"7" re:""` + BalanceTag string `index:"8" re:"\w+\s*"` + BalanceType string `index:"9" re:"\*\w+"` + BalanceDirections string `index:"10" re:"\*out"` + BalanceCategories string `index:"11" re:""` + BalanceDestinationTags string `index:"12" re:"\w+|\*any"` + BalanceRatingSubject string `index:"13" re:"\w+|\*any"` + BalanceSharedGroups string `index:"14" re:"\w+|\*any"` + BalanceExpiryTime string `index:"15" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` + BalanceTimingTags string `index:"16" re:"[0-9A-Za-z_;]*|\*any"` + BalanceWeight string `index:"17" re:"\d+\.?\d*"` + BalanceBlocker string `index:"18" re:""` + BalanceDisabled string `index:"19" re:""` + MinQueuedItems int `index:"20" re:"\d+"` + ActionsTag string `index:"21" re:"\w+"` + Weight float64 `index:"22" re:"\d+\.?\d*"` CreatedAt time.Time } @@ -341,6 +343,7 @@ type TpUser struct { AttributeName string `index:"3" re:""` AttributeValue string `index:"4" re:""` Weight float64 `index:"5" re:""` + CreatedAt time.Time } func (tu *TpUser) GetId() string { @@ -395,6 +398,20 @@ func (ta *TpAlias) GetId() string { return utils.ConcatenatedKey(ta.Direction, ta.Tenant, ta.Category, ta.Account, ta.Subject, ta.Context) } +type TpLimiter struct { + Id int64 + Tpid string + LimiterID string `index:"0" re:""` + ResourceID string `index:"1" re:""` + Filter string `index:"2" re:""` + TTL string `index:"3" re:""` + TimingIDs string `index:"4" re:""` + ActivationTime string `index:"5" re:""` + Limit float64 `index:"6" re:""` + ActionTriggers string `index:"7" re:""` + CreatedAt time.Time `index:"8" re:""` +} + type TBLCDRs struct { ID int64 Cgrid string diff --git a/engine/pubsub.go b/engine/pubsub.go index 59678d472..091f30bab 100644 --- a/engine/pubsub.go +++ b/engine/pubsub.go @@ -3,12 +3,11 @@ package engine import ( "errors" "fmt" - "reflect" - "strings" "sync" "time" "github.com/cgrates/cgrates/utils" + "github.com/cgrates/rpcclient" ) type SubscribeInfo struct { @@ -160,29 +159,73 @@ func (ps *PubSub) ShowSubscribers(in string, out *map[string]*SubscriberData) er } func (ps *PubSub) Call(serviceMethod string, args interface{}, reply interface{}) error { - parts := strings.Split(serviceMethod, ".") - if len(parts) != 2 { - return utils.ErrNotImplemented + switch serviceMethod { + case "PubSubV1.Subscribe": + argsConverted, canConvert := args.(SubscribeInfo) + if !canConvert { + return rpcclient.ErrWrongArgsType + } + replyConverted, canConvert := reply.(*string) + if !canConvert { + return rpcclient.ErrWrongReplyType + } + return ps.Subscribe(argsConverted, replyConverted) + case "PubSubV1.Unsubscribe": + argsConverted, canConvert := args.(SubscribeInfo) + if !canConvert { + return rpcclient.ErrWrongArgsType + } + replyConverted, canConvert := reply.(*string) + if !canConvert { + return rpcclient.ErrWrongReplyType + } + return ps.Unsubscribe(argsConverted, replyConverted) + case "PubSubV1.Publish": + argsConverted, canConvert := args.(CgrEvent) + if !canConvert { + return rpcclient.ErrWrongArgsType + } + replyConverted, canConvert := reply.(*string) + if !canConvert { + return rpcclient.ErrWrongReplyType + } + return ps.Publish(argsConverted, replyConverted) + case "PubSubV1.ShowSubscribers": + argsConverted, canConvert := args.(string) + if !canConvert { + return rpcclient.ErrWrongArgsType + } + replyConverted, canConvert := reply.(*map[string]*SubscriberData) + if !canConvert { + return rpcclient.ErrWrongReplyType + } + return ps.ShowSubscribers(argsConverted, replyConverted) } - // get method - method := reflect.ValueOf(ps).MethodByName(parts[1]) - if !method.IsValid() { - return utils.ErrNotImplemented - } - - // construct the params - params := []reflect.Value{reflect.ValueOf(args), reflect.ValueOf(reply)} - - ret := method.Call(params) - if len(ret) != 1 { - return utils.ErrServerError - } - if ret[0].Interface() == nil { - return nil - } - err, ok := ret[0].Interface().(error) - if !ok { - return utils.ErrServerError - } - return err + return rpcclient.ErrUnsupporteServiceMethod +} + +type ProxyPubSub struct { + Client *rpcclient.RpcClient +} + +func NewProxyPubSub(addr string, attempts, reconnects int) (*ProxyPubSub, error) { + client, err := rpcclient.NewRpcClient("tcp", addr, attempts, reconnects, utils.GOB, nil) + if err != nil { + return nil, err + } + return &ProxyPubSub{Client: client}, nil +} + +func (ps *ProxyPubSub) Subscribe(si SubscribeInfo, reply *string) error { + return ps.Client.Call("PubSubV1.Subscribe", si, reply) +} +func (ps *ProxyPubSub) Unsubscribe(si SubscribeInfo, reply *string) error { + return ps.Client.Call("PubSubV1.Unsubscribe", si, reply) +} +func (ps *ProxyPubSub) Publish(evt CgrEvent, reply *string) error { + return ps.Client.Call("PubSubV1.Publish", evt, reply) +} + +func (ps *ProxyPubSub) ShowSubscribers(in string, reply *map[string]*SubscriberData) error { + return ps.Client.Call("PubSubV1.ShowSubscribers", in, reply) } diff --git a/engine/rateinterval.go b/engine/rateinterval.go index 268c150bd..0ddcb241d 100644 --- a/engine/rateinterval.go +++ b/engine/rateinterval.go @@ -304,6 +304,9 @@ func (i *RateInterval) Equal(o *RateInterval) bool { if i == nil && o == nil { return true } + if i == nil || o == nil { + return false // considering the earlier test + } if i.Weight != o.Weight { return false } diff --git a/engine/ratingprofile.go b/engine/ratingprofile.go index f802252aa..f0ebf0ac3 100644 --- a/engine/ratingprofile.go +++ b/engine/ratingprofile.go @@ -22,6 +22,7 @@ import ( "encoding/json" "fmt" "sort" + "strings" "time" "github.com/cgrates/cgrates/cache2go" @@ -144,9 +145,9 @@ func (ris RatingInfos) String() string { return string(b) } -func (rp *RatingProfile) GetRatingPlansForPrefix(cd *CallDescriptor) (err error) { +func (rpf *RatingProfile) GetRatingPlansForPrefix(cd *CallDescriptor) (err error) { var ris RatingInfos - for index, rpa := range rp.RatingPlanActivations.GetActiveForCall(cd) { + for index, rpa := range rpf.RatingPlanActivations.GetActiveForCall(cd) { rpl, err := ratingStorage.GetRatingPlan(rpa.RatingPlanId, false) if err != nil || rpl == nil { utils.Logger.Err(fmt.Sprintf("Error checking destination: %v", err)) @@ -201,7 +202,7 @@ func (rp *RatingProfile) GetRatingPlansForPrefix(cd *CallDescriptor) (err error) } if len(prefix) > 0 { ris = append(ris, &RatingInfo{ - MatchedSubject: rp.Id, + MatchedSubject: rpf.Id, RatingPlanId: rpl.Id, MatchedPrefix: prefix, MatchedDestId: destinationId, @@ -244,3 +245,21 @@ func (rpf *RatingProfile) GetHistoryRecord(deleted bool) history.Record { type TenantRatingSubject struct { Tenant, Subject string } + +func RatingProfileSubjectPrefixMatching(key string) (rp *RatingProfile, err error) { + if !rpSubjectPrefixMatching || strings.HasSuffix(key, utils.ANY) { + return ratingStorage.GetRatingProfile(key, false) + } + if rp, err = ratingStorage.GetRatingProfile(key, false); err == nil { + return rp, err + } + lastIndex := strings.LastIndex(key, utils.CONCATENATED_KEY_SEP) + baseKey := key[:lastIndex] + subject := key[lastIndex:] + for i := 1; i < len(subject)-1; i++ { + if rp, err = ratingStorage.GetRatingProfile(baseKey+subject[:len(subject)-i], false); err == nil { + return rp, err + } + } + return +} diff --git a/engine/ratingprofile_test.go b/engine/ratingprofile_test.go index 95ef342c0..4f929029a 100644 --- a/engine/ratingprofile_test.go +++ b/engine/ratingprofile_test.go @@ -249,3 +249,17 @@ func TestRatingProfileRIforTSMidnight(t *testing.T) { t.Error("Wrong interval list: ", utils.ToIJSON(rIntervals)) } } + +func TestRatingProfileSubjectPrefixMatching(t *testing.T) { + rpSubjectPrefixMatching = true + rp, err := RatingProfileSubjectPrefixMatching("*out:cgrates.org:data:rif") + if rp == nil || err != nil { + t.Errorf("Error getting rating profile by prefix: %+v (%v)", rp, err) + } + + rp, err = RatingProfileSubjectPrefixMatching("*out:cgrates.org:data:rifescu") + if rp == nil || err != nil { + t.Errorf("Error getting rating profile by prefix: %+v (%v)", rp, err) + } + rpSubjectPrefixMatching = false +} diff --git a/engine/responder.go b/engine/responder.go index d9586df26..8a155ec48 100644 --- a/engine/responder.go +++ b/engine/responder.go @@ -24,7 +24,6 @@ import ( "net/rpc" "reflect" "runtime" - "strconv" "strings" "time" @@ -51,6 +50,7 @@ type Responder struct { Bal *balancer2go.Balancer ExitChan chan bool Stats rpcclient.RpcClientConnection + Timeout time.Duration Timezone string cnt int64 responseCache *cache2go.ResponseCache @@ -76,6 +76,10 @@ func (rs *Responder) GetCost(arg *CallDescriptor, reply *CallCost) (err error) { if arg.Subject == "" { arg.Subject = arg.Account } + // replace user profile fields + if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases if err := LoadAlias( &AttrMatchingAlias{ @@ -89,10 +93,7 @@ func (rs *Responder) GetCost(arg *CallDescriptor, reply *CallCost) (err error) { }, arg, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { return err } - // replace user profile fields - if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { - return err - } + if rs.Bal != nil { r, e := rs.getCallCost(arg, "Responder.GetCost") *reply, err = *r, e @@ -114,6 +115,10 @@ func (rs *Responder) Debit(arg *CallDescriptor, reply *CallCost) (err error) { if arg.Subject == "" { arg.Subject = arg.Account } + // replace user profile fields + if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases if err := LoadAlias( &AttrMatchingAlias{ @@ -127,10 +132,7 @@ func (rs *Responder) Debit(arg *CallDescriptor, reply *CallCost) (err error) { }, arg, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { return err } - // replace user profile fields - if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { - return err - } + if rs.Bal != nil { r, e := rs.getCallCost(arg, "Responder.Debit") *reply, err = *r, e @@ -145,50 +147,18 @@ func (rs *Responder) Debit(arg *CallDescriptor, reply *CallCost) (err error) { return } -func (rs *Responder) FakeDebit(arg *CallDescriptor, reply *CallCost) (err error) { - if arg.Subject == "" { - arg.Subject = arg.Account - } - // replace aliases - if err := LoadAlias( - &AttrMatchingAlias{ - Destination: arg.Destination, - Direction: arg.Direction, - Tenant: arg.Tenant, - Category: arg.Category, - Account: arg.Account, - Subject: arg.Subject, - Context: utils.ALIAS_CONTEXT_RATING, - }, arg, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { - return err - } - // replace user profile fields - if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { - return err - } - if rs.Bal != nil { - r, e := rs.getCallCost(arg, "Responder.FakeDebit") - *reply, err = *r, e - } else { - r, e := arg.FakeDebit() - if e != nil { - return e - } else if r != nil { - *reply = *r - } - } - return -} - func (rs *Responder) MaxDebit(arg *CallDescriptor, reply *CallCost) (err error) { - cacheKey := "MaxDebit" + arg.CgrId + strconv.FormatFloat(arg.LoopIndex, 'f', -1, 64) - if item, err := rs.getCache().Get(cacheKey); err == nil && item != nil { + if item, err := rs.getCache().Get(utils.MAX_DEBIT_CACHE_PREFIX + arg.CgrID + arg.RunID); err == nil && item != nil { *reply = *(item.Value.(*CallCost)) return item.Err } if arg.Subject == "" { arg.Subject = arg.Account } + // replace user profile fields + if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases if err := LoadAlias( &AttrMatchingAlias{ @@ -200,39 +170,42 @@ func (rs *Responder) MaxDebit(arg *CallDescriptor, reply *CallCost) (err error) Subject: arg.Subject, Context: utils.ALIAS_CONTEXT_RATING, }, arg, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) - return err - } - // replace user profile fields - if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } + if rs.Bal != nil { r, e := rs.getCallCost(arg, "Responder.MaxDebit") *reply, err = *r, e } else { r, e := arg.MaxDebit() if e != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: e}) + rs.getCache().Cache(utils.MAX_DEBIT_CACHE_PREFIX+arg.CgrID+arg.RunID, &cache2go.CacheItem{ + Err: e, + }) return e } else if r != nil { *reply = *r } } - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Value: reply, Err: err}) + rs.getCache().Cache(utils.MAX_DEBIT_CACHE_PREFIX+arg.CgrID+arg.RunID, &cache2go.CacheItem{ + Value: reply, + Err: err, + }) return } func (rs *Responder) RefundIncrements(arg *CallDescriptor, reply *float64) (err error) { - cacheKey := "RefundIncrements" + arg.CgrId - if item, err := rs.getCache().Get(cacheKey); err == nil && item != nil { + if item, err := rs.getCache().Get(utils.REFUND_INCR_CACHE_PREFIX + arg.CgrID + arg.RunID); err == nil && item != nil { *reply = *(item.Value.(*float64)) return item.Err } if arg.Subject == "" { arg.Subject = arg.Account } + // replace user profile fields + if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases if err := LoadAlias( &AttrMatchingAlias{ @@ -244,20 +217,56 @@ func (rs *Responder) RefundIncrements(arg *CallDescriptor, reply *float64) (err Subject: arg.Subject, Context: utils.ALIAS_CONTEXT_RATING, }, arg, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) - return err - } - // replace user profile fields - if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } + if rs.Bal != nil { *reply, err = rs.callMethod(arg, "Responder.RefundIncrements") } else { - *reply, err = arg.RefundIncrements() + err = arg.RefundIncrements() } - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Value: reply, Err: err}) + rs.getCache().Cache(utils.REFUND_INCR_CACHE_PREFIX+arg.CgrID+arg.RunID, &cache2go.CacheItem{ + Value: reply, + Err: err, + }) + return +} + +func (rs *Responder) RefundRounding(arg *CallDescriptor, reply *float64) (err error) { + if item, err := rs.getCache().Get(utils.REFUND_ROUND_CACHE_PREFIX + arg.CgrID + arg.RunID); err == nil && item != nil { + *reply = *(item.Value.(*float64)) + return item.Err + } + if arg.Subject == "" { + arg.Subject = arg.Account + } + // replace user profile fields + if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { + return err + } + // replace aliases + if err := LoadAlias( + &AttrMatchingAlias{ + Destination: arg.Destination, + Direction: arg.Direction, + Tenant: arg.Tenant, + Category: arg.Category, + Account: arg.Account, + Subject: arg.Subject, + Context: utils.ALIAS_CONTEXT_RATING, + }, arg, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { + return err + } + + if rs.Bal != nil { + *reply, err = rs.callMethod(arg, "Responder.RefundRounding") + } else { + err = arg.RefundRounding() + } + rs.getCache().Cache(utils.REFUND_ROUND_CACHE_PREFIX+arg.CgrID+arg.RunID, &cache2go.CacheItem{ + Value: reply, + Err: err, + }) return } @@ -265,6 +274,10 @@ func (rs *Responder) GetMaxSessionTime(arg *CallDescriptor, reply *float64) (err if arg.Subject == "" { arg.Subject = arg.Account } + // replace user profile fields + if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases if err := LoadAlias( &AttrMatchingAlias{ @@ -278,10 +291,7 @@ func (rs *Responder) GetMaxSessionTime(arg *CallDescriptor, reply *float64) (err }, arg, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { return err } - // replace user profile fields - if err := LoadUserProfile(arg, utils.EXTRA_FIELDS); err != nil { - return err - } + if rs.Bal != nil { *reply, err = rs.callMethod(arg, "Responder.GetMaxSessionTime") } else { @@ -293,19 +303,16 @@ func (rs *Responder) GetMaxSessionTime(arg *CallDescriptor, reply *float64) (err // Returns MaxSessionTime for an event received in SessionManager, considering DerivedCharging for it func (rs *Responder) GetDerivedMaxSessionTime(ev *CDR, reply *float64) error { - cacheKey := "GetDerivedMaxSessionTime" + ev.CGRID - if item, err := rs.getCache().Get(cacheKey); err == nil && item != nil { - *reply = item.Value.(float64) - return item.Err - } if rs.Bal != nil { - err := errors.New("unsupported method on the balancer") - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) - return err + return errors.New("unsupported method on the balancer") } if ev.Subject == "" { ev.Subject = ev.Account } + // replace user profile fields + if err := LoadUserProfile(ev, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases if err := LoadAlias( &AttrMatchingAlias{ @@ -317,20 +324,14 @@ func (rs *Responder) GetDerivedMaxSessionTime(ev *CDR, reply *float64) error { Subject: ev.Subject, Context: utils.ALIAS_CONTEXT_RATING, }, ev, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) - return err - } - // replace user profile fields - if err := LoadUserProfile(ev, utils.EXTRA_FIELDS); err != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } + maxCallDuration := -1.0 attrsDC := &utils.AttrDerivedChargers{Tenant: ev.GetTenant(utils.META_DEFAULT), Category: ev.GetCategory(utils.META_DEFAULT), Direction: ev.GetDirection(utils.META_DEFAULT), Account: ev.GetAccount(utils.META_DEFAULT), Subject: ev.GetSubject(utils.META_DEFAULT)} dcs := &utils.DerivedChargers{} if err := rs.GetDerivedChargers(attrsDC, dcs); err != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } dcs, _ = dcs.AppendDefaultRun() @@ -351,18 +352,19 @@ func (rs *Responder) GetDerivedMaxSessionTime(ev *CDR, reply *float64) error { } startTime, err := ev.GetSetupTime(utils.META_DEFAULT, rs.Timezone) if err != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } usage, err := ev.GetDuration(utils.META_DEFAULT) if err != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } if usage == 0 { usage = config.CgrConfig().MaxCallDuration } cd := &CallDescriptor{ + CgrID: ev.GetCgrId(rs.Timezone), + RunID: ev.RunID, + TOR: ev.ToR, Direction: ev.GetDirection(dc.DirectionField), Tenant: ev.GetTenant(dc.TenantField), Category: ev.GetCategory(dc.CategoryField), @@ -376,7 +378,6 @@ func (rs *Responder) GetDerivedMaxSessionTime(ev *CDR, reply *float64) error { err = rs.GetMaxSessionTime(cd, &remainingDuration) if err != nil { *reply = 0 - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } if utils.IsSliceMember([]string{utils.META_POSTPAID, utils.POSTPAID}, ev.GetReqType(dc.RequestTypeField)) { @@ -390,26 +391,23 @@ func (rs *Responder) GetDerivedMaxSessionTime(ev *CDR, reply *float64) error { maxCallDuration = remainingDuration } } - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Value: maxCallDuration}) *reply = maxCallDuration return nil } // Used by SM to get all the prepaid CallDescriptors attached to a session func (rs *Responder) GetSessionRuns(ev *CDR, sRuns *[]*SessionRun) error { - cacheKey := "GetSessionRuns" + ev.CGRID - if item, err := rs.getCache().Get(cacheKey); err == nil && item != nil { - *sRuns = item.Value.([]*SessionRun) - return item.Err - } if rs.Bal != nil { - err := errors.New("Unsupported method on the balancer") - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) - return err + return errors.New("Unsupported method on the balancer") } if ev.Subject == "" { ev.Subject = ev.Account } + //utils.Logger.Info(fmt.Sprintf("DC before: %+v", ev)) + // replace user profile fields + if err := LoadUserProfile(ev, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases if err := LoadAlias( &AttrMatchingAlias{ @@ -421,22 +419,22 @@ func (rs *Responder) GetSessionRuns(ev *CDR, sRuns *[]*SessionRun) error { Subject: ev.Subject, Context: utils.ALIAS_CONTEXT_RATING, }, ev, utils.EXTRA_FIELDS); err != nil && err != utils.ErrNotFound { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) - return err - } - // replace user profile fields - if err := LoadUserProfile(ev, utils.EXTRA_FIELDS); err != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } + + //utils.Logger.Info(fmt.Sprintf("DC after: %+v", ev)) attrsDC := &utils.AttrDerivedChargers{Tenant: ev.GetTenant(utils.META_DEFAULT), Category: ev.GetCategory(utils.META_DEFAULT), Direction: ev.GetDirection(utils.META_DEFAULT), - Account: ev.GetAccount(utils.META_DEFAULT), Subject: ev.GetSubject(utils.META_DEFAULT)} + Account: ev.GetAccount(utils.META_DEFAULT), Subject: ev.GetSubject(utils.META_DEFAULT), Destination: ev.GetDestination(utils.META_DEFAULT)} + //utils.Logger.Info(fmt.Sprintf("Derived chargers for: %+v", attrsDC)) dcs := &utils.DerivedChargers{} if err := rs.GetDerivedChargers(attrsDC, dcs); err != nil { - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) + rs.getCache().Cache(utils.GET_SESS_RUNS_CACHE_PREFIX+ev.CGRID, &cache2go.CacheItem{ + Err: err, + }) return err } dcs, _ = dcs.AppendDefaultRun() + //utils.Logger.Info(fmt.Sprintf("DCS: %v", len(dcs.Chargers))) sesRuns := make([]*SessionRun, 0) for _, dc := range dcs.Chargers { if !utils.IsSliceMember([]string{utils.META_PREPAID, utils.PREPAID}, ev.GetReqType(dc.RequestTypeField)) { @@ -444,12 +442,22 @@ func (rs *Responder) GetSessionRuns(ev *CDR, sRuns *[]*SessionRun) error { } startTime, err := ev.GetAnswerTime(dc.AnswerTimeField, rs.Timezone) if err != nil { - err := errors.New("Error parsing answer event start time") - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) - return err + rs.getCache().Cache(utils.GET_SESS_RUNS_CACHE_PREFIX+ev.CGRID, &cache2go.CacheItem{ + Err: err, + }) + return errors.New("Error parsing answer event start time") } + endTime, err := ev.GetEndTime("", rs.Timezone) + if err != nil { + rs.getCache().Cache(utils.GET_SESS_RUNS_CACHE_PREFIX+ev.CGRID, &cache2go.CacheItem{ + Err: err, + }) + return errors.New("Error parsing answer event end time") + } + extraFields := ev.GetExtraFields() cd := &CallDescriptor{ - CgrId: ev.GetCgrId(rs.Timezone), + CgrID: ev.GetCgrId(rs.Timezone), + RunID: ev.RunID, TOR: ev.ToR, Direction: ev.GetDirection(dc.DirectionField), Tenant: ev.GetTenant(dc.TenantField), @@ -458,11 +466,21 @@ func (rs *Responder) GetSessionRuns(ev *CDR, sRuns *[]*SessionRun) error { Account: ev.GetAccount(dc.AccountField), Destination: ev.GetDestination(dc.DestinationField), TimeStart: startTime, - ExtraFields: ev.GetExtraFields()} + TimeEnd: endTime, + ExtraFields: extraFields} + if flagsStr, hasFlags := extraFields[utils.CGRFlags]; hasFlags { // Force duration from extra fields + flags := utils.StringMapFromSlice(strings.Split(flagsStr, utils.INFIELD_SEP)) + if _, hasFD := flags[utils.FlagForceDuration]; hasFD { + cd.ForceDuration = true + } + } sesRuns = append(sesRuns, &SessionRun{DerivedCharger: dc, CallDescriptor: cd}) } + //utils.Logger.Info(fmt.Sprintf("RUNS: %v", len(sesRuns))) *sRuns = sesRuns - rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Value: sRuns}) + rs.getCache().Cache(utils.GET_SESS_RUNS_CACHE_PREFIX+ev.CGRID, &cache2go.CacheItem{ + Value: sRuns, + }) return nil } @@ -479,7 +497,7 @@ func (rs *Responder) GetDerivedChargers(attrs *utils.AttrDerivedChargers, dcs *u } func (rs *Responder) GetLCR(attrs *AttrGetLcr, reply *LCRCost) error { - cacheKey := "GetLCR" + attrs.CgrId + cacheKey := "GetLCR" + attrs.CgrID if item, err := rs.getCache().Get(cacheKey); err == nil && item != nil { *reply = *(item.Value.(*LCRCost)) return item.Err @@ -487,6 +505,10 @@ func (rs *Responder) GetLCR(attrs *AttrGetLcr, reply *LCRCost) error { if attrs.CallDescriptor.Subject == "" { attrs.CallDescriptor.Subject = attrs.CallDescriptor.Account } + // replace user profile fields + if err := LoadUserProfile(attrs.CallDescriptor, utils.EXTRA_FIELDS); err != nil { + return err + } // replace aliases cd := attrs.CallDescriptor if err := LoadAlias( @@ -502,16 +524,13 @@ func (rs *Responder) GetLCR(attrs *AttrGetLcr, reply *LCRCost) error { rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } - // replace user profile fields - if err := LoadUserProfile(attrs.CallDescriptor, utils.EXTRA_FIELDS); err != nil { - return err - } + lcrCost, err := attrs.CallDescriptor.GetLCR(rs.Stats, attrs.Paginator) if err != nil { rs.getCache().Cache(cacheKey, &cache2go.CacheItem{Err: err}) return err } - if lcrCost.Entry.Strategy == LCR_STRATEGY_LOAD { + if lcrCost.Entry != nil && lcrCost.Entry.Strategy == LCR_STRATEGY_LOAD { for _, suppl := range lcrCost.SupplierCosts { suppl.Cost = -1 // In case of load distribution we don't calculate costs } @@ -636,6 +655,11 @@ func (rs *Responder) UnRegisterRater(clientAddress string, replay *int) error { return nil } +func (rs *Responder) GetTimeout(i int, d *time.Duration) error { + *d = rs.Timeout + return nil +} + func (rs *Responder) Call(serviceMethod string, args interface{}, reply interface{}) error { parts := strings.Split(serviceMethod, ".") if len(parts) != 2 { diff --git a/engine/responder_test.go b/engine/responder_test.go index 680a45529..52826d87e 100644 --- a/engine/responder_test.go +++ b/engine/responder_test.go @@ -71,10 +71,10 @@ func TestResponderGetDerivedMaxSessionTime(t *testing.T) { if err := ratingStorage.SetDestination(deTMobile); err != nil { t.Error(err) } - b10 := &Balance{Value: 10, Weight: 10, DestinationIds: utils.NewStringMap("DE_TMOBILE")} - b20 := &Balance{Value: 20, Weight: 10, DestinationIds: utils.NewStringMap("DE_TMOBILE")} - rifsAccount := &Account{Id: utils.ConcatenatedKey(testTenant, "rif"), BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b10}}} - dansAccount := &Account{Id: utils.ConcatenatedKey(testTenant, "dan"), BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{b20}}} + b10 := &Balance{Value: 10, Weight: 10, DestinationIDs: utils.NewStringMap("DE_TMOBILE")} + b20 := &Balance{Value: 20, Weight: 10, DestinationIDs: utils.NewStringMap("DE_TMOBILE")} + rifsAccount := &Account{ID: utils.ConcatenatedKey(testTenant, "rif"), BalanceMap: map[string]Balances{utils.VOICE: Balances{b10}}} + dansAccount := &Account{ID: utils.ConcatenatedKey(testTenant, "dan"), BalanceMap: map[string]Balances{utils.VOICE: Balances{b20}}} if err := accountingStorage.SetAccount(rifsAccount); err != nil { t.Error(err) } @@ -151,20 +151,20 @@ func TestResponderGetSessionRuns(t *testing.T) { sesRuns := make([]*SessionRun, 0) eSRuns := []*SessionRun{ &SessionRun{DerivedCharger: extra1DC, - CallDescriptor: &CallDescriptor{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), Direction: "*out", Category: "0", - Tenant: "vdf", Subject: "rif", Account: "minitsboy", Destination: "0256", TimeStart: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), - TOR: utils.VOICE, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}}}, + CallDescriptor: &CallDescriptor{CgrID: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), RunID: "*default", Direction: "*out", Category: "0", + Tenant: "vdf", Subject: "rif", Account: "minitsboy", Destination: "0256", TimeStart: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), TimeEnd: time.Date(2013, 11, 7, 8, 42, 36, 0, time.UTC), TOR: utils.VOICE, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}}}, &SessionRun{DerivedCharger: extra2DC, - CallDescriptor: &CallDescriptor{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), Direction: "*out", Category: "call", - Tenant: "vdf", Subject: "ivo", Account: "ivo", Destination: "1002", TimeStart: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), - TOR: utils.VOICE, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}}}, + CallDescriptor: &CallDescriptor{CgrID: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), RunID: "*default", Direction: "*out", Category: "call", + Tenant: "vdf", Subject: "ivo", Account: "ivo", Destination: "1002", TimeStart: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), TimeEnd: time.Date(2013, 11, 7, 8, 42, 36, 0, time.UTC), TOR: utils.VOICE, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}}}, &SessionRun{DerivedCharger: dfDC, - CallDescriptor: &CallDescriptor{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), Direction: "*out", Category: "call", - Tenant: "vdf", Subject: "dan2", Account: "dan2", Destination: "1002", TimeStart: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), - TOR: utils.VOICE, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}}}} + CallDescriptor: &CallDescriptor{CgrID: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), RunID: "*default", Direction: "*out", Category: "call", + Tenant: "vdf", Subject: "dan2", Account: "dan2", Destination: "1002", TimeStart: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), TimeEnd: time.Date(2013, 11, 7, 8, 42, 36, 0, time.UTC), TOR: utils.VOICE, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}}}} if err := rsponder.GetSessionRuns(cdr, &sesRuns); err != nil { t.Error(err) } else if !reflect.DeepEqual(eSRuns, sesRuns) { + for _, sr := range sesRuns { + t.Logf("sr cd: %+v", sr.CallDescriptor) + } t.Errorf("Expecting: %+v, received: %+v", eSRuns, sesRuns) } } @@ -436,10 +436,10 @@ func TestResponderGetLCR(t *testing.T) { } else if !reflect.DeepEqual(eLcLcr.SupplierCosts, lcrLc.SupplierCosts) { t.Errorf("Expecting: %+v, received: %+v", eLcLcr.SupplierCosts, lcrLc.SupplierCosts) } - bRif12 := &Balance{Value: 40, Weight: 10, DestinationIds: utils.NewStringMap(dstDe.Id)} - bIvo12 := &Balance{Value: 60, Weight: 10, DestinationIds: utils.NewStringMap(dstDe.Id)} - rif12sAccount := &Account{Id: utils.ConcatenatedKey("tenant12", "rif12"), BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{bRif12}}, AllowNegative: true} - ivo12sAccount := &Account{Id: utils.ConcatenatedKey("tenant12", "ivo12"), BalanceMap: map[string]BalanceChain{utils.VOICE: BalanceChain{bIvo12}}, AllowNegative: true} + bRif12 := &Balance{Value: 40, Weight: 10, DestinationIDs: utils.NewStringMap(dstDe.Id)} + bIvo12 := &Balance{Value: 60, Weight: 10, DestinationIDs: utils.NewStringMap(dstDe.Id)} + rif12sAccount := &Account{ID: utils.ConcatenatedKey("tenant12", "rif12"), BalanceMap: map[string]Balances{utils.VOICE: Balances{bRif12}}, AllowNegative: true} + ivo12sAccount := &Account{ID: utils.ConcatenatedKey("tenant12", "ivo12"), BalanceMap: map[string]Balances{utils.VOICE: Balances{bIvo12}}, AllowNegative: true} for _, acnt := range []*Account{rif12sAccount, ivo12sAccount} { if err := accountingStorage.SetAccount(acnt); err != nil { t.Error(err) diff --git a/engine/sharedgroup.go b/engine/sharedgroup.go index ab8e4750b..3bdb9d88c 100644 --- a/engine/sharedgroup.go +++ b/engine/sharedgroup.go @@ -49,9 +49,9 @@ type SharingParameters struct { RatingSubject string } -func (sg *SharedGroup) SortBalancesByStrategy(myBalance *Balance, bc BalanceChain) BalanceChain { +func (sg *SharedGroup) SortBalancesByStrategy(myBalance *Balance, bc Balances) Balances { sharingParameters := sg.AccountParameters[utils.ANY] - if sp, hasParamsForAccount := sg.AccountParameters[myBalance.account.Id]; hasParamsForAccount { + if sp, hasParamsForAccount := sg.AccountParameters[myBalance.account.ID]; hasParamsForAccount { sharingParameters = sp } @@ -61,18 +61,18 @@ func (sg *SharedGroup) SortBalancesByStrategy(myBalance *Balance, bc BalanceChai } switch strategy { case STRATEGY_LOWEST, STRATEGY_MINE_LOWEST: - sort.Sort(LowestBalanceChainSorter(bc)) + sort.Sort(LowestBalancesSorter(bc)) case STRATEGY_HIGHEST, STRATEGY_MINE_HIGHEST: - sort.Sort(HighestBalanceChainSorter(bc)) + sort.Sort(HighestBalancesSorter(bc)) case STRATEGY_RANDOM, STRATEGY_MINE_RANDOM: - rbc := RandomBalanceChainSorter(bc) + rbc := RandomBalancesSorter(bc) (&rbc).Sort() - bc = BalanceChain(rbc) + bc = Balances(rbc) default: // use mine random for anything else strategy = STRATEGY_MINE_RANDOM - rbc := RandomBalanceChainSorter(bc) + rbc := RandomBalancesSorter(bc) (&rbc).Sort() - bc = BalanceChain(rbc) + bc = Balances(rbc) } if strings.HasPrefix(strategy, MINE_PREFIX) { // find index of my balance @@ -90,11 +90,11 @@ func (sg *SharedGroup) SortBalancesByStrategy(myBalance *Balance, bc BalanceChai } // Returns all shared group's balances collected from user accounts' -func (sg *SharedGroup) GetBalances(destination, category, direction, balanceType string, ub *Account) (bc BalanceChain) { +func (sg *SharedGroup) GetBalances(destination, category, direction, balanceType string, ub *Account) (bc Balances) { // if len(sg.members) == 0 { for ubId := range sg.MemberIds { var nUb *Account - if ubId == ub.Id { // skip the initiating user + if ubId == ub.ID { // skip the initiating user nUb = ub } else { nUb, _ = accountingStorage.GetAccount(ubId) @@ -115,37 +115,37 @@ func (sg *SharedGroup) GetBalances(destination, category, direction, balanceType return } -type LowestBalanceChainSorter []*Balance +type LowestBalancesSorter []*Balance -func (lbcs LowestBalanceChainSorter) Len() int { +func (lbcs LowestBalancesSorter) Len() int { return len(lbcs) } -func (lbcs LowestBalanceChainSorter) Swap(i, j int) { +func (lbcs LowestBalancesSorter) Swap(i, j int) { lbcs[i], lbcs[j] = lbcs[j], lbcs[i] } -func (lbcs LowestBalanceChainSorter) Less(i, j int) bool { +func (lbcs LowestBalancesSorter) Less(i, j int) bool { return lbcs[i].GetValue() < lbcs[j].GetValue() } -type HighestBalanceChainSorter []*Balance +type HighestBalancesSorter []*Balance -func (hbcs HighestBalanceChainSorter) Len() int { +func (hbcs HighestBalancesSorter) Len() int { return len(hbcs) } -func (hbcs HighestBalanceChainSorter) Swap(i, j int) { +func (hbcs HighestBalancesSorter) Swap(i, j int) { hbcs[i], hbcs[j] = hbcs[j], hbcs[i] } -func (hbcs HighestBalanceChainSorter) Less(i, j int) bool { +func (hbcs HighestBalancesSorter) Less(i, j int) bool { return hbcs[i].GetValue() > hbcs[j].GetValue() } -type RandomBalanceChainSorter []*Balance +type RandomBalancesSorter []*Balance -func (rbcs *RandomBalanceChainSorter) Sort() { +func (rbcs *RandomBalancesSorter) Sort() { src := *rbcs // randomize balance chain dest := make([]*Balance, len(src)) diff --git a/engine/sharedgroup_test.go b/engine/sharedgroup_test.go index b74df0846..fd71f1531 100644 --- a/engine/sharedgroup_test.go +++ b/engine/sharedgroup_test.go @@ -50,9 +50,9 @@ func TestSharedSetGet(t *testing.T) { } func TestSharedPopBalanceByStrategyLow(t *testing.T) { - bc := BalanceChain{ + bc := Balances{ &Balance{Value: 2.0}, - &Balance{Uuid: "uuuu", Value: 1.0, account: &Account{Id: "test"}}, + &Balance{Uuid: "uuuu", Value: 1.0, account: &Account{ID: "test"}}, &Balance{Value: 3.0}, } sg := &SharedGroup{AccountParameters: map[string]*SharingParameters{ @@ -67,8 +67,8 @@ func TestSharedPopBalanceByStrategyLow(t *testing.T) { } func TestSharedPopBalanceByStrategyHigh(t *testing.T) { - bc := BalanceChain{ - &Balance{Uuid: "uuuu", Value: 2.0, account: &Account{Id: "test"}}, + bc := Balances{ + &Balance{Uuid: "uuuu", Value: 2.0, account: &Account{ID: "test"}}, &Balance{Value: 1.0}, &Balance{Value: 3.0}, } @@ -84,8 +84,8 @@ func TestSharedPopBalanceByStrategyHigh(t *testing.T) { } func TestSharedPopBalanceByStrategyMineHigh(t *testing.T) { - bc := BalanceChain{ - &Balance{Uuid: "uuuu", Value: 2.0, account: &Account{Id: "test"}}, + bc := Balances{ + &Balance{Uuid: "uuuu", Value: 2.0, account: &Account{ID: "test"}}, &Balance{Value: 1.0}, &Balance{Value: 3.0}, } @@ -101,7 +101,7 @@ func TestSharedPopBalanceByStrategyMineHigh(t *testing.T) { } /*func TestSharedPopBalanceByStrategyRandomHigh(t *testing.T) { - bc := BalanceChain{ + bc := Balances{ &Balance{Uuid: "uuuu", Value: 2.0, account: &Account{Id: "test"}}, &Balance{Value: 1.0}, &Balance{Value: 3.0}, diff --git a/engine/stats.go b/engine/stats.go index e130a4e25..571615ec6 100644 --- a/engine/stats.go +++ b/engine/stats.go @@ -28,6 +28,19 @@ import ( "github.com/cgrates/cgrates/utils" ) +type StatsInterface interface { + GetValues(string, *map[string]float64) error + GetQueueIds(int, *[]string) error + GetQueue(string, *StatsQueue) error + GetQueueTriggers(string, *ActionTriggers) error + AppendCDR(*CDR, *int) error + AddQueue(*CdrStats, *int) error + RemoveQueue(string, *int) error + ReloadQueues([]string, *int) error + ResetQueues([]string, *int) error + Stop(int, *int) error +} + type Stats struct { queues map[string]*StatsQueue queueSavers map[string]*queueSaver @@ -151,6 +164,22 @@ func (s *Stats) AddQueue(cs *CdrStats, out *int) error { return nil } +func (s *Stats) RemoveQueue(qID string, out *int) error { + s.mux.Lock() + defer s.mux.Unlock() + if s.queues == nil { + s.queues = make(map[string]*StatsQueue) + } + if s.queueSavers == nil { + s.queueSavers = make(map[string]*queueSaver) + } + + delete(s.queues, qID) + delete(s.queueSavers, qID) + + return nil +} + func (s *Stats) ReloadQueues(ids []string, out *int) error { if len(ids) == 0 { if css, err := s.ratingDb.GetAllCdrStats(); err == nil { diff --git a/engine/storage_interface.go b/engine/storage_interface.go index fd5781dc5..8d082ae14 100644 --- a/engine/storage_interface.go +++ b/engine/storage_interface.go @@ -64,7 +64,7 @@ type RatingStorage interface { GetActionTriggers(string) (ActionTriggers, error) SetActionTriggers(string, ActionTriggers) error GetActionPlan(string, bool) (*ActionPlan, error) - SetActionPlan(string, *ActionPlan) error + SetActionPlan(string, *ActionPlan, bool) error GetAllActionPlans() (map[string]*ActionPlan, error) PushTask(*Task) error PopTask() (*Task, error) diff --git a/engine/storage_map.go b/engine/storage_map.go index a492bb70b..24315c1c4 100644 --- a/engine/storage_map.go +++ b/engine/storage_map.go @@ -464,7 +464,7 @@ func (ms *MapStorage) SetSharedGroup(sg *SharedGroup) (err error) { func (ms *MapStorage) GetAccount(key string) (ub *Account, err error) { if values, ok := ms.dict[utils.ACCOUNT_PREFIX+key]; ok { - ub = &Account{Id: key} + ub = &Account{ID: key} err = ms.ms.Unmarshal(values, ub) } else { return nil, utils.ErrNotFound @@ -477,7 +477,7 @@ func (ms *MapStorage) SetAccount(ub *Account) (err error) { // UPDATE: if all balances expired and were cleaned it makes // sense to write empty balance map if len(ub.BalanceMap) == 0 { - if ac, err := ms.GetAccount(ub.Id); err == nil && !ac.allBalancesExpired() { + if ac, err := ms.GetAccount(ub.ID); err == nil && !ac.allBalancesExpired() { ac.ActionTriggers = ub.ActionTriggers ac.UnitCounters = ub.UnitCounters ac.AllowNegative = ub.AllowNegative @@ -486,7 +486,7 @@ func (ms *MapStorage) SetAccount(ub *Account) (err error) { } } result, err := ms.ms.Marshal(ub) - ms.dict[utils.ACCOUNT_PREFIX+ub.Id] = result + ms.dict[utils.ACCOUNT_PREFIX+ub.ID] = result return } @@ -664,13 +664,24 @@ func (ms *MapStorage) GetActionPlan(key string, skipCache bool) (ats *ActionPlan return } -func (ms *MapStorage) SetActionPlan(key string, ats *ActionPlan) (err error) { +func (ms *MapStorage) SetActionPlan(key string, ats *ActionPlan, overwrite bool) (err error) { if len(ats.ActionTimings) == 0 { // delete the key delete(ms.dict, utils.ACTION_PLAN_PREFIX+key) cache2go.RemKey(utils.ACTION_PLAN_PREFIX + key) return } + if !overwrite { + // get existing action plan to merge the account ids + if existingAts, _ := ms.GetActionPlan(key, true); existingAts != nil { + if ats.AccountIDs == nil && len(existingAts.AccountIDs) > 0 { + ats.AccountIDs = make(utils.StringMap) + } + for accID := range existingAts.AccountIDs { + ats.AccountIDs[accID] = true + } + } + } result, err := ms.ms.Marshal(&ats) ms.dict[utils.ACTION_PLAN_PREFIX+key] = result return diff --git a/engine/storage_mongo_datadb.go b/engine/storage_mongo_datadb.go index 9d3fda88c..8139f3b19 100644 --- a/engine/storage_mongo_datadb.go +++ b/engine/storage_mongo_datadb.go @@ -870,7 +870,7 @@ func (ms *MongoStorage) SetAccount(acc *Account) error { // UPDATE: if all balances expired and were cleaned it makes // sense to write empty balance map if len(acc.BalanceMap) == 0 { - if ac, err := ms.GetAccount(acc.Id); err == nil && !ac.allBalancesExpired() { + if ac, err := ms.GetAccount(acc.ID); err == nil && !ac.allBalancesExpired() { ac.ActionTriggers = acc.ActionTriggers ac.UnitCounters = acc.UnitCounters ac.AllowNegative = acc.AllowNegative @@ -878,7 +878,7 @@ func (ms *MongoStorage) SetAccount(acc *Account) error { acc = ac } } - _, err := ms.db.C(colAcc).Upsert(bson.M{"id": acc.Id}, acc) + _, err := ms.db.C(colAcc).Upsert(bson.M{"id": acc.ID}, acc) return err } @@ -1156,7 +1156,7 @@ func (ms *MongoStorage) GetActionPlan(key string, skipCache bool) (ats *ActionPl return } -func (ms *MongoStorage) SetActionPlan(key string, ats *ActionPlan) error { +func (ms *MongoStorage) SetActionPlan(key string, ats *ActionPlan, overwrite bool) error { // clean dots from account ids map if len(ats.ActionTimings) == 0 { cache2go.RemKey(utils.ACTION_PLAN_PREFIX + key) @@ -1166,6 +1166,17 @@ func (ms *MongoStorage) SetActionPlan(key string, ats *ActionPlan) error { } return nil } + if !overwrite { + // get existing action plan to merge the account ids + if existingAts, _ := ms.GetActionPlan(key, true); existingAts != nil { + if ats.AccountIDs == nil && len(existingAts.AccountIDs) > 0 { + ats.AccountIDs = make(utils.StringMap) + } + for accID := range existingAts.AccountIDs { + ats.AccountIDs[accID] = true + } + } + } result, err := ms.ms.Marshal(ats) if err != nil { return err diff --git a/engine/storage_redis.go b/engine/storage_redis.go index 80568003e..c449ea0c1 100644 --- a/engine/storage_redis.go +++ b/engine/storage_redis.go @@ -621,7 +621,7 @@ func (rs *RedisStorage) GetAccount(key string) (*Account, error) { if err != nil { return nil, err } - ub := &Account{Id: key} + ub := &Account{ID: key} if err = rs.ms.Unmarshal(values, ub); err != nil { return nil, err } @@ -633,7 +633,7 @@ func (rs *RedisStorage) SetAccount(ub *Account) (err error) { // UPDATE: if all balances expired and were cleaned it makes // sense to write empty balance map if len(ub.BalanceMap) == 0 { - if ac, err := rs.GetAccount(ub.Id); err == nil && !ac.allBalancesExpired() { + if ac, err := rs.GetAccount(ub.ID); err == nil && !ac.allBalancesExpired() { ac.ActionTriggers = ub.ActionTriggers ac.UnitCounters = ub.UnitCounters ac.AllowNegative = ub.AllowNegative @@ -642,7 +642,7 @@ func (rs *RedisStorage) SetAccount(ub *Account) (err error) { } } result, err := rs.ms.Marshal(ub) - err = rs.db.Cmd("SET", utils.ACCOUNT_PREFIX+ub.Id, result).Err + err = rs.db.Cmd("SET", utils.ACCOUNT_PREFIX+ub.ID, result).Err return } @@ -920,13 +920,25 @@ func (rs *RedisStorage) GetActionPlan(key string, skipCache bool) (ats *ActionPl return } -func (rs *RedisStorage) SetActionPlan(key string, ats *ActionPlan) (err error) { +func (rs *RedisStorage) SetActionPlan(key string, ats *ActionPlan, overwrite bool) (err error) { if len(ats.ActionTimings) == 0 { // delete the key err = rs.db.Cmd("DEL", utils.ACTION_PLAN_PREFIX+key).Err cache2go.RemKey(utils.ACTION_PLAN_PREFIX + key) return err } + if !overwrite { + // get existing action plan to merge the account ids + if existingAts, _ := rs.GetActionPlan(key, true); existingAts != nil { + if ats.AccountIDs == nil && len(existingAts.AccountIDs) > 0 { + ats.AccountIDs = make(utils.StringMap) + } + for accID := range existingAts.AccountIDs { + ats.AccountIDs[accID] = true + } + } + } + result, err := rs.ms.Marshal(ats) if err != nil { return err diff --git a/engine/storage_test.go b/engine/storage_test.go index 98a312739..cdb9d6802 100644 --- a/engine/storage_test.go +++ b/engine/storage_test.go @@ -311,25 +311,26 @@ func TestStorageTask(t *testing.T) { func GetUB() *Account { uc := &UnitCounter{ - BalanceType: utils.SMS, - Balances: BalanceChain{&Balance{Value: 1}, &Balance{Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}, + Counters: CounterFilters{&CounterFilter{Value: 1}, &CounterFilter{Filter: &BalanceFilter{Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT"))}}, &CounterFilter{Filter: &BalanceFilter{Weight: utils.Float64Pointer(10), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET"))}}}, } at := &ActionTrigger{ - ID: "some_uuid", - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT), - ThresholdValue: 100.0, - BalanceDestinationIds: utils.NewStringMap("NAT"), - Weight: 10.0, - ActionsId: "Commando", + ID: "some_uuid", + ThresholdValue: 100.0, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), + }, + Weight: 10.0, + ActionsID: "Commando", } var zeroTime time.Time zeroTime = zeroTime.UTC() // for deep equal to find location ub := &Account{ - Id: "rif", + ID: "rif", AllowNegative: true, - BalanceMap: map[string]BalanceChain{utils.SMS: BalanceChain{&Balance{Value: 14, ExpirationDate: zeroTime}}, utils.DATA: BalanceChain{&Balance{Value: 1024, ExpirationDate: zeroTime}}, utils.VOICE: BalanceChain{&Balance{Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}}, - UnitCounters: UnitCounters{uc, uc}, + BalanceMap: map[string]Balances{utils.SMS: Balances{&Balance{Value: 14, ExpirationDate: zeroTime}}, utils.DATA: Balances{&Balance{Value: 1024, ExpirationDate: zeroTime}}, utils.VOICE: Balances{&Balance{Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, + UnitCounters: UnitCounters{utils.SMS: []*UnitCounter{uc, uc}}, ActionTriggers: ActionTriggers{at, at, at}, } return ub diff --git a/engine/storage_utils.go b/engine/storage_utils.go index ba093af58..8994020fd 100644 --- a/engine/storage_utils.go +++ b/engine/storage_utils.go @@ -20,6 +20,7 @@ package engine import ( "errors" + "fmt" "strconv" "github.com/cgrates/cgrates/utils" @@ -45,7 +46,8 @@ func ConfigureRatingStorage(db_type, host, port, name, user, pass, marshaler str d, err = NewMongoStorage(host, port, name, user, pass, nil) db = d.(RatingStorage) default: - err = errors.New("unknown db") + err = errors.New(fmt.Sprintf("Unknown db '%s' valid options are '%s' or '%s'", + db_type, utils.REDIS, utils.MONGO)) } if err != nil { return nil, err @@ -71,7 +73,8 @@ func ConfigureAccountingStorage(db_type, host, port, name, user, pass, marshaler d, err = NewMongoStorage(host, port, name, user, pass, nil) db = d.(AccountingStorage) default: - err = errors.New("unknown db") + err = errors.New(fmt.Sprintf("Unknown db '%s' valid options are '%s' or '%s'", + db_type, utils.REDIS, utils.MONGO)) } if err != nil { return nil, err @@ -102,7 +105,8 @@ func ConfigureLogStorage(db_type, host, port, name, user, pass, marshaler string case utils.MYSQL: d, err = NewMySQLStorage(host, port, name, user, pass, maxConn, maxIdleConn) default: - err = errors.New("unknown db") + err = errors.New(fmt.Sprintf("Unknown db '%s' valid options are [%s, %s, %s]", + db_type, utils.MYSQL, utils.MONGO, utils.POSTGRES)) } if err != nil { return nil, err @@ -120,7 +124,8 @@ func ConfigureLoadStorage(db_type, host, port, name, user, pass, marshaler strin case utils.MONGO: d, err = NewMongoStorage(host, port, name, user, pass, cdrsIndexes) default: - err = errors.New("unknown db") + err = errors.New(fmt.Sprintf("Unknown db '%s' valid options are [%s, %s, %s]", + db_type, utils.MYSQL, utils.MONGO, utils.POSTGRES)) } if err != nil { return nil, err @@ -138,7 +143,8 @@ func ConfigureCdrStorage(db_type, host, port, name, user, pass string, maxConn, case utils.MONGO: d, err = NewMongoStorage(host, port, name, user, pass, cdrsIndexes) default: - err = errors.New("unknown db") + err = errors.New(fmt.Sprintf("Unknown db '%s' valid options are [%s, %s, %s]", + db_type, utils.MYSQL, utils.MONGO, utils.POSTGRES)) } if err != nil { return nil, err diff --git a/engine/timespans.go b/engine/timespans.go index b89739552..8a716f307 100644 --- a/engine/timespans.go +++ b/engine/timespans.go @@ -33,49 +33,99 @@ A unit in which a call will be split that has a specific price related interval type TimeSpan struct { TimeStart, TimeEnd time.Time Cost float64 - ratingInfo *RatingInfo RateInterval *RateInterval DurationIndex time.Duration // the call duration so far till TimeEnd Increments Increments + RoundIncrement *Increment MatchedSubject, MatchedPrefix, MatchedDestId, RatingPlanId string CompressFactor int + ratingInfo *RatingInfo } type Increment struct { - Duration time.Duration - Cost float64 - BalanceInfo *BalanceInfo // need more than one for units with cost - BalanceRateInterval *RateInterval - UnitInfo *UnitInfo - CompressFactor int - paid bool -} - -// Holds the minute information related to a specified timespan -type UnitInfo struct { - DestinationId string - Quantity float64 - TOR string - //Price float64 -} - -func (mi *UnitInfo) Equal(other *UnitInfo) bool { - return mi.DestinationId == other.DestinationId && - mi.Quantity == other.Quantity && - mi.TOR == other.TOR + Duration time.Duration + Cost float64 + BalanceInfo *DebitInfo // need more than one for units with cost + CompressFactor int + paid bool } // Holds information about the balance that made a specific payment -type BalanceInfo struct { - UnitBalanceUuid string - MoneyBalanceUuid string - AccountId string // used when debited from shared balance +type DebitInfo struct { + Unit *UnitInfo + Monetary *MonetaryInfo + AccountID string // used when debited from shared balance } -func (bi *BalanceInfo) Equal(other *BalanceInfo) bool { - return bi.UnitBalanceUuid == other.UnitBalanceUuid && - bi.MoneyBalanceUuid == other.MoneyBalanceUuid && - bi.AccountId == other.AccountId +func (di *DebitInfo) Equal(other *DebitInfo) bool { + return di.Unit.Equal(other.Unit) && + di.Monetary.Equal(other.Monetary) && + di.AccountID == other.AccountID +} + +func (di *DebitInfo) Clone() *DebitInfo { + nDi := &DebitInfo{ + AccountID: di.AccountID, + } + if di.Unit != nil { + nDi.Unit = di.Unit.Clone() + } + if di.Monetary != nil { + nDi.Monetary = di.Monetary.Clone() + } + return nDi +} + +type MonetaryInfo struct { + UUID string + ID string + Value float64 + RateInterval *RateInterval +} + +func (mi *MonetaryInfo) Clone() *MonetaryInfo { + newMi := *mi + return &newMi +} + +func (mi *MonetaryInfo) Equal(other *MonetaryInfo) bool { + if mi == nil && other == nil { + return true + } + if mi == nil || other == nil { + return false + } + return mi.UUID == other.UUID && + reflect.DeepEqual(mi.RateInterval, other.RateInterval) +} + +type UnitInfo struct { + UUID string + ID string + Value float64 + DestinationID string + Consumed float64 + TOR string + RateInterval *RateInterval +} + +func (ui *UnitInfo) Clone() *UnitInfo { + newUi := *ui + return &newUi +} + +func (ui *UnitInfo) Equal(other *UnitInfo) bool { + if ui == nil && other == nil { + return true + } + if ui == nil || other == nil { + return false + } + return ui.UUID == other.UUID && + ui.DestinationID == other.DestinationID && + ui.Consumed == other.Consumed && + ui.TOR == other.TOR && + reflect.DeepEqual(ui.RateInterval, other.RateInterval) } type TimeSpans []*TimeSpan @@ -212,22 +262,20 @@ func (tss *TimeSpans) Decompress() { // must be pointer receiver } func (incr *Increment) Clone() *Increment { - nIncr := &Increment{ - Duration: incr.Duration, - Cost: incr.Cost, - BalanceRateInterval: incr.BalanceRateInterval, - UnitInfo: incr.UnitInfo, - BalanceInfo: incr.BalanceInfo, + nInc := &Increment{ + Duration: incr.Duration, + Cost: incr.Cost, } - return nIncr + if incr.BalanceInfo != nil { + nInc.BalanceInfo = incr.BalanceInfo.Clone() + } + return nInc } func (incr *Increment) Equal(other *Increment) bool { return incr.Duration == other.Duration && incr.Cost == other.Cost && - ((incr.BalanceInfo == nil && other.BalanceInfo == nil) || incr.BalanceInfo.Equal(other.BalanceInfo)) && - ((incr.BalanceRateInterval == nil && other.BalanceRateInterval == nil) || reflect.DeepEqual(incr.BalanceRateInterval, other.BalanceRateInterval)) && - ((incr.UnitInfo == nil && other.UnitInfo == nil) || incr.UnitInfo.Equal(other.UnitInfo)) + ((incr.BalanceInfo == nil && other.BalanceInfo == nil) || incr.BalanceInfo.Equal(other.BalanceInfo)) } func (incr *Increment) GetCompressFactor() int { @@ -260,6 +308,14 @@ func (incs *Increments) Compress() { // must be pointer receiver cIncrs = append(cIncrs, incr) } else { cIncrs[len(cIncrs)-1].CompressFactor++ + if cIncrs[len(cIncrs)-1].BalanceInfo != nil && incr.BalanceInfo != nil { + if cIncrs[len(cIncrs)-1].BalanceInfo.Monetary != nil && incr.BalanceInfo.Monetary != nil { + cIncrs[len(cIncrs)-1].BalanceInfo.Monetary.Value = incr.BalanceInfo.Monetary.Value + } + if cIncrs[len(cIncrs)-1].BalanceInfo.Unit != nil && incr.BalanceInfo.Unit != nil { + cIncrs[len(cIncrs)-1].BalanceInfo.Unit.Value = incr.BalanceInfo.Unit.Value + } + } } } *incs = cIncrs @@ -268,8 +324,19 @@ func (incs *Increments) Compress() { // must be pointer receiver func (incs *Increments) Decompress() { // must be pointer receiver var cIncrs Increments for _, cIncr := range *incs { - for i := 0; i < cIncr.GetCompressFactor(); i++ { - cIncrs = append(cIncrs, cIncr.Clone()) + cf := cIncr.GetCompressFactor() + for i := 0; i < cf; i++ { + incr := cIncr.Clone() + // set right Values + if incr.BalanceInfo != nil { + if incr.BalanceInfo.Monetary != nil { + incr.BalanceInfo.Monetary.Value += (float64(cf-(i+1)) * incr.Cost) + } + if incr.BalanceInfo.Unit != nil { + incr.BalanceInfo.Unit.Value += (float64(cf-(i+1)) * incr.BalanceInfo.Unit.Consumed) + } + } + cIncrs = append(cIncrs, incr) } } *incs = cIncrs @@ -280,7 +347,7 @@ func (incs Increments) GetTotalCost() float64 { for _, increment := range incs { cost += increment.GetCost() } - return cost + return utils.Round(cost, globalRoundingDecimals, utils.ROUNDING_MIDDLE) } func (incs Increments) Length() (length int) { @@ -321,15 +388,14 @@ func (ts *TimeSpan) SetRateInterval(interval *RateInterval) { // Returns the cost of the timespan according to the relevant cost interval. // It also sets the Cost field of this timespan (used for refund on session // manager debit loop where the cost cannot be recalculated) -func (ts *TimeSpan) calculateCost() float64 { +func (ts *TimeSpan) CalculateCost() float64 { if ts.Increments.Length() == 0 { if ts.RateInterval == nil { return 0 } return ts.RateInterval.GetCost(ts.GetDuration(), ts.GetGroupStart()) } else { - cost := ts.Increments.GetTotalCost() - return utils.Round(cost, globalRoundingDecimals, utils.ROUNDING_MIDDLE) + return ts.Increments.GetTotalCost() * float64(ts.GetCompressFactor()) } } @@ -348,17 +414,17 @@ func (ts *TimeSpan) createIncrementsSlice() { ts.Increments = make([]*Increment, 0) // create rated units series _, rateIncrement, _ := ts.RateInterval.GetRateParameters(ts.GetGroupStart()) - // we will use the cost calculated cost and devide by nb of increments + // we will use the calculated cost and devide by nb of increments // because ts cost is rounded //incrementCost := rate / rateUnit.Seconds() * rateIncrement.Seconds() nbIncrements := int(ts.GetDuration() / rateIncrement) - incrementCost := ts.calculateCost() / float64(nbIncrements) - incrementCost = utils.Round(incrementCost, ts.RateInterval.Rating.RoundingDecimals, ts.RateInterval.Rating.RoundingMethod) + incrementCost := ts.CalculateCost() / float64(nbIncrements) + incrementCost = utils.Round(incrementCost, globalRoundingDecimals, utils.ROUNDING_MIDDLE) for s := 0; s < nbIncrements; s++ { inc := &Increment{ Duration: rateIncrement, Cost: incrementCost, - BalanceInfo: &BalanceInfo{}, + BalanceInfo: &DebitInfo{}, } ts.Increments = append(ts.Increments, inc) } diff --git a/engine/timespans_test.go b/engine/timespans_test.go index 47b64315d..f8fb19dcd 100644 --- a/engine/timespans_test.go +++ b/engine/timespans_test.go @@ -214,7 +214,7 @@ func TestTSTimespanGetCost(t *testing.T) { t1 := time.Date(2012, time.February, 5, 17, 45, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 5, 17, 55, 0, 0, time.UTC) ts1 := TimeSpan{TimeStart: t1, TimeEnd: t2} - if ts1.calculateCost() != 0 { + if ts1.CalculateCost() != 0 { t.Error("No interval and still kicking") } ts1.SetRateInterval( @@ -223,12 +223,12 @@ func TestTSTimespanGetCost(t *testing.T) { Rating: &RIRate{Rates: RateGroups{&Rate{0, 1.0, 1 * time.Second, 1 * time.Second}}}, }, ) - if ts1.calculateCost() != 600 { + if ts1.CalculateCost() != 600 { t.Error("Expected 10 got ", ts1.Cost) } ts1.RateInterval = nil ts1.SetRateInterval(&RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{0, 1.0, 1 * time.Second, 60 * time.Second}}}}) - if ts1.calculateCost() != 10 { + if ts1.CalculateCost() != 10 { t.Error("Expected 6000 got ", ts1.Cost) } } @@ -239,8 +239,8 @@ func TestTSTimespanGetCostIntervals(t *testing.T) { for i := 0; i < 11; i++ { ts.Increments[i] = &Increment{Cost: 0.02} } - if ts.calculateCost() != 0.22 { - t.Error("Error caclulating timespan cost: ", ts.calculateCost()) + if ts.CalculateCost() != 0.22 { + t.Error("Error caclulating timespan cost: ", ts.CalculateCost()) } } @@ -742,7 +742,7 @@ func TestTSTimespanCreateIncrements(t *testing.T) { if len(ts.Increments) != 3 { t.Error("Error creating increment slice: ", len(ts.Increments)) } - if len(ts.Increments) < 3 || ts.Increments[2].Cost != 20.07 { + if len(ts.Increments) < 3 || ts.Increments[2].Cost != 20.066667 { t.Error("Wrong second slice: ", ts.Increments[2].Cost) } } @@ -1510,50 +1510,55 @@ func TestTSIncrementsCompressDecompress(t *testing.T) { &TimeSpan{ Increments: Increments{ &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 2, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", Value: 25, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 98}, + AccountID: "3"}, }, &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 2, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", Value: 24, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 96}, + AccountID: "3"}, }, &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 2, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", Value: 23, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 94}, + AccountID: "3"}, }, &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 1111 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 2, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", Value: 22, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 1111 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 92}, + AccountID: "3"}, }, &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 2, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", Value: 21, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 90}, + AccountID: "3"}, }, }, }, } tss.Compress() if len(tss[0].Increments) != 3 { - t.Error("Error compressing timespan: ", tss[0].Increments) + t.Error("Error compressing timespan: ", utils.ToIJSON(tss[0])) } tss.Decompress() if len(tss[0].Increments) != 5 { - t.Error("Error decompressing timespans: ", tss[0].Increments) + t.Error("Error decompressing timespans: ", utils.ToIJSON(tss[0])) } } @@ -1562,39 +1567,44 @@ func TestTSMultipleIncrementsCompressDecompress(t *testing.T) { &TimeSpan{ Increments: Increments{ &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 10.4, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2"}, + AccountID: "3"}, }, &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 10.4, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2"}, + AccountID: "3"}, }, &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 10.4, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2"}, + AccountID: "3"}, }, &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 1111 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 10.4, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 1111 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2"}, + AccountID: "3"}, }, &Increment{ - Duration: time.Minute, - Cost: 10.4, - BalanceInfo: &BalanceInfo{"1", "2", "3"}, - BalanceRateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}, - UnitInfo: &UnitInfo{"1", 2.3, utils.VOICE}, + Duration: time.Minute, + Cost: 10.4, + BalanceInfo: &DebitInfo{ + Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2"}, + AccountID: "3"}, }, }, }, diff --git a/engine/tp_reader.go b/engine/tp_reader.go index 517606d95..bf662a335 100644 --- a/engine/tp_reader.go +++ b/engine/tp_reader.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/cgrates/cgrates/structmatcher" "github.com/cgrates/cgrates/utils" ) @@ -506,28 +507,79 @@ func (tpr *TpReader) LoadActions() (err error) { for tag, tpacts := range storActs { acts := make([]*Action, len(tpacts)) for idx, tpact := range tpacts { + // check filter field + if len(tpact.Filter) > 0 { + if _, err := structmatcher.NewStructMatcher(tpact.Filter); err != nil { + return fmt.Errorf("error parsing action %s filter field: %v", tag, err) + } + } acts[idx] = &Action{ - Id: tag + strconv.Itoa(idx), - ActionType: tpact.Identifier, - BalanceType: tpact.BalanceType, + Id: tag + strconv.Itoa(idx), + ActionType: tpact.Identifier, + //BalanceType: tpact.BalanceType, Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, Filter: tpact.Filter, - Balance: &Balance{ - Id: tpact.BalanceId, - Value: tpact.Units, - Weight: tpact.BalanceWeight, - RatingSubject: tpact.RatingSubject, - Categories: utils.ParseStringMap(tpact.Categories), - Directions: utils.ParseStringMap(tpact.Directions), - DestinationIds: utils.ParseStringMap(tpact.DestinationIds), - SharedGroups: utils.ParseStringMap(tpact.SharedGroups), - TimingIDs: utils.ParseStringMap(tpact.TimingTags), - Blocker: tpact.BalanceBlocker, - Disabled: tpact.BalanceDisabled, - }, + Balance: &BalanceFilter{}, } + if tpact.BalanceId != "" && tpact.BalanceId != utils.ANY { + acts[idx].Balance.ID = utils.StringPointer(tpact.BalanceId) + } + if tpact.BalanceType != "" && tpact.BalanceType != utils.ANY { + acts[idx].Balance.Type = utils.StringPointer(tpact.BalanceType) + } + + if tpact.Units != "" && tpact.Units != utils.ANY { + u, err := strconv.ParseFloat(tpact.Units, 64) + if err != nil { + return err + } + acts[idx].Balance.Value = utils.Float64Pointer(u) + } + + if tpact.BalanceWeight != "" && tpact.BalanceWeight != utils.ANY { + u, err := strconv.ParseFloat(tpact.BalanceWeight, 64) + if err != nil { + return err + } + acts[idx].Balance.Weight = utils.Float64Pointer(u) + } + + if tpact.RatingSubject != "" && tpact.RatingSubject != utils.ANY { + acts[idx].Balance.RatingSubject = utils.StringPointer(tpact.RatingSubject) + } + + if tpact.Categories != "" && tpact.Categories != utils.ANY { + acts[idx].Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(tpact.Categories)) + } + if tpact.Directions != "" && tpact.Directions != utils.ANY { + acts[idx].Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(tpact.Directions)) + } + if tpact.DestinationIds != "" && tpact.DestinationIds != utils.ANY { + acts[idx].Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(tpact.DestinationIds)) + } + if tpact.SharedGroups != "" && tpact.SharedGroups != utils.ANY { + acts[idx].Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(tpact.SharedGroups)) + } + if tpact.TimingTags != "" && tpact.TimingTags != utils.ANY { + acts[idx].Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(tpact.TimingTags)) + } + if tpact.BalanceBlocker != "" && tpact.BalanceBlocker != utils.ANY { + u, err := strconv.ParseBool(tpact.BalanceBlocker) + if err != nil { + return err + } + acts[idx].Balance.Blocker = utils.BoolPointer(u) + } + if tpact.BalanceDisabled != "" && tpact.BalanceDisabled != utils.ANY { + u, err := strconv.ParseBool(tpact.BalanceDisabled) + if err != nil { + return err + } + acts[idx].Balance.Disabled = utils.BoolPointer(u) + } + // load action timings from tags if tpact.TimingTags != "" { timingIds := strings.Split(tpact.TimingTags, utils.INFIELD_SEP) @@ -618,7 +670,14 @@ func (tpr *TpReader) LoadActionTriggers() (err error) { for key, atrsLst := range storAts { atrs := make([]*ActionTrigger, len(atrsLst)) for idx, atr := range atrsLst { - balanceExpirationDate, _ := utils.ParseTimeDetectLayout(atr.BalanceExpirationDate, tpr.timezone) + expirationDate, err := utils.ParseTimeDetectLayout(atr.ExpirationDate, tpr.timezone) + if err != nil { + return err + } + activationDate, err := utils.ParseTimeDetectLayout(atr.ActivationDate, tpr.timezone) + if err != nil { + return err + } minSleep, err := utils.ParseDurationWithSecs(atr.MinSleep) if err != nil { return err @@ -627,27 +686,73 @@ func (tpr *TpReader) LoadActionTriggers() (err error) { atr.UniqueID = utils.GenUUID() } atrs[idx] = &ActionTrigger{ - ID: key, - UniqueID: atr.UniqueID, - ThresholdType: atr.ThresholdType, - ThresholdValue: atr.ThresholdValue, - Recurrent: atr.Recurrent, - MinSleep: minSleep, - BalanceId: atr.BalanceId, - BalanceType: atr.BalanceType, - BalanceDirections: utils.ParseStringMap(atr.BalanceDirections), - BalanceDestinationIds: utils.ParseStringMap(atr.BalanceDestinationIds), - BalanceWeight: atr.BalanceWeight, - BalanceExpirationDate: balanceExpirationDate, - BalanceTimingTags: utils.ParseStringMap(atr.BalanceTimingTags), - BalanceRatingSubject: atr.BalanceRatingSubject, - BalanceCategories: utils.ParseStringMap(atr.BalanceCategories), - BalanceSharedGroups: utils.ParseStringMap(atr.BalanceSharedGroups), - BalanceBlocker: atr.BalanceBlocker, - BalanceDisabled: atr.BalanceDisabled, - Weight: atr.Weight, - ActionsId: atr.ActionsId, - MinQueuedItems: atr.MinQueuedItems, + ID: key, + UniqueID: atr.UniqueID, + ThresholdType: atr.ThresholdType, + ThresholdValue: atr.ThresholdValue, + Recurrent: atr.Recurrent, + MinSleep: minSleep, + ExpirationDate: expirationDate, + ActivationDate: activationDate, + Balance: &BalanceFilter{}, + Weight: atr.Weight, + ActionsID: atr.ActionsId, + MinQueuedItems: atr.MinQueuedItems, + } + if atr.BalanceId != "" && atr.BalanceId != utils.ANY { + atrs[idx].Balance.ID = utils.StringPointer(atr.BalanceId) + } + + if atr.BalanceType != "" && atr.BalanceType != utils.ANY { + atrs[idx].Balance.Type = utils.StringPointer(atr.BalanceType) + } + + if atr.BalanceWeight != "" && atr.BalanceWeight != utils.ANY { + u, err := strconv.ParseFloat(atr.BalanceWeight, 64) + if err != nil { + return err + } + atrs[idx].Balance.Weight = utils.Float64Pointer(u) + } + if atr.BalanceExpirationDate != "" && atr.BalanceExpirationDate != utils.ANY && atr.ExpirationDate != utils.UNLIMITED { + u, err := utils.ParseTimeDetectLayout(atr.BalanceExpirationDate, tpr.timezone) + if err != nil { + return err + } + atrs[idx].Balance.ExpirationDate = utils.TimePointer(u) + } + if atr.BalanceRatingSubject != "" && atr.BalanceRatingSubject != utils.ANY { + atrs[idx].Balance.RatingSubject = utils.StringPointer(atr.BalanceRatingSubject) + } + + if atr.BalanceCategories != "" && atr.BalanceCategories != utils.ANY { + atrs[idx].Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceCategories)) + } + if atr.BalanceDirections != "" && atr.BalanceDirections != utils.ANY { + atrs[idx].Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceDirections)) + } + if atr.BalanceDestinationIds != "" && atr.BalanceDestinationIds != utils.ANY { + atrs[idx].Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceDestinationIds)) + } + if atr.BalanceSharedGroups != "" && atr.BalanceSharedGroups != utils.ANY { + atrs[idx].Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceSharedGroups)) + } + if atr.BalanceTimingTags != "" && atr.BalanceTimingTags != utils.ANY { + atrs[idx].Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceTimingTags)) + } + if atr.BalanceBlocker != "" && atr.BalanceBlocker != utils.ANY { + u, err := strconv.ParseBool(atr.BalanceBlocker) + if err != nil { + return err + } + atrs[idx].Balance.Blocker = utils.BoolPointer(u) + } + if atr.BalanceDisabled != "" && atr.BalanceDisabled != utils.ANY { + u, err := strconv.ParseBool(atr.BalanceDisabled) + if err != nil { + return err + } + atrs[idx].Balance.Disabled = utils.BoolPointer(u) } } tpr.actionsTriggers[key] = atrs @@ -667,11 +772,11 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error } for _, accountAction := range storAas { id := accountAction.KeyId() - var actionsIds []string // collects action ids + var actionIDs []string // collects action ids // action timings if accountAction.ActionPlanId != "" { // get old userBalanceIds - exitingAccountIds := make(map[string]struct{}) + exitingAccountIds := make(utils.StringMap) existingActionPlan, err := tpr.ratingStorage.GetActionPlan(accountAction.ActionPlanId, true) if err == nil && existingActionPlan != nil { exitingAccountIds = existingActionPlan.AccountIDs @@ -732,8 +837,8 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error ActionsID: at.ActionsId, }) // collect action ids from timings - actionsIds = append(actionsIds, at.ActionsId) - exitingAccountIds[id] = struct{}{} + actionIDs = append(actionIDs, at.ActionsId) + exitingAccountIds[id] = true actionPlan.AccountIDs = exitingAccountIds } @@ -753,7 +858,7 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error } } // write action plan - err = tpr.ratingStorage.SetActionPlan(accountAction.ActionPlanId, actionPlan) + err = tpr.ratingStorage.SetActionPlan(accountAction.ActionPlanId, actionPlan, false) if err != nil { return errors.New(err.Error() + " (SetActionPlan): " + accountAction.ActionPlanId) } @@ -774,33 +879,80 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error atrsMap := make(map[string][]*ActionTrigger) for key, atrsLst := range atrs { atrs := make([]*ActionTrigger, len(atrsLst)) - for idx, apiAtr := range atrsLst { - minSleep, _ := utils.ParseDurationWithSecs(apiAtr.MinSleep) - expTime, _ := utils.ParseDate(apiAtr.BalanceExpirationDate) - if apiAtr.UniqueID == "" { - apiAtr.UniqueID = utils.GenUUID() + for idx, atr := range atrsLst { + minSleep, _ := utils.ParseDurationWithSecs(atr.MinSleep) + expTime, _ := utils.ParseTimeDetectLayout(atr.ExpirationDate, tpr.timezone) + actTime, _ := utils.ParseTimeDetectLayout(atr.ActivationDate, tpr.timezone) + if atr.UniqueID == "" { + atr.UniqueID = utils.GenUUID() } atrs[idx] = &ActionTrigger{ - ID: key, - UniqueID: apiAtr.UniqueID, - ThresholdType: apiAtr.ThresholdType, - ThresholdValue: apiAtr.ThresholdValue, - Recurrent: apiAtr.Recurrent, - MinSleep: minSleep, - BalanceId: apiAtr.BalanceId, - BalanceType: apiAtr.BalanceType, - BalanceDirections: utils.ParseStringMap(apiAtr.BalanceDirections), - BalanceDestinationIds: utils.ParseStringMap(apiAtr.BalanceDestinationIds), - BalanceWeight: apiAtr.BalanceWeight, - BalanceExpirationDate: expTime, - BalanceTimingTags: utils.ParseStringMap(apiAtr.BalanceTimingTags), - BalanceRatingSubject: apiAtr.BalanceRatingSubject, - BalanceCategories: utils.ParseStringMap(apiAtr.BalanceCategories), - BalanceSharedGroups: utils.ParseStringMap(apiAtr.BalanceSharedGroups), - BalanceBlocker: apiAtr.BalanceBlocker, - BalanceDisabled: apiAtr.BalanceDisabled, - Weight: apiAtr.Weight, - ActionsId: apiAtr.ActionsId, + ID: key, + UniqueID: atr.UniqueID, + ThresholdType: atr.ThresholdType, + ThresholdValue: atr.ThresholdValue, + Recurrent: atr.Recurrent, + MinSleep: minSleep, + ExpirationDate: expTime, + ActivationDate: actTime, + Balance: &BalanceFilter{}, + Weight: atr.Weight, + ActionsID: atr.ActionsId, + } + if atr.BalanceId != "" && atr.BalanceId != utils.ANY { + atrs[idx].Balance.ID = utils.StringPointer(atr.BalanceId) + } + + if atr.BalanceType != "" && atr.BalanceType != utils.ANY { + atrs[idx].Balance.Type = utils.StringPointer(atr.BalanceType) + } + + if atr.BalanceWeight != "" && atr.BalanceWeight != utils.ANY { + u, err := strconv.ParseFloat(atr.BalanceWeight, 64) + if err != nil { + return err + } + atrs[idx].Balance.Weight = utils.Float64Pointer(u) + } + if atr.BalanceExpirationDate != "" && atr.BalanceExpirationDate != utils.ANY && atr.ExpirationDate != utils.UNLIMITED { + u, err := utils.ParseTimeDetectLayout(atr.BalanceExpirationDate, tpr.timezone) + if err != nil { + return err + } + atrs[idx].Balance.ExpirationDate = utils.TimePointer(u) + } + if atr.BalanceRatingSubject != "" && atr.BalanceRatingSubject != utils.ANY { + atrs[idx].Balance.RatingSubject = utils.StringPointer(atr.BalanceRatingSubject) + } + + if atr.BalanceCategories != "" && atr.BalanceCategories != utils.ANY { + atrs[idx].Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceCategories)) + } + if atr.BalanceDirections != "" && atr.BalanceDirections != utils.ANY { + atrs[idx].Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceDirections)) + } + if atr.BalanceDestinationIds != "" && atr.BalanceDestinationIds != utils.ANY { + atrs[idx].Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceDestinationIds)) + } + if atr.BalanceSharedGroups != "" && atr.BalanceSharedGroups != utils.ANY { + atrs[idx].Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceSharedGroups)) + } + if atr.BalanceTimingTags != "" && atr.BalanceTimingTags != utils.ANY { + atrs[idx].Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceTimingTags)) + } + if atr.BalanceBlocker != "" && atr.BalanceBlocker != utils.ANY { + u, err := strconv.ParseBool(atr.BalanceBlocker) + if err != nil { + return err + } + atrs[idx].Balance.Blocker = utils.BoolPointer(u) + } + if atr.BalanceDisabled != "" && atr.BalanceDisabled != utils.ANY { + u, err := strconv.ParseBool(atr.BalanceDisabled) + if err != nil { + return err + } + atrs[idx].Balance.Disabled = utils.BoolPointer(u) } } atrsMap[key] = atrs @@ -808,7 +960,7 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error actionTriggers = atrsMap[accountAction.ActionTriggersId] // collect action ids from triggers for _, atr := range actionTriggers { - actionsIds = append(actionsIds, atr.ActionsId) + actionIDs = append(actionIDs, atr.ActionsID) } // write action triggers err = tpr.ratingStorage.SetActionTriggers(accountAction.ActionTriggersId, actionTriggers) @@ -818,8 +970,8 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error } // actions - acts := make(map[string][]*Action) - for _, actId := range actionsIds { + facts := make(map[string][]*Action) + for _, actId := range actionIDs { tpas, err := tpr.lr.GetTpActions(tpr.tpid, actId) if err != nil { return err @@ -829,36 +981,103 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error return err } for tag, tpacts := range as { - enacts := make([]*Action, len(tpacts)) + acts := make([]*Action, len(tpacts)) for idx, tpact := range tpacts { - enacts[idx] = &Action{ - Id: tag + strconv.Itoa(idx), - ActionType: tpact.Identifier, - BalanceType: tpact.BalanceType, + // check filter field + if len(tpact.Filter) > 0 { + if _, err := structmatcher.NewStructMatcher(tpact.Filter); err != nil { + return fmt.Errorf("error parsing action %s filter field: %v", tag, err) + } + } + acts[idx] = &Action{ + Id: tag + strconv.Itoa(idx), + ActionType: tpact.Identifier, + //BalanceType: tpact.BalanceType, Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, Filter: tpact.Filter, - Balance: &Balance{ - Id: tpact.BalanceId, - Value: tpact.Units, - Weight: tpact.BalanceWeight, - RatingSubject: tpact.RatingSubject, - Categories: utils.ParseStringMap(tpact.Categories), - Directions: utils.ParseStringMap(tpact.Directions), - DestinationIds: utils.ParseStringMap(tpact.DestinationIds), - SharedGroups: utils.ParseStringMap(tpact.SharedGroups), - TimingIDs: utils.ParseStringMap(tpact.TimingTags), - Blocker: tpact.BalanceBlocker, - Disabled: tpact.BalanceDisabled, - }, + Balance: &BalanceFilter{}, + } + if tpact.BalanceId != "" && tpact.BalanceId != utils.ANY { + acts[idx].Balance.ID = utils.StringPointer(tpact.BalanceId) + } + if tpact.BalanceType != "" && tpact.BalanceType != utils.ANY { + acts[idx].Balance.Type = utils.StringPointer(tpact.BalanceType) + } + + if tpact.Units != "" && tpact.Units != utils.ANY { + u, err := strconv.ParseFloat(tpact.Units, 64) + if err != nil { + return err + } + acts[idx].Balance.Value = utils.Float64Pointer(u) + } + + if tpact.BalanceWeight != "" && tpact.BalanceWeight != utils.ANY { + u, err := strconv.ParseFloat(tpact.BalanceWeight, 64) + if err != nil { + return err + } + acts[idx].Balance.Weight = utils.Float64Pointer(u) + } + if tpact.RatingSubject != "" && tpact.RatingSubject != utils.ANY { + acts[idx].Balance.RatingSubject = utils.StringPointer(tpact.RatingSubject) + } + + if tpact.Categories != "" && tpact.Categories != utils.ANY { + acts[idx].Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(tpact.Categories)) + } + if tpact.Directions != "" && tpact.Directions != utils.ANY { + acts[idx].Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(tpact.Directions)) + } + if tpact.DestinationIds != "" && tpact.DestinationIds != utils.ANY { + acts[idx].Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(tpact.DestinationIds)) + } + if tpact.SharedGroups != "" && tpact.SharedGroups != utils.ANY { + acts[idx].Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(tpact.SharedGroups)) + } + if tpact.TimingTags != "" && tpact.TimingTags != utils.ANY { + acts[idx].Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(tpact.TimingTags)) + } + if tpact.BalanceBlocker != "" && tpact.BalanceBlocker != utils.ANY { + u, err := strconv.ParseBool(tpact.BalanceBlocker) + if err != nil { + return err + } + acts[idx].Balance.Blocker = utils.BoolPointer(u) + } + if tpact.BalanceDisabled != "" && tpact.BalanceDisabled != utils.ANY { + u, err := strconv.ParseBool(tpact.BalanceDisabled) + if err != nil { + return err + } + acts[idx].Balance.Disabled = utils.BoolPointer(u) + } + // load action timings from tags + if tpact.TimingTags != "" { + timingIds := strings.Split(tpact.TimingTags, utils.INFIELD_SEP) + for _, timingID := range timingIds { + if timing, found := tpr.timings[timingID]; found { + acts[idx].Balance.Timings = append(acts[idx].Balance.Timings, &RITiming{ + Years: timing.Years, + Months: timing.Months, + MonthDays: timing.MonthDays, + WeekDays: timing.WeekDays, + StartTime: timing.StartTime, + EndTime: timing.EndTime, + }) + } else { + return fmt.Errorf("could not find timing: %v", timingID) + } + } } } - acts[tag] = enacts + facts[tag] = acts } } // write actions - for k, as := range acts { + for k, as := range facts { err = tpr.ratingStorage.SetActions(k, as) if err != nil { return err @@ -867,7 +1086,7 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error ub, err := tpr.accountingStorage.GetAccount(id) if err != nil { ub = &Account{ - Id: id, + ID: id, } } ub.ActionTriggers = actionTriggers @@ -902,22 +1121,24 @@ func (tpr *TpReader) LoadAccountActions() (err error) { } } ub := &Account{ - Id: aa.KeyId(), + ID: aa.KeyId(), ActionTriggers: aTriggers, AllowNegative: aa.AllowNegative, Disabled: aa.Disabled, } ub.InitCounters() tpr.accountActions[aa.KeyId()] = ub - actionPlan, exists := tpr.actionPlans[aa.ActionPlanId] - if !exists { - log.Printf("could not get action plan for tag %v", aa.ActionPlanId) - // must not continue here + if aa.ActionPlanId != "" { + actionPlan, exists := tpr.actionPlans[aa.ActionPlanId] + if !exists { + log.Printf("could not get action plan for tag %v", aa.ActionPlanId) + // must not continue here + } + if actionPlan.AccountIDs == nil { + actionPlan.AccountIDs = make(utils.StringMap) + } + actionPlan.AccountIDs[aa.KeyId()] = true } - if actionPlan.AccountIDs == nil { - actionPlan.AccountIDs = make(map[string]struct{}) - } - actionPlan.AccountIDs[aa.KeyId()] = struct{}{} } return nil } @@ -973,7 +1194,7 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) { if err != nil { return err } - var actionsIds []string // collect action ids + var actionIDs []string // collect action ids for tag, tpStats := range storStats { for _, tpStat := range tpStats { var cs *CdrStats @@ -997,31 +1218,80 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) { for _, atrsLst := range atrsM { atrs := make([]*ActionTrigger, len(atrsLst)) - for idx, apiAtr := range atrsLst { - minSleep, _ := utils.ParseDurationWithSecs(apiAtr.MinSleep) - expTime, _ := utils.ParseDate(apiAtr.BalanceExpirationDate) - if apiAtr.UniqueID == "" { - apiAtr.UniqueID = utils.GenUUID() + for idx, atr := range atrsLst { + minSleep, _ := utils.ParseDurationWithSecs(atr.MinSleep) + expTime, _ := utils.ParseTimeDetectLayout(atr.ExpirationDate, tpr.timezone) + actTime, _ := utils.ParseTimeDetectLayout(atr.ActivationDate, tpr.timezone) + if atr.UniqueID == "" { + atr.UniqueID = utils.GenUUID() } atrs[idx] = &ActionTrigger{ - ID: triggerTag, - UniqueID: apiAtr.UniqueID, - ThresholdType: apiAtr.ThresholdType, - ThresholdValue: apiAtr.ThresholdValue, - Recurrent: apiAtr.Recurrent, - MinSleep: minSleep, - BalanceId: apiAtr.BalanceId, - BalanceType: apiAtr.BalanceType, - BalanceDirections: utils.ParseStringMap(apiAtr.BalanceDirections), - BalanceDestinationIds: utils.ParseStringMap(apiAtr.BalanceDestinationIds), - BalanceWeight: apiAtr.BalanceWeight, - BalanceExpirationDate: expTime, - BalanceRatingSubject: apiAtr.BalanceRatingSubject, - BalanceCategories: utils.ParseStringMap(apiAtr.BalanceCategories), - BalanceSharedGroups: utils.ParseStringMap(apiAtr.BalanceSharedGroups), - BalanceTimingTags: utils.ParseStringMap(apiAtr.BalanceTimingTags), - Weight: apiAtr.Weight, - ActionsId: apiAtr.ActionsId, + ID: triggerTag, + UniqueID: atr.UniqueID, + ThresholdType: atr.ThresholdType, + ThresholdValue: atr.ThresholdValue, + Recurrent: atr.Recurrent, + MinSleep: minSleep, + ExpirationDate: expTime, + ActivationDate: actTime, + Balance: &BalanceFilter{}, + Weight: atr.Weight, + ActionsID: atr.ActionsId, + } + if atr.BalanceId != "" && atr.BalanceId != utils.ANY { + atrs[idx].Balance.ID = utils.StringPointer(atr.BalanceId) + } + + if atr.BalanceType != "" && atr.BalanceType != utils.ANY { + atrs[idx].Balance.Type = utils.StringPointer(atr.BalanceType) + } + + if atr.BalanceWeight != "" && atr.BalanceWeight != utils.ANY { + u, err := strconv.ParseFloat(atr.BalanceWeight, 64) + if err != nil { + return err + } + atrs[idx].Balance.Weight = utils.Float64Pointer(u) + } + if atr.BalanceExpirationDate != "" && atr.BalanceExpirationDate != utils.ANY && atr.ExpirationDate != utils.UNLIMITED { + u, err := utils.ParseTimeDetectLayout(atr.BalanceExpirationDate, tpr.timezone) + if err != nil { + return err + } + atrs[idx].Balance.ExpirationDate = utils.TimePointer(u) + } + if atr.BalanceRatingSubject != "" && atr.BalanceRatingSubject != utils.ANY { + atrs[idx].Balance.RatingSubject = utils.StringPointer(atr.BalanceRatingSubject) + } + + if atr.BalanceCategories != "" && atr.BalanceCategories != utils.ANY { + atrs[idx].Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceCategories)) + } + if atr.BalanceDirections != "" && atr.BalanceDirections != utils.ANY { + atrs[idx].Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceDirections)) + } + if atr.BalanceDestinationIds != "" && atr.BalanceDestinationIds != utils.ANY { + atrs[idx].Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceDestinationIds)) + } + if atr.BalanceSharedGroups != "" && atr.BalanceSharedGroups != utils.ANY { + atrs[idx].Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceSharedGroups)) + } + if atr.BalanceTimingTags != "" && atr.BalanceTimingTags != utils.ANY { + atrs[idx].Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(atr.BalanceTimingTags)) + } + if atr.BalanceBlocker != "" && atr.BalanceBlocker != utils.ANY { + u, err := strconv.ParseBool(atr.BalanceBlocker) + if err != nil { + return err + } + atrs[idx].Balance.Blocker = utils.BoolPointer(u) + } + if atr.BalanceDisabled != "" && atr.BalanceDisabled != utils.ANY { + u, err := strconv.ParseBool(atr.BalanceDisabled) + if err != nil { + return err + } + atrs[idx].Balance.Disabled = utils.BoolPointer(u) } } tpr.actionsTriggers[triggerTag] = atrs @@ -1029,7 +1299,7 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) { } // collect action ids from triggers for _, atr := range tpr.actionsTriggers[triggerTag] { - actionsIds = append(actionsIds, atr.ActionsId) + actionIDs = append(actionIDs, atr.ActionsID) } } triggers, exists := tpr.actionsTriggers[triggerTag] @@ -1047,7 +1317,7 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) { } } // actions - for _, actId := range actionsIds { + for _, actId := range actionIDs { _, exists := tpr.actions[actId] if !exists { tpas, err := tpr.lr.GetTpActions(tpr.tpid, actId) @@ -1059,32 +1329,82 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) { return err } for tag, tpacts := range as { - enacts := make([]*Action, len(tpacts)) + acts := make([]*Action, len(tpacts)) for idx, tpact := range tpacts { - enacts[idx] = &Action{ - Id: tag + strconv.Itoa(idx), - ActionType: tpact.Identifier, - BalanceType: tpact.BalanceType, + // check filter field + if len(tpact.Filter) > 0 { + if _, err := structmatcher.NewStructMatcher(tpact.Filter); err != nil { + return fmt.Errorf("error parsing action %s filter field: %v", tag, err) + } + } + acts[idx] = &Action{ + Id: tag + strconv.Itoa(idx), + ActionType: tpact.Identifier, + //BalanceType: tpact.BalanceType, Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, Filter: tpact.Filter, - Balance: &Balance{ - Id: tpact.BalanceId, - Value: tpact.Units, - Weight: tpact.BalanceWeight, - RatingSubject: tpact.RatingSubject, - Categories: utils.ParseStringMap(tpact.Categories), - Directions: utils.ParseStringMap(tpact.Directions), - DestinationIds: utils.ParseStringMap(tpact.DestinationIds), - SharedGroups: utils.ParseStringMap(tpact.SharedGroups), - TimingIDs: utils.ParseStringMap(tpact.TimingTags), - Blocker: tpact.BalanceBlocker, - Disabled: tpact.BalanceDisabled, - }, + Balance: &BalanceFilter{}, } + if tpact.BalanceId != "" && tpact.BalanceId != utils.ANY { + acts[idx].Balance.ID = utils.StringPointer(tpact.BalanceId) + } + if tpact.BalanceType != "" && tpact.BalanceType != utils.ANY { + acts[idx].Balance.Type = utils.StringPointer(tpact.BalanceType) + } + + if tpact.Units != "" && tpact.Units != utils.ANY { + u, err := strconv.ParseFloat(tpact.Units, 64) + if err != nil { + return err + } + acts[idx].Balance.Value = utils.Float64Pointer(u) + } + + if tpact.BalanceWeight != "" && tpact.BalanceWeight != utils.ANY { + u, err := strconv.ParseFloat(tpact.BalanceWeight, 64) + if err != nil { + return err + } + acts[idx].Balance.Weight = utils.Float64Pointer(u) + } + if tpact.RatingSubject != "" && tpact.RatingSubject != utils.ANY { + acts[idx].Balance.RatingSubject = utils.StringPointer(tpact.RatingSubject) + } + + if tpact.Categories != "" && tpact.Categories != utils.ANY { + acts[idx].Balance.Categories = utils.StringMapPointer(utils.ParseStringMap(tpact.Categories)) + } + if tpact.Directions != "" && tpact.Directions != utils.ANY { + acts[idx].Balance.Directions = utils.StringMapPointer(utils.ParseStringMap(tpact.Directions)) + } + if tpact.DestinationIds != "" && tpact.DestinationIds != utils.ANY { + acts[idx].Balance.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(tpact.DestinationIds)) + } + if tpact.SharedGroups != "" && tpact.SharedGroups != utils.ANY { + acts[idx].Balance.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(tpact.SharedGroups)) + } + if tpact.TimingTags != "" && tpact.TimingTags != utils.ANY { + acts[idx].Balance.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(tpact.TimingTags)) + } + if tpact.BalanceBlocker != "" && tpact.BalanceBlocker != utils.ANY { + u, err := strconv.ParseBool(tpact.BalanceBlocker) + if err != nil { + return err + } + acts[idx].Balance.Blocker = utils.BoolPointer(u) + } + if tpact.BalanceDisabled != "" && tpact.BalanceDisabled != utils.ANY { + u, err := strconv.ParseBool(tpact.BalanceDisabled) + if err != nil { + return err + } + acts[idx].Balance.Disabled = utils.BoolPointer(u) + } + } - tpr.actions[tag] = enacts + tpr.actions[tag] = acts } } } @@ -1380,7 +1700,7 @@ func (tpr *TpReader) WriteToDatabase(flush, verbose bool) (err error) { } } } - err = tpr.ratingStorage.SetActionPlan(k, ap) + err = tpr.ratingStorage.SetActionPlan(k, ap, false) if err != nil { return err } @@ -1445,7 +1765,7 @@ func (tpr *TpReader) WriteToDatabase(flush, verbose bool) (err error) { return err } if verbose { - log.Println("\t", ub.Id) + log.Println("\t", ub.ID) } } if verbose { diff --git a/engine/units_counter.go b/engine/units_counter.go index a25602504..b4e7f6696 100644 --- a/engine/units_counter.go +++ b/engine/units_counter.go @@ -22,31 +22,65 @@ import "github.com/cgrates/cgrates/utils" // Amount of a trafic of a certain type type UnitCounter struct { - BalanceType string // *monetary/*voice/*sms/etc - CounterType string // *event or *balance - Balances BalanceChain // first balance is the general one (no destination) + CounterType string // *event or *balance + Counters CounterFilters // first balance is the general one (no destination) } -type UnitCounters []*UnitCounter +type CounterFilter struct { + Value float64 + Filter *BalanceFilter +} + +type CounterFilters []*CounterFilter + +func (cfs CounterFilters) HasCounter(cf *CounterFilter) bool { + for _, c := range cfs { + if c.Filter.Equal(cf.Filter) { + return true + } + } + return false +} + +// Returns true if the counters were of the same type +// Copies the value from old balances +func (uc *UnitCounter) CopyCounterValues(oldUc *UnitCounter) bool { + if uc.CounterType != oldUc.CounterType { // type check + return false + } + for _, c := range uc.Counters { + for _, oldC := range oldUc.Counters { + if c.Filter.Equal(oldC.Filter) { + c.Value = oldC.Value + break + } + } + } + return true +} + +type UnitCounters map[string][]*UnitCounter func (ucs UnitCounters) addUnits(amount float64, kind string, cc *CallCost, b *Balance) { - for _, uc := range ucs { + counters, found := ucs[kind] + if !found { + return + } + for _, uc := range counters { if uc == nil { // safeguard continue } - if uc.BalanceType != kind { - continue - } if uc.CounterType == "" { uc.CounterType = utils.COUNTER_EVENT } - for _, bal := range uc.Balances { - if uc.CounterType == utils.COUNTER_EVENT && cc != nil && bal.MatchCCFilter(cc) { - bal.AddValue(amount) + for _, c := range uc.Counters { + if uc.CounterType == utils.COUNTER_EVENT && cc != nil && cc.MatchCCFilter(c.Filter) { + c.Value += amount continue } - if uc.CounterType == utils.COUNTER_BALANCE && b != nil && b.MatchFilter(bal, true) { - bal.AddValue(amount) + + if uc.CounterType == utils.COUNTER_BALANCE && b != nil && b.MatchFilter(c.Filter, true) { + c.Value += amount continue } } @@ -55,16 +89,18 @@ func (ucs UnitCounters) addUnits(amount float64, kind string, cc *CallCost, b *B } func (ucs UnitCounters) resetCounters(a *Action) { - for _, uc := range ucs { - if uc == nil { // safeguard + for key, counters := range ucs { + if a != nil && a.Balance.Type != nil && a.Balance.GetType() != key { continue } - if a != nil && a.BalanceType != "" && a.BalanceType != uc.BalanceType { - continue - } - for _, b := range uc.Balances { - if a == nil || a.Balance == nil || b.MatchFilter(a.Balance, false) { - b.Value = 0 + for _, c := range counters { + if c == nil { // safeguard + continue + } + for _, cf := range c.Counters { + if a == nil || a.Balance == nil || cf.Filter.Equal(a.Balance) { + cf.Value = 0 + } } } } diff --git a/engine/units_counter_test.go b/engine/units_counter_test.go index f136c23b7..4e80006c0 100644 --- a/engine/units_counter_test.go +++ b/engine/units_counter_test.go @@ -26,22 +26,20 @@ import ( func TestUnitsCounterAddBalance(t *testing.T) { uc := &UnitCounter{ - BalanceType: utils.SMS, - Balances: BalanceChain{&Balance{Value: 1}, &Balance{Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}, + Counters: CounterFilters{&CounterFilter{Value: 1}, &CounterFilter{Filter: &BalanceFilter{Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT"))}}, &CounterFilter{Filter: &BalanceFilter{Weight: utils.Float64Pointer(10), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET"))}}}, } - UnitCounters{uc}.addUnits(20, utils.SMS, &CallCost{Destination: "test"}, nil) - if len(uc.Balances) != 3 { - t.Error("Error adding minute bucket: ", uc.Balances) + UnitCounters{utils.SMS: []*UnitCounter{uc}}.addUnits(20, utils.SMS, &CallCost{Destination: "test"}, nil) + if len(uc.Counters) != 3 { + t.Error("Error adding minute bucket: ", uc.Counters) } } func TestUnitsCounterAddBalanceExists(t *testing.T) { uc := &UnitCounter{ - BalanceType: utils.SMS, - Balances: BalanceChain{&Balance{Value: 1}, &Balance{Value: 10, Weight: 20, DestinationIds: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIds: utils.NewStringMap("RET")}}, + Counters: CounterFilters{&CounterFilter{Value: 1}, &CounterFilter{Value: 10, Filter: &BalanceFilter{Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT"))}}, &CounterFilter{Filter: &BalanceFilter{Weight: utils.Float64Pointer(10), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET"))}}}, } - UnitCounters{uc}.addUnits(5, utils.SMS, &CallCost{Destination: "0723"}, nil) - if len(uc.Balances) != 3 || uc.Balances[1].GetValue() != 15 { + UnitCounters{utils.SMS: []*UnitCounter{uc}}.addUnits(5, utils.SMS, &CallCost{Destination: "0723"}, nil) + if len(uc.Counters) != 3 || uc.Counters[1].Value != 15 { t.Error("Error adding minute bucket!") } } @@ -50,60 +48,75 @@ func TestUnitCountersCountAllMonetary(t *testing.T) { a := &Account{ ActionTriggers: ActionTriggers{ &ActionTrigger{ - UniqueID: "TestTR1", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR1", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR11", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR11", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR2", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR2", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR3", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR3", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR4", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR4", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR5", - ThresholdType: utils.TRIGGER_MAX_BALANCE, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR5", + ThresholdType: utils.TRIGGER_MAX_BALANCE, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, }, } a.InitCounters() a.UnitCounters.addUnits(10, utils.MONETARY, &CallCost{}, nil) - if len(a.UnitCounters) != 4 || - len(a.UnitCounters[0].Balances) != 2 || - a.UnitCounters[0].Balances[0].Value != 10 || - a.UnitCounters[0].Balances[1].Value != 10 { - for _, uc := range a.UnitCounters { - t.Logf("UC: %+v", uc) - for _, b := range uc.Balances { - t.Logf("B: %+v", b) + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.MONETARY][0].Counters) != 2 || + a.UnitCounters[utils.MONETARY][0].Counters[0].Value != 10 || + a.UnitCounters[utils.MONETARY][0].Counters[1].Value != 10 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, b := range uc.Counters { + t.Logf("B: %+v", b) + } } } t.Errorf("Error Initializing adding unit counters: %v", len(a.UnitCounters)) @@ -114,60 +127,74 @@ func TestUnitCountersCountAllMonetaryId(t *testing.T) { a := &Account{ ActionTriggers: ActionTriggers{ &ActionTrigger{ - UniqueID: "TestTR1", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR1", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR11", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 20, + UniqueID: "TestTR11", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(20), + }, }, &ActionTrigger{ - UniqueID: "TestTR2", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR2", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR3", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR3", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR4", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR4", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR5", - ThresholdType: utils.TRIGGER_MAX_BALANCE, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR5", + ThresholdType: utils.TRIGGER_MAX_BALANCE, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, }, } a.InitCounters() a.UnitCounters.addUnits(10, utils.MONETARY, nil, &Balance{Weight: 20, Directions: utils.NewStringMap(utils.OUT)}) - - if len(a.UnitCounters) != 4 || - len(a.UnitCounters[0].Balances) != 2 || - a.UnitCounters[0].Balances[0].Value != 0 || - a.UnitCounters[0].Balances[1].Value != 10 { - for _, uc := range a.UnitCounters { - t.Logf("UC: %+v", uc) - for _, b := range uc.Balances { - t.Logf("B: %+v", b) + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.MONETARY][0].Counters) != 2 || + a.UnitCounters[utils.MONETARY][0].Counters[0].Value != 0 || + a.UnitCounters[utils.MONETARY][0].Counters[1].Value != 10 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, b := range uc.Counters { + t.Logf("B: %+v", b) + } } } t.Errorf("Error adding unit counters: %v", len(a.UnitCounters)) @@ -178,150 +205,291 @@ func TestUnitCountersCountAllVoiceDestinationEvent(t *testing.T) { a := &Account{ ActionTriggers: ActionTriggers{ &ActionTrigger{ - UniqueID: "TestTR1", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR1", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR11", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 20, + UniqueID: "TestTR11", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(20), + }, }, &ActionTrigger{ - UniqueID: "TestTR2", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceDestinationIds: utils.NewStringMap("NAT"), - BalanceWeight: 10, + UniqueID: "TestTR2", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR22", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.VOICE, - BalanceDestinationIds: utils.NewStringMap("RET"), - BalanceWeight: 10, + UniqueID: "TestTR22", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR3", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR3", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR4", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR4", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR5", - ThresholdType: utils.TRIGGER_MAX_BALANCE, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT), - BalanceWeight: 10, + UniqueID: "TestTR5", + ThresholdType: utils.TRIGGER_MAX_BALANCE, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, }, }, } a.InitCounters() a.UnitCounters.addUnits(10, utils.VOICE, &CallCost{Destination: "0723045326"}, nil) - if len(a.UnitCounters) != 4 || - len(a.UnitCounters[1].Balances) != 2 || - a.UnitCounters[1].Balances[0].Value != 10 || - a.UnitCounters[1].Balances[1].Value != 10 { - for _, uc := range a.UnitCounters { - t.Logf("UC: %+v", uc) - for _, b := range uc.Balances { - t.Logf("B: %+v", b) + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.VOICE][0].Counters) != 2 || + a.UnitCounters[utils.VOICE][0].Counters[0].Value != 10 || + a.UnitCounters[utils.VOICE][0].Counters[1].Value != 10 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, b := range uc.Counters { + t.Logf("B: %+v", b) + } } } t.Errorf("Error adding unit counters: %v", len(a.UnitCounters)) } } +func TestUnitCountersKeepValuesAfterInit(t *testing.T) { + a := &Account{ + ActionTriggers: ActionTriggers{ + &ActionTrigger{ + UniqueID: "TestTR1", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, + }, + &ActionTrigger{ + UniqueID: "TestTR11", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(20), + }, + }, + &ActionTrigger{ + UniqueID: "TestTR2", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), + Weight: utils.Float64Pointer(10), + }, + }, + &ActionTrigger{ + UniqueID: "TestTR22", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), + Weight: utils.Float64Pointer(10), + }, + }, + &ActionTrigger{ + UniqueID: "TestTR3", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, + }, + &ActionTrigger{ + UniqueID: "TestTR4", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, + }, + &ActionTrigger{ + UniqueID: "TestTR5", + ThresholdType: utils.TRIGGER_MAX_BALANCE, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT)), + Weight: utils.Float64Pointer(10), + }, + }, + }, + } + a.InitCounters() + a.UnitCounters.addUnits(10, utils.VOICE, &CallCost{Destination: "0723045326"}, nil) + + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.VOICE][0].Counters) != 2 || + a.UnitCounters[utils.VOICE][0].Counters[0].Value != 10 || + a.UnitCounters[utils.VOICE][0].Counters[1].Value != 10 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, b := range uc.Counters { + t.Logf("B: %+v", b) + } + } + } + t.Errorf("Error adding unit counters: %v", len(a.UnitCounters)) + } + a.InitCounters() + + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.VOICE][0].Counters) != 2 || + a.UnitCounters[utils.VOICE][0].Counters[0].Value != 10 || + a.UnitCounters[utils.VOICE][0].Counters[1].Value != 10 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, b := range uc.Counters { + t.Logf("B: %+v", b) + } + } + } + t.Errorf("Error keeping counter values after init: %v", len(a.UnitCounters)) + } +} + func TestUnitCountersResetCounterById(t *testing.T) { a := &Account{ ActionTriggers: ActionTriggers{ &ActionTrigger{ - UniqueID: "TestTR1", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR1", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR11", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.MONETARY, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR11", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR2", - ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR2", + ThresholdType: utils.TRIGGER_MAX_EVENT_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR3", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.VOICE, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR3", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.VOICE), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR4", - ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR4", + ThresholdType: utils.TRIGGER_MAX_BALANCE_COUNTER, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, &ActionTrigger{ - UniqueID: "TestTR5", - ThresholdType: utils.TRIGGER_MAX_BALANCE, - BalanceType: utils.SMS, - BalanceDirections: utils.NewStringMap(utils.OUT, utils.IN), - BalanceWeight: 10, + UniqueID: "TestTR5", + ThresholdType: utils.TRIGGER_MAX_BALANCE, + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.SMS), + Directions: utils.StringMapPointer(utils.NewStringMap(utils.OUT, utils.IN)), + Weight: utils.Float64Pointer(10), + }, }, }, } a.InitCounters() a.UnitCounters.addUnits(10, utils.MONETARY, &CallCost{}, nil) - if len(a.UnitCounters) != 4 || - len(a.UnitCounters[0].Balances) != 2 || - a.UnitCounters[0].Balances[0].Value != 10 || - a.UnitCounters[0].Balances[1].Value != 10 { - for _, uc := range a.UnitCounters { - t.Logf("UC: %+v", uc) - for _, b := range uc.Balances { - t.Logf("B: %+v", b) + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.MONETARY][0].Counters) != 2 || + a.UnitCounters[utils.MONETARY][0].Counters[0].Value != 10 || + a.UnitCounters[utils.MONETARY][0].Counters[1].Value != 10 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, b := range uc.Counters { + t.Logf("B: %+v", b) + } } } t.Errorf("Error Initializing adding unit counters: %v", len(a.UnitCounters)) } a.UnitCounters.resetCounters(&Action{ - BalanceType: utils.MONETARY, - Balance: &Balance{ - Id: "TestTR11", + Balance: &BalanceFilter{ + Type: utils.StringPointer(utils.MONETARY), + ID: utils.StringPointer("TestTR11"), }, }) - if len(a.UnitCounters) != 4 || - len(a.UnitCounters[0].Balances) != 2 || - a.UnitCounters[0].Balances[0].Value != 10 || - a.UnitCounters[0].Balances[1].Value != 0 { - for _, uc := range a.UnitCounters { - t.Logf("UC: %+v", uc) - for _, b := range uc.Balances { - t.Logf("B: %+v", b) + if len(a.UnitCounters) != 3 || + len(a.UnitCounters[utils.MONETARY][0].Counters) != 2 || + a.UnitCounters[utils.MONETARY][0].Counters[0].Value != 10 || + a.UnitCounters[utils.MONETARY][0].Counters[1].Value != 0 { + for key, counters := range a.UnitCounters { + t.Log(key) + for _, uc := range counters { + t.Logf("UC: %+v", uc) + for _, b := range uc.Counters { + t.Logf("B: %+v", b) + } } } t.Errorf("Error Initializing adding unit counters: %v", len(a.UnitCounters)) diff --git a/engine/users.go b/engine/users.go index 5149b7c64..d49f14335 100644 --- a/engine/users.go +++ b/engine/users.go @@ -52,6 +52,16 @@ func (ud *UserProfile) SetId(id string) error { return nil } +type UserService interface { + SetUser(UserProfile, *string) error + RemoveUser(UserProfile, *string) error + UpdateUser(UserProfile, *string) error + GetUsers(UserProfile, *UserProfiles) error + AddIndex([]string, *string) error + GetIndexes(string, *map[string][]string) error + ReloadUsers(string, *string) error +} + type prop struct { masked bool weight float64 @@ -473,5 +483,5 @@ func LoadUserProfile(in interface{}, extraFields string) error { utils.SetMapExtraFields(in, m, extraFields) return nil } - return utils.ErrNotFound + return utils.ErrUserNotFound } diff --git a/engine/users_test.go b/engine/users_test.go index a22401014..fa91da244 100644 --- a/engine/users_test.go +++ b/engine/users_test.go @@ -708,7 +708,7 @@ func TestUsersExternalCDRGetLoadUserProfileExtraFieldsNotFound(t *testing.T) { } err := LoadUserProfile(ur, "ExtraFields") - if err != utils.ErrNotFound { + if err != utils.ErrUserNotFound { t.Error("Error detecting err in loading user profile: ", err) } } diff --git a/general_tests/acntacts_test.go b/general_tests/acntacts_test.go index 572f60180..060cef8db 100644 --- a/general_tests/acntacts_test.go +++ b/general_tests/acntacts_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" ) var ratingDbAcntActs engine.RatingStorage @@ -62,7 +63,7 @@ ENABLE_ACNT,*enable_account,,,,,,,,,,,,,,false,false,10` csvr.WriteToDatabase(false, false) ratingDbAcntActs.CacheRatingAll() acntDbAcntActs.CacheAccountingAll() - expectAcnt := &engine.Account{Id: "cgrates.org:1"} + expectAcnt := &engine.Account{ID: "cgrates.org:1"} if acnt, err := acntDbAcntActs.GetAccount("cgrates.org:1"); err != nil { t.Error(err) } else if acnt == nil { @@ -77,15 +78,15 @@ func TestAcntActsDisableAcnt(t *testing.T) { at := &engine.ActionTiming{ ActionsID: "DISABLE_ACNT", } - at.SetAccountIDs(map[string]struct{}{acnt1Tag: struct{}{}}) + at.SetAccountIDs(utils.StringMap{acnt1Tag: true}) if err := at.Execute(); err != nil { t.Error(err) } - expectAcnt := &engine.Account{Id: "cgrates.org:1", Disabled: true} + expectAcnt := &engine.Account{ID: "cgrates.org:1", Disabled: true} if acnt, err := acntDbAcntActs.GetAccount(acnt1Tag); err != nil { t.Error(err) } else if !reflect.DeepEqual(expectAcnt, acnt) { - t.Errorf("Expecting: %+v, received: %+v", expectAcnt, acnt) + t.Errorf("Expecting: %+v, received: %+v", utils.ToJSON(expectAcnt), utils.ToJSON(acnt)) } } @@ -94,11 +95,11 @@ func TestAcntActsEnableAcnt(t *testing.T) { at := &engine.ActionTiming{ ActionsID: "ENABLE_ACNT", } - at.SetAccountIDs(map[string]struct{}{acnt1Tag: struct{}{}}) + at.SetAccountIDs(utils.StringMap{acnt1Tag: true}) if err := at.Execute(); err != nil { t.Error(err) } - expectAcnt := &engine.Account{Id: "cgrates.org:1", Disabled: false} + expectAcnt := &engine.Account{ID: "cgrates.org:1", Disabled: false} if acnt, err := acntDbAcntActs.GetAccount(acnt1Tag); err != nil { t.Error(err) } else if !reflect.DeepEqual(expectAcnt, acnt) { diff --git a/general_tests/multiplecdrc_local_test.go b/general_tests/multiplecdrc_local_test.go index 0d393554e..651404bda 100644 --- a/general_tests/multiplecdrc_local_test.go +++ b/general_tests/multiplecdrc_local_test.go @@ -135,7 +135,7 @@ func TestMCDRCApierLoadTariffPlanFromFolder(t *testing.T) { } reply := "" // Simple test that command is executed without errors - attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "prepaid1centpsec")} + attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "testtp")} if err := rater.Call("ApierV1.LoadTariffPlanFromFolder", attrs, &reply); err != nil { t.Error("Got error on ApierV1.LoadTariffPlanFromFolder: ", err.Error()) } else if reply != "OK" { diff --git a/general_tests/tut_smgeneric_it_test.go b/general_tests/tut_smgeneric_it_test.go index 0f666f5e7..889686b1f 100644 --- a/general_tests/tut_smgeneric_it_test.go +++ b/general_tests/tut_smgeneric_it_test.go @@ -114,7 +114,7 @@ func TestTutSMGCacheStats(t *testing.T) { } var rcvStats *utils.CacheStats - expectedStats := &utils.CacheStats{Destinations: 4, RatingPlans: 3, RatingProfiles: 8, Actions: 7, ActionPlans: 4, SharedGroups: 1, Aliases: 1, + expectedStats := &utils.CacheStats{Destinations: 4, RatingPlans: 4, RatingProfiles: 9, Actions: 7, ActionPlans: 4, SharedGroups: 1, Aliases: 1, DerivedChargers: 1, LcrProfiles: 5, CdrStats: 6, Users: 3, LastLoadId: smgLoadInst.LoadId, LastLoadTime: smgLoadInst.LoadTime.Format(time.RFC3339)} var args utils.AttrCacheStats if err := tutSMGRpc.Call("ApierV2.GetCacheStats", args, &rcvStats); err != nil { diff --git a/general_tests/tutorial_local_test.go b/general_tests/tutorial_local_test.go index 9fca1de15..0f188d6c6 100644 --- a/general_tests/tutorial_local_test.go +++ b/general_tests/tutorial_local_test.go @@ -26,6 +26,7 @@ import ( "testing" "time" + "github.com/cgrates/cgrates/apier/v2" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" @@ -114,7 +115,7 @@ func TestTutLocalCacheStats(t *testing.T) { } var rcvStats *utils.CacheStats - expectedStats := &utils.CacheStats{Destinations: 4, RatingPlans: 3, RatingProfiles: 8, Actions: 7, ActionPlans: 4, SharedGroups: 1, Aliases: 1, + expectedStats := &utils.CacheStats{Destinations: 4, RatingPlans: 4, RatingProfiles: 9, Actions: 7, ActionPlans: 4, SharedGroups: 1, Aliases: 1, DerivedChargers: 1, LcrProfiles: 5, CdrStats: 6, Users: 3, LastLoadId: loadInst.LoadId, LastLoadTime: loadInst.LoadTime.Format(time.RFC3339)} var args utils.AttrCacheStats if err := tutLocalRpc.Call("ApierV2.GetCacheStats", args, &rcvStats); err != nil { @@ -193,7 +194,7 @@ func TestTutLocalGetCosts(t *testing.T) { } if err := tutLocalRpc.Call("Responder.GetCost", cd, &cc); err != nil { t.Error("Got error on Responder.GetCost: ", err.Error()) - } else if cc.Cost != 0.6425 { // 0.01 first minute, 0.04 25 seconds with RT_20CNT + } else if cc.Cost != 0.6418 { // 0.01 first minute, 0.04 25 seconds with RT_20CNT t.Errorf("Calling Responder.GetCost got callcost: %v", cc.Cost) } tStart, _ = utils.ParseDate("2014-08-04T13:00:00Z") @@ -229,7 +230,7 @@ func TestTutLocalGetCosts(t *testing.T) { } if err := tutLocalRpc.Call("Responder.GetCost", cd, &cc); err != nil { t.Error("Got error on Responder.GetCost: ", err.Error()) - } else if cc.Cost != 1.3002 { + } else if cc.Cost != 1.3 { t.Errorf("Calling Responder.GetCost got callcost: %v", cc.Cost) } tStart, _ = utils.ParseDate("2014-08-04T13:00:00Z") @@ -265,7 +266,7 @@ func TestTutLocalGetCosts(t *testing.T) { } if err := tutLocalRpc.Call("Responder.GetCost", cd, &cc); err != nil { t.Error("Got error on Responder.GetCost: ", err.Error()) - } else if cc.Cost != 1.3002 { + } else if cc.Cost != 1.3 { t.Errorf("Calling Responder.GetCost got callcost: %v", cc.Cost) } tStart = time.Date(2014, 8, 4, 13, 0, 0, 0, time.UTC) @@ -341,7 +342,7 @@ func TestTutLocalGetCosts(t *testing.T) { } if err := tutLocalRpc.Call("Responder.GetCost", cd, &cc); err != nil { t.Error("Got error on Responder.GetCost: ", err.Error()) - } else if cc.Cost != 0.327 { // + } else if cc.Cost != 0.3249 { // t.Errorf("Calling Responder.GetCost got callcost: %s", cc.AsJSON()) } cd = engine.CallDescriptor{ @@ -356,7 +357,7 @@ func TestTutLocalGetCosts(t *testing.T) { } if err := tutLocalRpc.Call("Responder.GetCost", cd, &cc); err != nil { t.Error("Got error on Responder.GetCost: ", err.Error()) - } else if cc.Cost != 1.3002 { // + } else if cc.Cost != 1.3 { // t.Errorf("Calling Responder.GetCost got callcost: %s", cc.AsJSON()) } cd = engine.CallDescriptor{ @@ -371,7 +372,7 @@ func TestTutLocalGetCosts(t *testing.T) { } if err := tutLocalRpc.Call("Responder.GetCost", cd, &cc); err != nil { t.Error("Got error on Responder.GetCost: ", err.Error()) - } else if cc.Cost != 0.354 { // + } else if cc.Cost != 0.3498 { // t.Errorf("Calling Responder.GetCost got callcost: %s", cc.AsJSON()) } } @@ -755,7 +756,8 @@ func TestTutLocalLcrStatic(t *testing.T) { }, } var lcr engine.LCRCost - cd.CgrId = "1" + cd.CgrID = "1" + cd.RunID = "1" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eStLcr.Entry, lcr.Entry) { @@ -780,7 +782,8 @@ func TestTutLocalLcrStatic(t *testing.T) { &engine.LCRSupplierCost{Supplier: "*out:cgrates.org:lcr_profile1:suppl2", Cost: 1.2, Duration: 60 * time.Second}, }, } - cd.CgrId = "2" + cd.CgrID = "2" + cd.RunID = "2" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eStLcr.Entry, lcr.Entry) { @@ -859,7 +862,8 @@ func TestTutLocalLcrQos(t *testing.T) { } var lcr engine.LCRCost // Since there is no real quality difference, the suppliers will come in random order here - cd.CgrId = "3" + cd.CgrID = "3" + cd.RunID = "3" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eStLcr.Entry, lcr.Entry) { @@ -898,7 +902,8 @@ func TestTutLocalLcrQos(t *testing.T) { QOS: map[string]float64{engine.TCD: 90, engine.ACC: 0.325, engine.TCC: 0.325, engine.ASR: 100, engine.ACD: 90}}, }, } - cd.CgrId = "4" + cd.CgrID = "4" + cd.RunID = "4" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eStLcr.Entry, lcr.Entry) { @@ -926,7 +931,8 @@ func TestTutLocalLcrQos(t *testing.T) { QOS: map[string]float64{engine.TCD: 240, engine.ACC: 0.35, engine.TCC: 0.7, engine.ASR: 100, engine.ACD: 120}}, }, } - cd.CgrId = "5" + cd.CgrID = "5" + cd.RunID = "5" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eStLcr.Entry, lcr.Entry) { @@ -962,7 +968,8 @@ func TestTutLocalLcrQosThreshold(t *testing.T) { }, } var lcr engine.LCRCost - cd.CgrId = "6" + cd.CgrID = "6" + cd.RunID = "6" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eLcr.Entry, lcr.Entry) { @@ -988,7 +995,8 @@ func TestTutLocalLcrQosThreshold(t *testing.T) { QOS: map[string]float64{engine.TCD: 240, engine.ACC: 0.35, engine.TCC: 0.7, engine.ASR: 100, engine.ACD: 120}}, }, } - cd.CgrId = "7" + cd.CgrID = "7" + cd.RunID = "7" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eLcr.Entry, lcr.Entry) { @@ -1025,7 +1033,8 @@ func TestTutLocalLcrQosThreshold(t *testing.T) { }, } */ - cd.CgrId = "8" + cd.CgrID = "8" + cd.RunID = "8" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eLcr.Entry, lcr.Entry) { @@ -1050,7 +1059,8 @@ func TestTutLocalLcrQosThreshold(t *testing.T) { QOS: map[string]float64{engine.TCD: 240, engine.ACC: 0.35, engine.TCC: 0.7, engine.ASR: 100, engine.ACD: 120}}, }, } - cd.CgrId = "9" + cd.CgrID = "9" + cd.RunID = "9" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eLcr.Entry, lcr.Entry) { @@ -1085,7 +1095,8 @@ func TestTutLocalLeastCost(t *testing.T) { }, } var lcr engine.LCRCost - cd.CgrId = "10" + cd.CgrID = "10" + cd.RunID = "10" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eStLcr.Entry, lcr.Entry) { @@ -1117,7 +1128,8 @@ func TestTutLocalLeastCost(t *testing.T) { &engine.LCRSupplierCost{Supplier: "*out:cgrates.org:lcr_profile1:suppl1", Cost: 1.2, Duration: 60 * time.Second}, }, } - cd.CgrId = "11" + cd.CgrID = "11" + cd.RunID = "11" if err := tutLocalRpc.Call("Responder.GetLCR", cd, &lcr); err != nil { t.Error(err) } else if !reflect.DeepEqual(eStLcr.Entry, lcr.Entry) { @@ -1133,7 +1145,7 @@ func TestTutLocalSetAccount(t *testing.T) { return } var reply string - attrs := &utils.AttrSetAccount{Tenant: "cgrates.org", Account: "tutacnt1", ActionPlanId: "PACKAGE_10", ActionTriggersId: "STANDARD_TRIGGERS", ReloadScheduler: true} + attrs := &v2.AttrSetAccount{Tenant: "cgrates.org", Account: "tutacnt1", ActionPlanIDs: &[]string{"PACKAGE_10"}, ActionTriggerIDs: &[]string{"STANDARD_TRIGGERS"}, ReloadScheduler: true} if err := tutLocalRpc.Call("ApierV2.SetAccount", attrs, &reply); err != nil { t.Error("Got error on ApierV2.SetAccount: ", err.Error()) } else if reply != "OK" { @@ -1154,9 +1166,9 @@ func TestTutLocalSetAccount(t *testing.T) { t.Errorf("Accounts received: %+v", acnts) } else { acnt := acnts[0] - dta, _ := utils.NewTAFromAccountKey(acnt.Id) + dta, _ := utils.NewTAFromAccountKey(acnt.ID) if dta.Tenant != attrs.Tenant || dta.Account != attrs.Account { - t.Error("Unexpected account id received: ", acnt.Id) + t.Error("Unexpected account id received: ", acnt.ID) } if balances := acnt.BalanceMap["*monetary"]; len(balances) != 1 { t.Errorf("Unexpected balances found: %+v", balances) @@ -1171,7 +1183,8 @@ func TestTutLocalSetAccount(t *testing.T) { t.Error("Disabled should not be set") } } - attrs = &utils.AttrSetAccount{Tenant: "cgrates.org", Account: "tutacnt1", AllowNegative: utils.BoolPointer(true), Disabled: utils.BoolPointer(true), ReloadScheduler: true} + attrs = &v2.AttrSetAccount{Tenant: "cgrates.org", Account: "tutacnt1", ActionPlanIDs: &[]string{"PACKAGE_10"}, ActionTriggerIDs: &[]string{"STANDARD_TRIGGERS"}, AllowNegative: utils.BoolPointer(true), Disabled: utils.BoolPointer(true), ReloadScheduler: true} + if err := tutLocalRpc.Call("ApierV2.SetAccount", attrs, &reply); err != nil { t.Error("Got error on ApierV2.SetAccount: ", err.Error()) } else if reply != "OK" { @@ -1183,9 +1196,9 @@ func TestTutLocalSetAccount(t *testing.T) { t.Errorf("Accounts received: %+v", acnts) } else { acnt := acnts[0] - dta, _ := utils.NewTAFromAccountKey(acnt.Id) + dta, _ := utils.NewTAFromAccountKey(acnt.ID) if dta.Tenant != attrs.Tenant || dta.Account != attrs.Account { - t.Error("Unexpected account id received: ", acnt.Id) + t.Error("Unexpected account id received: ", acnt.ID) } if balances := acnt.BalanceMap["*monetary"]; len(balances) != 1 { t.Errorf("Unexpected balances found: %+v", balances) @@ -1200,7 +1213,37 @@ func TestTutLocalSetAccount(t *testing.T) { t.Error("Disabled should be set") } } + attrs = &v2.AttrSetAccount{Tenant: "cgrates.org", Account: "tutacnt1", ActionPlanIDs: &[]string{"PACKAGE_1001"}, ActionTriggerIDs: &[]string{"CDRST1_WARN"}, AllowNegative: utils.BoolPointer(true), Disabled: utils.BoolPointer(true), ReloadScheduler: true} + if err := tutLocalRpc.Call("ApierV2.SetAccount", attrs, &reply); err != nil { + t.Error("Got error on ApierV2.SetAccount: ", err.Error()) + } else if reply != "OK" { + t.Errorf("Calling ApierV2.SetAccount received: %s", reply) + } + time.Sleep(100*time.Millisecond + time.Duration(*waitRater)*time.Millisecond) // Give time for scheduler to execute topups + if err := tutLocalRpc.Call("ApierV2.GetAccounts", utils.AttrGetAccounts{Tenant: attrs.Tenant, AccountIds: []string{attrs.Account}}, &acnts); err != nil { + t.Error(err) + } else if len(acnts) != 1 { + t.Errorf("Accounts received: %+v", acnts) + } else { + acnt := acnts[0] + dta, _ := utils.NewTAFromAccountKey(acnt.ID) + if dta.Tenant != attrs.Tenant || dta.Account != attrs.Account { + t.Error("Unexpected account id received: ", acnt.ID) + } + if balances := acnt.BalanceMap["*monetary"]; len(balances) != 3 { + t.Errorf("Unexpected balances found: %+v", balances) + } + if len(acnt.ActionTriggers) != 7 { + t.Errorf("Unexpected action triggers for account: %+v", acnt.ActionTriggers) + } + if !acnt.AllowNegative { + t.Error("AllowNegative should be set") + } + if !acnt.Disabled { + t.Error("Disabled should be set") + } + } } /* diff --git a/glide.lock b/glide.lock index 6a8bae15e..a2a7e9c28 100644 --- a/glide.lock +++ b/glide.lock @@ -1,32 +1,32 @@ -hash: 855bc23b0e58452edf1d31f228430476a2602b79d225397d33b805b252cebb83 -updated: 2016-01-21T12:30:17.296295607+02:00 +hash: c4e3a1bdd7452ec3af195e09b8b3b1b9a61e36edfad557aeb01686706019c352 +updated: 2016-03-09T00:08:37.493018177+02:00 imports: - name: github.com/cenkalti/hub version: 57d753b5f4856e77b3cf8ecce78c97215a7d324d - name: github.com/cenkalti/rpc2 version: 2d1be381ce47537e9e076b2b76dc70933162e4e9 - name: github.com/cgrates/fsock - version: c3b1d274ae0e42742ba1bce2bf3a138d72fb82ee + version: a8ffdbdfc8440016df008ba91e6f05f806d7a69f - name: github.com/cgrates/kamevapi version: a376b1f937ba959857929fa3e111c0f3243278c0 - name: github.com/cgrates/osipsdagram version: 3d6beed663452471dec3ca194137a30d379d9e8f - name: github.com/cgrates/rpcclient version: 79661b1e514823a9ac93b2b9e97e037ee190ba47 -- name: github.com/cgrates/structmatcher - version: 98feee0bab15ce165540fe5f0fa006db2e9f898c - name: github.com/DisposaBoy/JsonConfigReader version: 33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4 - name: github.com/fiorix/go-diameter - version: 30569fd7282321ac213d8db9f39eac9321ab6bd5 + version: 1a8912836dee1f666f882b2e388e340a3a10ecdd subpackages: - - /diam + - diam - diam/avp - diam/datatype - diam/dict - diam/sm + - diam/sm/smparser + - diam/sm/smpeer - name: github.com/go-sql-driver/mysql - version: bb006fd699a123d3eb514561dbefc352e978949d + version: 0f2db9e6c9cff80a97ca5c2c5096242cc1554e16 - name: github.com/gorhill/cronexpr version: a557574d6c024ed6e36acc8b610f5f211c91568a - name: github.com/jinzhu/gorm @@ -36,32 +36,35 @@ imports: - name: github.com/kr/pty version: f7ee69f31298ecbe5d2b349c711e2547a617d398 - name: github.com/lib/pq - version: 11fc39a580a008f1f39bb3d11d984fb34ed778d9 -- name: github.com/mediocregopher/radix.v2 - version: 91435107718b55ff544323a2b0f25fdd8475d283 + version: 165a3529e799da61ab10faed1fabff3662d6193f subpackages: - - /pool + - oid +- name: github.com/mediocregopher/radix.v2 + version: 7bdaf7c45ec452ca691ab20535471e24460f0876 + subpackages: + - pool - redis - name: github.com/peterh/liner - version: 3f1c20449d1836aa4cbe38731b96f95cdf89634d + version: ad1edfd30321d8f006ccf05f1e0524adeb943060 - name: github.com/ugorji/go - version: 646ae4a518c1c3be0739df898118d9bccf993858 + version: 187fa0f8af224437e08ecb3f208c4d1a94859a61 subpackages: - - /codec -- name: golang.org/x/crypto - version: 552e9d568fde9701ea1944fb01c8aadaceaa7353 + - codec - name: golang.org/x/net - version: 961116aeebe66bfb58bb4d51818c70d256acbbb8 + version: a4bbce9fcae005b22ae5443f6af064d80a6f5a55 subpackages: - - /websocket -- name: golang.org/x/text - version: cf4986612c83df6c55578ba198316d1684a9a287 + - websocket + - context - name: gopkg.in/fsnotify.v1 - version: 508915b7500b6e42a87132e4afeb4729cebc7cbb + version: 875cf421b32f8f1b31bd43776297876d01542279 - name: gopkg.in/mgo.v2 - version: e30de8ac9ae3b30df7065f766c71f88bba7d4e49 + version: d90005c5262a3463800497ea5a89aed5fe22c886 subpackages: - bson - name: gopkg.in/tomb.v2 version: 14b3d72120e8d10ea6e6b7f87f7175734b1faab8 -devImports: [] \ No newline at end of file +devImports: [] +======= + - internal/sasl + - internal/scram +devImports: [] diff --git a/glide.yaml b/glide.yaml index d1d548281..fa8001b97 100644 --- a/glide.yaml +++ b/glide.yaml @@ -6,10 +6,9 @@ import: - package: github.com/cgrates/kamevapi - package: github.com/cgrates/osipsdagram - package: github.com/cgrates/rpcclient -- package: github.com/cgrates/structmatcher - package: github.com/fiorix/go-diameter subpackages: - - /diam + - diam - diam/avp - diam/datatype - diam/dict @@ -17,20 +16,19 @@ import: - package: github.com/go-sql-driver/mysql - package: github.com/gorhill/cronexpr - package: github.com/jinzhu/gorm -- package: github.com/jinzhu/inflection - package: github.com/kr/pty - package: github.com/lib/pq - package: github.com/mediocregopher/radix.v2 subpackages: - - /pool + - pool - redis - package: github.com/peterh/liner - package: github.com/ugorji/go subpackages: - - /codec + - codec - package: golang.org/x/net subpackages: - - /websocket + - websocket - package: gopkg.in/fsnotify.v1 - package: gopkg.in/mgo.v2 subpackages: diff --git a/local_test.sh b/local_test.sh index b85f15363..05577d629 100755 --- a/local_test.sh +++ b/local_test.sh @@ -1,5 +1,4 @@ #! /usr/bin/env sh -./build.sh ./test.sh gen=$? echo 'go test github.com/cgrates/cgrates/apier/v1 -local' diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index e8c6896a9..2cfb53b8d 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -79,7 +79,7 @@ func (s *Scheduler) Loop() { select { case <-s.timer.C: // timer has expired - utils.Logger.Info(fmt.Sprintf(" Time for action on %v", a0.ActionsID)) + utils.Logger.Info(fmt.Sprintf(" Time for action on %s", a0.ActionsID)) case <-s.restartLoop: // nothing to do, just continue the loop } @@ -127,7 +127,6 @@ func (s *Scheduler) loadActionPlans() { if at.IsASAP() { continue } - now := time.Now() if at.GetNextStartTime(now).Before(now) { // the task is obsolete, do not add it to the queue diff --git a/sessionmanager/fssessionmanager.go b/sessionmanager/fssessionmanager.go index 6332bbe1d..7b64860be 100644 --- a/sessionmanager/fssessionmanager.go +++ b/sessionmanager/fssessionmanager.go @@ -100,7 +100,7 @@ func (sm *FSSessionManager) setCgrLcr(ev engine.Event, connId string) error { return err } cd := &engine.CallDescriptor{ - CgrId: ev.GetCgrId(sm.Timezone()), + CgrID: ev.GetCgrId(sm.Timezone()), Direction: ev.GetDirection(utils.META_DEFAULT), Tenant: ev.GetTenant(utils.META_DEFAULT), Category: ev.GetCategory(utils.META_DEFAULT), @@ -149,7 +149,7 @@ func (sm *FSSessionManager) onChannelPark(ev engine.Event, connId string) { // ComputeLcr if ev.ComputeLcr() { cd, err := fsev.AsCallDescriptor() - cd.CgrId = fsev.GetCgrId(sm.Timezone()) + cd.CgrID = fsev.GetCgrId(sm.Timezone()) if err != nil { utils.Logger.Info(fmt.Sprintf(" LCR_PREPROCESS_ERROR: %s", err.Error())) sm.unparkCall(ev.GetUUID(), connId, ev.GetCallDestNr(utils.META_DEFAULT), SYSTEM_ERROR) diff --git a/sessionmanager/kamailiosm.go b/sessionmanager/kamailiosm.go index 7d6dc3a15..b56d0c96d 100644 --- a/sessionmanager/kamailiosm.go +++ b/sessionmanager/kamailiosm.go @@ -103,7 +103,7 @@ func (self *KamailioSessionManager) onCgrLcrReq(evData []byte, connId string) { func (self *KamailioSessionManager) getSuppliers(kev KamEvent) (string, error) { cd, err := kev.AsCallDescriptor() - cd.CgrId = kev.GetCgrId(self.timezone) + cd.CgrID = kev.GetCgrId(self.timezone) if err != nil { utils.Logger.Info(fmt.Sprintf(" LCR_PREPROCESS_ERROR error: %s", err.Error())) return "", errors.New("LCR_PREPROCESS_ERROR") diff --git a/sessionmanager/session.go b/sessionmanager/session.go index e6e91b01f..9e4b0c1b6 100644 --- a/sessionmanager/session.go +++ b/sessionmanager/session.go @@ -70,7 +70,7 @@ func NewSession(ev engine.Event, connId string, sm SessionManager) *Session { // the debit loop method (to be stoped by sending somenthing on stopDebit channel) func (s *Session) debitLoop(runIdx int) { nextCd := s.sessionRuns[runIdx].CallDescriptor - nextCd.CgrId = s.eventStart.GetCgrId(s.sessionManager.Timezone()) + nextCd.CgrID = s.eventStart.GetCgrId(s.sessionManager.Timezone()) index := 0.0 debitPeriod := s.sessionManager.DebitInterval() for { @@ -180,6 +180,7 @@ func (s *Session) Refund(lastCC *engine.CallCost, hangupTime time.Time) error { lastCC.Timespans = lastCC.Timespans[:i] } else { ts.SplitByIncrement(lastRefundedIncrementIndex) + ts.Cost = ts.CalculateCost() } break // do not go to other timespans } else { @@ -195,7 +196,7 @@ func (s *Session) Refund(lastCC *engine.CallCost, hangupTime time.Time) error { // utils.Logger.Info(fmt.Sprintf("Refund duration: %v", initialRefundDuration-refundDuration)) if len(refundIncrements) > 0 { cd := &engine.CallDescriptor{ - CgrId: s.eventStart.GetCgrId(s.sessionManager.Timezone()), + CgrID: s.eventStart.GetCgrId(s.sessionManager.Timezone()), Direction: lastCC.Direction, Tenant: lastCC.Tenant, Category: lastCC.Category, @@ -215,6 +216,7 @@ func (s *Session) Refund(lastCC *engine.CallCost, hangupTime time.Time) error { } //utils.Logger.Debug(fmt.Sprintf("REFUND INCR: %s", utils.ToJSON(refundIncrements))) lastCC.Cost -= refundIncrements.GetTotalCost() + lastCC.UpdateRatedUsage() lastCC.Timespans.Compress() return nil } @@ -237,6 +239,16 @@ func (s *Session) SaveOperations() { } firstCC.Timespans.Compress() + firstCC.Round() + roundIncrements := firstCC.GetRoundIncrements() + if len(roundIncrements) != 0 { + cd := firstCC.CreateCallDescriptor() + cd.Increments = roundIncrements + var response float64 + if err := s.sessionManager.Rater().Call("Responder.RefundRounding", cd, &response); err != nil { + utils.Logger.Err(fmt.Sprintf(" ERROR failed to refund rounding: %v", err)) + } + } var reply string err := s.sessionManager.CdrSrv().Call("CdrServer.LogCallCost", &engine.CallCostLog{ CgrId: s.eventStart.GetCgrId(s.sessionManager.Timezone()), diff --git a/sessionmanager/session_test.go b/sessionmanager/session_test.go index f53c566d4..5de8a536f 100644 --- a/sessionmanager/session_test.go +++ b/sessionmanager/session_test.go @@ -89,6 +89,20 @@ func (mc *MockRpcClient) Call(methodName string, arg interface{}, reply interfac } return nil } +func (mc *MockConnector) GetCost(*engine.CallDescriptor, *engine.CallCost) error { return nil } +func (mc *MockConnector) Debit(*engine.CallDescriptor, *engine.CallCost) error { return nil } +func (mc *MockConnector) MaxDebit(*engine.CallDescriptor, *engine.CallCost) error { return nil } +func (mc *MockConnector) RefundIncrements(cd *engine.CallDescriptor, reply *float64) error { + mc.refundCd = cd + return nil +} +func (mc *MockConnector) RefundRounding(cd *engine.CallDescriptor, reply *float64) error { + return nil +} +func (mc *MockConnector) GetMaxSessionTime(*engine.CallDescriptor, *float64) error { return nil } +func (mc *MockConnector) GetDerivedChargers(*utils.AttrDerivedChargers, *utils.DerivedChargers) error { + return nil +} func TestSessionRefund(t *testing.T) { mc := &MockRpcClient{} diff --git a/sessionmanager/smg_event.go b/sessionmanager/smg_event.go index c775c45a1..643c8693f 100644 --- a/sessionmanager/smg_event.go +++ b/sessionmanager/smg_event.go @@ -20,6 +20,7 @@ package sessionmanager import ( "encoding/json" + "fmt" "strconv" "time" @@ -49,8 +50,9 @@ func (self SMGenericEvent) GetTOR(fieldName string) string { } func (self SMGenericEvent) GetCgrId(timezone string) string { - setupTime, _ := self.GetSetupTime(utils.META_DEFAULT, timezone) - return utils.Sha1(self.GetUUID(), setupTime.UTC().String()) + //setupTime, _ := self.GetSetupTime(utils.META_DEFAULT, timezone) + //return utils.Sha1(self.GetUUID(), setupTime.UTC().String()) + return utils.Sha1(self.GetUUID()) } func (self SMGenericEvent) GetUUID() string { @@ -155,7 +157,23 @@ func (self SMGenericEvent) GetUsage(fieldName string) (time.Duration, error) { if fieldName == utils.META_DEFAULT { fieldName = utils.USAGE } - result, _ := utils.ConvertIfaceToString(self[fieldName]) + valIf, hasVal := self[fieldName] + if !hasVal { + return nilDuration, utils.ErrNotFound + } + result, _ := utils.ConvertIfaceToString(valIf) + return utils.ParseDurationWithSecs(result) +} + +func (self SMGenericEvent) GetLastUsed(fieldName string) (time.Duration, error) { + if fieldName == utils.META_DEFAULT { + fieldName = utils.LastUsed + } + valStr, hasVal := self[fieldName] + if !hasVal { + return nilDuration, utils.ErrNotFound + } + result, _ := utils.ConvertIfaceToString(valStr) return utils.ParseDurationWithSecs(result) } @@ -210,7 +228,7 @@ func (self SMGenericEvent) GetCdrSource() string { func (self SMGenericEvent) GetExtraFields() map[string]string { extraFields := make(map[string]string) for key, val := range self { - primaryFields := append(utils.PrimaryCdrFields, utils.EVENT_NAME) + primaryFields := append(utils.PrimaryCdrFields, utils.EVENT_NAME, utils.LastUsed) if utils.IsSliceMember(primaryFields, key) { continue } @@ -348,3 +366,16 @@ func (self SMGenericEvent) AsLcrRequest() *engine.LcrRequest { Duration: usageStr, } } + +// AsMapStringString Converts into map[string]string, used for example as pubsub event +func (self SMGenericEvent) AsMapStringString() (map[string]string, error) { + mp := make(map[string]string) + for k, v := range self { + if strV, casts := utils.CastIfToString(v); !casts { + return nil, fmt.Errorf("Value %+v does not cast to string", v) + } else { + mp[k] = strV + } + } + return mp, nil +} diff --git a/sessionmanager/smg_event_test.go b/sessionmanager/smg_event_test.go index f6c395b7b..4cc80781c 100644 --- a/sessionmanager/smg_event_test.go +++ b/sessionmanager/smg_event_test.go @@ -45,6 +45,7 @@ func TestSMGenericEventParseFields(t *testing.T) { smGev[utils.SETUP_TIME] = "2015-11-09 14:21:24" smGev[utils.ANSWER_TIME] = "2015-11-09 14:22:02" smGev[utils.USAGE] = "1m23s" + smGev[utils.LastUsed] = "21s" smGev[utils.PDD] = "300ms" smGev[utils.SUPPLIER] = "supplier1" smGev[utils.DISCONNECT_CAUSE] = "NORMAL_DISCONNECT" @@ -54,7 +55,7 @@ func TestSMGenericEventParseFields(t *testing.T) { if smGev.GetName() != "TEST_EVENT" { t.Error("Unexpected: ", smGev.GetName()) } - if smGev.GetCgrId("UTC") != "0711eaa78e53937f1593dabc08c83ea04a915f2e" { + if smGev.GetCgrId("UTC") != "8cb2237d0679ca88db6464eac60da96345513964" { t.Error("Unexpected: ", smGev.GetCgrId("UTC")) } if smGev.GetUUID() != "12345" { @@ -107,6 +108,11 @@ func TestSMGenericEventParseFields(t *testing.T) { } else if dur != time.Duration(83)*time.Second { t.Error("Unexpected: ", dur) } + if lastUsed, err := smGev.GetLastUsed(utils.META_DEFAULT); err != nil { + t.Error(err) + } else if lastUsed != time.Duration(21)*time.Second { + t.Error("Unexpected: ", lastUsed) + } if pdd, err := smGev.GetPdd(utils.META_DEFAULT); err != nil { t.Error(err) } else if pdd != time.Duration(300)*time.Millisecond { @@ -147,7 +153,7 @@ func TestSMGenericEventAsStoredCdr(t *testing.T) { smGev[utils.CDRHOST] = "10.0.3.15" smGev["Extra1"] = "Value1" smGev["Extra2"] = 5 - eStoredCdr := &engine.CDR{CGRID: "0711eaa78e53937f1593dabc08c83ea04a915f2e", + eStoredCdr := &engine.CDR{CGRID: "8cb2237d0679ca88db6464eac60da96345513964", ToR: utils.SMS, OriginID: "12345", OriginHost: "10.0.3.15", Source: "SMG_TEST_EVENT", RequestType: utils.META_PREPAID, Direction: utils.OUT, Tenant: "cgrates.org", Category: "call", Account: "account1", Subject: "subject1", Destination: "+4986517174963", SetupTime: time.Date(2015, 11, 9, 14, 21, 24, 0, time.UTC), AnswerTime: time.Date(2015, 11, 9, 14, 22, 2, 0, time.UTC), diff --git a/sessionmanager/smg_it_test.go b/sessionmanager/smg_it_test.go index f29f5a598..cd246596e 100644 --- a/sessionmanager/smg_it_test.go +++ b/sessionmanager/smg_it_test.go @@ -138,7 +138,7 @@ func TestSMGMonetaryRefund(t *testing.T) { } var acnt *engine.Account attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} - eAcntVal := 8.699800 + eAcntVal := 8.700010 if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { t.Error(err) } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { @@ -268,7 +268,7 @@ func TestSMGMixedRefund(t *testing.T) { //var acnt *engine.Account //attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} eVoiceVal := 90.0 - eMoneyVal := 8.739 + eMoneyVal := 8.7399 if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { t.Error(err) } else if acnt.BalanceMap[utils.VOICE].GetTotalValue() != eVoiceVal || @@ -305,3 +305,120 @@ func TestSMGMixedRefund(t *testing.T) { t.Logf("After monetary: %f", acnt.BalanceMap[utils.MONETARY].GetTotalValue()) t.Logf("After voice: %f", acnt.BalanceMap[utils.VOICE].GetTotalValue()) } + +func TestSMGLastUsed(t *testing.T) { + if !*testIntegration { + return + } + var acnt *engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + eAcntVal := 8.790000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) + } + smgEv := SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.VOICE, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1001", + utils.SUBJECT: "1001", + utils.DESTINATION: "1006", + utils.CATEGORY: "call", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.SETUP_TIME: "2016-01-05 18:30:49", + utils.ANSWER_TIME: "2016-01-05 18:31:05", + utils.USAGE: "2m", + } + var maxUsage float64 + if err := smgRPC.Call("SMGenericV1.SessionStart", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 120 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 7.39002 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) + } + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.VOICE, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1001", + utils.SUBJECT: "1001", + utils.DESTINATION: "1006", + utils.CATEGORY: "call", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.USAGE: "2m", + utils.LastUsed: "1m30s", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 120 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 7.09005 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) + } + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.VOICE, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1001", + utils.SUBJECT: "1001", + utils.DESTINATION: "1006", + utils.CATEGORY: "call", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.USAGE: "2m", + utils.LastUsed: "2m30s", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 120 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 6.5901 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) + } + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.VOICE, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1001", + utils.SUBJECT: "1001", + utils.DESTINATION: "1006", + utils.CATEGORY: "call", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.USAGE: "1m", + } + var rpl string + if err = smgRPC.Call("SMGenericV1.SessionEnd", smgEv, &rpl); err != nil || rpl != utils.OK { + t.Error(err) + } + eAcntVal = 7.59 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.MONETARY].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) + } +} diff --git a/sessionmanager/smg_session.go b/sessionmanager/smg_session.go index 2bd906513..a9eae0920 100644 --- a/sessionmanager/smg_session.go +++ b/sessionmanager/smg_session.go @@ -42,6 +42,8 @@ type SMGSession struct { sessionCds []*engine.CallDescriptor callCosts []*engine.CallCost extraDuration time.Duration // keeps the current duration debited on top of what heas been asked + lastUsage time.Duration // Keep record of the last debit for LastUsed functionality + totalUsage time.Duration } // Called in case of automatic debits @@ -53,7 +55,7 @@ func (self *SMGSession) debitLoop(debitInterval time.Duration) { return default: } - if maxDebit, err := self.debit(debitInterval); err != nil { + if maxDebit, err := self.debit(debitInterval, nilDuration); err != nil { utils.Logger.Err(fmt.Sprintf(" Could not complete debit opperation on session: %s, error: %s", self.eventStart.GetUUID(), err.Error())) disconnectReason := SYSTEM_ERROR if err.Error() == utils.ErrUnauthorizedDestination.Error() { @@ -76,11 +78,21 @@ func (self *SMGSession) debitLoop(debitInterval time.Duration) { } // Attempts to debit a duration, returns maximum duration which can be debitted or error -func (self *SMGSession) debit(dur time.Duration) (time.Duration, error) { +func (self *SMGSession) debit(dur time.Duration, lastUsed time.Duration) (time.Duration, error) { + lastUsedCorrection := time.Duration(0) // Used if lastUsed influences the debit + if self.cd.DurationIndex != 0 && lastUsed != 0 { + if self.lastUsage > lastUsed { // We have debitted more than we have used, refund in the duration debitted + lastUsedCorrection = -(self.lastUsage - lastUsed) + } else { // We have debitted less than we have consumed, add the difference to duration debitted + lastUsedCorrection = lastUsed - self.lastUsage + } + } + // apply the lastUsed correction + dur += lastUsedCorrection + self.totalUsage += dur // Should reflect the total usage so far // apply correction from previous run dur -= self.extraDuration self.extraDuration = 0 - if self.cd.LoopIndex > 0 { self.cd.TimeStart = self.cd.TimeEnd } @@ -88,6 +100,7 @@ func (self *SMGSession) debit(dur time.Duration) (time.Duration, error) { self.cd.DurationIndex += dur cc := &engine.CallCost{} if err := self.rater.Call("Responder.MaxDebit", self.cd, cc); err != nil { + self.lastUsage = 0 return 0, err } // cd corrections @@ -103,17 +116,22 @@ func (self *SMGSession) debit(dur time.Duration) (time.Duration, error) { self.cd.LoopIndex += 1 self.sessionCds = append(self.sessionCds, self.cd.Clone()) self.callCosts = append(self.callCosts, cc) + ccDuration -= lastUsedCorrection + if ccDuration < 0 { // if correction has pushed ccDuration bellow 0 + ccDuration = 0 + } + self.lastUsage = ccDuration // Reset the lastUsage for later reference return ccDuration, nil } // Attempts to refund a duration, error on failure func (self *SMGSession) refund(refundDuration time.Duration) error { initialRefundDuration := refundDuration - lastCC := self.callCosts[len(self.callCosts)-1] - lastCC.Timespans.Decompress() + firstCC := self.callCosts[0] // use merged cc (from close function) + firstCC.Timespans.Decompress() var refundIncrements engine.Increments - for i := len(lastCC.Timespans) - 1; i >= 0; i-- { - ts := lastCC.Timespans[i] + for i := len(firstCC.Timespans) - 1; i >= 0; i-- { + ts := firstCC.Timespans[i] tsDuration := ts.GetDuration() if refundDuration <= tsDuration { @@ -129,17 +147,18 @@ func (self *SMGSession) refund(refundDuration time.Duration) error { } } if lastRefundedIncrementIndex == 0 { - lastCC.Timespans[i] = nil - lastCC.Timespans = lastCC.Timespans[:i] + firstCC.Timespans[i] = nil + firstCC.Timespans = firstCC.Timespans[:i] } else { ts.SplitByIncrement(lastRefundedIncrementIndex) + ts.Cost = ts.CalculateCost() } break // do not go to other timespans } else { refundIncrements = append(refundIncrements, ts.Increments...) // remove the timespan entirely - lastCC.Timespans[i] = nil - lastCC.Timespans = lastCC.Timespans[:i] + firstCC.Timespans[i] = nil + firstCC.Timespans = firstCC.Timespans[:i] // continue to the next timespan with what is left to refund refundDuration -= tsDuration } @@ -147,16 +166,8 @@ func (self *SMGSession) refund(refundDuration time.Duration) error { // show only what was actualy refunded (stopped in timespan) // utils.Logger.Info(fmt.Sprintf("Refund duration: %v", initialRefundDuration-refundDuration)) if len(refundIncrements) > 0 { - cd := &engine.CallDescriptor{ - Direction: lastCC.Direction, - Tenant: lastCC.Tenant, - Category: lastCC.Category, - Subject: lastCC.Subject, - Account: lastCC.Account, - Destination: lastCC.Destination, - TOR: lastCC.TOR, - Increments: refundIncrements, - } + cd := firstCC.CreateCallDescriptor() + cd.Increments = refundIncrements cd.Increments.Compress() utils.Logger.Info(fmt.Sprintf("Refunding duration %v with cd: %s", initialRefundDuration, utils.ToJSON(cd))) var response float64 @@ -165,17 +176,20 @@ func (self *SMGSession) refund(refundDuration time.Duration) error { return err } } - //utils.Logger.Debug(fmt.Sprintf("REFUND INCR: %s", utils.ToJSON(refundIncrements))) - lastCC.Cost -= refundIncrements.GetTotalCost() - lastCC.Timespans.Compress() + firstCC.Cost -= refundIncrements.GetTotalCost() + firstCC.UpdateRatedUsage() + firstCC.Timespans.Compress() return nil } // Session has ended, check debits and refund the extra charged duration func (self *SMGSession) close(endTime time.Time) error { + firstCC := self.callCosts[0] + for _, cc := range self.callCosts[1:] { + firstCC.Merge(cc) + } if len(self.callCosts) != 0 { // We have had at least one cost calculation - lastCC := self.callCosts[len(self.callCosts)-1] - end := lastCC.GetEndTime() + end := firstCC.GetEndTime() refundDuration := end.Sub(endTime) self.refund(refundDuration) } @@ -206,11 +220,19 @@ func (self *SMGSession) saveOperations() error { if len(self.callCosts) == 0 { return nil // There are no costs to save, ignore the operation } - firstCC := self.callCosts[0] - for _, cc := range self.callCosts[1:] { - firstCC.Merge(cc) - } + firstCC := self.callCosts[0] // was merged in close methos firstCC.Timespans.Compress() + firstCC.Round() + roundIncrements := firstCC.GetRoundIncrements() + if len(roundIncrements) != 0 { + cd := firstCC.CreateCallDescriptor() + cd.Increments = roundIncrements + var response float64 + if err := self.rater.Call("Responder.RefundRounding", cd, &response); err != nil { + return err + } + } + var reply string err := self.cdrsrv.Call("CdrServer.LogCallCost", &engine.CallCostLog{ CgrId: self.eventStart.GetCgrId(self.timezone), @@ -231,3 +253,44 @@ func (self *SMGSession) saveOperations() error { } return nil } + +func (self *SMGSession) TotalUsage() time.Duration { + return self.totalUsage +} + +func (self *SMGSession) AsActiveSession(timezone string) *ActiveSession { + sTime, _ := self.eventStart.GetSetupTime(utils.META_DEFAULT, timezone) + aTime, _ := self.eventStart.GetAnswerTime(utils.META_DEFAULT, timezone) + usage, _ := self.eventStart.GetUsage(utils.META_DEFAULT) + pdd, _ := self.eventStart.GetPdd(utils.META_DEFAULT) + aSession := &ActiveSession{ + CgrId: self.eventStart.GetCgrId(timezone), + TOR: utils.VOICE, + RunId: self.runId, + AccId: self.eventStart.GetUUID(), + CdrHost: self.eventStart.GetOriginatorIP(utils.META_DEFAULT), + CdrSource: self.eventStart.GetCdrSource(), + ReqType: self.eventStart.GetReqType(utils.META_DEFAULT), + Direction: self.eventStart.GetDirection(utils.META_DEFAULT), + Tenant: self.eventStart.GetTenant(utils.META_DEFAULT), + Category: self.eventStart.GetCategory(utils.META_DEFAULT), + Account: self.eventStart.GetAccount(utils.META_DEFAULT), + Subject: self.eventStart.GetSubject(utils.META_DEFAULT), + Destination: self.eventStart.GetDestination(utils.META_DEFAULT), + SetupTime: sTime, + AnswerTime: aTime, + Usage: usage, + Pdd: pdd, + ExtraFields: self.eventStart.GetExtraFields(), + Supplier: self.eventStart.GetSupplier(utils.META_DEFAULT), + SMId: "CGR-DA", + } + if self.cd != nil { + aSession.LoopIndex = self.cd.LoopIndex + aSession.DurationIndex = self.cd.DurationIndex + aSession.MaxRate = self.cd.MaxRate + aSession.MaxRateUnit = self.cd.MaxRateUnit + aSession.MaxCostSoFar = self.cd.MaxCostSoFar + } + return aSession +} diff --git a/sessionmanager/smgeneric.go b/sessionmanager/smgeneric.go index 3e77de53f..9e7ed87fb 100644 --- a/sessionmanager/smgeneric.go +++ b/sessionmanager/smgeneric.go @@ -97,6 +97,7 @@ func (self *SMGeneric) sessionStart(evStart SMGenericEvent, connId string) error s := &SMGSession{eventStart: evStart, connId: connId, runId: sessionRun.DerivedCharger.RunID, timezone: self.timezone, rater: self.rater, cdrsrv: self.cdrsrv, cd: sessionRun.CallDescriptor} self.indexSession(sessionId, s) + //utils.Logger.Info(fmt.Sprintf(" Starting session: %s, runId: %s", sessionId, s.runId)) if self.cgrCfg.SmGenericConfig.DebitInterval != 0 { s.stopDebit = stopDebitChan go s.debitLoop(self.cgrCfg.SmGenericConfig.DebitInterval) @@ -118,6 +119,7 @@ func (self *SMGeneric) sessionEnd(sessionId string, usage time.Duration) error { return nil, nil // Did not find the session so no need to close it anymore } for idx, s := range ss { + //utils.Logger.Info(fmt.Sprintf(" Ending session: %s, runId: %s", sessionId, s.runId)) if idx == 0 && s.stopDebit != nil { close(s.stopDebit) // Stop automatic debits } @@ -153,7 +155,7 @@ func (self *SMGeneric) GetMaxUsage(gev SMGenericEvent, clnt *rpc2.Client) (time. func (self *SMGeneric) GetLcrSuppliers(gev SMGenericEvent, clnt *rpc2.Client) ([]string, error) { gev[utils.EVENT_NAME] = utils.CGR_LCR_REQUEST cd, err := gev.AsLcrRequest().AsCallDescriptor(self.timezone) - cd.CgrId = gev.GetCgrId(self.timezone) + cd.CgrID = gev.GetCgrId(self.timezone) if err != nil { return nil, err } @@ -170,18 +172,23 @@ func (self *SMGeneric) GetLcrSuppliers(gev SMGenericEvent, clnt *rpc2.Client) ([ // Execute debits for usage/maxUsage func (self *SMGeneric) SessionUpdate(gev SMGenericEvent, clnt *rpc2.Client) (time.Duration, error) { + evLastUsed, err := gev.GetLastUsed(utils.META_DEFAULT) + if err != nil && err != utils.ErrNotFound { + return nilDuration, err + } evMaxUsage, err := gev.GetMaxUsage(utils.META_DEFAULT, self.cgrCfg.MaxCallDuration) if err != nil { + if err == utils.ErrNotFound { + err = utils.ErrMandatoryIeMissing + } return nilDuration, err } evUuid := gev.GetUUID() for _, s := range self.getSession(evUuid) { - if maxDur, err := s.debit(evMaxUsage); err != nil { + if maxDur, err := s.debit(evMaxUsage, evLastUsed); err != nil { return nilDuration, err - } else { - if maxDur < evMaxUsage { - evMaxUsage = maxDur - } + } else if maxDur < evMaxUsage { + evMaxUsage = maxDur } } return evMaxUsage, nil @@ -199,7 +206,25 @@ func (self *SMGeneric) SessionStart(gev SMGenericEvent, clnt *rpc2.Client) (time func (self *SMGeneric) SessionEnd(gev SMGenericEvent, clnt *rpc2.Client) error { usage, err := gev.GetUsage(utils.META_DEFAULT) if err != nil { - return err + if err != utils.ErrNotFound { + return err + + } + lastUsed, err := gev.GetLastUsed(utils.META_DEFAULT) + if err != nil { + if err == utils.ErrNotFound { + err = utils.ErrMandatoryIeMissing + } + return err + } + var s *SMGSession + for _, s = range self.getSession(gev.GetUUID()) { + break + } + if s == nil { + return nil + } + usage = s.TotalUsage() + lastUsed } if err := self.sessionEnd(gev.GetUUID(), usage); err != nil { return err @@ -215,18 +240,18 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) (maxDu } else if len(sessionRuns) == 0 { return nilDuration, nil } + var maxDurInit bool // Avoid differences between default 0 and received 0 for _, sR := range sessionRuns { cc := new(engine.CallCost) - if err := self.rater.Call("Responder.MaxDebit", sR.CallDescriptor, cc); err != nil { - withErrors = true + if err = self.rater.Call("Responder.MaxDebit", sR.CallDescriptor, cc); err != nil { utils.Logger.Err(fmt.Sprintf(" Could not Debit CD: %+v, RunID: %s, error: %s", sR.CallDescriptor, sR.DerivedCharger.RunID, err.Error())) break } sR.CallCosts = append(sR.CallCosts, cc) // Save it so we can revert on issues if ccDur := cc.GetDuration(); ccDur == 0 { - err = errors.New("INSUFFICIENT_FUNDS") + err = utils.ErrInsufficientCredit break - } else if ccDur < maxDur { + } else if !maxDurInit || ccDur < maxDur { maxDur = ccDur } } @@ -262,7 +287,7 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) (maxDu cd.Increments.Compress() utils.Logger.Info(fmt.Sprintf("Refunding session run callcost: %s", utils.ToJSON(cd))) var response float64 - err := self.rater.RefundIncrements(cd, &response) + err := self.rater.Call("Responder.RefundIncrements", cd, &response) if err != nil { return nilDuration, err } @@ -281,8 +306,19 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) (maxDu cc.Merge(ccSR) } } + cc.Round() + roundIncrements := cc.GetRoundIncrements() + if len(roundIncrements) != 0 { + cd := cc.CreateCallDescriptor() + cd.Increments = roundIncrements + var response float64 + if err := self.rater.Call("Responder.RefundRounding", cd, &response); err != nil { + utils.Logger.Err(fmt.Sprintf(" ERROR failed to refund rounding: %v", err)) + } + } + var reply string - if err := self.cdrsrv.Call("CdrServer.LogCallCost", &engine.CallCostLog{ + if err := self.cdrsrv.Call("CdrsV2.LogCallCost", &engine.CallCostLog{ CgrId: gev.GetCgrId(self.timezone), Source: utils.SESSION_MANAGER_SOURCE, RunId: sR.DerivedCharger.RunID, @@ -311,6 +347,15 @@ func (self *SMGeneric) Connect() error { return nil } +// Used by APIer to retrieve sessions +func (self *SMGeneric) Sessions() map[string][]*SMGSession { + return self.getSessions() +} + +func (self *SMGeneric) Timezone() string { + return self.timezone +} + // System shutdown func (self *SMGeneric) Shutdown() error { for ssId := range self.getSessions() { // Force sessions shutdown diff --git a/structmatcher/.travis.yml b/structmatcher/.travis.yml new file mode 100644 index 000000000..d2e57b3a9 --- /dev/null +++ b/structmatcher/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - 1.5 + +branches: + only: master diff --git a/structmatcher/README.md b/structmatcher/README.md new file mode 100644 index 000000000..6220a301d --- /dev/null +++ b/structmatcher/README.md @@ -0,0 +1,27 @@ +# structmatcher +Query language for matching structures + +[![Build Status](https://secure.travis-ci.org/cgrates/structmatcher.png)](http://travis-ci.org/cgrates/structmatcher) + +The StructMatcher type will parse a condition string and match it against a given structure. + +The condition syntax is a json encoded string similar to mongodb query language. + +Examples: +- {"Weight":{"*gt":50}} checks for a balance with weight greater than 50 +- {"*or":[{"Value":{"*eq":0}},{"Value":{"*gte":100}}] checks for a balance with value equal to 0 or equal or highr than 100 + +Available operators: +- *eq: equal +- *gt: greater than +- *gte: greater or equal than +- *lt: less then +- *lte: less or equal than +- *exp: expired +- *or: logical or +- *and: logical and +- *has: receives a list of elements and checks that the elements are present in the specified field (StringMap type) + +Equal (*eq) and local and (*and) operators are implicit for shortcuts. In this way: + +{"*and":[{"Value":{"*eq":3}},{"Weight":{"*eq":10}}]} is equivalent to: {"Value":3, "Weight":10}. diff --git a/structmatcher/structmatcher.go b/structmatcher/structmatcher.go new file mode 100644 index 000000000..419e23095 --- /dev/null +++ b/structmatcher/structmatcher.go @@ -0,0 +1,327 @@ +package structmatcher + +/* +The condition syntax is a json encoded string similar to mongodb query language. + +Examples: +- {"Weight":{"*gt":50}} checks for a balance with weight greater than 50 +- {"*or":[{"Value":{"*eq":0}},{"Value":{"*gte":100}}] checks for a balance with value equal to 0 or equal or highr than 100 + +Available operators: +- *eq: equal +- *gt: greater than +- *gte: greater or equal than +- *lt: less then +- *lte: less or equal than +- *exp: expired +- *or: logical or +- *and: logical and +- *has: receives a list of elements and checks that the elements are present in the specified field (StringMap type) + +Equal (*eq) and local and (*and) operators are implicit for shortcuts. In this way: + +{"*and":[{"Value":{"*eq":3}},{"Weight":{"*eq":10}}]} is equivalent to: {"Value":3, "Weight":10}. +*/ + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "time" + + "github.com/cgrates/cgrates/utils" +) + +const ( + CondEQ = "*eq" + CondGT = "*gt" + CondGTE = "*gte" + CondLT = "*lt" + CondLTE = "*lte" + CondEXP = "*exp" + CondOR = "*or" + CondAND = "*and" + CondHAS = "*has" + CondRSR = "*rsr" +) + +func NewErrInvalidArgument(arg interface{}) error { + return fmt.Errorf("INVALID_ARGUMENT: %v", arg) +} + +type StringMap map[string]bool + +var ( + ErrParserError = errors.New("PARSER_ERROR") + + operatorMap = map[string]func(field, value interface{}) (bool, error){ + CondEQ: func(field, value interface{}) (bool, error) { + return value == field, nil + }, + CondGT: func(field, value interface{}) (bool, error) { + var of, vf float64 + var ok bool + if of, ok = field.(float64); !ok { + return false, NewErrInvalidArgument(field) + } + if vf, ok = value.(float64); !ok { + return false, NewErrInvalidArgument(value) + } + return of > vf, nil + }, + CondGTE: func(field, value interface{}) (bool, error) { + var of, vf float64 + var ok bool + if of, ok = field.(float64); !ok { + return false, NewErrInvalidArgument(field) + } + if vf, ok = value.(float64); !ok { + return false, NewErrInvalidArgument(value) + } + return of >= vf, nil + }, + CondLT: func(field, value interface{}) (bool, error) { + var of, vf float64 + var ok bool + if of, ok = field.(float64); !ok { + return false, NewErrInvalidArgument(field) + } + if vf, ok = value.(float64); !ok { + return false, NewErrInvalidArgument(value) + } + return of < vf, nil + }, + CondLTE: func(field, value interface{}) (bool, error) { + var of, vf float64 + var ok bool + if of, ok = field.(float64); !ok { + return false, NewErrInvalidArgument(field) + } + if vf, ok = value.(float64); !ok { + return false, NewErrInvalidArgument(value) + } + return of <= vf, nil + }, + CondEXP: func(field, value interface{}) (bool, error) { + var expDate time.Time + var ok bool + if expDate, ok = field.(time.Time); !ok { + return false, NewErrInvalidArgument(field) + } + var expired bool + if expired, ok = value.(bool); !ok { + return false, NewErrInvalidArgument(value) + } + if expired { // check for expiration + return !expDate.IsZero() && expDate.Before(time.Now()), nil + } else { // check not expired + return expDate.IsZero() || expDate.After(time.Now()), nil + } + }, + CondHAS: func(field, value interface{}) (bool, error) { + var strMap StringMap + var ok bool + if strMap, ok = field.(StringMap); !ok { + return false, NewErrInvalidArgument(field) + } + var strSlice []interface{} + if strSlice, ok = value.([]interface{}); !ok { + return false, NewErrInvalidArgument(value) + } + for _, str := range strSlice { + if !strMap[str.(string)] { + return false, nil + } + } + return true, nil + }, + CondRSR: func(field, value interface{}) (bool, error) { + fltr, err := utils.NewRSRFilter(value.(string)) + if err != nil { + return false, err + } + return fltr.Pass(fmt.Sprintf("%v", field)), nil + }, + } +) + +type compositeElement interface { + element + addChild(element) error +} + +type element interface { + checkStruct(interface{}) (bool, error) +} + +type operatorSlice struct { + operator string + slice []element +} + +func (os *operatorSlice) addChild(ce element) error { + os.slice = append(os.slice, ce) + return nil +} +func (os *operatorSlice) checkStruct(o interface{}) (bool, error) { + switch os.operator { + case CondOR: + for _, cond := range os.slice { + check, err := cond.checkStruct(o) + if err != nil { + return false, err + } + if check { + return true, nil + } + } + case CondAND: + accumulator := true + for _, cond := range os.slice { + check, err := cond.checkStruct(o) + if err != nil { + return false, err + } + accumulator = accumulator && check + } + return accumulator, nil + } + return false, nil +} + +type keyStruct struct { + key string + elem element +} + +func (ks *keyStruct) addChild(ce element) error { + ks.elem = ce + return nil +} +func (ks *keyStruct) checkStruct(o interface{}) (bool, error) { + obj := reflect.ValueOf(o) + if obj.Kind() == reflect.Ptr { + obj = obj.Elem() + } + value := obj.FieldByName(ks.key) + if !value.IsValid() { + return false, NewErrInvalidArgument(ks.key) + } + return ks.elem.checkStruct(value.Interface()) +} + +type operatorValue struct { + operator string + value interface{} +} + +func (ov *operatorValue) checkStruct(o interface{}) (bool, error) { + if f, ok := operatorMap[ov.operator]; ok { + return f(o, ov.value) + } + return false, nil +} + +type keyValue struct { + key string + value interface{} +} + +func (kv *keyValue) checkStruct(o interface{}) (bool, error) { + obj := reflect.ValueOf(o) + if obj.Kind() == reflect.Ptr { + obj = obj.Elem() + } + value := obj.FieldByName(kv.key) + if !value.IsValid() { + return false, NewErrInvalidArgument(kv.key) + } + return value.Interface() == kv.value, nil +} + +type trueElement struct{} + +func (te *trueElement) checkStruct(o interface{}) (bool, error) { + return true, nil +} + +func isOperator(s string) bool { + return strings.HasPrefix(s, "*") +} + +func notEmpty(x interface{}) bool { + return !reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) +} + +type StructMatcher struct { + rootElement element +} + +func NewStructMatcher(q string) (sm *StructMatcher, err error) { + sm = &StructMatcher{} + err = sm.Parse(q) + return +} + +func (sm *StructMatcher) load(a map[string]interface{}, parentElement compositeElement) (element, error) { + for key, value := range a { + var currentElement element + switch t := value.(type) { + case []interface{}: + if key == CondHAS { + currentElement = &operatorValue{operator: key, value: t} + } else { + currentElement = &operatorSlice{operator: key} + for _, e := range t { + sm.load(e.(map[string]interface{}), currentElement.(compositeElement)) + } + } + case map[string]interface{}: + currentElement = &keyStruct{key: key} + //log.Print("map: ", t) + sm.load(t, currentElement.(compositeElement)) + case interface{}: + if isOperator(key) { + currentElement = &operatorValue{operator: key, value: t} + } else { + currentElement = &keyValue{key: key, value: t} + } + //log.Print("generic interface: ", t) + default: + return nil, ErrParserError + } + if parentElement != nil { // normal recurrent action + parentElement.addChild(currentElement) + } else { + if len(a) > 1 { // we have more keys in the map + parentElement = &operatorSlice{operator: CondAND} + parentElement.addChild(currentElement) + } else { // it was only one key value + return currentElement, nil + } + } + } + return parentElement, nil +} + +func (sm *StructMatcher) Parse(s string) (err error) { + a := make(map[string]interface{}) + if len(s) != 0 { + if err := json.Unmarshal([]byte([]byte(s)), &a); err != nil { + return err + } + sm.rootElement, err = sm.load(a, nil) + } else { + sm.rootElement = &trueElement{} + } + return +} + +func (sm *StructMatcher) Match(o interface{}) (bool, error) { + if sm.rootElement == nil { + return false, ErrParserError + } + return sm.rootElement.checkStruct(o) +} diff --git a/structmatcher/structmatcher_test.go b/structmatcher/structmatcher_test.go new file mode 100644 index 000000000..e221d1272 --- /dev/null +++ b/structmatcher/structmatcher_test.go @@ -0,0 +1,429 @@ +package structmatcher + +import ( + "encoding/json" + "strings" + "testing" + "time" +) + +func toJSON(v interface{}) string { + b, _ := json.MarshalIndent(v, "", " ") + return string(b) +} + +func TestStructMatcher(t *testing.T) { + cl := &StructMatcher{} + err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + + err = cl.Parse(`{"*has":["NAT","RET","EUR"]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + err = cl.Parse(`{"Field":7, "Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + err = cl.Parse(``) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } +} + +func TestStructMatcherKeyValue(t *testing.T) { + o := struct { + Test string + Field float64 + Other bool + ExpDate time.Time + }{ + Test: "test", + Field: 6.0, + Other: true, + ExpDate: time.Date(2016, 1, 19, 20, 47, 0, 0, time.UTC), + } + cl := &StructMatcher{} + err := cl.Parse(`{"Test":"test"}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6, "Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":7, "Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6, "Other":false}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true, "Field":{"*gt":5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true, "Field":{"*gt":7}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(``) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"ExpDate":{"*exp":true}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"ExpDate":{"*exp":false}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"*and":[{"Field":{"*gte":50}},{"Test":{"*eq":"test"}}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"WrongFieldName":{"*eq":1}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err == nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherKeyValuePointer(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"Test":"test"}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherOperatorValue(t *testing.T) { + root := &operatorValue{operator: CondGT, value: 3.4} + if check, err := root.checkStruct(3.5); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(root)) + } + root = &operatorValue{operator: CondEQ, value: 3.4} + if check, err := root.checkStruct(3.5); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(root)) + } + root = &operatorValue{operator: CondEQ, value: 3.4} + if check, err := root.checkStruct(3.4); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(root)) + } + root = &operatorValue{operator: CondEQ, value: "zinc"} + if check, err := root.checkStruct("zinc"); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(root)) + } + root = &operatorValue{operator: CondHAS, value: []interface{}{"NAT", "RET", "EUR"}} + if check, err := root.checkStruct(StringMap{"WOR": true, "EUR": true, "NAT": true, "RET": true, "ROM": true}); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, toJSON(root)) + } +} + +func TestStructMatcherKeyStruct(t *testing.T) { + o := struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"Field":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*gte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lt": 7}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*eq": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*eq": "test"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherKeyStructPointer(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"Field":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*gte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lt": 7}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*eq": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*eq": "test"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherOperatorSlice(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"*or":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"*or":[{"Test":"test"},{"Field":{"*gt":7}},{"Other":false}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":7}},{"Other":false}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherMixed(t *testing.T) { + o := &struct { + Test string + Field float64 + Categories StringMap + Other bool + }{ + Test: "test", + Field: 6.0, + Categories: StringMap{"call": true, "data": true, "voice": true}, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true},{"Categories":{"*has":["data", "call"]}}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherBalanceType(t *testing.T) { + type Balance struct { + Value float64 + } + + o := &struct { + BalanceType string + Balance + }{ + BalanceType: "*monetary", + Balance: Balance{Value: 10}, + } + cl := &StructMatcher{} + err := cl.Parse(`{"BalanceType":"*monetary","Value":10}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherRSR(t *testing.T) { + o := &struct { + BalanceType string + }{ + BalanceType: "*monetary", + } + cl := &StructMatcher{} + err := cl.Parse(`{"BalanceType":{"*rsr":"^*mon"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"BalanceType":{"*rsr":"^*min"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, toJSON(cl.rootElement)) + } +} diff --git a/test.sh b/test.sh index e0c162a50..65c8dfcd4 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,5 @@ #! /usr/bin/env sh +./build.sh go test -i github.com/cgrates/cgrates/apier/v1 go test -i github.com/cgrates/cgrates/apier/v2 diff --git a/utils/apitpdata.go b/utils/apitpdata.go index f235c33ed..56dcd006f 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -273,22 +273,22 @@ type TPActions struct { } type TPAction struct { - Identifier string // Identifier mapped in the code - BalanceId string // Balance identification string (account scope) - BalanceType string // Type of balance the action will operate on - Directions string // Balance direction - Units float64 // Number of units to add/deduct - ExpiryTime string // Time when the units will expire - Filter string // The condition on balances that is checked before the action - TimingTags string // Timing when balance is active - DestinationIds string // Destination profile id - RatingSubject string // Reference a rate subject defined in RatingProfiles - Categories string // category filter for balances - SharedGroups string // Reference to a shared group - BalanceWeight float64 // Balance weight + Identifier string // Identifier mapped in the code + BalanceId string // Balance identification string (account scope) + BalanceType string // Type of balance the action will operate on + Directions string // Balance direction + Units string // Number of units to add/deduct + ExpiryTime string // Time when the units will expire + Filter string // The condition on balances that is checked before the action + TimingTags string // Timing when balance is active + DestinationIds string // Destination profile id + RatingSubject string // Reference a rate subject defined in RatingProfiles + Categories string // category filter for balances + SharedGroups string // Reference to a shared group + BalanceWeight string // Balance weight ExtraParameters string - BalanceBlocker bool - BalanceDisabled bool + BalanceBlocker string + BalanceDisabled string Weight float64 // Action's weight } @@ -480,18 +480,20 @@ type TPActionTrigger struct { ThresholdValue float64 // Threshold Recurrent bool // reset executed flag each run MinSleep string // Minimum duration between two executions in case of recurrent triggers + ExpirationDate string // Trigger expiration + ActivationDate string // Trigger activation BalanceId string // The id of the balance in the account BalanceType string // Type of balance this trigger monitors BalanceDirections string // Traffic direction BalanceDestinationIds string // filter for balance - BalanceWeight float64 // filter for balance + BalanceWeight string // filter for balance BalanceExpirationDate string // filter for balance BalanceTimingTags string // filter for balance BalanceRatingSubject string // filter for balance BalanceCategories string // filter for balance BalanceSharedGroups string // filter for balance - BalanceBlocker bool // filter for balance - BalanceDisabled bool // filter for balance + BalanceBlocker string // filter for balance + BalanceDisabled string // filter for balance MinQueuedItems int // Trigger actions only if this number is hit (stats only) ActionsId string // Actions which will execute on threshold reached Weight float64 // weight @@ -1154,3 +1156,16 @@ type AliasValue struct { Alias string Weight float64 } + +// AttrSMGGetActiveSessions will filter returned sessions by SMGenericV1 +type AttrSMGGetActiveSessions struct { + ToR *string + RunID *string + RequestType *string + Tenant *string + Category *string + Account *string + Subject *string + Destination *string + Supplier *string +} diff --git a/utils/consts.go b/utils/consts.go index fc09a9194..72c15b145 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -28,6 +28,9 @@ var ( ErrUnauthorizedDestination = errors.New("UNAUTHORIZED_DESTINATION") ErrRatingPlanNotFound = errors.New("RATING_PLAN_NOT_FOUND") ErrAccountNotFound = errors.New("ACCOUNT_NOT_FOUND") + ErrAccountDisabled = errors.New("ACCOUNT_DISABLED") + ErrUserNotFound = errors.New("USER_NOT_FOUND") + ErrInsufficientCredit = errors.New("INSUFFICENT_CREDIT") ) const ( @@ -89,6 +92,8 @@ const ( ROUNDING_MIDDLE = "*middle" ROUNDING_DOWN = "*down" ANY = "*any" + UNLIMITED = "*unlimited" + ZERO = "*zero" ASAP = "*asap" USERS = "*users" COMMENT_CHAR = '#' @@ -120,6 +125,7 @@ const ( SETUP_TIME = "SetupTime" ANSWER_TIME = "AnswerTime" USAGE = "Usage" + LastUsed = "LastUsed" PDD = "PDD" SUPPLIER = "Supplier" MEDI_RUNID = "RunID" @@ -225,8 +231,14 @@ const ( CGR_DISCONNECT_CAUSE = "cgr_disconnectcause" CGR_COMPUTELCR = "cgr_computelcr" CGR_SUPPLIERS = "cgr_suppliers" + CGRFlags = "cgr_flags" KAM_FLATSTORE = "kamailio_flatstore" OSIPS_FLATSTORE = "opensips_flatstore" + MAX_DEBIT_CACHE_PREFIX = "MAX_DEBIT_" + REFUND_INCR_CACHE_PREFIX = "REFUND_INCR_" + REFUND_ROUND_CACHE_PREFIX = "REFUND_ROUND_" + GET_SESS_RUNS_CACHE_PREFIX = "GET_SESS_RUNS_" + LOG_CALL_COST_CACHE_PREFIX = "LOG_CALL_COSTS_" ALIAS_CONTEXT_RATING = "*rating" NOT_AVAILABLE = "N/A" CALL = "call" @@ -261,6 +273,8 @@ const ( MetaRaw = "*raw" CreatedAt = "CreatedAt" UpdatedAt = "UpdatedAt" + HandlerArgSep = "|" + FlagForceDuration = "fd" ) var ( diff --git a/utils/coreutils.go b/utils/coreutils.go index 9646d6e1e..0f05b1044 100644 --- a/utils/coreutils.go +++ b/utils/coreutils.go @@ -128,6 +128,9 @@ func Round(x float64, prec int, method string) float64 { func ParseTimeDetectLayout(tmStr string, timezone string) (time.Time, error) { var nilTime time.Time + if len(tmStr) == 0 { + return nilTime, nil + } loc, err := time.LoadLocation(timezone) if err != nil { return nilTime, err @@ -190,6 +193,8 @@ func ParseDate(date string) (expDate time.Time, err error) { expDate = time.Now().AddDate(0, 1, 0) // add one month case date == "*yearly": expDate = time.Now().AddDate(1, 0, 0) // add one year + case date == "*month_end": + expDate = GetEndOfMonth(time.Now()) case strings.HasSuffix(date, "Z"): expDate, err = time.Parse(time.RFC3339, date) default: @@ -231,12 +236,10 @@ func CopyHour(src, dest time.Time) time.Time { // Parses duration, considers s as time unit if not provided, seconds as float to specify subunits func ParseDurationWithSecs(durStr string) (time.Duration, error) { - if durSecs, err := strconv.ParseFloat(durStr, 64); err == nil { // Seconds format considered - durNanosecs := int(durSecs * NANO_MULTIPLIER) - return time.Duration(durNanosecs), nil - } else { - return time.ParseDuration(durStr) + if _, err := strconv.ParseFloat(durStr, 64); err == nil { // Seconds format considered + durStr += "s" } + return time.ParseDuration(durStr) } func AccountKey(tenant, account string) string { @@ -334,6 +337,10 @@ func Fib() func() time.Duration { // Utilities to provide pointers where we need to define ad-hoc func StringPointer(str string) *string { + if str == ZERO { + str = "" + return &str + } return &str } @@ -361,6 +368,14 @@ func Float64SlicePointer(slc []float64) *[]float64 { return &slc } +func StringMapPointer(sm StringMap) *StringMap { + return &sm +} + +func TimePointer(t time.Time) *time.Time { + return &t +} + func ReflectFuncLocation(handler interface{}) (file string, line int) { f := runtime.FuncForPC(reflect.ValueOf(handler).Pointer()) entry := f.Entry() @@ -474,3 +489,47 @@ func FmtFieldWidth(source string, width int, strip, padding string, mandatory bo } return source, nil } + +// Returns the string representation of iface or error if not convertible +func CastIfToString(iface interface{}) (strVal string, casts bool) { + switch iface.(type) { + case string: + strVal = iface.(string) + casts = true + case int: + strVal = strconv.Itoa(iface.(int)) + casts = true + case int64: + strVal = strconv.FormatInt(iface.(int64), 10) + casts = true + case float64: + strVal = strconv.FormatFloat(iface.(float64), 'f', -1, 64) + casts = true + case bool: + strVal = strconv.FormatBool(iface.(bool)) + casts = true + case []uint8: + var byteVal []byte + if byteVal, casts = iface.([]byte); casts { + strVal = string(byteVal) + } + default: // Maybe we are lucky and the value converts to string + strVal, casts = iface.(string) + } + return strVal, casts +} + +func GetEndOfMonth(ref time.Time) time.Time { + if ref.IsZero() { + return time.Now() + } + year, month, _ := ref.Date() + if month == time.December { + year++ + month = time.January + } else { + month++ + } + eom := time.Date(year, month, 1, 0, 0, 0, 0, ref.Location()) + return eom.Add(-time.Second) +} diff --git a/utils/map.go b/utils/map.go index 6fa89fe0f..0bf197a7a 100644 --- a/utils/map.go +++ b/utils/map.go @@ -74,18 +74,16 @@ func NewStringMap(s ...string) StringMap { } func ParseStringMap(s string) StringMap { - slice := strings.Split(s, INFIELD_SEP) - result := make(StringMap) - for _, v := range slice { - v = strings.TrimSpace(v) - if v != "" { - result[v] = true - } + if s == ZERO { + return make(StringMap) } - return result + return StringMapFromSlice(strings.Split(s, INFIELD_SEP)) } func (sm StringMap) Equal(om StringMap) bool { + if sm == nil && om != nil { + return false + } if len(sm) != len(om) { return false } @@ -109,14 +107,6 @@ func (sm StringMap) Includes(om StringMap) bool { return true } -func (sm StringMap) Clone() StringMap { - result := make(StringMap, len(sm)) - for k := range sm { - result[k] = true - } - return result -} - func (sm StringMap) Slice() []string { result := make([]string, len(sm)) i := 0 @@ -127,12 +117,35 @@ func (sm StringMap) Slice() []string { return result } +func (sm StringMap) IsEmpty() bool { + return sm == nil || + len(sm) == 0 || + sm[ANY] == true +} + +func StringMapFromSlice(s []string) StringMap { + result := make(StringMap) + for _, v := range s { + v = strings.TrimSpace(v) + if v != "" { + result[v] = true + } + } + return result +} + func (sm StringMap) Copy(o StringMap) { for k, v := range o { sm[k] = v } } +func (sm StringMap) Clone() StringMap { + result := make(StringMap, len(sm)) + result.Copy(sm) + return result +} + func (sm StringMap) String() string { return strings.Join(sm.Slice(), INFIELD_SEP) } diff --git a/utils/rsrfield.go b/utils/rsrfield.go index b454500fb..d8ca48c2b 100644 --- a/utils/rsrfield.go +++ b/utils/rsrfield.go @@ -166,6 +166,9 @@ func (rsrFltr *RSRFilter) Pass(val string) bool { if rsrFltr.filterRule[:1] == REGEXP_PREFIX { return rsrFltr.fltrRgxp.MatchString(val) != rsrFltr.negative } + if rsrFltr.filterRule == "^$" { // Special case to test empty value + return len(val) == 0 != rsrFltr.negative + } if rsrFltr.filterRule[:1] == MatchStartPrefix { return strings.HasPrefix(val, rsrFltr.filterRule[1:]) != rsrFltr.negative } diff --git a/utils/rsrfield_test.go b/utils/rsrfield_test.go index 1da60b703..c98c32fb1 100644 --- a/utils/rsrfield_test.go +++ b/utils/rsrfield_test.go @@ -341,4 +341,14 @@ func TestRSRFilterPass(t *testing.T) { if fltr.Pass("CGRateS") { t.Error("Passing!") } + fltr, err = NewRSRFilter("^$") // Empty value + if err != nil { + t.Error(err) + } + if fltr.Pass("CGRateS") { + t.Error("Passing!") + } + if !fltr.Pass("") { + t.Error("Not passing!") + } } diff --git a/utils/utils_test.go b/utils/utils_test.go index 1f5951c07..1edd0ada0 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -50,6 +50,19 @@ func TestRoundUp(t *testing.T) { } } +func TestRoundUpTwice(t *testing.T) { + result := Round(0.641666666667, 4, ROUNDING_UP) + expected := 0.6417 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } + result = Round(result, 4, ROUNDING_UP) + expected = 0.6417 + if result != expected { + t.Errorf("Error rounding up: sould be %v was %v", expected, result) + } +} + func TestRoundUpMiddle(t *testing.T) { result := Round(12.5, 0, ROUNDING_UP) expected := 13.0 @@ -579,3 +592,52 @@ func TestPaddingNotAllowed(t *testing.T) { t.Error("Expected error") } } + +func TestCastIfToString(t *testing.T) { + v := interface{}("somestr") + if sOut, casts := CastIfToString(v); !casts { + t.Error("Does not cast") + } else if sOut != "somestr" { + t.Errorf("Received: %+v", sOut) + } + v = interface{}(1) + if sOut, casts := CastIfToString(v); !casts { + t.Error("Does not cast") + } else if sOut != "1" { + t.Errorf("Received: %+v", sOut) + } + v = interface{}(1.2) + if sOut, casts := CastIfToString(v); !casts { + t.Error("Does not cast") + } else if sOut != "1.2" { + t.Errorf("Received: %+v", sOut) + } +} + +func TestEndOfMonth(t *testing.T) { + eom := GetEndOfMonth(time.Date(2016, time.February, 5, 10, 1, 2, 3, time.UTC)) + expected := time.Date(2016, time.February, 29, 23, 59, 59, 0, time.UTC) + if !eom.Equal(expected) { + t.Errorf("Expected %v was %v", expected, eom) + } + eom = GetEndOfMonth(time.Date(2015, time.February, 5, 10, 1, 2, 3, time.UTC)) + expected = time.Date(2015, time.February, 28, 23, 59, 59, 0, time.UTC) + if !eom.Equal(expected) { + t.Errorf("Expected %v was %v", expected, eom) + } + eom = GetEndOfMonth(time.Date(2016, time.January, 31, 10, 1, 2, 3, time.UTC)) + expected = time.Date(2016, time.January, 31, 23, 59, 59, 0, time.UTC) + if !eom.Equal(expected) { + t.Errorf("Expected %v was %v", expected, eom) + } + eom = GetEndOfMonth(time.Date(2016, time.December, 31, 10, 1, 2, 3, time.UTC)) + expected = time.Date(2016, time.December, 31, 23, 59, 59, 0, time.UTC) + if !eom.Equal(expected) { + t.Errorf("Expected %v was %v", expected, eom) + } + eom = GetEndOfMonth(time.Date(2016, time.July, 31, 23, 59, 59, 0, time.UTC)) + expected = time.Date(2016, time.July, 31, 23, 59, 59, 0, time.UTC) + if !eom.Equal(expected) { + t.Errorf("Expected %v was %v", expected, eom) + } +}