diff --git a/cdrc/xml.go b/cdrc/xml.go index 9e0560632..6c1d49d0f 100644 --- a/cdrc/xml.go +++ b/cdrc/xml.go @@ -19,9 +19,7 @@ along with this program. If not, see package cdrc import ( - "bytes" "encoding/json" - "encoding/xml" "errors" "fmt" "io" @@ -29,9 +27,7 @@ import ( "strings" "time" - "github.com/ChrisTrenkamp/goxpath" - "github.com/ChrisTrenkamp/goxpath/tree" - "github.com/ChrisTrenkamp/goxpath/tree/xmltree" + "github.com/beevik/etree" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" @@ -39,36 +35,17 @@ import ( // getElementText will process the node to extract the elementName's text out of it (only first one found) // returns utils.ErrNotFound if the element is not found in the node -func elementText(xmlNode tree.Node, elmntPath string) (string, error) { - xp, err := goxpath.Parse(elmntPath) - if err != nil { - return "", err - } - elmntBuf := bytes.NewBufferString(xml.Header) - if err := goxpath.Marshal(xmlNode, elmntBuf); err != nil { - return "", err - } - xmlTree, err := xmltree.ParseXML(elmntBuf) - if err != nil { - return "", err - } - elmnt, err := xp.ExecNode(xmlTree) - if err != nil { - return "", err - } - if len(elmnt) == 0 { +func elementText(xmlElement *etree.Element, elmntPath string) (string, error) { + elmnt := xmlElement.FindElement(elmntPath) + if elmnt == nil { return "", utils.ErrNotFound } - elem, ok := elmnt[0].(tree.Elem) - if !ok { - return "", fmt.Errorf("Cannot parse ") - } - return elem.ResValue(), nil + return elmnt.Text(), nil } // handlerUsageDiff will calculate the usage as difference between timeEnd and timeStart // Expects the 2 arguments in template separated by | -func handlerSubstractUsage(xmlElmnt tree.Node, argsTpl config.RSRParsers, cdrPath utils.HierarchyPath, timezone string) (time.Duration, error) { +func handlerSubstractUsage(xmlElement *etree.Element, argsTpl config.RSRParsers, cdrPath utils.HierarchyPath, timezone string) (time.Duration, error) { var argsStr string for _, rsrArg := range argsTpl { if rsrArg.Rules == utils.HandlerArgSep { @@ -76,8 +53,8 @@ func handlerSubstractUsage(xmlElmnt tree.Node, argsTpl config.RSRParsers, cdrPat continue } absolutePath := utils.ParseHierarchyPath(rsrArg.Rules, "") - relPath := utils.HierarchyPath(absolutePath[len(cdrPath)-1:]) // Need relative path to the xmlElmnt - argStr, _ := elementText(xmlElmnt, relPath.AsString("/", true)) + relPath := utils.HierarchyPath(absolutePath[len(cdrPath):]) // Need relative path to the xmlElmnt + argStr, _ := elementText(xmlElement, relPath.AsString("/", false)) argsStr += argStr } handlerArgs := strings.Split(argsStr, utils.HandlerArgSep) @@ -97,29 +74,21 @@ func handlerSubstractUsage(xmlElmnt tree.Node, argsTpl config.RSRParsers, cdrPat func NewXMLRecordsProcessor(recordsReader io.Reader, cdrPath utils.HierarchyPath, timezone string, httpSkipTlsCheck bool, cdrcCfgs []*config.CdrcConfig, filterS *engine.FilterS) (*XMLRecordsProcessor, error) { - xp, err := goxpath.Parse(cdrPath.AsString("/", true)) - if err != nil { - return nil, err + xmlDocument := etree.NewDocument() // create new document + xmlDocument.ReadSettings.CharsetReader = func(label string, input io.Reader) (io.Reader, error) { // fix for enconding != UTF-8 + return input, nil } - optsNotStrict := func(s *xmltree.ParseOptions) { - s.Strict = false - } - xmlNode, err := xmltree.ParseXML(recordsReader, optsNotStrict) - if err != nil { + if _, err := xmlDocument.ReadFrom(recordsReader); err != nil { // read file and build xml document return nil, err } xmlProc := &XMLRecordsProcessor{cdrPath: cdrPath, timezone: timezone, httpSkipTlsCheck: httpSkipTlsCheck, cdrcCfgs: cdrcCfgs, filterS: filterS} - prov, err := xp.ExecNode(xmlNode) - if err != nil { - return nil, err - } - xmlProc.cdrXmlElmts = prov + xmlProc.cdrXmlElmts = xmlDocument.Root().FindElements(cdrPath.AsString("/", true)) return xmlProc, nil } type XMLRecordsProcessor struct { - cdrXmlElmts tree.NodeSet // result of splitting the XML doc into CDR elements + cdrXmlElmts []*etree.Element // result of splitting the XML doc into CDR elements procItems int // current number of processed records from file cdrPath utils.HierarchyPath // path towards one CDR element timezone string @@ -163,7 +132,7 @@ func (xmlProc *XMLRecordsProcessor) ProcessNextRecord() (cdrs []*engine.CDR, err return cdrs, nil } -func (xmlProc *XMLRecordsProcessor) recordToCDR(xmlEntity tree.Node, cdrcCfg *config.CdrcConfig) (*engine.CDR, error) { +func (xmlProc *XMLRecordsProcessor) recordToCDR(xmlEntity *etree.Element, cdrcCfg *config.CdrcConfig) (*engine.CDR, error) { cdr := &engine.CDR{OriginHost: "0.0.0.0", Source: cdrcCfg.CdrSourceId, ExtraFields: make(map[string]string), Cost: -1} var lazyHttpFields []*config.FCTemplate var err error @@ -236,14 +205,14 @@ func (xmlProc *XMLRecordsProcessor) recordToCDR(xmlEntity tree.Node, cdrcCfg *co } // newXmlProvider constructs a DataProvider -func newXmlProvider(req tree.Node, cdrPath utils.HierarchyPath) (dP config.DataProvider) { +func newXmlProvider(req *etree.Element, cdrPath utils.HierarchyPath) (dP config.DataProvider) { dP = &xmlProvider{req: req, cdrPath: cdrPath, cache: config.NewNavigableMap(nil)} return } // xmlProvider implements engine.DataProvider so we can pass it to filters type xmlProvider struct { - req tree.Node + req *etree.Element cdrPath utils.HierarchyPath //used to compute relative path cache *config.NavigableMap } @@ -265,8 +234,8 @@ func (xP *xmlProvider) FieldAsInterface(fldPath []string) (data interface{}, err } err = nil // cancel previous err absolutePath := utils.ParseHierarchyPath(fldPath[0], "") - relPath := utils.HierarchyPath(absolutePath[len(xP.cdrPath)-1:]) // Need relative path to the xmlElmnt - data, err = elementText(xP.req, relPath.AsString("/", true)) + relPath := utils.HierarchyPath(absolutePath[len(xP.cdrPath):]) // Need relative path to the xmlElmnt + data, err = elementText(xP.req, relPath.AsString("/", false)) xP.cache.Set(fldPath, data, false) return } diff --git a/cdrc/xml_it_test.go b/cdrc/xml_it_test.go index 7daa42bd2..3f660a304 100644 --- a/cdrc/xml_it_test.go +++ b/cdrc/xml_it_test.go @@ -149,7 +149,7 @@ func TestXmlIT2InitCdrDb(t *testing.T) { func TestXmlIT2CreateCdrDirs(t *testing.T) { for _, cdrcProfiles := range xmlCfg.CdrcProfiles { - for i, cdrcInst := range cdrcProfiles { + for _, cdrcInst := range cdrcProfiles { for _, dir := range []string{cdrcInst.CdrInDir, cdrcInst.CdrOutDir} { if err := os.RemoveAll(dir); err != nil { t.Fatal("Error removing folder: ", dir, err) @@ -158,7 +158,7 @@ func TestXmlIT2CreateCdrDirs(t *testing.T) { t.Fatal("Error creating folder: ", dir, err) } } - if i == 0 { // Initialize the folders to check later + if cdrcInst.ID == "XMLWithFilter" { // Initialize the folders to check later xmlPathIn1 = cdrcInst.CdrInDir xmlPathOut1 = cdrcInst.CdrOutDir } @@ -216,3 +216,89 @@ func TestXmlIT2KillEngine(t *testing.T) { t.Error(err) } } + +// Begin tests for cdrc xml with new filters +func TestXmlIT3InitConfig(t *testing.T) { + var err error + xmlCfgPath = path.Join(*dataDir, "conf", "samples", "cdrcxmlwithfilter") + if xmlCfg, err = config.NewCGRConfigFromFolder(xmlCfgPath); err != nil { + t.Fatal("Got config error: ", err.Error()) + } +} + +// InitDb so we can rely on count +func TestXmlIT3InitCdrDb(t *testing.T) { + if err := engine.InitStorDb(xmlCfg); err != nil { + t.Fatal(err) + } +} + +func TestXmlIT3CreateCdrDirs(t *testing.T) { + for _, cdrcProfiles := range xmlCfg.CdrcProfiles { + for _, cdrcInst := range cdrcProfiles { + for _, dir := range []string{cdrcInst.CdrInDir, cdrcInst.CdrOutDir} { + if err := os.RemoveAll(dir); err != nil { + t.Fatal("Error removing folder: ", dir, err) + } + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatal("Error creating folder: ", dir, err) + } + } + if cdrcInst.ID == "msw_xml" { // Initialize the folders to check later + xmlPathIn1 = cdrcInst.CdrInDir + xmlPathOut1 = cdrcInst.CdrOutDir + } + } + } +} + +func TestXmlIT3StartEngine(t *testing.T) { + if _, err := engine.StopStartEngine(xmlCfgPath, *waitRater); err != nil { + t.Fatal(err) + } +} + +// Connect rpc client to rater +func TestXmlIT3RpcConn(t *testing.T) { + var err error + cdrcXmlRPC, err = jsonrpc.Dial("tcp", xmlCfg.RPCJSONListen) // We connect over JSON so we can also troubleshoot if needed + if err != nil { + t.Fatal("Could not connect to rater: ", err.Error()) + } +} + +// The default scenario, out of cdrc defined in .cfg file +func TestXmlIT3HandleCdr1File(t *testing.T) { + fileName := "file1.xml" + tmpFilePath := path.Join("/tmp", fileName) + if err := ioutil.WriteFile(tmpFilePath, []byte(xmlContent), 0644); err != nil { + t.Fatal(err.Error()) + } + if err := os.Rename(tmpFilePath, path.Join(xmlPathIn1, fileName)); err != nil { + t.Fatal("Error moving file to processing directory: ", err) + } +} + +func TestXmlIT3ProcessedFiles(t *testing.T) { + time.Sleep(time.Duration(2**waitRater) * time.Millisecond) + if outContent1, err := ioutil.ReadFile(path.Join(xmlPathOut1, "file1.xml")); err != nil { + t.Error(err) + } else if xmlContent != string(outContent1) { + t.Errorf("Expecting: %q, received: %q", xmlContent, string(outContent1)) + } +} + +func TestXmlIT3AnalyseCDRs(t *testing.T) { + var reply []*engine.ExternalCDR + if err := cdrcXmlRPC.Call("ApierV2.GetCdrs", utils.RPCCDRsFilter{}, &reply); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(reply) != 3 { + t.Error("Unexpected number of CDRs returned: ", len(reply)) + } +} + +func TestXmlIT3KillEngine(t *testing.T) { + if err := engine.KillEngine(*waitRater); err != nil { + t.Error(err) + } +} diff --git a/cdrc/xml_test.go b/cdrc/xml_test.go index 795797f34..b92798fb1 100644 --- a/cdrc/xml_test.go +++ b/cdrc/xml_test.go @@ -19,13 +19,13 @@ package cdrc import ( "bytes" + "io" "path" "reflect" "testing" "time" - "github.com/ChrisTrenkamp/goxpath" - "github.com/ChrisTrenkamp/goxpath/tree/xmltree" + "github.com/beevik/etree" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" @@ -166,28 +166,27 @@ var cdrXmlBroadsoft = ` ` -func optsNotStrict(s *xmltree.ParseOptions) { - s.Strict = false -} - func TestXMLElementText(t *testing.T) { - xp := goxpath.MustParse(path.Join("/broadWorksCDR/cdrData/")) - xmlTree := xmltree.MustParseXML(bytes.NewBufferString(cdrXmlBroadsoft), optsNotStrict) - cdrs, err := xp.ExecNode(xmlTree) - if err != nil { + doc := etree.NewDocument() + doc.ReadSettings.CharsetReader = func(label string, input io.Reader) (io.Reader, error) { + return input, nil + } + if err := doc.ReadFromBytes([]byte(cdrXmlBroadsoft)); err != nil { t.Error(err) } + + cdrs := doc.FindElements(path.Join("/broadWorksCDR/cdrData/")) cdrWithoutUserNr := cdrs[0] - if _, err := elementText(cdrWithoutUserNr, "cdrData/basicModule/userNumber"); err != utils.ErrNotFound { + if _, err := elementText(cdrWithoutUserNr, "basicModule/userNumber"); err != utils.ErrNotFound { t.Error(err) } cdrWithUser := cdrs[1] - if val, err := elementText(cdrWithUser, "cdrData/basicModule/userNumber"); err != nil { + if val, err := elementText(cdrWithUser, "basicModule/userNumber"); err != nil { t.Error(err) } else if val != "1001" { t.Errorf("Expecting: 1001, received: %s", val) } - if val, err := elementText(cdrWithUser, "/cdrData/centrexModule/locationList/locationInformation/locationType"); err != nil { + if val, err := elementText(cdrWithUser, "centrexModule/locationList/locationInformation/locationType"); err != nil { t.Error(err) } else if val != "Primary Device" { t.Errorf("Expecting: , received: <%s>", val) @@ -195,12 +194,15 @@ func TestXMLElementText(t *testing.T) { } func TestXMLHandlerSubstractUsage(t *testing.T) { - xp := goxpath.MustParse(path.Join("/broadWorksCDR/cdrData/")) - xmlTree := xmltree.MustParseXML(bytes.NewBufferString(cdrXmlBroadsoft), optsNotStrict) - cdrs, err := xp.ExecNode(xmlTree) - if err != nil { + doc := etree.NewDocument() + doc.ReadSettings.CharsetReader = func(label string, input io.Reader) (io.Reader, error) { + return input, nil + } + if err := doc.ReadFromBytes([]byte(cdrXmlBroadsoft)); err != nil { t.Error(err) } + + cdrs := doc.FindElements(path.Join("/broadWorksCDR/cdrData/")) cdrWithUsage := cdrs[1] if usage, err := handlerSubstractUsage(cdrWithUsage, config.NewRSRParsersMustCompile("~broadWorksCDR>cdrData>basicModule>releaseTime;|;~broadWorksCDR>cdrData>basicModule>answerTime", true), @@ -352,3 +354,268 @@ func TestXMLRPProcessWithNewFilters(t *testing.T) { t.Errorf("Expecting: %+v\n, received: %+v\n", expectedCDRs, cdrs) } } + +var xmlContent = ` + + + Metaswitch CFS + + 1510225200002 + + + + National + + Orig + 19 + 480 + No answer + 223007622 + + +27110493421 + +27110493421 + +27110493421 + Ro_test + 0gQAAC8WAAACBAAALxYAAD57SAEV7ekv/OSKkO7qmD82OmbfHO+Z7wIZJkXdCv8z@10.170.248.200 + + + + 1 + 0 + + 8824071D@10.170.248.140 + 19 + 480 + + + 0763371551 + +270763371551 + 0763371551 + 110493421 + 110493421 + + + False + NetworkDefault + 0 + + + Speech + + 13442698e525ad2c21251f76479ab2b4 + voice.lab.liquidtelecom.net + + + 1510225513055 + 1510225513304 + 20160419210005.247 + 20160419210006.813 + 1510225516981 + 20160419210020.296 + 1510225516981 + + + Premium + + Orig + 16 + No error + 223007623 + + +27110493421 + +27110493421 + +27110493421 + Ro_test + 0gQAAC8WAAACBAAALxYAAPkyWDO29Do1SyxNi5UV71mJYEIEkfNa9wCFCCjY2asU@10.170.248.200 + + + + 1 + 0 + + 8E450FA1@10.170.248.140 + + + 0843073451 + +270843073451 + 0843073451 + 110493421 + 110493421 + + + False + NetworkDefault + 0 + + + Speech + + 46d7974398c2671016afccc3f2c428c7 + voice.lab.liquidtelecom.net + + + 1510225531933 + 1510225532183 + 20160419210005.247 + 20160419210006.813 + 1510225593101 + 20160419210020.296 + 1510225593101 + + + International + + Orig + 16 + No error + 223007624 + + +27110493421 + +27110493421 + +27110493421 + Ro_test + 0gQAAC8WAAACBAAALxYAAJrUscTicyU5GtjPyQnpAeuNmz9p/bdOoR/Mk9RXciOI@10.170.248.200 + + + + 1 + 0 + + BC8B2801@10.170.248.140 + + + 263772822306 + +263772822306 + 263772822306 + 110493421 + 110493421 + + + False + NetworkDefault + 0 + + + Speech + + 750b8b73e41ba7b59b21240758522268 + voice.lab.liquidtelecom.net + + + 1510225865894 + 1510225866144 + 20160419210005.247 + 20160419210006.813 + 1510225916144 + 20160419210020.296 + 1510225916144 + + + + 1510227591467 + 3 + 0 + 0 + + +` + +func TestXMLElementText2(t *testing.T) { + doc := etree.NewDocument() + if err := doc.ReadFromString(xmlContent); err != nil { + t.Error(err) + } + hPath2 := utils.ParseHierarchyPath("File>CDRs>Call", "") + cdrs := doc.Root().FindElements(hPath2.AsString("/", true)) + if len(cdrs) != 3 { + t.Errorf("Expecting: 3, received: %+v", len(cdrs)) + } + + if _, err := elementText(cdrs[0], "SignalingInfo/PChargingVector/test"); err != utils.ErrNotFound { + t.Error(err) + } + + absolutePath := utils.ParseHierarchyPath("File>CDRs>Call>SignalingInfo>PChargingVector>icidvalue", "") + hPath := utils.ParseHierarchyPath(cdrs[1].GetPath(), "") + relPath := utils.HierarchyPath(absolutePath[len(hPath):]) // Need relative path to the xmlElmnt + + if val, err := elementText(cdrs[1], relPath.AsString("/", false)); err != nil { + t.Error(err) + } else if val != "46d7974398c2671016afccc3f2c428c7" { + t.Errorf("Expecting: 46d7974398c2671016afccc3f2c428c7, received: %s", val) + } +} + +func TestXMLRPProcessWithNewFilters2(t *testing.T) { + cdrcCfgs := []*config.CdrcConfig{ + &config.CdrcConfig{ + ID: "msw_xml", + Enabled: true, + CdrFormat: "xml", + DataUsageMultiplyFactor: 1024, + CDRPath: utils.HierarchyPath([]string{"File", "CDRs", "Call"}), + CdrSourceId: "zw_cfs1", + Filters: []string{}, + ContentFields: []*config.FCTemplate{ + &config.FCTemplate{ID: "TOR", Type: utils.META_COMPOSED, FieldId: utils.ToR, + Value: config.NewRSRParsersMustCompile("*voice", true), Mandatory: true}, + &config.FCTemplate{ID: "OriginID", Type: utils.META_COMPOSED, FieldId: utils.OriginID, + Value: config.NewRSRParsersMustCompile("~File>CDRs>Call>SignalingInfo>PChargingVector>icidvalue", true), Mandatory: true}, + &config.FCTemplate{ID: "RequestType", Type: utils.META_COMPOSED, FieldId: utils.RequestType, + Value: config.NewRSRParsersMustCompile("*rated", true), Mandatory: true}, + &config.FCTemplate{ID: "Tenant", Type: utils.META_COMPOSED, FieldId: utils.Tenant, + Value: config.NewRSRParsersMustCompile("XX.liquid.tel", true), Mandatory: true}, + &config.FCTemplate{ID: "Category", Type: utils.META_COMPOSED, FieldId: utils.Category, + Value: config.NewRSRParsersMustCompile("call", true), Mandatory: true}, + &config.FCTemplate{ID: "Account", Type: utils.META_COMPOSED, FieldId: utils.Account, + Value: config.NewRSRParsersMustCompile("~File>CDRs>Call>OrigParty>SubscriberAddr", true), Mandatory: true}, + &config.FCTemplate{ID: "Destination", Type: utils.META_COMPOSED, FieldId: utils.Destination, + Value: config.NewRSRParsersMustCompile("~File>CDRs>Call>RoutingInfo>DestAddr", true), Mandatory: true}, + &config.FCTemplate{ID: "SetupTime", Type: utils.META_COMPOSED, FieldId: utils.SetupTime, + Value: config.NewRSRParsersMustCompile("~File>CDRs>Call>RingingTime", true), Mandatory: true}, + &config.FCTemplate{ID: "AnswerTime", Type: utils.META_COMPOSED, FieldId: utils.AnswerTime, + Value: config.NewRSRParsersMustCompile("~File>CDRs>Call>ConnectTime", true), Mandatory: true}, + &config.FCTemplate{ID: "Usage", Type: utils.META_HANDLER, + FieldId: utils.Usage, HandlerId: utils.HandlerSubstractUsage, + Value: config.NewRSRParsersMustCompile("~File>CDRs>Call>ReleaseTime;|;~File>CDRs>Call>ConnectTime", + true), Mandatory: true}, + &config.FCTemplate{ID: "UsageSeconds", Type: utils.META_COMPOSED, FieldId: utils.Usage, + Value: config.NewRSRParsersMustCompile("s", true), Mandatory: true}, + }, + }, + } + data, _ := engine.NewMapStorage() + defaultCfg, err := config.NewDefaultCGRConfig() + if err != nil { + t.Errorf("Error: %+v", err) + } + xmlRP, err := NewXMLRecordsProcessor(bytes.NewBufferString(xmlContent), + utils.HierarchyPath([]string{"File", "CDRs", "Call"}), "UTC", true, + cdrcCfgs, engine.NewFilterS(defaultCfg, nil, engine.NewDataManager(data))) + if err != nil { + t.Error(err) + } + var cdrs []*engine.CDR + for i := 0; i < 4; i++ { + cdrs, err = xmlRP.ProcessNextRecord() + if i == 1 { // Take second CDR since the first one cannot be processed + break + } + } + if err != nil { + t.Error(err) + } + expectedCDRs := []*engine.CDR{ + &engine.CDR{CGRID: "7c6170203bd3d5b34e9fce366b5ee7d621217824", + OriginHost: "0.0.0.0", Source: "zw_cfs1", OriginID: "46d7974398c2671016afccc3f2c428c7", + ToR: "*voice", RequestType: "*rated", Tenant: "XX.liquid.tel", + Category: "call", Account: "+27110493421", Destination: "+270843073451", + SetupTime: time.Date(2016, 4, 19, 21, 0, 5, 247000000, time.UTC), + AnswerTime: time.Date(2016, 4, 19, 21, 0, 6, 813000000, time.UTC), + Usage: time.Duration(13483000000), + ExtraFields: map[string]string{}, Cost: -1}, + } + if !reflect.DeepEqual(expectedCDRs, cdrs) { + t.Errorf("Expecting: %+v\n, received: %+v\n", expectedCDRs, cdrs) + } +} diff --git a/data/conf/samples/cdrcxmlwithfilter/cgrates.json b/data/conf/samples/cdrcxmlwithfilter/cgrates.json index 9058608da..bc2327412 100755 --- a/data/conf/samples/cdrcxmlwithfilter/cgrates.json +++ b/data/conf/samples/cdrcxmlwithfilter/cgrates.json @@ -41,6 +41,29 @@ {"id": "Usage", "field_id": "Usage", "type": "*handler", "handler_id": "*substract_usage", "value": "~broadWorksCDR>cdrData>basicModule>releaseTime;|;~broadWorksCDR>cdrData>basicModule>answerTime", "mandatory": true}, ], }, + { + "id": "msw_xml", // identifier of the CDRC runner + "enabled": true, // enable CDR client functionality + "cdr_format": "xml", // CDR file format + "cdr_in_dir": "/tmp/cdrcxmlwithfilters2/xmlit2/in", + "cdr_out_dir": "/tmp/cdrcxmlwithfilters2/xmlit2/out", + "cdr_path": "File>CDRs>Call", // path towards one CDR element in case of XML CDRs + "cdr_source_id": "zw_cfs1", // free form field, tag identifying the source of the CDRs within CDRS database + "content_fields":[ // import content_fields template, id will match internally CDR field, in case of .csv value will be represented by index of the field value + {"id": "TOR", "field_id": "ToR", "type": "*composed", "value": "*voice", "mandatory": true}, + {"id": "OriginID", "field_id": "OriginID", "type": "*composed", "value": "~File>CDRs>Call>SignalingInfo>PChargingVector>icidvalue", "mandatory": true}, + {"id": "RequestType", "field_id": "RequestType", "type": "*composed", "value": "*rated", "mandatory": true}, + {"id": "Direction", "field_id": "Direction", "type": "*composed", "value": "*out", "mandatory": true}, + {"id": "Tenant", "field_id": "Tenant", "type": "*composed", "value": "XX.liquid.tel", "mandatory": true}, + {"id": "Category", "field_id": "Category", "type": "*composed", "value": "call", "mandatory": true}, + {"id": "Account", "field_id": "Account", "type": "*composed", "value": "~File>CDRs>Call>OrigParty>SubscriberAddr", "mandatory": true}, + {"id": "Subject", "field_id": "Subject", "type": "*composed", "value": "~File>CDRs>Call>OrigParty>SubscriberAddr", "mandatory": true}, + {"id": "Destination", "field_id": "Destination", "type": "*composed", "value": "~File>CDRs>Call>RoutingInfo>DestAddr", "mandatory": true}, + {"id": "SetupTime", "field_id": "SetupTime", "type": "*composed", "value": "~File>CDRs>Call>RingingTime", "mandatory": true}, + {"id": "AnswerTime", "field_id": "AnswerTime", "type": "*composed", "value": "~File>CDRs>Call>ConnectTime", "mandatory": true}, + {"id": "Usage", "field_id": "Usage", "type": "*handler", "handler_id": "*substract_usage", "value": "~File>CDRs>Call>ReleaseTime;|;~File>CDRs>Call>ConnectTime", "mandatory": true} + ], + }, ], diff --git a/glide.lock b/glide.lock index 4774c0223..18c4c71c0 100644 --- a/glide.lock +++ b/glide.lock @@ -125,4 +125,6 @@ imports: - internal/scram - name: github.com/dlintw/goconf version: dcc070983490608a14480e3bf943bad464785df5 +- name: github.com/beevik/etree + version: 90dafc1e1f114dfb9576218bb43c03fb854d1b34 testImports: [] diff --git a/glide.yaml b/glide.yaml index 6bccd8a74..3fa2f3a97 100644 --- a/glide.yaml +++ b/glide.yaml @@ -41,4 +41,5 @@ import: - package: github.com/streadway/amqp - package: github.com/cgrates/radigo - package: github.com/cgrates/ltcache -- package: github.com/dlintw/goconf \ No newline at end of file +- package: github.com/dlintw/goconf +- package: github.com/beevik/etree \ No newline at end of file