diff --git a/.travis.yml b/.travis.yml index 936b99045..9b4f37383 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,11 @@ language: go go: - 1.6 -script: $TRAVIS_BUILD_DIR/test.sh +install: + - go get github.com/Masterminds/glide + - glide install + +script: go test -v $(glide novendor) branches: only: master diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b2a3821bc..6c3e9a5e2 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -42,6 +42,7 @@ information, please see the [`CONTRIBUTING.md`](CONTRIBUTING.md) file. | @bhepp | Brice Heppner | | @noahmehl | Noah Mehl | | @elfranne | Tom Braarup Cuykens | +| @rbarrabe | Régis Barrabé | diff --git a/agents/libdmt.go b/agents/libdmt.go index bb60b4f75..f3ee75dee 100644 --- a/agents/libdmt.go +++ b/agents/libdmt.go @@ -244,8 +244,8 @@ func metaValueExponent(m *diam.Message, argsTpl utils.RSRFields, roundingDecimal 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) +func metaSum(m *diam.Message, argsTpl utils.RSRFields, passAtIndex, roundingDecimals int) (string, error) { + valStr := composedFieldvalue(m, argsTpl, passAtIndex, nil) handlerArgs := strings.Split(valStr, utils.HandlerArgSep) var summed float64 for _, arg := range handlerArgs { @@ -398,7 +398,7 @@ func fieldOutVal(m *diam.Message, cfgFld *config.CfgCdrField, extraParam interfa 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) + outVal, err = metaSum(m, cfgFld.Value, passAtIndex, 10) default: outVal, err = metaHandler(m, cfgFld.HandlerId, cfgFld.Layout, extraParam.(time.Duration)) if err != nil { diff --git a/agents/libdmt_test.go b/agents/libdmt_test.go index 7d6924c42..5bb1d7c01 100644 --- a/agents/libdmt_test.go +++ b/agents/libdmt_test.go @@ -27,6 +27,7 @@ import ( "time" "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/sessionmanager" "github.com/cgrates/cgrates/utils" "github.com/fiorix/go-diameter/diam" "github.com/fiorix/go-diameter/diam/avp" @@ -133,12 +134,12 @@ func TestMetaSum(t *testing.T) { }), }, }) - 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 { + 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), 0, 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 { + 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), 0, 10); err == nil { t.Error("Should have received error") // Insufficient number arguments } } @@ -396,3 +397,79 @@ func TestCCASetProcessorAVPs(t *testing.T) { t.Errorf("Expecting: %+v, received: %+v", eMessage, ccaMsg) } } + +func TestCCRAsSMGenericEvent(t *testing.T) { + ccr := &CCR{ // Bare information, just the one needed for answer + SessionId: "ccrasgen1", + AuthApplicationId: 4, + CCRequestType: 3, + } + ccr.diamMessage = ccr.AsBareDiameterMessage() + ccr.diamMessage.NewAVP("Multiple-Services-Credit-Control", avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(446, avp.Mbit, 0, &diam.GroupedAVP{ // Used-Service-Unit + AVP: []*diam.AVP{ + diam.NewAVP(420, avp.Mbit, 0, datatype.Unsigned32(17)), // CC-Time + diam.NewAVP(412, avp.Mbit, 0, datatype.Unsigned64(1341)), // CC-Input-Octets + diam.NewAVP(414, avp.Mbit, 0, datatype.Unsigned64(3079)), // CC-Output-Octets + }, + }), + diam.NewAVP(432, avp.Mbit, 0, datatype.Unsigned32(99)), + }, + }) + ccr.diamMessage.NewAVP("Multiple-Services-Credit-Control", avp.Mbit, 0, &diam.GroupedAVP{ + AVP: []*diam.AVP{ + diam.NewAVP(446, avp.Mbit, 0, &diam.GroupedAVP{ // Used-Service-Unit + AVP: []*diam.AVP{ + diam.NewAVP(452, avp.Mbit, 0, datatype.Enumerated(0)), // Tariff-Change-Usage + diam.NewAVP(420, avp.Mbit, 0, datatype.Unsigned32(20)), // CC-Time + diam.NewAVP(412, avp.Mbit, 0, datatype.Unsigned64(8046)), // CC-Input-Octets + diam.NewAVP(414, avp.Mbit, 0, datatype.Unsigned64(46193)), // CC-Output-Octets + }, + }), + diam.NewAVP(432, avp.Mbit, 0, datatype.Unsigned32(1)), + }, + }) + ccr.diamMessage.NewAVP("FramedIPAddress", avp.Mbit, 0, datatype.OctetString("0AE40041")) + cfgFlds := make([]*config.CfgCdrField, 0) + eSMGEv := sessionmanager.SMGenericEvent{"EventName": "DIAMETER_CCR"} + if rSMGEv, err := ccr.AsSMGenericEvent(cfgFlds); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eSMGEv, rSMGEv) { + t.Errorf("Expecting: %+v, received: %+v", eSMGEv, rSMGEv) + } + cfgFlds = []*config.CfgCdrField{ + &config.CfgCdrField{ + Tag: "LastUsed", + FieldFilter: utils.ParseRSRFieldsMustCompile("~Multiple-Services-Credit-Control>Used-Service-Unit>CC-Input-Octets:s/^(.*)$/test/(test);Multiple-Services-Credit-Control>Rating-Group(1)", utils.INFIELD_SEP), + FieldId: "LastUsed", + Type: "*handler", + HandlerId: "*sum", + Value: utils.ParseRSRFieldsMustCompile("Multiple-Services-Credit-Control>Used-Service-Unit>CC-Input-Octets;^|;Multiple-Services-Credit-Control>Used-Service-Unit>CC-Output-Octets", utils.INFIELD_SEP), + Mandatory: true, + }, + } + eSMGEv = sessionmanager.SMGenericEvent{"EventName": "DIAMETER_CCR", "LastUsed": "54239"} + if rSMGEv, err := ccr.AsSMGenericEvent(cfgFlds); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eSMGEv, rSMGEv) { + t.Errorf("Expecting: %+v, received: %+v", eSMGEv, rSMGEv) + } + cfgFlds = []*config.CfgCdrField{ + &config.CfgCdrField{ + Tag: "LastUsed", + FieldFilter: utils.ParseRSRFieldsMustCompile("~Multiple-Services-Credit-Control>Used-Service-Unit>CC-Input-Octets:s/^(.*)$/test/(test);Multiple-Services-Credit-Control>Rating-Group(99)", utils.INFIELD_SEP), + FieldId: "LastUsed", + Type: "*handler", + HandlerId: "*sum", + Value: utils.ParseRSRFieldsMustCompile("Multiple-Services-Credit-Control>Used-Service-Unit>CC-Input-Octets;^|;Multiple-Services-Credit-Control>Used-Service-Unit>CC-Output-Octets", utils.INFIELD_SEP), + Mandatory: true, + }, + } + eSMGEv = sessionmanager.SMGenericEvent{"EventName": "DIAMETER_CCR", "LastUsed": "4420"} + if rSMGEv, err := ccr.AsSMGenericEvent(cfgFlds); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eSMGEv, rSMGEv) { + t.Errorf("Expecting: %+v, received: %+v", eSMGEv, rSMGEv) + } +} diff --git a/apier/v1/accounts.go b/apier/v1/accounts.go index 6d006da25..c6c161331 100644 --- a/apier/v1/accounts.go +++ b/apier/v1/accounts.go @@ -318,7 +318,7 @@ func (self *ApierV1) GetAccounts(attr utils.AttrGetAccounts, reply *[]interface{ var accountKeys []string var err error if len(attr.AccountIds) == 0 { - if accountKeys, err = self.AccountDb.GetKeysForPrefix(utils.ACCOUNT_PREFIX + attr.Tenant); err != nil { + if accountKeys, err = self.AccountDb.GetKeysForPrefix(utils.ACCOUNT_PREFIX+attr.Tenant, true); err != nil { return err } } else { diff --git a/apier/v1/apier_local_test.go b/apier/v1/apier_local_test.go index 47da716b5..88ad8c819 100644 --- a/apier/v1/apier_local_test.go +++ b/apier/v1/apier_local_test.go @@ -1294,10 +1294,10 @@ func TestApierResetDataAfterLoadFromFolder(t *testing.T) { if err := rater.Call("ApierV1.GetCacheStats", args, &rcvStats); err != nil { t.Error("Got error on ApierV1.GetCacheStats: ", err.Error()) } else { - if rcvStats.Destinations != 4 || - rcvStats.RatingPlans != 3 || - rcvStats.RatingProfiles != 3 || - rcvStats.Actions != 6 || + if rcvStats.Destinations != 5 || + rcvStats.RatingPlans != 4 || + rcvStats.RatingProfiles != 4 || + rcvStats.Actions != 7 || rcvStats.DerivedChargers != 2 { t.Errorf("Calling ApierV1.GetCacheStats received: %+v", rcvStats) } diff --git a/apier/v1/cdrs.go b/apier/v1/cdrs.go index fe52b49ab..39dcd5378 100644 --- a/apier/v1/cdrs.go +++ b/apier/v1/cdrs.go @@ -26,19 +26,19 @@ import ( ) // Retrieves the callCost out of CGR logDb -func (apier *ApierV1) GetCallCostLog(attrs utils.AttrGetCallCost, reply *engine.CallCost) error { +func (apier *ApierV1) GetCallCostLog(attrs utils.AttrGetCallCost, reply *engine.SMCost) error { if attrs.CgrId == "" { return utils.NewErrMandatoryIeMissing("CgrId") } if attrs.RunId == "" { attrs.RunId = utils.META_DEFAULT } - if cc, err := apier.CdrDb.GetCallCostLog(attrs.CgrId, attrs.RunId); err != nil { + if smc, err := apier.CdrDb.GetCallCostLog(attrs.CgrId, attrs.RunId); err != nil { return utils.NewErrServerError(err) - } else if cc == nil { + } else if smc == nil { return utils.ErrNotFound } else { - *reply = *cc + *reply = *smc } return nil } diff --git a/apier/v2/accounts.go b/apier/v2/accounts.go index c3c495edd..08f6832dd 100644 --- a/apier/v2/accounts.go +++ b/apier/v2/accounts.go @@ -33,7 +33,7 @@ func (self *ApierV2) GetAccounts(attr utils.AttrGetAccounts, reply *[]*engine.Ac var accountKeys []string var err error if len(attr.AccountIds) == 0 { - if accountKeys, err = self.AccountDb.GetKeysForPrefix(utils.ACCOUNT_PREFIX + utils.ConcatenatedKey(attr.Tenant)); err != nil { + if accountKeys, err = self.AccountDb.GetKeysForPrefix(utils.ACCOUNT_PREFIX+utils.ConcatenatedKey(attr.Tenant), true); err != nil { return err } } else { diff --git a/data/conf/samples/cgradmin/cgradmin.json b/data/conf/samples/cgradmin/cgradmin.json index 2f6f9e4a1..9dd30abcf 100644 --- a/data/conf/samples/cgradmin/cgradmin.json +++ b/data/conf/samples/cgradmin/cgradmin.json @@ -10,19 +10,19 @@ "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", -//}, +"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: diff --git a/data/diameter/dict/huawei/huawei.xml b/data/diameter/dict/huawei/huawei.xml index b180087a7..8553ad05b 100644 --- a/data/diameter/dict/huawei/huawei.xml +++ b/data/diameter/dict/huawei/huawei.xml @@ -133,7 +133,7 @@ - + @@ -143,4 +143,4 @@ - \ No newline at end of file + diff --git a/data/diameter/dict/huawei/vodafone.xml b/data/diameter/dict/huawei/vodafone.xml index ff56332c0..be03b5746 100644 --- a/data/diameter/dict/huawei/vodafone.xml +++ b/data/diameter/dict/huawei/vodafone.xml @@ -43,8 +43,8 @@ - + - \ No newline at end of file + diff --git a/data/storage/mysql/create_cdrs_tables.sql b/data/storage/mysql/create_cdrs_tables.sql index 3f5e0269d..463ea922e 100644 --- a/data/storage/mysql/create_cdrs_tables.sql +++ b/data/storage/mysql/create_cdrs_tables.sql @@ -42,6 +42,7 @@ CREATE TABLE sm_costs ( cgrid char(40) NOT NULL, run_id varchar(64) NOT NULL, cost_source varchar(64) NOT NULL, + `usage` DECIMAL(30,9) NOT NULL, cost_details text, created_at TIMESTAMP, deleted_at TIMESTAMP, diff --git a/data/storage/postgres/create_cdrs_tables.sql b/data/storage/postgres/create_cdrs_tables.sql index d4425fb89..2c710dc4e 100644 --- a/data/storage/postgres/create_cdrs_tables.sql +++ b/data/storage/postgres/create_cdrs_tables.sql @@ -45,6 +45,7 @@ CREATE TABLE sm_costs ( cgrid CHAR(40) NOT NULL, run_id VARCHAR(64) NOT NULL, cost_source VARCHAR(64) NOT NULL, + usage NUMERIC(30,9) NOT NULL, cost_details jsonb, created_at TIMESTAMP, deleted_at TIMESTAMP, diff --git a/data/tariffplans/testtp/AccountActions.csv b/data/tariffplans/testtp/AccountActions.csv index ca940949b..4cf06f896 100644 --- a/data/tariffplans/testtp/AccountActions.csv +++ b/data/tariffplans/testtp/AccountActions.csv @@ -4,4 +4,5 @@ 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 +cgrates.org,1009,TEST_EXE,,, +cgrates.org,1010,TEST_DATA_r,,true, diff --git a/data/tariffplans/testtp/ActionPlans.csv b/data/tariffplans/testtp/ActionPlans.csv index 670cf8c93..f41668f9b 100644 --- a/data/tariffplans/testtp/ActionPlans.csv +++ b/data/tariffplans/testtp/ActionPlans.csv @@ -1,4 +1,5 @@ #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 +TEST_EXE,TOPUP_EXE,ALWAYS,10 +TEST_DATA_r,TOPUP_DATA_r,ASAP,10 \ No newline at end of file diff --git a/data/tariffplans/testtp/Actions.csv b/data/tariffplans/testtp/Actions.csv index 045fc0666..666038a38 100644 --- a/data/tariffplans/testtp/Actions.csv +++ b/data/tariffplans/testtp/Actions.csv @@ -4,4 +4,6 @@ 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 +TOPUP_EXE,*topup,,,,*monetary,*out,,*any,,,*unlimited,,5,10,false,false,10 +TOPUP_DATA_r,*topup,,,,*monetary,*out,,DATA_DEST,,,*unlimited,,5000000,10,false,false,10 +TOPUP_DATA_r,*topup,,,,*data,*out,,DATA_DEST,datar,,*unlimited,,50000000000,10,false,false,10 \ No newline at end of file diff --git a/data/tariffplans/testtp/DestinationRates.csv b/data/tariffplans/testtp/DestinationRates.csv index b5d2474b3..9be903aef 100644 --- a/data/tariffplans/testtp/DestinationRates.csv +++ b/data/tariffplans/testtp/DestinationRates.csv @@ -3,3 +3,4 @@ DR_RETAIL,GERMANY,RT_1CENT,*up,4,0, DR_RETAIL,GERMANY_MOBILE,RT_1CENT,*up,4,0, DR_DATA_1,*any,RT_DATA_2c,*up,4,0, DR_SMS_1,*any,RT_SMS_5c,*up,4,0, +DR_DATA_r,DATA_DEST,RT_DATA_r,*up,5,0, \ No newline at end of file diff --git a/data/tariffplans/testtp/Destinations.csv b/data/tariffplans/testtp/Destinations.csv index 192146546..37385ea00 100644 --- a/data/tariffplans/testtp/Destinations.csv +++ b/data/tariffplans/testtp/Destinations.csv @@ -3,3 +3,4 @@ GERMANY,+49 GERMANY_MOBILE,+4915 GERMANY_MOBILE,+4916 GERMANY_MOBILE,+4917 +DATA_DEST,222 diff --git a/data/tariffplans/testtp/Rates.csv b/data/tariffplans/testtp/Rates.csv index 991ce0720..21c9abc4a 100644 --- a/data/tariffplans/testtp/Rates.csv +++ b/data/tariffplans/testtp/Rates.csv @@ -2,3 +2,4 @@ RT_1CENT,0,1,1s,1s,0s RT_DATA_2c,0,0.002,10,10,0 RT_SMS_5c,0,0.005,1,1,0 +RT_DATA_r,0,0.1,1048576,10240,0 diff --git a/data/tariffplans/testtp/RatingPlans.csv b/data/tariffplans/testtp/RatingPlans.csv index 7d15f1a92..f7003dcf4 100644 --- a/data/tariffplans/testtp/RatingPlans.csv +++ b/data/tariffplans/testtp/RatingPlans.csv @@ -2,3 +2,4 @@ RP_RETAIL,DR_RETAIL,ALWAYS,10 RP_DATA1,DR_DATA_1,ALWAYS,10 RP_SMS1,DR_SMS_1,ALWAYS,10 +RP_DATAr,DR_DATA_r,ALWAYS,10 diff --git a/data/tariffplans/testtp/RatingProfiles.csv b/data/tariffplans/testtp/RatingProfiles.csv index db758126b..ffa6bf2f6 100644 --- a/data/tariffplans/testtp/RatingProfiles.csv +++ b/data/tariffplans/testtp/RatingProfiles.csv @@ -2,3 +2,4 @@ *out,cgrates.org,call,*any,2012-01-01T00:00:00Z,RP_RETAIL,, *out,cgrates.org,data,*any,2012-01-01T00:00:00Z,RP_DATA1,, *out,cgrates.org,sms,*any,2012-01-01T00:00:00Z,RP_SMS1,, +*out,cgrates.org,data,datar,2016-01-01T00:00:00Z,RP_DATAr,, diff --git a/docs/installation.rst b/docs/installation.rst index 060939f2f..ebe40b156 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -27,23 +27,16 @@ After post-install actions are performed, CGRateS will be configured in */etc/cg 3.2. Using source ----------------- -After the go environment is installed_ (at least go1.2) and configured_ issue the following commands: -:: - - go get github.com/cgrates/cgrates/... - -This command will install the trunk version of CGRateS together with all the necessary dependencies. - -For developing CGRateS and switching betwen lts versions we are using the new (experimental) vendor directory feature introduced in go 1.5. In a nutshell all the dependencies are installed and used from a folder named vendor placed in the root of the project. +For developing CGRateS and switching betwen lts versions we are using the new vendor directory feature introduced in go 1.6. In a nutshell all the dependencies are installed and used from a folder named vendor placed in the root of the project. To manage this vendor folder we use a tool named glide_ which will download specific versions of the external packages used by CGRateS. To configure the project with glide use the following commands: :: - export GO15VENDOREXPERIMENT=1 #this should be placed in the rc script of your shell + go get github.com/Masterminds/glide go get github.com/cgrates/cgrates cd $GOPATH/src/github.com/cgrates/cgrates glide install -The glide install command will install the external dependencies versions specified in the glide.lock file in the vendor folder. There are different versions for each CGRateS branch, versions that are recorded in the yaml file when the GCRateS releases are made (using glide up command). +The glide install command will install the external dependencies versions specified in the glide.lock file in the vendor folder. There are different versions for each CGRateS branch, versions that are recorded in the yaml file when the GCRateS releases are made. Note that the vendor folder should not be registered with the VCS we are using. For more information and command options for use glide_ readme page. diff --git a/engine/cdrs.go b/engine/cdrs.go index 7fda4abc1..3004d8e69 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -41,6 +41,7 @@ type CallCostLog struct { CgrId string Source string RunId string + Usage float64 // real usage (not increment rounded) CallCost *CallCost CheckDuplicate bool } @@ -149,11 +150,11 @@ func (self *CdrServer) LogCallCost(ccl *CallCostLog) error { if cc != nil { return nil, utils.ErrExists } - return nil, self.cdrDb.LogCallCost(ccl.CgrId, ccl.RunId, ccl.Source, ccl.CallCost) + return nil, self.cdrDb.LogCallCost(&SMCost{CGRID: ccl.CgrId, RunID: ccl.RunId, CostSource: ccl.Source, Usage: ccl.Usage, CostDetails: ccl.CallCost}) }, 0, ccl.CgrId) return err } - return self.cdrDb.LogCallCost(ccl.CgrId, ccl.RunId, ccl.Source, ccl.CallCost) + return self.cdrDb.LogCallCost(&SMCost{CGRID: ccl.CgrId, RunID: ccl.RunId, CostSource: ccl.Source, Usage: ccl.Usage, CostDetails: ccl.CallCost}) } // Called by rate/re-rate API @@ -363,12 +364,16 @@ func (self *CdrServer) rateCDR(cdr *CDR) error { if cdr.RequestType == utils.META_NONE { return nil } - if utils.IsSliceMember([]string{utils.META_PREPAID, utils.PREPAID}, cdr.RequestType) && cdr.Usage != 0 { // ToDo: Get rid of PREPAID as soon as we don't want to support it backwards + _, hasLastUsed := cdr.ExtraFields["LastUsed"] + if utils.IsSliceMember([]string{utils.META_PREPAID, utils.PREPAID}, cdr.RequestType) && (cdr.Usage != 0 || hasLastUsed) { // ToDo: Get rid of PREPAID as soon as we don't want to support it backwards // Should be previously calculated and stored in DB delay := utils.Fib() + var usage float64 for i := 0; i < 4; i++ { - qryCC, err = self.cdrDb.GetCallCostLog(cdr.CGRID, cdr.RunID) + qrySMC, err := self.cdrDb.GetCallCostLog(cdr.CGRID, cdr.RunID) if err == nil { + qryCC = qrySMC.CostDetails + usage = qrySMC.Usage break } time.Sleep(delay()) @@ -377,6 +382,9 @@ func (self *CdrServer) rateCDR(cdr *CDR) error { utils.Logger.Warning(fmt.Sprintf(" WARNING: Could not find CallCostLog for cgrid: %s, source: %s, runid: %s, will recalculate", cdr.CGRID, utils.SESSION_MANAGER_SOURCE, cdr.RunID)) qryCC, err = self.getCostFromRater(cdr) } + if cdr.Usage == 0 { + cdr.Usage = time.Duration(usage) + } } else { qryCC, err = self.getCostFromRater(cdr) diff --git a/engine/guardian_test.go b/engine/guardian_test.go index a8e3cc4f8..6e436e9d5 100644 --- a/engine/guardian_test.go +++ b/engine/guardian_test.go @@ -19,31 +19,33 @@ along with this program. If not, see package engine import ( - "log" "testing" "time" ) -func ATestAccountLock(t *testing.T) { +func BenchmarkGuard(b *testing.B) { for i := 0; i < 100; i++ { go Guardian.Guard(func() (interface{}, error) { - log.Print("first 1") time.Sleep(1 * time.Millisecond) - log.Print("end first 1") return 0, nil }, 0, "1") go Guardian.Guard(func() (interface{}, error) { - log.Print("first 2") time.Sleep(1 * time.Millisecond) - log.Print("end first 2") return 0, nil }, 0, "2") go Guardian.Guard(func() (interface{}, error) { - log.Print("second 1") time.Sleep(1 * time.Millisecond) - log.Print("end second 1") return 0, nil }, 0, "1") } - time.Sleep(10 * time.Second) + +} + +func BenchmarkGuardian(b *testing.B) { + for i := 0; i < 100; i++ { + go Guardian.Guard(func() (interface{}, error) { + time.Sleep(1 * time.Millisecond) + return 0, nil + }, 0, "1") + } } diff --git a/engine/models.go b/engine/models.go index 85ce94362..1da2db987 100644 --- a/engine/models.go +++ b/engine/models.go @@ -452,6 +452,7 @@ type TBLSMCosts struct { Cgrid string RunID string CostSource string + Usage float64 CostDetails string CreatedAt time.Time DeletedAt time.Time diff --git a/engine/pubsub.go b/engine/pubsub.go index 091f30bab..520d06031 100644 --- a/engine/pubsub.go +++ b/engine/pubsub.go @@ -132,13 +132,13 @@ func (ps *PubSub) Publish(evt CgrEvent, reply *string) error { } transport := split[0] address := split[1] - + ttlVerify := ps.ttlVerify switch transport { case utils.META_HTTP_POST: go func() { delay := utils.Fib() for i := 0; i < 5; i++ { // Loop so we can increase the success rate on best effort - if _, err := ps.pubFunc(address, ps.ttlVerify, evt); err == nil { + if _, err := ps.pubFunc(address, ttlVerify, evt); err == nil { break // Success, no need to reinterate } else if i == 4 { // Last iteration, syslog the warning utils.Logger.Warning(fmt.Sprintf(" Failed calling url: [%s], error: [%s], event type: %s", address, err.Error(), evt["EventName"])) diff --git a/engine/storage_cdrs_it_test.go b/engine/storage_cdrs_it_test.go index f264ec237..4e934d4e4 100644 --- a/engine/storage_cdrs_it_test.go +++ b/engine/storage_cdrs_it_test.go @@ -222,13 +222,13 @@ func testSMCosts(cfg *config.CGRConfig) error { }, TOR: utils.VOICE, } - if err := cdrStorage.LogCallCost("164b0422fdc6a5117031b427439482c6a4f90e41", utils.META_DEFAULT, utils.UNIT_TEST, cc); err != nil { + if err := cdrStorage.LogCallCost(&SMCost{CGRID: "164b0422fdc6a5117031b427439482c6a4f90e41", RunID: utils.META_DEFAULT, CostSource: utils.UNIT_TEST, CostDetails: cc}); err != nil { return err } - if rcvCC, err := cdrStorage.GetCallCostLog("164b0422fdc6a5117031b427439482c6a4f90e41", utils.META_DEFAULT); err != nil { + if rcvSMC, err := cdrStorage.GetCallCostLog("164b0422fdc6a5117031b427439482c6a4f90e41", utils.META_DEFAULT); err != nil { return err - } else if len(cc.Timespans) != len(rcvCC.Timespans) { // cc.Timespans[0].RateInterval.Rating.Rates[0], rcvCC.Timespans[0].RateInterval.Rating.Rates[0]) - return fmt.Errorf("Expecting: %+v, received: %+v", cc, rcvCC) + } else if len(cc.Timespans) != len(rcvSMC.CostDetails.Timespans) { // cc.Timespans[0].RateInterval.Rating.Rates[0], rcvCC.Timespans[0].RateInterval.Rating.Rates[0]) + return fmt.Errorf("Expecting: %+v, received: %+s", cc, utils.ToIJSON(rcvSMC)) } return nil } diff --git a/engine/storage_interface.go b/engine/storage_interface.go index 8d082ae14..dbf8a8632 100644 --- a/engine/storage_interface.go +++ b/engine/storage_interface.go @@ -33,7 +33,7 @@ import ( type Storage interface { Close() Flush(string) error - GetKeysForPrefix(string) ([]string, error) + GetKeysForPrefix(string, bool) ([]string, error) } // Interface for storage providers. @@ -97,8 +97,8 @@ type AccountingStorage interface { type CdrStorage interface { Storage SetCDR(*CDR, bool) error - LogCallCost(cgrid, runid, source string, cc *CallCost) error - GetCallCostLog(cgrid, runid string) (*CallCost, error) + LogCallCost(smc *SMCost) error + GetCallCostLog(cgrid, runid string) (*SMCost, error) GetCDRs(*utils.CDRsFilter, bool) ([]*CDR, int64, error) } diff --git a/engine/storage_map.go b/engine/storage_map.go index 24315c1c4..cde93a9ed 100644 --- a/engine/storage_map.go +++ b/engine/storage_map.go @@ -53,14 +53,17 @@ func (ms *MapStorage) Flush(ignore string) error { return nil } -func (ms *MapStorage) GetKeysForPrefix(prefix string) ([]string, error) { - keysForPrefix := make([]string, 0) - for key := range ms.dict { - if strings.HasPrefix(key, prefix) { - keysForPrefix = append(keysForPrefix, key) +func (ms *MapStorage) GetKeysForPrefix(prefix string, skipCache bool) ([]string, error) { + if skipCache { + keysForPrefix := make([]string, 0) + for key := range ms.dict { + if strings.HasPrefix(key, prefix) { + keysForPrefix = append(keysForPrefix, key) + } } + return keysForPrefix, nil } - return keysForPrefix, nil + return cache2go.GetEntriesKeys(prefix), nil } func (ms *MapStorage) CacheRatingAll() error { diff --git a/engine/storage_mongo_datadb.go b/engine/storage_mongo_datadb.go index 8139f3b19..c1c5b5c89 100644 --- a/engine/storage_mongo_datadb.go +++ b/engine/storage_mongo_datadb.go @@ -244,8 +244,60 @@ func (ms *MongoStorage) Close() { ms.session.Close() } -func (ms *MongoStorage) GetKeysForPrefix(prefix string) ([]string, error) { - return nil, nil +func (ms *MongoStorage) GetKeysForPrefix(prefix string, skipCache bool) ([]string, error) { + var category, subject string + length := len(utils.DESTINATION_PREFIX) + if len(prefix) >= length { + category = prefix[:length] // prefix lenght + subject = fmt.Sprintf("^%s", prefix[length:]) + } else { + return nil, fmt.Errorf("unsupported prefix in GetKeysForPrefix: %s", prefix) + } + var result []string + if skipCache { + keyResult := struct{ Key string }{} + idResult := struct{ Id string }{} + switch category { + case utils.DESTINATION_PREFIX: + iter := ms.db.C(colDst).Find(bson.M{"key": bson.M{"$regex": bson.RegEx{Pattern: subject}}}).Select(bson.M{"key": 1}).Iter() + for iter.Next(&keyResult) { + result = append(result, utils.DESTINATION_PREFIX+keyResult.Key) + } + return result, nil + case utils.RATING_PLAN_PREFIX: + iter := ms.db.C(colRpl).Find(bson.M{"key": bson.M{"$regex": bson.RegEx{Pattern: subject}}}).Select(bson.M{"key": 1}).Iter() + for iter.Next(&keyResult) { + result = append(result, utils.RATING_PLAN_PREFIX+keyResult.Key) + } + return result, nil + case utils.RATING_PROFILE_PREFIX: + iter := ms.db.C(colRpf).Find(bson.M{"id": bson.M{"$regex": bson.RegEx{Pattern: subject}}}).Select(bson.M{"id": 1}).Iter() + for iter.Next(&idResult) { + result = append(result, utils.RATING_PROFILE_PREFIX+idResult.Id) + } + return result, nil + case utils.ACTION_PREFIX: + iter := ms.db.C(colAct).Find(bson.M{"key": bson.M{"$regex": bson.RegEx{Pattern: subject}}}).Select(bson.M{"key": 1}).Iter() + for iter.Next(&keyResult) { + result = append(result, utils.ACTION_PREFIX+keyResult.Key) + } + return result, nil + case utils.ACTION_PLAN_PREFIX: + iter := ms.db.C(colApl).Find(bson.M{"key": bson.M{"$regex": bson.RegEx{Pattern: subject}}}).Select(bson.M{"key": 1}).Iter() + for iter.Next(&keyResult) { + result = append(result, utils.ACTION_PLAN_PREFIX+keyResult.Key) + } + return result, nil + case utils.ACCOUNT_PREFIX: + iter := ms.db.C(colAcc).Find(bson.M{"id": bson.M{"$regex": bson.RegEx{Pattern: subject}}}).Select(bson.M{"id": 1}).Iter() + for iter.Next(&idResult) { + result = append(result, utils.ACCOUNT_PREFIX+idResult.Id) + } + return result, nil + } + return result, fmt.Errorf("unsupported prefix in GetKeysForPrefix: %s", prefix) + } + return cache2go.GetEntriesKeys(prefix), nil } func (ms *MongoStorage) Flush(ignore string) (err error) { @@ -627,7 +679,7 @@ func (ms *MongoStorage) HasData(category, subject string) (bool, error) { count, err := ms.db.C(colAcc).Find(bson.M{"id": subject}).Count() return count > 0, err } - return false, errors.New("Unsupported category in HasData") + return false, errors.New("unsupported category in HasData") } func (ms *MongoStorage) GetRatingPlan(key string, skipCache bool) (rp *RatingPlan, err error) { diff --git a/engine/storage_mongo_stordb.go b/engine/storage_mongo_stordb.go index 0e0090def..a0aaffe09 100644 --- a/engine/storage_mongo_stordb.go +++ b/engine/storage_mongo_stordb.go @@ -698,16 +698,16 @@ func (ms *MongoStorage) LogActionTiming(source string, at *ActionTiming, as Acti }{at, as, time.Now(), source}) } -func (ms *MongoStorage) LogCallCost(cgrid, runid, source string, cc *CallCost) error { - return ms.db.C(utils.TBLSMCosts).Insert(&SMCost{CGRID: cgrid, RunID: runid, CostSource: source, CostDetails: cc}) +func (ms *MongoStorage) LogCallCost(smc *SMCost) error { + return ms.db.C(utils.TBLSMCosts).Insert(smc) } -func (ms *MongoStorage) GetCallCostLog(cgrid, runid string) (cc *CallCost, err error) { +func (ms *MongoStorage) GetCallCostLog(cgrid, runid string) (smc *SMCost, err error) { var result SMCost if err = ms.db.C(utils.TBLSMCosts).Find(bson.M{CGRIDLow: cgrid, RunIDLow: runid}).One(&result); err != nil { return nil, err } - return result.CostDetails, nil + return &result, nil } func (ms *MongoStorage) SetCDR(cdr *CDR, allowUpdate bool) (err error) { diff --git a/engine/storage_redis.go b/engine/storage_redis.go index c449ea0c1..757960f4c 100644 --- a/engine/storage_redis.go +++ b/engine/storage_redis.go @@ -84,12 +84,15 @@ func (rs *RedisStorage) Flush(ignore string) error { return rs.db.Cmd("FLUSHDB").Err } -func (rs *RedisStorage) GetKeysForPrefix(prefix string) ([]string, error) { - r := rs.db.Cmd("KEYS", prefix+"*") - if r.Err != nil { - return nil, r.Err +func (rs *RedisStorage) GetKeysForPrefix(prefix string, skipCache bool) ([]string, error) { + if skipCache { + r := rs.db.Cmd("KEYS", prefix+"*") + if r.Err != nil { + return nil, r.Err + } + return r.List() } - return r.List() + return cache2go.GetEntriesKeys(prefix), nil } func (rs *RedisStorage) CacheRatingAll() error { diff --git a/engine/storage_sql.go b/engine/storage_sql.go index e7796e34f..1e0f5368a 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -55,7 +55,7 @@ func (self *SQLStorage) Flush(scriptsPath string) (err error) { return nil } -func (self *SQLStorage) GetKeysForPrefix(prefix string) ([]string, error) { +func (self *SQLStorage) GetKeysForPrefix(prefix string, skipCache bool) ([]string, error) { return nil, utils.ErrNotImplemented } @@ -569,21 +569,22 @@ func (self *SQLStorage) SetTpAccountActions(aas []TpAccountAction) error { return nil } -func (self *SQLStorage) LogCallCost(cgrid, runid, source string, cc *CallCost) error { - if cc == nil { +func (self *SQLStorage) LogCallCost(smc *SMCost) error { + if smc.CostDetails == nil { return nil } - tss, err := json.Marshal(cc) + tss, err := json.Marshal(smc.CostDetails) if err != nil { utils.Logger.Err(fmt.Sprintf("Error marshalling timespans to json: %v", err)) return err } tx := self.db.Begin() cd := &TBLSMCosts{ - Cgrid: cgrid, - RunID: runid, - CostSource: source, + Cgrid: smc.CGRID, + RunID: smc.RunID, + CostSource: smc.CostSource, CostDetails: string(tss), + Usage: smc.Usage, CreatedAt: time.Now(), } if tx.Save(cd).Error != nil { // Check further since error does not properly reflect duplicates here (sql: no rows in result set) @@ -594,7 +595,7 @@ func (self *SQLStorage) LogCallCost(cgrid, runid, source string, cc *CallCost) e return nil } -func (self *SQLStorage) GetCallCostLog(cgrid, runid string) (*CallCost, error) { +func (self *SQLStorage) GetCallCostLog(cgrid, runid string) (*SMCost, error) { var tpCostDetail TBLSMCosts if err := self.db.Where(&TBLSMCosts{Cgrid: cgrid, RunID: runid}).First(&tpCostDetail).Error; err != nil { return nil, err @@ -602,11 +603,17 @@ func (self *SQLStorage) GetCallCostLog(cgrid, runid string) (*CallCost, error) { if len(tpCostDetail.CostDetails) == 0 { return nil, nil // No costs returned } - var cc CallCost - if err := json.Unmarshal([]byte(tpCostDetail.CostDetails), &cc); err != nil { + smc := &SMCost{ + CGRID: tpCostDetail.Cgrid, + RunID: tpCostDetail.RunID, + CostSource: tpCostDetail.CostSource, + Usage: tpCostDetail.Usage, + CostDetails: &CallCost{}, + } + if err := json.Unmarshal([]byte(tpCostDetail.CostDetails), smc.CostDetails); err != nil { return nil, err } - return &cc, nil + return smc, nil } func (self *SQLStorage) LogActionTrigger(ubId, source string, at *ActionTrigger, as Actions) (err error) { diff --git a/engine/storage_utils.go b/engine/storage_utils.go index 8994020fd..16012f2e1 100644 --- a/engine/storage_utils.go +++ b/engine/storage_utils.go @@ -157,5 +157,6 @@ type SMCost struct { CGRID string RunID string CostSource string + Usage float64 CostDetails *CallCost } diff --git a/engine/tp_reader.go b/engine/tp_reader.go index bf662a335..e458e130e 100644 --- a/engine/tp_reader.go +++ b/engine/tp_reader.go @@ -433,7 +433,7 @@ func (tpr *TpReader) LoadLCRs() (err error) { } } if !found && tpr.ratingStorage != nil { - if keys, err := tpr.ratingStorage.GetKeysForPrefix(utils.RATING_PROFILE_PREFIX + ratingProfileSearchKey); err != nil { + if keys, err := tpr.ratingStorage.GetKeysForPrefix(utils.RATING_PROFILE_PREFIX+ratingProfileSearchKey, true); err != nil { return fmt.Errorf("[LCR] error querying ratingDb %s", err.Error()) } else if len(keys) != 0 { found = true diff --git a/sessionmanager/data_it_test.go b/sessionmanager/data_it_test.go new file mode 100644 index 000000000..69b6d95a5 --- /dev/null +++ b/sessionmanager/data_it_test.go @@ -0,0 +1,344 @@ +package sessionmanager + +import ( + "net/rpc/jsonrpc" + "path" + "testing" + "time" + + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + +func TestSMGDataInitCfg(t *testing.T) { + if !*testIntegration { + return + } + daCfgPath = path.Join(*dataDir, "conf", "samples", "smg") + // Init config first + var err error + daCfg, err = config.NewCGRConfigFromFolder(daCfgPath) + if err != nil { + t.Error(err) + } + daCfg.DataFolderPath = *dataDir // Share DataFolderPath through config towards StoreDb for Flush() + config.SetCgrConfig(daCfg) +} + +// Remove data in both rating and accounting db +func TestSMGDataResetDataDb(t *testing.T) { + if !*testIntegration { + return + } + if err := engine.InitDataDb(daCfg); err != nil { + t.Fatal(err) + } +} + +// Wipe out the cdr database +func TestSMGDataResetStorDb(t *testing.T) { + if !*testIntegration { + return + } + if err := engine.InitStorDb(daCfg); err != nil { + t.Fatal(err) + } +} + +// Start CGR Engine +func TestSMGDataStartEngine(t *testing.T) { + if !*testIntegration { + return + } + if _, err := engine.StopStartEngine(daCfgPath, *waitRater); err != nil { + t.Fatal(err) + } +} + +// Connect rpc client to rater +func TestSMGDataApierRpcConn(t *testing.T) { + if !*testIntegration { + return + } + var err error + smgRPC, err = jsonrpc.Dial("tcp", daCfg.RPCJSONListen) // We connect over JSON so we can also troubleshoot if needed + if err != nil { + t.Fatal(err) + } +} + +// Load the tariff plan, creating accounts and their balances +func TestSMGDataTPFromFolder(t *testing.T) { + if !*testIntegration { + return + } + attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "testtp")} + var loadInst engine.LoadInstance + if err := smgRPC.Call("ApierV2.LoadTariffPlanFromFolder", attrs, &loadInst); err != nil { + t.Error(err) + } + time.Sleep(time.Duration(*waitRater) * time.Millisecond) // Give time for scheduler to execute topups +} + +func TestSMGDataLastUsedData(t *testing.T) { + if !*testIntegration { + return + } + var acnt *engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1010"} + eAcntVal := 50000000000.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + smgEv := SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + 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: "1048576", + } + var maxUsage float64 + if err := smgRPC.Call("SMGenericV1.SessionStart", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 1.048576e+06 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 49998945280.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.USAGE: "1048576", + utils.LastUsed: "20000", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 1.048576e+06 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 49998924800.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.LastUsed: "0", + } + var rpl string + if err = smgRPC.Call("SMGenericV1.SessionEnd", smgEv, &rpl); err != nil || rpl != utils.OK { + t.Error(err) + } + eAcntVal = 49999979520.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } +} + +func TestSMGDataLastUsedMultipleData(t *testing.T) { + if !*testIntegration { + return + } + var acnt *engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1010"} + eAcntVal := 49999979520.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + smgEv := SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + 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: "1048576", + } + var maxUsage float64 + if err := smgRPC.Call("SMGenericV1.SessionStart", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 1.048576e+06 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 49998924800.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.USAGE: "1048576", + utils.LastUsed: "20000", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 1.048576e+06 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 49998904320.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.USAGE: "1048576", + utils.LastUsed: "20000", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 1.048576e+06 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 49998883840.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.USAGE: "1048576", + utils.LastUsed: "20000", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 1.048576e+06 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 49998863360.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.USAGE: "1048576", + utils.LastUsed: "20000", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 1.048576e+06 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 49998842880.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } + + smgEv = SMGenericEvent{ + utils.EVENT_NAME: "TEST_EVENT", + utils.TOR: utils.DATA, + utils.ACCID: "12349", + utils.DIRECTION: utils.OUT, + utils.ACCOUNT: "1010", + utils.SUBJECT: "1010", + utils.DESTINATION: "222", + utils.CATEGORY: "data", + utils.TENANT: "cgrates.org", + utils.REQTYPE: utils.META_PREPAID, + utils.LastUsed: "0", + } + var rpl string + if err = smgRPC.Call("SMGenericV1.SessionEnd", smgEv, &rpl); err != nil || rpl != utils.OK { + t.Error(err) + } + eAcntVal = 49999897600.000000 + if err := smgRPC.Call("ApierV2.GetAccount", attrs, &acnt); err != nil { + t.Error(err) + } else if acnt.BalanceMap[utils.DATA].GetTotalValue() != eAcntVal { + t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.DATA].GetTotalValue()) + } +} diff --git a/sessionmanager/smg_event.go b/sessionmanager/smg_event.go index 643c8693f..890e9a1c9 100644 --- a/sessionmanager/smg_event.go +++ b/sessionmanager/smg_event.go @@ -228,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, utils.LastUsed) + primaryFields := append(utils.PrimaryCdrFields, utils.EVENT_NAME) if utils.IsSliceMember(primaryFields, key) { continue } @@ -238,6 +238,18 @@ func (self SMGenericEvent) GetExtraFields() map[string]string { return extraFields } +func (self SMGenericEvent) GetFieldAsString(fieldName string) (string, error) { + valIf, hasVal := self[fieldName] + if !hasVal { + return "", utils.ErrNotFound + } + result, converted := utils.ConvertIfaceToString(valIf) + if !converted { + return "", utils.ErrNotConvertible + } + return result, nil +} + func (self SMGenericEvent) MissingParameter(timezone string) bool { switch self.GetName() { case utils.CGR_AUTHORIZATION: diff --git a/sessionmanager/smg_event_test.go b/sessionmanager/smg_event_test.go index 4cc80781c..336996943 100644 --- a/sessionmanager/smg_event_test.go +++ b/sessionmanager/smg_event_test.go @@ -127,7 +127,7 @@ func TestSMGenericEventParseFields(t *testing.T) { if smGev.GetOriginatorIP(utils.META_DEFAULT) != "127.0.0.1" { t.Error("Unexpected: ", smGev.GetOriginatorIP(utils.META_DEFAULT)) } - if extrFlds := smGev.GetExtraFields(); !reflect.DeepEqual(extrFlds, map[string]string{"Extra1": "Value1", "Extra2": "5"}) { + if extrFlds := smGev.GetExtraFields(); !reflect.DeepEqual(extrFlds, map[string]string{"Extra1": "Value1", "Extra2": "5", "LastUsed": "21s"}) { t.Error("Unexpected: ", extrFlds) } } @@ -191,3 +191,19 @@ func TestSMGenericEventAsLcrRequest(t *testing.T) { t.Errorf("Expecting: %+v, received: %+v", eLcrReq, lcrReq) } } + +func TestSMGenericEventGetFieldAsString(t *testing.T) { + smGev := SMGenericEvent{} + smGev[utils.EVENT_NAME] = "TEST_EVENT" + smGev[utils.TOR] = utils.VOICE + smGev[utils.ACCID] = "12345" + smGev[utils.DIRECTION] = utils.OUT + smGev[utils.ACCOUNT] = "account1" + smGev[utils.SUBJECT] = "subject1" + eFldVal := utils.VOICE + if strVal, err := smGev.GetFieldAsString(utils.TOR); err != nil { + t.Error(err) + } else if strVal != eFldVal { + t.Errorf("Expecting: %s, received: %s", eFldVal, strVal) + } +} diff --git a/sessionmanager/smg_it_test.go b/sessionmanager/smg_it_test.go index cd246596e..435778513 100644 --- a/sessionmanager/smg_it_test.go +++ b/sessionmanager/smg_it_test.go @@ -422,3 +422,185 @@ func TestSMGLastUsed(t *testing.T) { t.Errorf("Expected: %f, received: %f", eAcntVal, acnt.BalanceMap[utils.MONETARY].GetTotalValue()) } } + +func TestSMGLastUsedEnd(t *testing.T) { + if !*testIntegration { + return + } + var acnt *engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + eAcntVal := 7.59000 + 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 = 6.190020 + 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: "30s", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 120 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 6.090030 + 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.LastUsed: "0s", + } + var rpl string + if err = smgRPC.Call("SMGenericV1.SessionEnd", smgEv, &rpl); err != nil || rpl != utils.OK { + t.Error(err) + } + eAcntVal = 6.590000 + 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()) + } +} + +func TestSMGLastUsedNotFixed(t *testing.T) { + if !*testIntegration { + return + } + var acnt *engine.Account + attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} + eAcntVal := 6.59000 + 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 = 5.190020 + 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: "13s", + } + if err := smgRPC.Call("SMGenericV1.SessionUpdate", smgEv, &maxUsage); err != nil { + t.Error(err) + } + if maxUsage != 120 { + t.Error("Bad max usage: ", maxUsage) + } + eAcntVal = 5.123360 + 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.LastUsed: "0s", + } + var rpl string + if err = smgRPC.Call("SMGenericV1.SessionEnd", smgEv, &rpl); err != nil || rpl != utils.OK { + t.Error(err) + } + eAcntVal = 5.590000 + 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 a9eae0920..a1160b319 100644 --- a/sessionmanager/smg_session.go +++ b/sessionmanager/smg_session.go @@ -86,12 +86,14 @@ func (self *SMGSession) debit(dur time.Duration, lastUsed time.Duration) (time.D } 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 += lastUsed // Should reflect the total usage so far + } else { + // apply correction from previous run + dur -= self.extraDuration } - // 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 @@ -176,7 +178,8 @@ func (self *SMGSession) refund(refundDuration time.Duration) error { return err } } - firstCC.Cost -= refundIncrements.GetTotalCost() + //firstCC.Cost -= refundIncrements.GetTotalCost() // use updateCost instead + firstCC.UpdateCost() firstCC.UpdateRatedUsage() firstCC.Timespans.Compress() return nil @@ -184,11 +187,12 @@ func (self *SMGSession) refund(refundDuration time.Duration) error { // 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 + firstCC := self.callCosts[0] + for _, cc := range self.callCosts[1:] { + firstCC.Merge(cc) + } + //utils.Logger.Debug("MergedCC: " + utils.ToJSON(firstCC)) end := firstCC.GetEndTime() refundDuration := end.Sub(endTime) self.refund(refundDuration) @@ -220,9 +224,9 @@ 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] // was merged in close methos - firstCC.Timespans.Compress() + firstCC := self.callCosts[0] // was merged in close method firstCC.Round() + //utils.Logger.Debug("Saved CC: " + utils.ToJSON(firstCC)) roundIncrements := firstCC.GetRoundIncrements() if len(roundIncrements) != 0 { cd := firstCC.CreateCallDescriptor() @@ -238,6 +242,7 @@ func (self *SMGSession) saveOperations() error { CgrId: self.eventStart.GetCgrId(self.timezone), Source: utils.SESSION_MANAGER_SOURCE, RunId: self.runId, + Usage: float64(self.totalUsage), CallCost: firstCC, CheckDuplicate: true, }, &reply) diff --git a/sessionmanager/smgeneric.go b/sessionmanager/smgeneric.go index 9e7ed87fb..5c53adc34 100644 --- a/sessionmanager/smgeneric.go +++ b/sessionmanager/smgeneric.go @@ -119,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 { + s.totalUsage = usage // save final usage as totalUsage //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 @@ -140,6 +141,27 @@ func (self *SMGeneric) sessionEnd(sessionId string, usage time.Duration) error { return err } +// Used when an update will relocate an initial session (eg multiple data streams) +func (self *SMGeneric) sessionRelocate(sessionID, initialID string) error { + _, err := self.guard.Guard(func() (interface{}, error) { // Lock it on initialID level + if utils.IsSliceMember([]string{sessionID, initialID}, "") { + return nil, utils.ErrMandatoryIeMissing + } + ss := self.getSession(initialID) + if len(ss) == 0 { // No need of relocation + return nil, utils.ErrNotFound + } + for i, s := range ss { + self.indexSession(sessionID, s) + if i == 0 { + self.unindexSession(initialID) + } + } + return nil, nil + }, time.Duration(2)*time.Second, initialID) + return err +} + // Methods to apply on sessions, mostly exported through RPC/Bi-RPC //Calculates maximum usage allowed for gevent func (self *SMGeneric) GetMaxUsage(gev SMGenericEvent, clnt *rpc2.Client) (time.Duration, error) { @@ -172,6 +194,15 @@ 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) { + if initialID, err := gev.GetFieldAsString(utils.InitialOriginID); err == nil { + err := self.sessionRelocate(gev.GetUUID(), initialID) + if err == utils.ErrNotFound { // Session was already relocated, create a new session with this update + err = self.sessionStart(gev, getClientConnId(clnt)) + } + if err != nil { + return nilDuration, err + } + } evLastUsed, err := gev.GetLastUsed(utils.META_DEFAULT) if err != nil && err != utils.ErrNotFound { return nilDuration, err diff --git a/structmatcher/README.md b/structmatcher/README.md index 6220a301d..6f5539eed 100644 --- a/structmatcher/README.md +++ b/structmatcher/README.md @@ -20,7 +20,9 @@ Available operators: - *exp: expired - *or: logical or - *and: logical and +- *not: logical not - *has: receives a list of elements and checks that the elements are present in the specified field (StringMap type) +- *rsr: will apply a rsr check to the field (see utils/rsrfield.go) Equal (*eq) and local and (*and) operators are implicit for shortcuts. In this way: diff --git a/structmatcher/structmatcher.go b/structmatcher/structmatcher.go index 419e23095..50cfa37ca 100644 --- a/structmatcher/structmatcher.go +++ b/structmatcher/structmatcher.go @@ -16,7 +16,9 @@ Available operators: - *exp: expired - *or: logical or - *and: logical and +- *not: logical not - *has: receives a list of elements and checks that the elements are present in the specified field (StringMap type) +- *rsr: will apply a rsr check to the field (see utils/rsrfield.go) Equal (*eq) and local and (*and) operators are implicit for shortcuts. In this way: @@ -43,6 +45,7 @@ const ( CondEXP = "*exp" CondOR = "*or" CondAND = "*and" + CondNOT = "*not" CondHAS = "*has" CondRSR = "*rsr" ) @@ -177,7 +180,7 @@ func (os *operatorSlice) checkStruct(o interface{}) (bool, error) { return true, nil } } - case CondAND: + case CondAND, CondNOT: accumulator := true for _, cond := range os.slice { check, err := cond.checkStruct(o) @@ -186,7 +189,11 @@ func (os *operatorSlice) checkStruct(o interface{}) (bool, error) { } accumulator = accumulator && check } - return accumulator, nil + if os.operator == CondAND { + return accumulator, nil + } else { + return !accumulator, nil + } } return false, nil } diff --git a/structmatcher/structmatcher_test.go b/structmatcher/structmatcher_test.go index e221d1272..e4a30c51e 100644 --- a/structmatcher/structmatcher_test.go +++ b/structmatcher/structmatcher_test.go @@ -95,6 +95,13 @@ func TestStructMatcherKeyValue(t *testing.T) { 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(`{"*not":[{"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) @@ -345,6 +352,13 @@ func TestStructMatcherOperatorSlice(t *testing.T) { 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(`{"*not":[{"*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) @@ -352,6 +366,13 @@ func TestStructMatcherOperatorSlice(t *testing.T) { 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(`{"*not":[{"*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) diff --git a/test.sh b/test.sh index 65c8dfcd4..a54a4907e 100755 --- a/test.sh +++ b/test.sh @@ -1,48 +1,4 @@ #! /usr/bin/env sh ./build.sh - -go test -i github.com/cgrates/cgrates/apier/v1 -go test -i github.com/cgrates/cgrates/apier/v2 -go test -i github.com/cgrates/cgrates/engine -go test -i github.com/cgrates/cgrates/general_tests -go test -i github.com/cgrates/cgrates/sessionmanager -go test -i github.com/cgrates/cgrates/config -go test -i github.com/cgrates/cgrates/cmd/cgr-engine -go test -i github.com/cgrates/cgrates/cache2go -go test -i github.com/cgrates/cgrates/cdrc -go test -i github.com/cgrates/cgrates/utils -go test -i github.com/cgrates/cgrates/history -go test -i github.com/cgrates/cgrates/cdre -go test -i github.com/cgrates/cgrates/agents - -go test github.com/cgrates/cgrates/apier/v1 -v1=$? -go test github.com/cgrates/cgrates/apier/v2 -v2=$? -go test github.com/cgrates/cgrates/engine -en=$? -go test github.com/cgrates/cgrates/general_tests -gt=$? -go test github.com/cgrates/cgrates/sessionmanager -sm=$? -go test github.com/cgrates/cgrates/config -cfg=$? -go test github.com/cgrates/cgrates/cmd/cgr-engine -cr=$? -go test github.com/cgrates/cgrates/console -con=$? -go test github.com/cgrates/cgrates/cdrc -cdrcs=$? -go test github.com/cgrates/cgrates/utils -ut=$? -go test github.com/cgrates/cgrates/history -hs=$? -go test github.com/cgrates/cgrates/cache2go -c2g=$? -go test github.com/cgrates/cgrates/cdre -cdre=$? -go test github.com/cgrates/cgrates/agents -ag=$? - - -exit $v1 && $v2 && $en && $gt && $sm && $cfg && $bl && $cr && $con && $cdrc && $ut && $hs && $c2g && $cdre && $ag +go test $(glide novendor) +exit $? \ No newline at end of file diff --git a/utils/consts.go b/utils/consts.go index 72c15b145..36001e3c9 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -31,6 +31,7 @@ var ( ErrAccountDisabled = errors.New("ACCOUNT_DISABLED") ErrUserNotFound = errors.New("USER_NOT_FOUND") ErrInsufficientCredit = errors.New("INSUFFICENT_CREDIT") + ErrNotConvertible = errors.New("NOT_CONVERTIBLE") ) const ( @@ -113,6 +114,7 @@ const ( TOR = "ToR" ORDERID = "OrderID" ACCID = "OriginID" + InitialOriginID = "InitialOriginID" CDRSOURCE = "Source" CDRHOST = "OriginHost" REQTYPE = "RequestType"