From f7abbacfe53b7cb46923a59aa396e7fca02249e7 Mon Sep 17 00:00:00 2001 From: DanB Date: Mon, 5 May 2014 20:14:40 +0200 Subject: [PATCH] Refactoring CDRs to support RSRFields --- apier/apier_local_test.go | 8 +- apier/cdre.go | 8 +- apier/derivedcharging_test.go | 8 +- cdrc/cdrc.go | 94 ++--- cdrc/cdrc_test.go | 35 +- cdre/csv.go | 8 +- cdre/fixedwidth.go | 10 +- cdrs/cdrs.go | 16 +- cdrs/fscdr.go | 223 ++---------- cdrs/fscdr_test.go | 141 ++------ config/config.go | 140 +++----- config/config_test.go | 63 ++-- config/helpers.go | 38 +- config/helpers_test.go | 40 ++- config/test_data.txt | 28 +- data/conf/cgrates.cfg | 8 +- data/storage/mysql/create_cdrs_tables.sql | 2 +- .../mysql/create_costdetails_tables.sql | 2 +- .../mysql/create_tariffplan_tables.sql | 16 +- .../prepaid1centpsec/ActionTriggers.csv | 2 +- engine/loader_csv.go | 4 +- engine/loader_helpers.go | 4 +- engine/loader_helpers_test.go | 6 +- engine/loader_local_test.go | 2 + engine/storage_interface.go | 2 +- engine/storage_sql.go | 83 +++-- engine/storage_sql_local_test.go | 31 +- engine/tpimporter_csv.go | 9 +- general_tests/fsevcorelate_test.go | 13 +- mediator/mediator.go | 70 ++-- mediator/mediator_local_test.go | 2 +- utils/cgrcdr.go | 153 ++------ utils/cgrcdr_test.go | 88 +---- utils/consts.go | 9 +- utils/rawcdr.go | 27 +- utils/rsrfield.go | 17 +- utils/rsrfield_test.go | 17 + utils/storedcdr.go | 295 ++++++++-------- utils/storedcdr_test.go | 331 ++++++++++-------- 39 files changed, 848 insertions(+), 1205 deletions(-) diff --git a/apier/apier_local_test.go b/apier/apier_local_test.go index b6c677ccb..fe66ce686 100644 --- a/apier/apier_local_test.go +++ b/apier/apier_local_test.go @@ -1422,9 +1422,9 @@ func TestLocalSetDC(t *testing.T) { return } dcs1 := utils.DerivedChargers{ - &utils.DerivedCharger{RunId: "extra1", ReqTypeField: "^prepaid", DirectionField: "*default", TenantField: "*default", TorField: "*default", + &utils.DerivedCharger{RunId: "extra1", ReqTypeField: "^prepaid", DirectionField: "*default", TenantField: "*default", CategoryField: "*default", AccountField: "rif", SubjectField: "rif", DestinationField: "*default", SetupTimeField: "*default", AnswerTimeField: "*default", DurationField: "*default"}, - &utils.DerivedCharger{RunId: "extra2", ReqTypeField: "*default", DirectionField: "*default", TenantField: "*default", TorField: "*default", + &utils.DerivedCharger{RunId: "extra2", ReqTypeField: "*default", DirectionField: "*default", TenantField: "*default", CategoryField: "*default", AccountField: "ivo", SubjectField: "ivo", DestinationField: "*default", SetupTimeField: "*default", AnswerTimeField: "*default", DurationField: "*default"}, } attrs := AttrSetDerivedChargers{Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "dan", Subject: "dan", DerivedChargers: dcs1} @@ -1442,9 +1442,9 @@ func TestLocalGetDC(t *testing.T) { } attrs := utils.AttrDerivedChargers{Tenant: "cgrates.org", Category: "call", Direction: "*out", Account: "dan", Subject: "dan"} eDcs := utils.DerivedChargers{ - &utils.DerivedCharger{RunId: "extra1", ReqTypeField: "^prepaid", DirectionField: "*default", TenantField: "*default", TorField: "*default", + &utils.DerivedCharger{RunId: "extra1", ReqTypeField: "^prepaid", DirectionField: "*default", TenantField: "*default", CategoryField: "*default", AccountField: "rif", SubjectField: "rif", DestinationField: "*default", SetupTimeField: "*default", AnswerTimeField: "*default", DurationField: "*default"}, - &utils.DerivedCharger{RunId: "extra2", ReqTypeField: "*default", DirectionField: "*default", TenantField: "*default", TorField: "*default", + &utils.DerivedCharger{RunId: "extra2", ReqTypeField: "*default", DirectionField: "*default", TenantField: "*default", CategoryField: "*default", AccountField: "ivo", SubjectField: "ivo", DestinationField: "*default", SetupTimeField: "*default", AnswerTimeField: "*default", DurationField: "*default"}, } var dcs utils.DerivedChargers diff --git a/apier/cdre.go b/apier/cdre.go index 60b48d707..5f04edbb1 100644 --- a/apier/cdre.go +++ b/apier/cdre.go @@ -1,6 +1,6 @@ /* -Rating system designed to be used in VoIP Carriers World -Copyright (C) 2013 ITsysCOM +Real-time Charging System for Telecom & ISP environments +Copyright (C) 2012-2014 ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -85,9 +85,9 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E exportedIds[idxCdr] = cdr.CgrId } *reply = utils.ExportedFileCdrs{ExportedFilePath: utils.CDRE_DRYRUN, TotalRecords: len(cdrs), ExportedCgrIds: exportedIds} - case utils.CDRE_CSV: + case utils.CSV: if len(exportDir) == 0 { - exportDir = path.Join(self.Config.CdreDir, utils.CDRE_CSV) + exportDir = path.Join(self.Config.CdreDir, utils.CSV) } if len(fileName) == 0 { fileName = fmt.Sprintf("cdre_%s.csv", exportId) diff --git a/apier/derivedcharging_test.go b/apier/derivedcharging_test.go index 78f75cf81..317c75195 100644 --- a/apier/derivedcharging_test.go +++ b/apier/derivedcharging_test.go @@ -47,9 +47,9 @@ func TestGetEmptyDC(t *testing.T) { func TestSetDC(t *testing.T) { dcs1 := utils.DerivedChargers{ - &utils.DerivedCharger{RunId: "extra1", ReqTypeField: "^prepaid", DirectionField: "*default", TenantField: "*default", TorField: "*default", + &utils.DerivedCharger{RunId: "extra1", ReqTypeField: "^prepaid", DirectionField: "*default", TenantField: "*default", CategoryField: "*default", AccountField: "rif", SubjectField: "rif", DestinationField: "*default", SetupTimeField: "*default", AnswerTimeField: "*default", DurationField: "*default"}, - &utils.DerivedCharger{RunId: "extra2", ReqTypeField: "*default", DirectionField: "*default", TenantField: "*default", TorField: "*default", + &utils.DerivedCharger{RunId: "extra2", ReqTypeField: "*default", DirectionField: "*default", TenantField: "*default", CategoryField: "*default", AccountField: "ivo", SubjectField: "ivo", DestinationField: "*default", SetupTimeField: "*default", AnswerTimeField: "*default", DurationField: "*default"}, } attrs := AttrSetDerivedChargers{Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "dan", Subject: "dan", DerivedChargers: dcs1} @@ -64,9 +64,9 @@ func TestSetDC(t *testing.T) { func TestGetDC(t *testing.T) { attrs := utils.AttrDerivedChargers{Tenant: "cgrates.org", Category: "call", Direction: "*out", Account: "dan", Subject: "dan"} eDcs := utils.DerivedChargers{ - &utils.DerivedCharger{RunId: "extra1", ReqTypeField: "^prepaid", DirectionField: "*default", TenantField: "*default", TorField: "*default", + &utils.DerivedCharger{RunId: "extra1", ReqTypeField: "^prepaid", DirectionField: "*default", TenantField: "*default", CategoryField: "*default", AccountField: "rif", SubjectField: "rif", DestinationField: "*default", SetupTimeField: "*default", AnswerTimeField: "*default", DurationField: "*default"}, - &utils.DerivedCharger{RunId: "extra2", ReqTypeField: "*default", DirectionField: "*default", TenantField: "*default", TorField: "*default", + &utils.DerivedCharger{RunId: "extra2", ReqTypeField: "*default", DirectionField: "*default", TenantField: "*default", CategoryField: "*default", AccountField: "ivo", SubjectField: "ivo", DestinationField: "*default", SetupTimeField: "*default", AnswerTimeField: "*default", DurationField: "*default"}, } var dcs utils.DerivedChargers diff --git a/cdrc/cdrc.go b/cdrc/cdrc.go index 8f61ce45d..25d3c07a7 100644 --- a/cdrc/cdrc.go +++ b/cdrc/cdrc.go @@ -21,7 +21,6 @@ package cdrc import ( "bufio" "encoding/csv" - "errors" "fmt" "io" "io/ioutil" @@ -29,7 +28,6 @@ import ( "os" "path" "strconv" - "strings" "time" "github.com/cgrates/cgrates/cdrs" @@ -52,9 +50,6 @@ func NewCdrc(config *config.CGRConfig, cdrServer *cdrs.CDRS) (*Cdrc, error) { return nil, fmt.Errorf("Folder %s does not exist", dir) } } - if err := cdrc.parseFieldsConfig(); err != nil { - return nil, err - } cdrc.httpClient = new(http.Client) return cdrc, nil } @@ -78,100 +73,57 @@ func (self *Cdrc) Run() error { } } -// Loads all fields (primary and extra) into cfgCdrFields, do some pre-checks (eg: in case of csv make sure that values are integers) -func (self *Cdrc) parseFieldsConfig() error { - var err error - self.cfgCdrFields = map[string]string{ - utils.ACCID: self.cgrCfg.CdrcAccIdField, - utils.REQTYPE: self.cgrCfg.CdrcReqTypeField, - utils.DIRECTION: self.cgrCfg.CdrcDirectionField, - utils.TENANT: self.cgrCfg.CdrcTenantField, - utils.Category: self.cgrCfg.CdrcCategoryField, - utils.ACCOUNT: self.cgrCfg.CdrcAccountField, - utils.SUBJECT: self.cgrCfg.CdrcSubjectField, - utils.DESTINATION: self.cgrCfg.CdrcDestinationField, - utils.SETUP_TIME: self.cgrCfg.CdrcSetupTimeField, - utils.ANSWER_TIME: self.cgrCfg.CdrcAnswerTimeField, - utils.DURATION: self.cgrCfg.CdrcDurationField, - } - - // Add extra fields here, config extra fields in the form of []string{"fieldName1:indxInCsv1","fieldName2: indexInCsv2"} - for _, fieldWithIdx := range self.cgrCfg.CdrcExtraFields { - splt := strings.Split(fieldWithIdx, ":") - if len(splt) != 2 { - return errors.New("Cannot parse cdrc.extra_fields") - } - if utils.IsSliceMember(utils.PrimaryCdrFields, splt[0]) { - return errors.New("Extra cdrc.extra_fields overwriting primary fields") - } - self.cfgCdrFields[splt[0]] = splt[1] - } - // Fields populated, do some sanity checks here - for cdrField, cfgVal := range self.cfgCdrFields { - if utils.IsSliceMember([]string{CSV, FS_CSV}, self.cgrCfg.CdrcCdrType) && !strings.HasPrefix(cfgVal, utils.STATIC_VALUE_PREFIX) { - if _, err = strconv.Atoi(cfgVal); err != nil { - return fmt.Errorf("Cannot parse configuration field %s into integer", cdrField) - } - } - } - return nil -} - // Takes the record out of csv and turns it into http form which can be posted func (self *Cdrc) recordForkCdr(record []string) (*utils.StoredCdr, error) { - ratedCdr := &utils.StoredCdr{CdrSource: self.cgrCfg.CdrcSourceId, ExtraFields: map[string]string{}, Cost: -1} + storedCdr := &utils.StoredCdr{CdrSource: self.cgrCfg.CdrcSourceId, ExtraFields: map[string]string{}, Cost: -1} var err error - for cfgFieldName, cfgFieldVal := range self.cfgCdrFields { + for cfgFieldName, cfgFieldRSR := range self.cgrCfg.CdrcCdrFields { var fieldVal string - if strings.HasPrefix(cfgFieldVal, utils.STATIC_VALUE_PREFIX) { - fieldVal = cfgFieldVal[1:] - } else if utils.IsSliceMember([]string{CSV, FS_CSV}, self.cgrCfg.CdrcCdrType) { - if cfgFieldIdx, err := strconv.Atoi(cfgFieldVal); err != nil { // Should in theory never happen since we have already parsed config - return nil, err - } else if len(record) <= cfgFieldIdx { + if utils.IsSliceMember([]string{CSV, FS_CSV}, self.cgrCfg.CdrcCdrType) { + if cfgFieldIdx, _ := strconv.Atoi(cfgFieldRSR.Id); len(record) <= cfgFieldIdx { return nil, fmt.Errorf("Ignoring record: %v - cannot extract field %s", record, cfgFieldName) } else { - fieldVal = record[cfgFieldIdx] + fieldVal = cfgFieldRSR.ParseValue(record[cfgFieldIdx]) } } else { // Modify here when we add more supported cdr formats fieldVal = "UNKNOWN" } switch cfgFieldName { case utils.ACCID: - ratedCdr.AccId = fieldVal + storedCdr.AccId = fieldVal case utils.REQTYPE: - ratedCdr.ReqType = fieldVal + storedCdr.ReqType = fieldVal case utils.DIRECTION: - ratedCdr.Direction = fieldVal + storedCdr.Direction = fieldVal case utils.TENANT: - ratedCdr.Tenant = fieldVal - case utils.Category: - ratedCdr.Category = fieldVal + storedCdr.Tenant = fieldVal + case utils.CATEGORY: + storedCdr.Category = fieldVal case utils.ACCOUNT: - ratedCdr.Account = fieldVal + storedCdr.Account = fieldVal case utils.SUBJECT: - ratedCdr.Subject = fieldVal + storedCdr.Subject = fieldVal case utils.DESTINATION: - ratedCdr.Destination = fieldVal + storedCdr.Destination = fieldVal case utils.SETUP_TIME: - if ratedCdr.SetupTime, err = utils.ParseTimeDetectLayout(fieldVal); err != nil { + if storedCdr.SetupTime, err = utils.ParseTimeDetectLayout(fieldVal); err != nil { return nil, fmt.Errorf("Cannot parse answer time field, err: %s", err.Error()) } case utils.ANSWER_TIME: - if ratedCdr.AnswerTime, err = utils.ParseTimeDetectLayout(fieldVal); err != nil { + if storedCdr.AnswerTime, err = utils.ParseTimeDetectLayout(fieldVal); err != nil { return nil, fmt.Errorf("Cannot parse answer time field, err: %s", err.Error()) } case utils.DURATION: - if ratedCdr.Duration, err = utils.ParseDurationWithSecs(fieldVal); err != nil { + if storedCdr.Duration, err = utils.ParseDurationWithSecs(fieldVal); err != nil { return nil, fmt.Errorf("Cannot parse duration field, err: %s", err.Error()) } default: // Extra fields will not match predefined so they all show up here - ratedCdr.ExtraFields[cfgFieldName] = fieldVal + storedCdr.ExtraFields[cfgFieldName] = fieldVal } } - ratedCdr.CgrId = utils.Sha1(ratedCdr.AccId, ratedCdr.SetupTime.String()) - return ratedCdr, nil + storedCdr.CgrId = utils.Sha1(storedCdr.AccId, storedCdr.SetupTime.String()) + return storedCdr, nil } // One run over the CDR folder @@ -233,18 +185,18 @@ func (self *Cdrc) processFile(filePath string) error { engine.Logger.Err(fmt.Sprintf(" Error in csv file: %s", err.Error())) continue // Other csv related errors, ignore } - rawCdr, err := self.recordForkCdr(record) + storedCdr, err := self.recordForkCdr(record) if err != nil { engine.Logger.Err(fmt.Sprintf(" Error in csv file: %s", err.Error())) continue } if self.cgrCfg.CdrcCdrs == utils.INTERNAL { - if err := self.cdrServer.ProcessRawCdr(rawCdr); err != nil { + if err := self.cdrServer.ProcessRawCdr(storedCdr); err != nil { engine.Logger.Err(fmt.Sprintf(" Failed posting CDR, error: %s", err.Error())) continue } } else { // CDRs listening on IP - if _, err := self.httpClient.PostForm(fmt.Sprintf("http://%s/cgr", self.cgrCfg.HTTPListen), rawCdr.AsRawCdrHttpForm()); err != nil { + if _, err := self.httpClient.PostForm(fmt.Sprintf("http://%s/cgr", self.cgrCfg.HTTPListen), storedCdr.AsHttpForm()); err != nil { engine.Logger.Err(fmt.Sprintf(" Failed posting CDR, error: %s", err.Error())) continue } diff --git a/cdrc/cdrc_test.go b/cdrc/cdrc_test.go index edf20070d..868e846dd 100644 --- a/cdrc/cdrc_test.go +++ b/cdrc/cdrc_test.go @@ -26,43 +26,10 @@ import ( "time" ) -func TestParseFieldsConfig(t *testing.T) { - // Test default config - cgrConfig, _ := config.NewDefaultCGRConfig() - // Test primary field index definition - cgrConfig.CdrcAccIdField = "detect_me" - cdrc := &Cdrc{cgrCfg: cgrConfig} - if err := cdrc.parseFieldsConfig(); err == nil { - t.Error("Failed detecting error in accounting id definition", err) - } - cgrConfig.CdrcAccIdField = "^static_val" - cgrConfig.CdrcSubjectField = "1" - cdrc = &Cdrc{cgrCfg: cgrConfig} - if err := cdrc.parseFieldsConfig(); err != nil { - t.Error("Failed to corectly parse primary fields %v", cdrc.cfgCdrFields) - } - cgrConfig.CdrcExtraFields = []string{"^static_val:orig_ip"} - // Test extra field index definition - cgrConfig.CdrcAccIdField = "0" // Put back as int - cgrConfig.CdrcExtraFields = []string{"supplier1", "orig_ip:11"} - cdrc = &Cdrc{cgrCfg: cgrConfig} - if err := cdrc.parseFieldsConfig(); err == nil { - t.Error("Failed detecting error in extra fields definition", err) - } - cgrConfig.CdrcExtraFields = []string{"supplier1:^top_supplier", "orig_ip:11"} - cdrc = &Cdrc{cgrCfg: cgrConfig} - if err := cdrc.parseFieldsConfig(); err != nil { - t.Errorf("Failed to corectly parse extra fields %v", cdrc.cfgCdrFields) - } -} - func TestRecordForkCdr(t *testing.T) { cgrConfig, _ := config.NewDefaultCGRConfig() - cgrConfig.CdrcExtraFields = []string{"supplier:11"} + cgrConfig.CdrcCdrFields["supplier"] = &utils.RSRField{Id: "11"} cdrc := &Cdrc{cgrCfg: cgrConfig} - if err := cdrc.parseFieldsConfig(); err != nil { - t.Error("Failed parsing default fieldIndexesFromConfig", err) - } cdrRow := []string{"firstField", "secondField"} _, err := cdrc.recordForkCdr(cdrRow) if err == nil { diff --git a/cdre/csv.go b/cdre/csv.go index e02989992..7dfb6ed80 100644 --- a/cdre/csv.go +++ b/cdre/csv.go @@ -1,6 +1,6 @@ /* -Rating system designed to be used in VoIP Carriers World -Copyright (C) 2013 ITsysCOM +Real-time Charging System for Telecom & ISP environments +Copyright (C) 2012-2014 ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -59,12 +59,12 @@ func (csvwr *CsvCdrWriter) WriteCdr(cdr *utils.StoredCdr) error { if fld.Id == utils.COST { fldVal = cdr.FormatCost(csvwr.costShiftDigits, csvwr.roundDecimals) } else if fld.Id == utils.DESTINATION { - fldVal = cdr.ExportFieldValue(utils.DESTINATION) + fldVal = cdr.FieldAsString(&utils.RSRField{Id: utils.DESTINATION}) if len(csvwr.maskDestId) != 0 && csvwr.maskLen > 0 && engine.CachedDestHasPrefix(csvwr.maskDestId, fldVal) { fldVal = MaskDestination(fldVal, csvwr.maskLen) } } else { - fldVal = cdr.ExportFieldValue(fld.Id) + fldVal = cdr.FieldAsString(fld) } row[idx] = fld.ParseValue(fldVal) } diff --git a/cdre/fixedwidth.go b/cdre/fixedwidth.go index d3a7f6715..84482f971 100644 --- a/cdre/fixedwidth.go +++ b/cdre/fixedwidth.go @@ -1,6 +1,6 @@ /* -Rating system designed to be used in VoIP Carriers World -Copyright (C) 2013 ITsysCOM +Real-time Charging System for Telecom & ISP environments +Copyright (C) 2012-2014 ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -125,12 +125,12 @@ func (fwv *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layo case utils.ANSWER_TIME: // Format time based on layout cdrVal = cdr.AnswerTime.Format(layout) case utils.DESTINATION: - cdrVal = cdr.ExportFieldValue(utils.DESTINATION) + cdrVal = cdr.FieldAsString(&utils.RSRField{Id: utils.DESTINATION}) if fwv.maskLen != -1 && fwv.maskedDestination(cdrVal) { cdrVal = MaskDestination(cdrVal, fwv.maskLen) } default: - cdrVal = cdr.ExportFieldValue(rsrField.Id) + cdrVal = cdr.FieldAsString(rsrField) } return rsrField.ParseValue(cdrVal), nil } @@ -273,7 +273,7 @@ func (fwv *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error { } case METATAG: if cfgFld.Value == META_MASKDESTINATION { - outVal, err = fwv.metaHandler(cfgFld.Value, cdr.ExportFieldValue(utils.DESTINATION)) + outVal, err = fwv.metaHandler(cfgFld.Value, cdr.FieldAsString(&utils.RSRField{Id: utils.DESTINATION})) } else { outVal, err = fwv.metaHandler(cfgFld.Value, cfgFld.Layout) } diff --git a/cdrs/cdrs.go b/cdrs/cdrs.go index 19721c9e8..7122b20cc 100644 --- a/cdrs/cdrs.go +++ b/cdrs/cdrs.go @@ -36,13 +36,13 @@ var ( ) // Returns error if not able to properly store the CDR, mediation is async since we can always recover offline -func storeAndMediate(rawCdr utils.RawCDR) error { - if err := storage.SetCdr(rawCdr); err != nil { +func storeAndMediate(storedCdr *utils.StoredCdr) error { + if err := storage.SetCdr(storedCdr); err != nil { return err } if cfg.CDRSMediator == utils.INTERNAL { go func() { - if err := medi.RateCdr(rawCdr); err != nil { + if err := medi.RateCdr(storedCdr); err != nil { engine.Logger.Err(fmt.Sprintf("Could not run mediation on CDR: %s", err.Error())) } }() @@ -56,7 +56,7 @@ func cgrCdrHandler(w http.ResponseWriter, r *http.Request) { if err != nil { engine.Logger.Err(fmt.Sprintf("Could not create CDR entry: %s", err.Error())) } - if err := storeAndMediate(cgrCdr); err != nil { + if err := storeAndMediate(cgrCdr.AsStoredCdr()); err != nil { engine.Logger.Err(fmt.Sprintf("Errors when storing CDR entry: %s", err.Error())) } } @@ -64,11 +64,11 @@ func cgrCdrHandler(w http.ResponseWriter, r *http.Request) { // Handler for fs http func fsCdrHandler(w http.ResponseWriter, r *http.Request) { body, _ := ioutil.ReadAll(r.Body) - fsCdr, err := new(FSCdr).New(body) + fsCdr, err := NewFSCdr(body) if err != nil { engine.Logger.Err(fmt.Sprintf("Could not create CDR entry: %s", err.Error())) } - if err := storeAndMediate(fsCdr); err != nil { + if err := storeAndMediate(fsCdr.AsStoredCdr()); err != nil { engine.Logger.Err(fmt.Sprintf("Errors when storing CDR entry: %s", err.Error())) } } @@ -88,6 +88,6 @@ func (cdrs *CDRS) RegisterHanlersToServer(server *engine.Server) { } // Used to internally process CDR -func (cdrs *CDRS) ProcessRawCdr(rawCdr utils.RawCDR) error { - return storeAndMediate(rawCdr) +func (cdrs *CDRS) ProcessRawCdr(rawCdr utils.RawCdr) error { + return storeAndMediate(rawCdr.AsStoredCdr()) } diff --git a/cdrs/fscdr.go b/cdrs/fscdr.go index 9a6d73d5f..56ddde00f 100644 --- a/cdrs/fscdr.go +++ b/cdrs/fscdr.go @@ -20,12 +20,9 @@ package cdrs import ( "encoding/json" - "errors" "fmt" "reflect" - "strconv" "strings" - "time" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" @@ -54,12 +51,8 @@ const ( FS_SIP_REQUSER = "sip_req_user" // Apps like FusionPBX do not set dialed_extension, alternative being destination_number but that comes in customer profile, not in vars ) -type FSCdr struct { - vars map[string]string - body map[string]interface{} // keeps the loaded body for extra field search -} - -func (fsCdr FSCdr) New(body []byte) (utils.RawCDR, error) { +func NewFSCdr(body []byte) (*FSCdr, error) { + fsCdr := new(FSCdr) fsCdr.vars = make(map[string]string) var err error if err = json.Unmarshal(body, &fsCdr.body); err == nil { @@ -75,49 +68,23 @@ func (fsCdr FSCdr) New(body []byte) (utils.RawCDR, error) { return nil, err } -func (fsCdr FSCdr) GetCgrId() string { - setupTime, _ := fsCdr.GetSetupTime() +type FSCdr struct { + vars map[string]string + body map[string]interface{} // keeps the loaded body for extra field search +} + +func (fsCdr FSCdr) getCgrId() string { + setupTime, _ := utils.ParseTimeDetectLayout(fsCdr.vars[FS_SETUP_TIME]) return utils.Sha1(fsCdr.vars[FS_UUID], setupTime.String()) } -func (fsCdr FSCdr) GetAccId() string { - return fsCdr.vars[FS_UUID] -} -func (fsCdr FSCdr) GetCdrHost() string { - return fsCdr.vars[FS_IP] -} -func (fsCdr FSCdr) GetCdrSource() string { - return FS_CDR_SOURCE -} -func (fsCdr FSCdr) GetDirection() string { - //TODO: implement direction, not related to FS_DIRECTION but traffic towards or from subject/account - return "*out" -} -func (fsCdr FSCdr) GetSubject() string { - return utils.FirstNonEmpty(fsCdr.vars[FS_SUBJECT], fsCdr.vars[FS_USERNAME]) -} -func (fsCdr FSCdr) GetAccount() string { - return utils.FirstNonEmpty(fsCdr.vars[FS_ACCOUNT], fsCdr.vars[FS_USERNAME]) -} -// Charging destination number -func (fsCdr FSCdr) GetDestination() string { - return utils.FirstNonEmpty(fsCdr.vars[FS_DESTINATION], fsCdr.vars[FS_CALL_DEST_NR], fsCdr.vars[FS_SIP_REQUSER]) -} - -func (fsCdr FSCdr) GetCategory() string { - return utils.FirstNonEmpty(fsCdr.vars[FS_CATEGORY], cfg.DefaultCategory) -} - -func (fsCdr FSCdr) GetTenant() string { - return utils.FirstNonEmpty(fsCdr.vars[FS_CSTMID], cfg.DefaultTenant) -} -func (fsCdr FSCdr) GetReqType() string { - return utils.FirstNonEmpty(fsCdr.vars[FS_REQTYPE], cfg.DefaultReqType) -} -func (fsCdr FSCdr) GetExtraFields() map[string]string { +func (fsCdr FSCdr) getExtraFields() map[string]string { extraFields := make(map[string]string, len(cfg.CDRSExtraFields)) for _, field := range cfg.CDRSExtraFields { origFieldVal, foundInVars := fsCdr.vars[field.Id] + if strings.HasPrefix(field.Id, utils.STATIC_VALUE_PREFIX) { // Support for static values injected in the CDRS. it will show up as {^value:value} + foundInVars = true + } if !foundInVars { origFieldVal = fsCdr.searchExtraField(field.Id, fsCdr.body) } @@ -154,149 +121,23 @@ func (fsCdr FSCdr) searchExtraField(field string, body map[string]interface{}) ( return } -func (fsCdr FSCdr) GetSetupTime() (t time.Time, err error) { - return utils.ParseTimeDetectLayout(fsCdr.vars[FS_SETUP_TIME]) -} -func (fsCdr FSCdr) GetAnswerTime() (t time.Time, err error) { - return utils.ParseTimeDetectLayout(fsCdr.vars[FS_ANSWER_TIME]) -} -func (fsCdr FSCdr) GetHangupTime() (t time.Time, err error) { - return utils.ParseTimeDetectLayout(fsCdr.vars[FS_HANGUP_TIME]) -} - -// Extracts duration as considered by the telecom switch -func (fsCdr FSCdr) GetDuration() (time.Duration, error) { - return utils.ParseDurationWithSecs(fsCdr.vars[FS_DURATION]) -} - -func (fsCdr FSCdr) Store() (result string, err error) { - result += fsCdr.GetCgrId() + "|" - result += fsCdr.GetAccId() + "|" - result += fsCdr.GetCdrHost() + "|" - result += fsCdr.GetDirection() + "|" - result += fsCdr.GetSubject() + "|" - result += fsCdr.GetAccount() + "|" - result += fsCdr.GetDestination() + "|" - result += fsCdr.GetCategory() + "|" - result += fsCdr.GetAccId() + "|" - result += fsCdr.GetTenant() + "|" - result += fsCdr.GetReqType() + "|" - st, err := fsCdr.GetAnswerTime() - if err != nil { - return "", err - } - result += strconv.FormatInt(st.UnixNano(), 10) + "|" - et, err := fsCdr.GetHangupTime() - if err != nil { - return "", err - } - result += strconv.FormatInt(et.UnixNano(), 10) + "|" - dur, _ := fsCdr.GetDuration() - result += strconv.FormatInt(int64(dur.Seconds()), 10) + "|" - return -} - -func (fsCdr FSCdr) Restore(input string) error { - return errors.New("Not implemented") -} - -// Used in extra mediation -func (fsCdr FSCdr) ForkCdr(runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, setupTimeFld, answerTimeFld, durationFld string, extraFlds []string, fieldsMandatory bool) (*utils.StoredCdr, error) { - if utils.IsSliceMember([]string{runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, answerTimeFld, durationFld}, "") { - return nil, errors.New(fmt.Sprintf("%s:FieldName", utils.ERR_MANDATORY_IE_MISSING)) // All input field names are mandatory - } - var err error - var hasKey bool - var sTimeStr, aTimeStr, durStr string - rtCdr := new(utils.StoredCdr) - rtCdr.MediationRunId = runId - rtCdr.Cost = -1.0 // Default for non-rated CDR - if rtCdr.AccId = fsCdr.GetAccId(); len(rtCdr.AccId) == 0 { - if fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, utils.ACCID)) - } else { // Not mandatory, need to generate here CgrId - rtCdr.CgrId = utils.GenUUID() - } - } - if rtCdr.CdrHost = fsCdr.GetCdrHost(); len(rtCdr.CdrHost) == 0 && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, utils.CDRHOST)) - } - if rtCdr.CdrSource = fsCdr.GetCdrSource(); len(rtCdr.CdrSource) == 0 && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, utils.CDRSOURCE)) - } - if strings.HasPrefix(reqTypeFld, utils.STATIC_VALUE_PREFIX) { // Values starting with prefix are not dynamically populated - rtCdr.ReqType = reqTypeFld[1:] - } else if rtCdr.ReqType, hasKey = fsCdr.vars[reqTypeFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, reqTypeFld)) - } - if strings.HasPrefix(directionFld, utils.STATIC_VALUE_PREFIX) { - rtCdr.Direction = directionFld[1:] - } else if rtCdr.Direction, hasKey = fsCdr.vars[directionFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, directionFld)) - } - if strings.HasPrefix(tenantFld, utils.STATIC_VALUE_PREFIX) { - rtCdr.Tenant = tenantFld[1:] - } else if rtCdr.Tenant, hasKey = fsCdr.vars[tenantFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, tenantFld)) - } - if strings.HasPrefix(torFld, utils.STATIC_VALUE_PREFIX) { - rtCdr.Category = torFld[1:] - } else if rtCdr.Category, hasKey = fsCdr.vars[torFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, torFld)) - } - if strings.HasPrefix(accountFld, utils.STATIC_VALUE_PREFIX) { - rtCdr.Account = accountFld[1:] - } else if rtCdr.Account, hasKey = fsCdr.vars[accountFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, accountFld)) - } - if strings.HasPrefix(subjectFld, utils.STATIC_VALUE_PREFIX) { - rtCdr.Subject = subjectFld[1:] - } else if rtCdr.Subject, hasKey = fsCdr.vars[subjectFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, subjectFld)) - } - if strings.HasPrefix(destFld, utils.STATIC_VALUE_PREFIX) { - rtCdr.Destination = destFld[1:] - } else if rtCdr.Destination, hasKey = fsCdr.vars[destFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, destFld)) - } - if sTimeStr, hasKey = fsCdr.vars[setupTimeFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(setupTimeFld, utils.STATIC_VALUE_PREFIX) { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, setupTimeFld)) - } else { - if strings.HasPrefix(setupTimeFld, utils.STATIC_VALUE_PREFIX) { - sTimeStr = setupTimeFld[1:] - } - if rtCdr.SetupTime, err = utils.ParseTimeDetectLayout(sTimeStr); err != nil && fieldsMandatory { - return nil, err - } - } - if aTimeStr, hasKey = fsCdr.vars[answerTimeFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(answerTimeFld, utils.STATIC_VALUE_PREFIX) { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, answerTimeFld)) - } else { - if strings.HasPrefix(answerTimeFld, utils.STATIC_VALUE_PREFIX) { - aTimeStr = answerTimeFld[1:] - } - if rtCdr.AnswerTime, err = utils.ParseTimeDetectLayout(aTimeStr); err != nil && fieldsMandatory { - return nil, err - } - } - if durStr, hasKey = fsCdr.vars[durationFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(durationFld, utils.STATIC_VALUE_PREFIX) { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, durationFld)) - } else { - if strings.HasPrefix(durationFld, utils.STATIC_VALUE_PREFIX) { - durStr = durationFld[1:] - } - if rtCdr.Duration, err = utils.ParseDurationWithSecs(durStr); err != nil && fieldsMandatory { - return nil, err - } - } - rtCdr.CgrId = utils.Sha1(rtCdr.AccId, rtCdr.SetupTime.String()) - rtCdr.ExtraFields = make(map[string]string, len(extraFlds)) - for _, fldName := range extraFlds { - if fldVal, hasKey := fsCdr.vars[fldName]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, fldName)) - } else { - rtCdr.ExtraFields[fldName] = fldVal - } - } - return rtCdr, nil +func (fsCdr FSCdr) AsStoredCdr() *utils.StoredCdr { + storCdr := new(utils.StoredCdr) + storCdr.CgrId = fsCdr.getCgrId() + storCdr.AccId = fsCdr.vars[FS_UUID] + storCdr.CdrHost = fsCdr.vars[FS_IP] + storCdr.CdrSource = FS_CDR_SOURCE + storCdr.ReqType = utils.FirstNonEmpty(fsCdr.vars[FS_REQTYPE], cfg.DefaultReqType) + storCdr.Direction = "*out" + storCdr.Tenant = utils.FirstNonEmpty(fsCdr.vars[FS_CSTMID], cfg.DefaultTenant) + storCdr.Category = utils.FirstNonEmpty(fsCdr.vars[FS_CATEGORY], cfg.DefaultCategory) + storCdr.Account = utils.FirstNonEmpty(fsCdr.vars[FS_ACCOUNT], fsCdr.vars[FS_USERNAME]) + storCdr.Subject = utils.FirstNonEmpty(fsCdr.vars[FS_SUBJECT], fsCdr.vars[FS_USERNAME]) + storCdr.Destination = utils.FirstNonEmpty(fsCdr.vars[FS_DESTINATION], fsCdr.vars[FS_CALL_DEST_NR], fsCdr.vars[FS_SIP_REQUSER]) + storCdr.SetupTime, _ = utils.ParseTimeDetectLayout(fsCdr.vars[FS_SETUP_TIME]) // Not interested to process errors, should do them if necessary in a previous step + storCdr.AnswerTime, _ = utils.ParseTimeDetectLayout(fsCdr.vars[FS_ANSWER_TIME]) + storCdr.Duration, _ = utils.ParseDurationWithSecs(fsCdr.vars[FS_DURATION]) + storCdr.ExtraFields = fsCdr.getExtraFields() + storCdr.Cost = -1 + return storCdr } diff --git a/cdrs/fscdr_test.go b/cdrs/fscdr_test.go index ad7de3ec1..0e94e4546 100644 --- a/cdrs/fscdr_test.go +++ b/cdrs/fscdr_test.go @@ -30,138 +30,63 @@ import ( var body = []byte(`{"core-uuid":"844715f9-d8a1-44d6-a4bf-358bec5e10b8","channel_data":{"state":"CS_REPORTING","direction":"inbound","state_number":"11","flags":"0=1;1=1;3=1;19=1;23=1;36=1;37=1;39=1;42=1;47=1;52=1","caps":"1=1;2=1;3=1;4=1;5=1;6=1"},"variables":{"direction":"inbound","uuid":"01df56f4-d99a-4ef6-b7fe-b924b2415b7f","session_id":"33","sip_from_user":"dan","sip_from_uri":"dan@ipbx.itsyscom.com","sip_from_host":"ipbx.itsyscom.com","channel_name":"sofia/ipbxas/dan@ipbx.itsyscom.com","sip_local_network_addr":"127.0.0.1","sip_network_ip":"2.3.4.5","sip_network_port":"5060","sip_received_ip":"2.3.4.5","sip_received_port":"5060","sip_via_protocol":"udp","sip_from_user_stripped":"dan","sofia_profile_name":"ipbxas","recovery_profile_name":"ipbxas","sip_invite_record_route":"","sip_req_user":"+4986517174963","sip_req_port":"5080","sip_req_uri":"+4986517174963@127.0.0.1:5080","sip_req_host":"127.0.0.1","sip_to_user":"+4986517174963","sip_to_uri":"+4986517174963@ipbx.itsyscom.com","sip_to_host":"ipbx.itsyscom.com","sip_contact_params":"alias=1.2.3.4~5060~1;transport=udp;registering_acc=ipbx_itsyscom_com","sip_contact_user":"dan","sip_contact_port":"5060","sip_contact_uri":"dan@10.10.10.154:5060","sip_contact_host":"10.10.10.154","sip_user_agent":"Jitsi2.2.4603.9615Linux","sip_via_host":"2.3.4.5","presence_id":"dan@ipbx.itsyscom.com","sip_h_X-AuthType":"SUA","sip_h_X-AuthUser":"dan","sip_h_X-AuthDomain":"ipbx.itsyscom.com","sip_h_X-BalancerIP":"2.3.4.5","switch_r_sdp":"v=0\r\no=dan 0 0 IN IP4 10.10.10.154\r\ns=-\r\nc=IN IP4 10.10.10.154\r\nt=0 0\r\nm=audio 5004 RTP/AVP 96 8 0\r\na=rtpmap:96 opus/48000\r\na=fmtp:96 usedtx=1\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:0 PCMU/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:csrc-audio-level\r\nm=video 5006 RTP/AVP 97 99\r\na=rtpmap:97 H264/90000\r\na=fmtp:97 profile-level-id=4DE01f;packetization-mode=1\r\na=rtpmap:99 H264/90000\r\na=fmtp:99 profile-level-id=4DE01f\r\na=recvonly\r\na=imageattr:97 send [x=[0-640],y=[0-480]] recv [x=[0-1280],y=[0-800]]\r\na=imageattr:99 send [x=[0-640],y=[0-480]] recv [x=[0-1280],y=[0-800]]\r\n","ep_codec_string":"PCMA@8000h@20i@64000b,PCMU@8000h@20i@64000b,H264@90000h","effective_caller_id_number":"+4986517174960","hangup_after_bridge":"true","continue_on_fail":"true","cgr_tenant":"ipbx.itsyscom.com","cgr_tor":"call","cgr_account":"dan","cgr_subject":"dan","cgr_destination":"+4986517174963","sip_redirect_contact_0":";q=1","sip_redirected_to":";q=1","sip_redirect_contact_user_0":"dan","sip_redirect_contact_host_0":"10.10.10.141","sip_redirect_contact_params_0":"alias=1.2.3.4~3072~1;line=x81npwse;rcv=sip:1.2.3.4:3072","sip_redirect_dialstring_0":"sofia/ipbxas/sip:dan@10.10.10.141:3072;alias=1.2.3.4~3072~1;line=x81npwse;rcv=sip:1.2.3.4:3072","sip_redirect_contact_1":"","sip_redirect_contact_user_1":"dan","sip_redirect_contact_host_1":"10.10.10.154","sip_redirect_contact_params_1":"alias=1.2.3.4~5060~1;transport=udp;registering_acc=ipbx_itsyscom_com;rcv=sip:1.2.3.4:5060","sip_redirect_dialstring_1":"sofia/ipbxas/sip:dan@10.10.10.154:5060;alias=1.2.3.4~5060~1;transport=udp;registering_acc=ipbx_itsyscom_com;rcv=sip:1.2.3.4:5060","sip_redirect_dialstring":"sofia/ipbxas/sip:dan@10.10.10.141:3072;alias=1.2.3.4~3072~1;line=x81npwse;rcv=sip:1.2.3.4:3072,sofia/ipbxas/sip:dan@10.10.10.154:5060;alias=1.2.3.4~5060~1;transport=udp;registering_acc=ipbx_itsyscom_com;rcv=sip:1.2.3.4:5060","max_forwards":"15","transfer_history":"1375609854:d2300128-6724-471c-a495-a3f7a985a2b6:bl_xfer:dan/redirected/XML","transfer_source":"1375609854:d2300128-6724-471c-a495-a3f7a985a2b6:bl_xfer:dan/redirected/XML","call_uuid":"01df56f4-d99a-4ef6-b7fe-b924b2415b7f","current_application_data":"sofia/ipbxas/sip:dan@10.10.10.154:5060;alias=1.2.3.4~5060~1;transport=udp;registering_acc=ipbx_itsyscom_com;rcv=sip:1.2.3.4:5060;fs_path=sip:2.3.4.5,sofia/ipbxas/sip:dan@10.10.10.141:3072;alias=1.2.3.4~3072~1;line=x81npwse;rcv=sip:1.2.3.4:3072;fs_path=sip:2.3.4.5","current_application":"bridge","originated_legs":"ARRAY::e377c077-0f1f-4b7d-b036-1aeef13eff32;Outbound Call;dan|:8f7c860f-0619-4d3c-9515-cc23b0fa3997;Outbound Call;dan","switch_m_sdp":"v=0\r\no=root 975388641 975388642 IN IP4 10.10.10.141\r\ns=call\r\nc=IN IP4 10.10.10.141\r\nt=0 0\r\nm=audio 59976 RTP/AVP 8 0 9 3 101\r\na=rtpmap:8 pcma/8000\r\na=rtpmap:0 pcmu/8000\r\na=rtpmap:9 g722/8000\r\na=rtpmap:3 gsm/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-16\r\na=ptime:20\r\nm=video 0 RTP/AVP 98 99\r\na=rtpmap:98 H264/90000\r\na=fmtp:98 profile-level-id=4DE01f\r\na=rtpmap:99 H264/90000\r\na=fmtp:99 profile-level-id=4DE01f\r\n","rtp_use_codec_string":"G722,G722.1,G729,PCMU,PCMA,GSM,H264","sip_audio_recv_pt":"0","sip_use_codec_name":"PCMU","sip_use_codec_rate":"8000","sip_use_codec_ptime":"20","read_codec":"PCMU","read_rate":"8000","write_codec":"PCMU","write_rate":"8000","dtmf_type":"info","video_possible":"true","remote_video_ip":"10.10.10.154","remote_video_port":"5006","sip_video_fmtp":"profile-level-id=4DE01f;packetization-mode=1","sip_video_pt":"97","sip_video_recv_pt":"97","video_read_codec":"H264","video_read_rate":"90000","video_write_codec":"H264","video_write_rate":"90000","sip_use_video_codec_name":"H264","sip_use_video_codec_fmtp":"profile-level-id=4DE01f;packetization-mode=1","sip_use_video_codec_rate":"90000","sip_use_video_codec_ptime":"0","local_media_ip":"2.3.4.5","local_media_port":"29452","advertised_media_ip":"2.3.4.5","sip_use_pt":"0","rtp_use_ssrc":"1408273224","local_video_ip":"2.3.4.5","local_video_port":"22648","sip_use_video_pt":"97","rtp_use_video_ssrc":"1408273224","sip_local_sdp_str":"v=0\no=iPBXCell 1375580404 1375580405 IN IP4 2.3.4.5\ns=iPBXCell\nc=IN IP4 2.3.4.5\nt=0 0\nm=audio 29452 RTP/AVP 0\na=rtpmap:0 PCMU/8000\na=silenceSupp:off - - - -\na=ptime:20\na=sendrecv\nm=video 22648 RTP/AVP 97\na=rtpmap:97 H264/90000\n","endpoint_disposition":"ANSWER","originate_disposition":"SUCCESS","DIALSTATUS":"SUCCESS","originate_causes":"ARRAY::e377c077-0f1f-4b7d-b036-1aeef13eff32;LOSE_RACE|:8f7c860f-0619-4d3c-9515-cc23b0fa3997;NONE","last_bridge_to":"8f7c860f-0619-4d3c-9515-cc23b0fa3997","bridge_channel":"sofia/ipbxas/sip:dan@10.10.10.141:3072","bridge_uuid":"8f7c860f-0619-4d3c-9515-cc23b0fa3997","signal_bond":"8f7c860f-0619-4d3c-9515-cc23b0fa3997","sip_to_tag":"4X345vQvQyetD","sip_from_tag":"9f90cc40","sip_cseq":"2","sip_call_id":"ca9c5e20caeaa6596be8cf66261f13e5@0:0:0:0:0:0:0:0","sip_full_via":"SIP/2.0/UDP 2.3.4.5;branch=z9hG4bKcydzigwkX,SIP/2.0/UDP 10.10.10.154:5060;rport=5060;received=1.2.3.4;branch=z9hG4bK-313937-b78ce2a1daafe532fc34b1b3735727ac","sip_from_display":"dan","sip_full_from":"\"dan\" ;tag=9f90cc40","sip_full_to":";tag=4X345vQvQyetD","last_sent_callee_id_name":"Outbound Call","last_sent_callee_id_number":"dan","remote_media_ip_reported":"10.10.10.154","remote_media_ip":"1.2.3.4","remote_media_port_reported":"5004","remote_media_port":"5004","rtp_auto_adjust":"true","sip_hangup_phrase":"OK","last_bridge_hangup_cause":"NORMAL_CLEARING","last_bridge_proto_specific_hangup_cause":"sip:200","bridge_hangup_cause":"NORMAL_CLEARING","hangup_cause":"NORMAL_CLEARING","hangup_cause_q850":"16","digits_dialed":"none","start_stamp":"2013-08-04 11:50:54","profile_start_stamp":"2013-08-04 11:50:54","answer_stamp":"2013-08-04 11:50:56","bridge_stamp":"2013-08-04 11:50:56","progress_stamp":"2013-08-04 11:50:54","progress_media_stamp":"2013-08-04 11:50:56","end_stamp":"2013-08-04 11:51:00","start_epoch":"1375609854","start_uepoch":"1375609854385581","profile_start_epoch":"1375609854","profile_start_uepoch":"1375609854385581","answer_epoch":"1375609856","answer_uepoch":"1375609856285587","bridge_epoch":"1375609856","bridge_uepoch":"1375609856285587","last_hold_epoch":"0","last_hold_uepoch":"0","hold_accum_seconds":"0","hold_accum_usec":"0","hold_accum_ms":"0","resurrect_epoch":"0","resurrect_uepoch":"0","progress_epoch":"1375609854","progress_uepoch":"1375609854505584","progress_media_epoch":"1375609856","progress_media_uepoch":"1375609856285587","end_epoch":"1375609860","end_uepoch":"1375609860205563","last_app":"bridge","last_arg":"sofia/ipbxas/sip:dan@10.10.10.154:5060;alias=1.2.3.4~5060~1;transport=udp;registering_acc=ipbx_itsyscom_com;rcv=sip:1.2.3.4:5060;fs_path=sip:2.3.4.5,sofia/ipbxas/sip:dan@10.10.10.141:3072;alias=1.2.3.4~3072~1;line=x81npwse;rcv=sip:1.2.3.4:3072;fs_path=sip:2.3.4.5","caller_id":"\"dan\" ","duration":"6","billsec":"4","progresssec":"0","answersec":"2","waitsec":"2","progress_mediasec":"2","flow_billsec":"6","mduration":"5820","billmsec":"3920","progressmsec":"120","answermsec":"1900","waitmsec":"1900","progress_mediamsec":"1900","flow_billmsec":"5820","uduration":"5819982","billusec":"3919976","progressusec":"120003","answerusec":"1900006","waitusec":"1900006","progress_mediausec":"1900006","flow_billusec":"5819982","sip_hangup_disposition":"send_bye","rtp_audio_in_raw_bytes":"32968","rtp_audio_in_media_bytes":"32960","rtp_audio_in_packet_count":"207","rtp_audio_in_media_packet_count":"205","rtp_audio_in_skip_packet_count":"6","rtp_audio_in_jb_packet_count":"0","rtp_audio_in_dtmf_packet_count":"0","rtp_audio_in_cng_packet_count":"0","rtp_audio_in_flush_packet_count":"2","rtp_audio_in_largest_jb_size":"0","rtp_audio_out_raw_bytes":"31648","rtp_audio_out_media_bytes":"31648","rtp_audio_out_packet_count":"184","rtp_audio_out_media_packet_count":"184","rtp_audio_out_skip_packet_count":"0","rtp_audio_out_dtmf_packet_count":"0","rtp_audio_out_cng_packet_count":"0","rtp_audio_rtcp_packet_count":"0","rtp_audio_rtcp_octet_count":"0","rtp_video_in_raw_bytes":"0","rtp_video_in_media_bytes":"0","rtp_video_in_packet_count":"0","rtp_video_in_media_packet_count":"0","rtp_video_in_skip_packet_count":"0","rtp_video_in_jb_packet_count":"0","rtp_video_in_dtmf_packet_count":"0","rtp_video_in_cng_packet_count":"0","rtp_video_in_flush_packet_count":"0","rtp_video_in_largest_jb_size":"0","rtp_video_out_raw_bytes":"0","rtp_video_out_media_bytes":"0","rtp_video_out_packet_count":"0","rtp_video_out_media_packet_count":"0","rtp_video_out_skip_packet_count":"0","rtp_video_out_dtmf_packet_count":"0","rtp_video_out_cng_packet_count":"0","rtp_video_rtcp_packet_count":"0","rtp_video_rtcp_octet_count":"0"},"app_log":{"applications":[{"app_name":"set","app_data":"effective_caller_id_number=+4986517174960"},{"app_name":"set","app_data":"hangup_after_bridge=true"},{"app_name":"set","app_data":"continue_on_fail=true"},{"app_name":"set","app_data":"cgr_tenant=ipbx.itsyscom.com"},{"app_name":"set","app_data":"cgr_tor=call"},{"app_name":"set","app_data":"cgr_account=dan"},{"app_name":"set","app_data":"cgr_subject=dan"},{"app_name":"set","app_data":"cgr_destination=+4986517174963"},{"app_name":"bridge","app_data":"{presence_id=dan@ipbx.itsyscom.com,sip_redirect_fork=true}sofia/ipbxas/dan@ipbx.itsyscom.com;fs_path=sip:2.3.4.5"},{"app_name":"bridge","app_data":"sofia/ipbxas/sip:dan@10.10.10.154:5060;alias=1.2.3.4~5060~1;transport=udp;registering_acc=ipbx_itsyscom_com;rcv=sip:1.2.3.4:5060;fs_path=sip:2.3.4.5,sofia/ipbxas/sip:dan@10.10.10.141:3072;alias=1.2.3.4~3072~1;line=x81npwse;rcv=sip:1.2.3.4:3072;fs_path=sip:2.3.4.5"}]},"callflow":{"dialplan":"XML","profile_index":"2","extension":{"name":"Redirected call","number":"dan","applications":[{"app_name":"bridge","app_data":"sofia/ipbxas/sip:dan@10.10.10.154:5060;alias=1.2.3.4~5060~1;transport=udp;registering_acc=ipbx_itsyscom_com;rcv=sip:1.2.3.4:5060;fs_path=sip:2.3.4.5,sofia/ipbxas/sip:dan@10.10.10.141:3072;alias=1.2.3.4~3072~1;line=x81npwse;rcv=sip:1.2.3.4:3072;fs_path=sip:2.3.4.5"}]},"caller_profile":{"username":"dan","dialplan":"XML","caller_id_name":"dan","ani":"dan","aniii":"","caller_id_number":"dan","network_addr":"2.3.4.5","rdnis":"+4986517174963","destination_number":"dan","uuid":"01df56f4-d99a-4ef6-b7fe-b924b2415b7f","source":"mod_sofia","context":"redirected","chan_name":"sofia/ipbxas/dan@ipbx.itsyscom.com","originatee":{"originatee_caller_profiles":[{"username":"dan","dialplan":"XML","caller_id_name":"dan","ani":"dan","aniii":"","caller_id_number":"+4986517174960","network_addr":"2.3.4.5","rdnis":"+4986517174963","destination_number":"dan","uuid":"8f7c860f-0619-4d3c-9515-cc23b0fa3997","source":"mod_sofia","context":"redirected","chan_name":"sofia/ipbxas/sip:dan@10.10.10.141:3072"}]}},"times":{"created_time":"1375609854385581","profile_created_time":"1375609854385581","progress_time":"1375609854505584","progress_media_time":"1375609856285587","answered_time":"1375609856285587","hangup_time":"1375609860205563","resurrect_time":"0","transfer_time":"0"}},"callflow":{"dialplan":"XML","profile_index":"1","extension":{"name":"OnNet Call","number":"+4986517174963","applications":[{"app_name":"set","app_data":"effective_caller_id_number=+4986517174960"},{"app_name":"set","app_data":"hangup_after_bridge=true"},{"app_name":"set","app_data":"continue_on_fail=true"},{"app_name":"set","app_data":"cgr_tenant=ipbx.itsyscom.com"},{"app_name":"set","app_data":"cgr_tor=call"},{"app_name":"set","app_data":"cgr_account=dan"},{"app_name":"set","app_data":"cgr_subject=dan"},{"app_name":"set","app_data":"cgr_destination=+4986517174963"},{"app_name":"bridge","app_data":"{presence_id=dan@ipbx.itsyscom.com,sip_redirect_fork=true}sofia/ipbxas/dan@ipbx.itsyscom.com;fs_path=sip:2.3.4.5"}]},"caller_profile":{"username":"dan","dialplan":"XML","caller_id_name":"dan","ani":"dan","aniii":"","caller_id_number":"dan","network_addr":"2.3.4.5","rdnis":"","destination_number":"+4986517174963","uuid":"01df56f4-d99a-4ef6-b7fe-b924b2415b7f","source":"mod_sofia","context":"ipbxas","chan_name":"sofia/ipbxas/dan@ipbx.itsyscom.com"},"times":{"created_time":"1375609854385581","profile_created_time":"1375609854385581","progress_time":"0","progress_media_time":"0","answered_time":"0","hangup_time":"0","resurrect_time":"0","transfer_time":"1375609854385581"}}}`) +func TestFsCdrInterfaces(t *testing.T) { + var _ utils.RawCdr = new(FSCdr) +} + func TestFirstNonEmpty(t *testing.T) { - fsCdr, err := new(FSCdr).New(body) + fsCdr, err := NewFSCdr(body) if err != nil { t.Errorf("Error loading cdr: %v", err) } - fsc := fsCdr.(FSCdr) - if _, ok := fsc.vars["cgr_destination"]; !ok { + //fsc := fsCdr.(FSCdr) + if _, ok := fsCdr.vars["cgr_destination"]; !ok { t.Error("Error parsing cdr: ", fsCdr) } } func TestCDRFields(t *testing.T) { cfg, _ = config.NewDefaultCGRConfig() - fsCdr, err := new(FSCdr).New(body) + cfg.CDRSExtraFields = []*utils.RSRField{&utils.RSRField{Id: "sip_user_agent"}} + fsCdr, err := NewFSCdr(body) if err != nil { t.Errorf("Error loading cdr: %v", err) } - setupTime, _ := fsCdr.GetSetupTime() - if fsCdr.GetCgrId() != utils.Sha1("01df56f4-d99a-4ef6-b7fe-b924b2415b7f", setupTime.String()) { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetAccId() != "01df56f4-d99a-4ef6-b7fe-b924b2415b7f" { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetCdrHost() != "127.0.0.1" { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetDirection() != "*out" { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetSubject() != "dan" { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetAccount() != "dan" { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetDestination() != "+4986517174963" { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetCategory() != "call" { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetTenant() != "ipbx.itsyscom.com" { - t.Error("Error parsing cdr: ", fsCdr) - } - if fsCdr.GetReqType() != utils.RATED { - t.Error("Error parsing cdr: ", fsCdr) - } - expectedSTime := time.Date(2013, 8, 4, 9, 50, 54, 0, time.UTC) - if setupTime.UTC() != expectedSTime { - t.Error("Error parsing setupTime: ", setupTime.UTC()) - } - answerTime, _ := fsCdr.GetAnswerTime() - expectedATime := time.Date(2013, 8, 4, 9, 50, 56, 0, time.UTC) - if answerTime.UTC() != expectedATime { - t.Error("Error parsing answerTime: ", answerTime.UTC()) - } - dur, _ := fsCdr.GetDuration() - if dur != 4000000000 { - t.Error("Error parsing duration: ", dur) - } - cfg.CDRSExtraFields = []*utils.RSRField{&utils.RSRField{Id: "sip_user_agent"}, &utils.RSRField{Id: "read_codec"}, &utils.RSRField{Id: "write_codec"}} - extraFields := fsCdr.GetExtraFields() - if len(extraFields) != 3 { - t.Error("Error parsing extra fields: ", extraFields) - } - if extraFields["sip_user_agent"] != "Jitsi2.2.4603.9615Linux" { - t.Error("Error parsing extra fields: ", extraFields) - } - -} - -func TestFsCdrForkCdr(t *testing.T) { - cfg, _ = config.NewDefaultCGRConfig() - fsCdr, err := new(FSCdr).New(body) - if err != nil { - t.Errorf("Error loading cdr: %v", err) - } - rtCdrOut, err := fsCdr.ForkCdr("wholesale_run", "^"+utils.RATED, "^*out", "cgr_tenant", "cgr_tor", "cgr_account", "cgr_subject", "cgr_destination", "start_epoch", - "answer_epoch", "billsec", []string{"effective_caller_id_number"}, true) - if err != nil { - t.Error("Unexpected error received", err) - } - expctRatedCdr := &utils.StoredCdr{CgrId: utils.Sha1("01df56f4-d99a-4ef6-b7fe-b924b2415b7f", time.Date(2013, 8, 4, 9, 50, 54, 0, time.UTC).Local().String()), AccId: "01df56f4-d99a-4ef6-b7fe-b924b2415b7f", - CdrHost: "127.0.0.1", CdrSource: FS_CDR_SOURCE, ReqType: utils.RATED, - Direction: "*out", Tenant: "ipbx.itsyscom.com", Category: "call", Account: "dan", Subject: "dan", Destination: "+4986517174963", - SetupTime: time.Date(2013, 8, 4, 9, 50, 54, 0, time.UTC).Local(), - AnswerTime: time.Date(2013, 8, 4, 9, 50, 56, 0, time.UTC).Local(), Duration: time.Duration(4) * time.Second, - ExtraFields: map[string]string{"effective_caller_id_number": "+4986517174960"}, MediationRunId: "wholesale_run", Cost: -1} - if !reflect.DeepEqual(rtCdrOut, expctRatedCdr) { - t.Errorf("Received: %v, expected: %v", rtCdrOut, expctRatedCdr) - } - rtCdrOut2, err := fsCdr.ForkCdr("wholesale_run", "^postpaid", "^*in", "^cgrates.com", "^premium_call", "^first_account", "^first_subject", "cgr_destination", - "^2013-12-07T08:42:24Z", "^2013-12-07T08:42:26Z", "^12s", []string{"effective_caller_id_number"}, true) - if err != nil { - t.Error("Unexpected error received", err) - } - expctRatedCdr2 := &utils.StoredCdr{CgrId: utils.Sha1("01df56f4-d99a-4ef6-b7fe-b924b2415b7f", time.Date(2013, 12, 7, 8, 42, 24, 0, time.UTC).String()), AccId: "01df56f4-d99a-4ef6-b7fe-b924b2415b7f", CdrHost: "127.0.0.1", - CdrSource: FS_CDR_SOURCE, ReqType: "postpaid", - Direction: "*in", Tenant: "cgrates.com", Category: "premium_call", Account: "first_account", Subject: "first_subject", Destination: "+4986517174963", - SetupTime: time.Date(2013, 12, 7, 8, 42, 24, 0, time.UTC), - AnswerTime: time.Date(2013, 12, 7, 8, 42, 26, 0, time.UTC), Duration: time.Duration(12) * time.Second, - ExtraFields: map[string]string{"effective_caller_id_number": "+4986517174960"}, MediationRunId: "wholesale_run", Cost: -1} - if !reflect.DeepEqual(rtCdrOut2, expctRatedCdr2) { - t.Errorf("Received: %v, expected: %v", rtCdrOut2, expctRatedCdr2) - } - _, err = fsCdr.ForkCdr("wholesale_run", "dummy_header", "direction", "tenant", "tor", "account", "subject", "destination", "setup_time", "answer_time", "duration", []string{"field_extr1", "fieldextr2"}, true) - if err == nil { - t.Error("Failed to detect missing header") + setupTime, _ := utils.ParseTimeDetectLayout(fsCdr.vars[FS_SETUP_TIME]) + answerTime, _ := utils.ParseTimeDetectLayout(fsCdr.vars[FS_ANSWER_TIME]) + expctStoredCdr := &utils.StoredCdr{CgrId: utils.Sha1("01df56f4-d99a-4ef6-b7fe-b924b2415b7f", setupTime.String()), AccId: "01df56f4-d99a-4ef6-b7fe-b924b2415b7f", + CdrHost: "127.0.0.1", CdrSource: "freeswitch_json", Direction: "*out", Category: "call", ReqType: utils.RATED, Tenant: "ipbx.itsyscom.com", Account: "dan", Subject: "dan", + Destination: "+4986517174963", SetupTime: setupTime, AnswerTime: answerTime, Duration: time.Duration(4) * time.Second, + ExtraFields: map[string]string{"sip_user_agent": "Jitsi2.2.4603.9615Linux"}, Cost: -1} + if storedCdr := fsCdr.AsStoredCdr(); !reflect.DeepEqual(expctStoredCdr, storedCdr) { + t.Errorf("Expecting: %v, received: %v", expctStoredCdr, storedCdr) } } func TestSearchExtraFieldLast(t *testing.T) { - fsCdr, _ := new(FSCdr).New(body) - value := fsCdr.(FSCdr).searchExtraField("transfer_time", fsCdr.(FSCdr).body) + fsCdr, _ := NewFSCdr(body) + value := fsCdr.searchExtraField("transfer_time", fsCdr.body) if value != "1375609854385581" { t.Error("Error finding extra field: ", value) } } func TestSearchExtraField(t *testing.T) { - fsCdr, _ := new(FSCdr).New(body) - cfg.CDRSExtraFields = []*utils.RSRField{&utils.RSRField{Id: "caller_id_name"}} - extraFields := fsCdr.GetExtraFields() - if len(extraFields) != 1 || extraFields["caller_id_name"] != "dan" { + fsCdr, _ := NewFSCdr(body) + rsrSt1, _ := utils.NewRSRField("^injected_value") + rsrSt2, _ := utils.NewRSRField("^injected_hdr/injected_value") + cfg.CDRSExtraFields = []*utils.RSRField{&utils.RSRField{Id: "caller_id_name"}, rsrSt1, rsrSt2} + extraFields := fsCdr.getExtraFields() + if len(extraFields) != 3 || extraFields["caller_id_name"] != "dan" || + extraFields["injected_value"] != "injected_value" || + extraFields["injected_hdr"] != "injected_value" { t.Error("Error parsing extra fields: ", extraFields) } } func TestSearchExtraFieldInSlice(t *testing.T) { - fsCdr, _ := new(FSCdr).New(body) - value := fsCdr.(FSCdr).searchExtraField("app_data", fsCdr.(FSCdr).body) + fsCdr, _ := NewFSCdr(body) + value := fsCdr.searchExtraField("app_data", fsCdr.body) if value != "effective_caller_id_number=+4986517174960" { t.Error("Error finding extra field: ", value) } @@ -172,8 +97,8 @@ func TestSearchReplaceInExtraFields(t *testing.T) { cfg.CDRSExtraFields = []*utils.RSRField{&utils.RSRField{Id: "read_codec"}, &utils.RSRField{Id: "sip_user_agent", RSRules: []*utils.ReSearchReplace{&utils.ReSearchReplace{regexp.MustCompile(`([A-Za-z]*).+`), "$1"}}}, &utils.RSRField{Id: "write_codec"}} - fsCdr, _ := new(FSCdr).New(body) - extraFields := fsCdr.GetExtraFields() + fsCdr, _ := NewFSCdr(body) + extraFields := fsCdr.getExtraFields() if len(extraFields) != 3 { t.Error("Error parsing extra fields: ", extraFields) } @@ -226,11 +151,11 @@ extra_fields = ~effective_caller_id_number:s/(\d+)/+$1/ RSRules: []*utils.ReSearchReplace{&utils.ReSearchReplace{regexp.MustCompile(`(\d+)`), "+$1"}}}}) { t.Errorf("Unexpected value for config CdrsExtraFields: %v", cfg.CDRSExtraFields) } - fsCdr, err := new(FSCdr).New(simpleJsonCdr) + fsCdr, err := NewFSCdr(simpleJsonCdr) if err != nil { t.Error("Could not parse cdr", err.Error()) } - extraFields := fsCdr.GetExtraFields() + extraFields := fsCdr.getExtraFields() if extraFields["effective_caller_id_number"] != "+4986517174963" { t.Error("Unexpected effective_caller_id_number received", extraFields["effective_caller_id_number"]) } diff --git a/config/config.go b/config/config.go index 7e6eb3213..213a95753 100644 --- a/config/config.go +++ b/config/config.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "os" + "strconv" "strings" "time" @@ -87,36 +88,25 @@ type CGRConfig struct { RaterBalancer string // balancer address host:port BalancerEnabled bool SchedulerEnabled bool - CDRSEnabled bool // Enable CDR Server service - CDRSExtraFields []*utils.RSRField // Extra fields to store in CDRs - CDRSMediator string // Address where to reach the Mediator. Empty for disabling mediation. <""|internal> - CdreCdrFormat string // Format of the exported CDRs. - CdreMaskDestId string // Id of the destination list to be masked in CDRs - CdreMaskLength int // Number of digits to mask in the destination suffix if destination is in the MaskDestinationdsId - CdreCostShiftDigits int // Shift digits in the cost on export (eg: convert from EUR to cents) - CdreDir string // Path towards exported cdrs directory - CdreExportedFields []*utils.RSRField // List of fields in the exported CDRs - CdreFWXmlTemplate *CgrXmlCdreFwCfg // Use this configuration as export template in case of fixed fields length - CdrcEnabled bool // Enable CDR client functionality - CdrcCdrs string // Address where to reach CDR server - CdrcCdrsMethod string // Mechanism to use when posting CDRs on server - CdrcRunDelay time.Duration // Sleep interval between consecutive runs, 0 to use automation via inotify - CdrcCdrType string // CDR file format . - CdrcCdrInDir string // Absolute path towards the directory where the CDRs are stored. - CdrcCdrOutDir string // Absolute path towards the directory where processed CDRs will be moved. - CdrcSourceId string // Tag identifying the source of the CDRs within CGRS database. - CdrcAccIdField string // Accounting id field identifier. Use index number in case of .csv cdrs. - CdrcReqTypeField string // Request type field identifier. Use index number in case of .csv cdrs. - CdrcDirectionField string // Direction field identifier. Use index numbers in case of .csv cdrs. - CdrcTenantField string // Tenant field identifier. Use index numbers in case of .csv cdrs. - CdrcCategoryField string // Type of Record field identifier. Use index numbers in case of .csv cdrs. - CdrcAccountField string // Account field identifier. Use index numbers in case of .csv cdrs. - CdrcSubjectField string // Subject field identifier. Use index numbers in case of .csv CDRs. - CdrcDestinationField string // Destination field identifier. Use index numbers in case of .csv cdrs. - CdrcSetupTimeField string // Setup time field identifier. Use index numbers in case of .csv cdrs. - CdrcAnswerTimeField string // Answer time field identifier. Use index numbers in case of .csv cdrs. - CdrcDurationField string // Duration field identifier. Use index numbers in case of .csv cdrs. - CdrcExtraFields []string // Extra fields to extract, special format in case of .csv "field1:index1,field2:index2" + CDRSEnabled bool // Enable CDR Server service + CDRSExtraFields []*utils.RSRField // Extra fields to store in CDRs + CDRSMediator string // Address where to reach the Mediator. Empty for disabling mediation. <""|internal> + CdreCdrFormat string // Format of the exported CDRs. + CdreMaskDestId string // Id of the destination list to be masked in CDRs + CdreMaskLength int // Number of digits to mask in the destination suffix if destination is in the MaskDestinationdsId + CdreCostShiftDigits int // Shift digits in the cost on export (eg: convert from EUR to cents) + CdreDir string // Path towards exported cdrs directory + CdreExportedFields []*utils.RSRField // List of fields in the exported CDRs + CdreFWXmlTemplate *CgrXmlCdreFwCfg // Use this configuration as export template in case of fixed fields length + CdrcEnabled bool // Enable CDR client functionality + CdrcCdrs string // Address where to reach CDR server + CdrcCdrsMethod string // Mechanism to use when posting CDRs on server + CdrcRunDelay time.Duration // Sleep interval between consecutive runs, 0 to use automation via inotify + CdrcCdrType string // CDR file format . + CdrcCdrInDir string // Absolute path towards the directory where the CDRs are stored. + CdrcCdrOutDir string // Absolute path towards the directory where processed CDRs will be moved. + CdrcSourceId string // Tag identifying the source of the CDRs within CGRS database. + CdrcCdrFields map[string]*utils.RSRField // FieldName/RSRField format. Index number in case of .csv cdrs. SMEnabled bool SMSwitchType string SMRater string // address where to access rater. Can be internal, direct rater address or the address of a balancer @@ -188,22 +178,23 @@ func (self *CGRConfig) setDefaults() error { self.CdrcCdrs = utils.INTERNAL self.CdrcCdrsMethod = "http_cgr" self.CdrcRunDelay = time.Duration(0) - self.CdrcCdrType = "csv" + self.CdrcCdrType = utils.CSV self.CdrcCdrInDir = "/var/log/cgrates/cdrc/in" self.CdrcCdrOutDir = "/var/log/cgrates/cdrc/out" self.CdrcSourceId = "freeswitch_csv" - self.CdrcAccIdField = "0" - self.CdrcReqTypeField = "1" - self.CdrcDirectionField = "2" - self.CdrcTenantField = "3" - self.CdrcCategoryField = "4" - self.CdrcAccountField = "5" - self.CdrcSubjectField = "6" - self.CdrcDestinationField = "7" - self.CdrcSetupTimeField = "8" - self.CdrcAnswerTimeField = "9" - self.CdrcDurationField = "10" - self.CdrcExtraFields = []string{} + self.CdrcCdrFields = map[string]*utils.RSRField{ + utils.ACCID: &utils.RSRField{Id: "0"}, + utils.REQTYPE: &utils.RSRField{Id: "1"}, + utils.DIRECTION: &utils.RSRField{Id: "2"}, + utils.TENANT: &utils.RSRField{Id: "3"}, + utils.CATEGORY: &utils.RSRField{Id: "4"}, + utils.ACCOUNT: &utils.RSRField{Id: "5"}, + utils.SUBJECT: &utils.RSRField{Id: "6"}, + utils.DESTINATION: &utils.RSRField{Id: "7"}, + utils.SETUP_TIME: &utils.RSRField{Id: "8"}, + utils.ANSWER_TIME: &utils.RSRField{Id: "9"}, + utils.DURATION: &utils.RSRField{Id: "10"}, + } self.MediatorEnabled = false self.MediatorRater = "internal" self.MediatorRaterReconnects = 3 @@ -235,7 +226,7 @@ func (self *CGRConfig) setDefaults() error { &utils.RSRField{Id: utils.REQTYPE}, &utils.RSRField{Id: utils.DIRECTION}, &utils.RSRField{Id: utils.TENANT}, - &utils.RSRField{Id: utils.Category}, + &utils.RSRField{Id: utils.CATEGORY}, &utils.RSRField{Id: utils.ACCOUNT}, &utils.RSRField{Id: utils.SUBJECT}, &utils.RSRField{Id: utils.DESTINATION}, @@ -256,6 +247,13 @@ func (self *CGRConfig) checkConfigSanity() error { return errors.New("Need XmlTemplate for fixed_width cdr export") } } + if self.CdrcCdrType == utils.CSV { + for _, rsrFld := range self.CdrcCdrFields { + if _, errConv := strconv.Atoi(rsrFld.Id); errConv != nil { + return fmt.Errorf("CDR fields must be indices in case of .csv files, have instead: %s", rsrFld.Id) + } + } + } return nil } @@ -483,44 +481,22 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) { if hasOpt = c.HasOption("cdrc", "cdr_source_id"); hasOpt { cfg.CdrcSourceId, _ = c.GetString("cdrc", "cdr_source_id") } - if hasOpt = c.HasOption("cdrc", "accid_field"); hasOpt { - cfg.CdrcAccIdField, _ = c.GetString("cdrc", "accid_field") - } - if hasOpt = c.HasOption("cdrc", "reqtype_field"); hasOpt { - cfg.CdrcReqTypeField, _ = c.GetString("cdrc", "reqtype_field") - } - if hasOpt = c.HasOption("cdrc", "direction_field"); hasOpt { - cfg.CdrcDirectionField, _ = c.GetString("cdrc", "direction_field") - } - if hasOpt = c.HasOption("cdrc", "tenant_field"); hasOpt { - cfg.CdrcTenantField, _ = c.GetString("cdrc", "tenant_field") - } - if hasOpt = c.HasOption("cdrc", "tor_field"); hasOpt { - cfg.CdrcCategoryField, _ = c.GetString("cdrc", "tor_field") - } - if hasOpt = c.HasOption("cdrc", "account_field"); hasOpt { - cfg.CdrcAccountField, _ = c.GetString("cdrc", "account_field") - } - if hasOpt = c.HasOption("cdrc", "subject_field"); hasOpt { - cfg.CdrcSubjectField, _ = c.GetString("cdrc", "subject_field") - } - if hasOpt = c.HasOption("cdrc", "destination_field"); hasOpt { - cfg.CdrcDestinationField, _ = c.GetString("cdrc", "destination_field") - } - if hasOpt = c.HasOption("cdrc", "setup_time_field"); hasOpt { - cfg.CdrcSetupTimeField, _ = c.GetString("cdrc", "setup_time_field") - } - if hasOpt = c.HasOption("cdrc", "answer_time_field"); hasOpt { - cfg.CdrcAnswerTimeField, _ = c.GetString("cdrc", "answer_time_field") - } - if hasOpt = c.HasOption("cdrc", "duration_field"); hasOpt { - cfg.CdrcDurationField, _ = c.GetString("cdrc", "duration_field") - } - if hasOpt = c.HasOption("cdrc", "extra_fields"); hasOpt { - eFldsStr, _ := c.GetString("cdrc", "extra_fields") - if cfg.CdrcExtraFields, err = ConfigSlice(eFldsStr); err != nil { - return nil, err - } + // ParseCdrcCdrFields + accIdFld, _ := c.GetString("cdrc", "accid_field") + reqtypeFld, _ := c.GetString("cdrc", "reqtype_field") + directionFld, _ := c.GetString("cdrc", "direction_field") + tenantFld, _ := c.GetString("cdrc", "tenant_field") + categoryFld, _ := c.GetString("cdrc", "category_field") + acntFld, _ := c.GetString("cdrc", "account_field") + subjectFld, _ := c.GetString("cdrc", "subject_field") + destFld, _ := c.GetString("cdrc", "destination_field") + setupTimeFld, _ := c.GetString("cdrc", "setup_time_field") + answerTimeFld, _ := c.GetString("cdrc", "answer_time_field") + durFld, _ := c.GetString("cdrc", "duration_field") + extraFlds, _ := c.GetString("cdrc", "extra_fields") + if cfg.CdrcCdrFields, err = ParseCdrcCdrFields(accIdFld, reqtypeFld, directionFld, tenantFld, categoryFld, acntFld, subjectFld, destFld, + setupTimeFld, answerTimeFld, durFld, extraFlds); err != nil { + return nil, err } if hasOpt = c.HasOption("mediator", "enabled"); hasOpt { cfg.MediatorEnabled, _ = c.GetBool("mediator", "enabled") diff --git a/config/config_test.go b/config/config_test.go index 63dfff233..7525da522 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -95,18 +95,19 @@ func TestDefaults(t *testing.T) { eCfg.CdrcCdrInDir = "/var/log/cgrates/cdrc/in" eCfg.CdrcCdrOutDir = "/var/log/cgrates/cdrc/out" eCfg.CdrcSourceId = "freeswitch_csv" - eCfg.CdrcAccIdField = "0" - eCfg.CdrcReqTypeField = "1" - eCfg.CdrcDirectionField = "2" - eCfg.CdrcTenantField = "3" - eCfg.CdrcCategoryField = "4" - eCfg.CdrcAccountField = "5" - eCfg.CdrcSubjectField = "6" - eCfg.CdrcDestinationField = "7" - eCfg.CdrcSetupTimeField = "8" - eCfg.CdrcAnswerTimeField = "9" - eCfg.CdrcDurationField = "10" - eCfg.CdrcExtraFields = []string{} + eCfg.CdrcCdrFields = map[string]*utils.RSRField{ + utils.ACCID: &utils.RSRField{Id: "0"}, + utils.REQTYPE: &utils.RSRField{Id: "1"}, + utils.DIRECTION: &utils.RSRField{Id: "2"}, + utils.TENANT: &utils.RSRField{Id: "3"}, + utils.CATEGORY: &utils.RSRField{Id: "4"}, + utils.ACCOUNT: &utils.RSRField{Id: "5"}, + utils.SUBJECT: &utils.RSRField{Id: "6"}, + utils.DESTINATION: &utils.RSRField{Id: "7"}, + utils.SETUP_TIME: &utils.RSRField{Id: "8"}, + utils.ANSWER_TIME: &utils.RSRField{Id: "9"}, + utils.DURATION: &utils.RSRField{Id: "10"}, + } eCfg.MediatorEnabled = false eCfg.MediatorRater = "internal" eCfg.MediatorRaterReconnects = 3 @@ -138,7 +139,7 @@ func TestDefaults(t *testing.T) { &utils.RSRField{Id: utils.REQTYPE}, &utils.RSRField{Id: utils.DIRECTION}, &utils.RSRField{Id: utils.TENANT}, - &utils.RSRField{Id: utils.Category}, + &utils.RSRField{Id: utils.CATEGORY}, &utils.RSRField{Id: utils.ACCOUNT}, &utils.RSRField{Id: utils.SUBJECT}, &utils.RSRField{Id: utils.DESTINATION}, @@ -168,6 +169,16 @@ func TestSanityCheck(t *testing.T) { if err := cfg.checkConfigSanity(); err == nil { t.Error("Failed to detect fixed_width dependency on xml configuration") } + cfg.CdrcCdrFields = map[string]*utils.RSRField{utils.ACCID: &utils.RSRField{Id: "test"}} + if err := cfg.checkConfigSanity(); err == nil { + t.Error("Failed to detect improper use of CDR field names") + } + cfg = &CGRConfig{} + cfg.CdrcCdrType = utils.CSV + cfg.CdrcCdrFields = map[string]*utils.RSRField{"extra1": &utils.RSRField{Id: "test"}} + if err := cfg.checkConfigSanity(); err == nil { + t.Error("Failed to detect improper use of CDR field names") + } } // Load config from file and make sure we have all set @@ -229,18 +240,20 @@ func TestConfigFromFile(t *testing.T) { eCfg.CdrcCdrInDir = "test" eCfg.CdrcCdrOutDir = "test" eCfg.CdrcSourceId = "test" - eCfg.CdrcAccIdField = "test" - eCfg.CdrcReqTypeField = "test" - eCfg.CdrcDirectionField = "test" - eCfg.CdrcTenantField = "test" - eCfg.CdrcCategoryField = "test" - eCfg.CdrcAccountField = "test" - eCfg.CdrcSubjectField = "test" - eCfg.CdrcDestinationField = "test" - eCfg.CdrcSetupTimeField = "test" - eCfg.CdrcAnswerTimeField = "test" - eCfg.CdrcDurationField = "test" - eCfg.CdrcExtraFields = []string{"test"} + eCfg.CdrcCdrFields = map[string]*utils.RSRField{ + utils.ACCID: &utils.RSRField{Id: "test"}, + utils.REQTYPE: &utils.RSRField{Id: "test"}, + utils.DIRECTION: &utils.RSRField{Id: "test"}, + utils.TENANT: &utils.RSRField{Id: "test"}, + utils.CATEGORY: &utils.RSRField{Id: "test"}, + utils.ACCOUNT: &utils.RSRField{Id: "test"}, + utils.SUBJECT: &utils.RSRField{Id: "test"}, + utils.DESTINATION: &utils.RSRField{Id: "test"}, + utils.SETUP_TIME: &utils.RSRField{Id: "test"}, + utils.ANSWER_TIME: &utils.RSRField{Id: "test"}, + utils.DURATION: &utils.RSRField{Id: "test"}, + "test": &utils.RSRField{Id: "test"}, + } eCfg.MediatorEnabled = true eCfg.MediatorRater = "test" eCfg.MediatorRaterReconnects = 99 diff --git a/config/helpers.go b/config/helpers.go index 258c55384..850e16eac 100644 --- a/config/helpers.go +++ b/config/helpers.go @@ -21,6 +21,7 @@ package config import ( "code.google.com/p/goconf/conf" "errors" + "fmt" "strings" "github.com/cgrates/cgrates/utils" @@ -81,7 +82,7 @@ func ParseCfgDerivedCharging(c *conf.ConfigFile) (dcs utils.DerivedChargers, err if tenantFlds, err = ConfigSlice(cfgVal); err != nil { return nil, err } - cfgVal, _ = c.GetString("derived_charging", "tor_fields") + cfgVal, _ = c.GetString("derived_charging", "category_fields") if torFlds, err = ConfigSlice(cfgVal); err != nil { return nil, err } @@ -136,3 +137,38 @@ func ParseCfgDerivedCharging(c *conf.ConfigFile) (dcs utils.DerivedChargers, err } return dcs, nil } + +func ParseCdrcCdrFields(accIdFld, reqtypeFld, directionFld, tenantFld, categoryFld, acntFld, subjectFld, destFld, + setupTimeFld, answerTimeFld, durFld, extraFlds string) (map[string]*utils.RSRField, error) { + cdrcCdrFlds := make(map[string]*utils.RSRField) + if len(extraFlds) != 0 { + if sepExtraFlds, err := ConfigSlice(extraFlds); err != nil { + return nil, err + } else { + for _, fldStr := range sepExtraFlds { + // extra fields defined as: : + if spltLbl := strings.Split(fldStr, utils.CONCATENATED_KEY_SEP); len(spltLbl) != 2 { + return nil, fmt.Errorf("Wrong format for cdrc.extra_fields: %s", fldStr) + } else { + if rsrFld, err := utils.NewRSRField(spltLbl[1]); err != nil { + return nil, err + } else { + cdrcCdrFlds[spltLbl[0]] = rsrFld + } + } + } + } + } + for fldTag, fldVal := range map[string]string{utils.ACCID: accIdFld, utils.REQTYPE: reqtypeFld, utils.DIRECTION: directionFld, utils.TENANT: tenantFld, + utils.CATEGORY: categoryFld, utils.ACCOUNT: acntFld, utils.SUBJECT: subjectFld, utils.DESTINATION: destFld, utils.SETUP_TIME: setupTimeFld, + utils.ANSWER_TIME: answerTimeFld, utils.DURATION: durFld} { + if len(fldVal) != 0 { + if rsrFld, err := utils.NewRSRField(fldVal); err != nil { + return nil, err + } else { + cdrcCdrFlds[fldTag] = rsrFld + } + } + } + return cdrcCdrFlds, nil +} diff --git a/config/helpers_test.go b/config/helpers_test.go index 3a5d2c305..244727823 100644 --- a/config/helpers_test.go +++ b/config/helpers_test.go @@ -44,7 +44,7 @@ run_ids = run1, run2 reqtype_fields = test1, test2 direction_fields = test1, test2 tenant_fields = test1, test2 -tor_fields = test1, test2 +category_fields = test1, test2 account_fields = test1, test2 subject_fields = test1, test2 destination_fields = test1, test2 @@ -63,3 +63,41 @@ duration_fields = test1, test2 t.Errorf("Expecting: %v, received: %v", edcs, cfg.DerivedChargers) } } + +func TestParseCdrcCdrFields(t *testing.T) { + eFieldsCfg := []byte(`[cdrc] +cdr_type = test +accid_field = accid1 +reqtype_field = reqtype1 +direction_field = direction1 +tenant_field = tenant1 +category_field = category1 +account_field = account1 +subject_field = subject1 +destination_field = destination1 +setup_time_field = setuptime1 +answer_time_field = answertime1 +duration_field = duration1 +extra_fields = extra1:extraval1,extra2:extraval1 +`) + eCdrcCdrFlds := map[string]*utils.RSRField{ + utils.ACCID: &utils.RSRField{Id: "accid1"}, + utils.REQTYPE: &utils.RSRField{Id: "reqtype1"}, + utils.DIRECTION: &utils.RSRField{Id: "direction1"}, + utils.TENANT: &utils.RSRField{Id: "tenant1"}, + utils.CATEGORY: &utils.RSRField{Id: "category1"}, + utils.ACCOUNT: &utils.RSRField{Id: "account1"}, + utils.SUBJECT: &utils.RSRField{Id: "subject1"}, + utils.DESTINATION: &utils.RSRField{Id: "destination1"}, + utils.SETUP_TIME: &utils.RSRField{Id: "setuptime1"}, + utils.ANSWER_TIME: &utils.RSRField{Id: "answertime1"}, + utils.DURATION: &utils.RSRField{Id: "duration1"}, + "extra1": &utils.RSRField{Id: "extraval1"}, + "extra2": &utils.RSRField{Id: "extraval1"}, + } + if cfg, err := NewCGRConfigFromBytes(eFieldsCfg); err != nil { + t.Error("Could not parse the config", err.Error()) + } else if !reflect.DeepEqual(cfg.CdrcCdrFields, eCdrcCdrFlds) { + t.Errorf("Expecting: %v, received: %v", eCdrcCdrFlds, cfg.CdrcCdrFields) + } +} diff --git a/config/test_data.txt b/config/test_data.txt index 6ec72b04f..7a0108b80 100644 --- a/config/test_data.txt +++ b/config/test_data.txt @@ -14,13 +14,13 @@ accountdb_port = test # Accounting subsystem port to reach the database. accountdb_name = test # Accounting subsystem database name to connect to. accountdb_user = test # Accounting subsystem username to use when connecting to database. accountdb_passwd = test # Accounting subsystem password to use when connecting to database. -stordb_type = test # Log/stored database type to use: +stordb_type = test # Log/scategoryed database type to use: stordb_host = test # The host to connect to. Values that start with / are for UNIX domain sockets. stordb_port = test # The port to reach the logdb. stordb_name = test # The name of the log database to connect to. stordb_user = test # Username to use when connecting to logdb. stordb_passwd = test # Password to use when connecting to logdb. -dbdata_encoding = test # The encoding used to store object data in strings: +dbdata_encoding = test # The encoding used to scategorye object data in strings: rpc_json_listen = test # RPC JSON listening address rpc_gob_listen = test # RPC GOB listening address http_listen = test # HTTP listening address @@ -44,8 +44,8 @@ enabled = true # Starts Scheduler service: . [cdrs] enabled = true # Start the CDR Server service: . -extra_fields = test # Extra fields to store in CDRs -mediator = test # Address where to reach the Mediator. Empty for disabling mediation. <""|internal> +extra_fields = test # Extra fields to scategorye in CDRs +mediator = test # Address where to reach the Mediacategory. Empty for disabling mediation. <""|internal> [cdre] cdr_format = test # Exported CDRs format @@ -61,24 +61,24 @@ cdrs = test # Address where to reach CDR server cdrs_method = test # Mechanism to use when posting CDRs on server run_delay = 99 # Period to sleep between two runs, 0 to use automation via inotify cdr_type = test # CDR file format . -cdr_in_dir = test # Absolute path towards the directory where the CDRs are kept (file stored CDRs). -cdr_out_dir = test # Absolute path towards the directory where processed CDRs will be moved after processing. +cdr_in_dir = test # Absolute path towards the direccategoryy where the CDRs are kept (file scategoryed CDRs). +cdr_out_dir = test # Absolute path towards the direccategoryy where processed CDRs will be moved after processing. cdr_source_id = test # Tag identifying the source of the CDRs within CGRS database. accid_field = test # Accounting id field identifier. Use index number in case of .csv cdrs. reqtype_field = test # Request type field identifier. Use index number in case of .csv cdrs. direction_field = test # Direction field identifier. Use index numbers in case of .csv cdrs. tenant_field = test # Tenant field identifier. Use index numbers in case of .csv cdrs. -tor_field = test # Type of Record field identifier. Use index numbers in case of .csv cdrs. +category_field = test # Type of Record field identifier. Use index numbers in case of .csv cdrs. account_field = test # Account field identifier. Use index numbers in case of .csv cdrs. subject_field = test # Subject field identifier. Use index numbers in case of .csv CDRs. destination_field = test # Destination field identifier. Use index numbers in case of .csv cdrs. setup_time_field = test # Answer time field identifier. Use index numbers in case of .csv cdrs. answer_time_field = test # Answer time field identifier. Use index numbers in case of .csv cdrs. duration_field = test # Duration field identifier. Use index numbers in case of .csv cdrs. -extra_fields = test # Field identifiers of the fields to add in extra fields section, special format in case of .csv "index1:field1,index2:field2" +extra_fields = test:test # Field identifiers of the fields to add in extra fields section, special format in case of .csv "index1|field1,index2|field2" [mediator] -enabled = true # Starts Mediator service: . +enabled = true # Starts Mediacategory service: . rater = test # Address where to reach the Rater: rater_reconnects = 99 # Number of reconnects to rater before giving up. @@ -100,7 +100,7 @@ run_ids = test # Identifiers of additional sessions control. reqtype_fields = test # Name of request type fields to be used during additional sessions control <""|*default|field_name>. direction_fields = test # Name of direction fields to be used during additional sessions control <""|*default|field_name>. tenant_fields = test # Name of tenant fields to be used during additional sessions control <""|*default|field_name>. -tor_fields = test # Name of tor fields to be used during additional sessions control <""|*default|field_name>. +category_fields = test # Name of category fields to be used during additional sessions control <""|*default|field_name>. account_fields = test # Name of account fields to be used during additional sessions control <""|*default|field_name>. subject_fields = test # Name of fields to be used during additional sessions control <""|*default|field_name>. destination_fields = test # Name of destination fields to be used during additional sessions control <""|*default|field_name>. @@ -110,13 +110,13 @@ duration_fields = test # Name of duration fields to be used during additional s combined_chargers = true # Combine accounts specific derived_chargers with server configured ones . [history_server] -enabled = true # Starts History service: . -history_dir = test # Location on disk where to store history files. +enabled = true # Starts Hiscategoryy service: . +history_dir = test # Location on disk where to scategorye hiscategoryy files. save_interval = 99 # Timeout duration between saves [history_agent] -enabled = true # Starts History as a client: . -server = test # Address where to reach the master history server: +enabled = true # Starts Hiscategoryy as a client: . +server = test # Address where to reach the master hiscategoryy server: [mailer] server = test # The server to use when sending emails out diff --git a/data/conf/cgrates.cfg b/data/conf/cgrates.cfg index aa7255655..1eed2c987 100644 --- a/data/conf/cgrates.cfg +++ b/data/conf/cgrates.cfg @@ -28,7 +28,7 @@ # rpc_gob_listen = 127.0.0.1:2013 # RPC GOB listening address # http_listen = 127.0.0.1:2080 # HTTP listening address # default_reqtype = rated # Default request type to consider when missing from requests: <""|prepaid|postpaid|pseudoprepaid|rated>. -# default_tor = call # Default Type of Record to consider when missing from requests. +# default_category = call # Default Type of Record to consider when missing from requests. # default_tenant = cgrates.org # Default Tenant to consider when missing from requests. # default_subject = cgrates # Default rating Subject to consider when missing from requests. # rounding_method = *middle # Rounding method for floats/costs: <*up|*middle|*down> @@ -56,7 +56,7 @@ # mask_length = 0 # Length of the destination suffix to be masked # cost_shift_digits = 0 # Shift cost on export with the number of digits digits defined here (eg: convert from Eur to cent). # export_dir = /var/log/cgrates/cdrexport/csv # Path where the exported CDRs will be placed -# export_template = cgrid,mediation_runid,accid,cdrhost,reqtype,direction,tenant,tor,account,subject,destination,setup_time,answer_time,duration,cost +# export_template = cgrid,mediation_runid,accid,cdrhost,reqtype,direction,tenant,category,account,subject,destination,setup_time,answer_time,duration,cost # Exported fields template <""|fld1,fld2|*xml:instance_name> [cdrc] # enabled = false # Enable CDR client functionality @@ -71,7 +71,7 @@ # reqtype_field = 1 # Request type field identifier. Use index number in case of .csv cdrs. # direction_field = 2 # Direction field identifier. Use index numbers in case of .csv cdrs. # tenant_field = 3 # Tenant field identifier. Use index numbers in case of .csv cdrs. -# tor_field = 4 # Type of Record field identifier. Use index numbers in case of .csv cdrs. +# category_field = 4 # Type of Record field identifier. Use index numbers in case of .csv cdrs. # account_field = 5 # Account field identifier. Use index numbers in case of .csv cdrs. # subject_field = 6 # Subject field identifier. Use index numbers in case of .csv CDRs. # destination_field = 7 # Destination field identifier. Use index numbers in case of .csv cdrs. @@ -103,7 +103,7 @@ # reqtype_fields = # Name of request type fields to be used during additional sessions control <""|*default|field_name>. # direction_fields = # Name of direction fields to be used during additional sessions control <""|*default|field_name>. # tenant_fields = # Name of tenant fields to be used during additional sessions control <""|*default|field_name>. -# tor_fields = # Name of tor fields to be used during additional sessions control <""|*default|field_name>. +# category_fields = # Name of tor fields to be used during additional sessions control <""|*default|field_name>. # account_fields = # Name of account fields to be used during additional sessions control <""|*default|field_name>. # subject_fields = # Name of fields to be used during additional sessions control <""|*default|field_name>. # destination_fields = # Name of destination fields to be used during additional sessions control <""|*default|field_name>. diff --git a/data/storage/mysql/create_cdrs_tables.sql b/data/storage/mysql/create_cdrs_tables.sql index a37462f4d..0c7c48275 100644 --- a/data/storage/mysql/create_cdrs_tables.sql +++ b/data/storage/mysql/create_cdrs_tables.sql @@ -9,7 +9,7 @@ CREATE TABLE cdrs_primary ( reqtype varchar(24) NOT NULL, direction varchar(8) NOT NULL, tenant varchar(64) NOT NULL, - tor varchar(16) NOT NULL, + category varchar(16) NOT NULL, account varchar(128) NOT NULL, subject varchar(128) NOT NULL, destination varchar(128) NOT NULL, diff --git a/data/storage/mysql/create_costdetails_tables.sql b/data/storage/mysql/create_costdetails_tables.sql index 7b5ee0631..986949627 100644 --- a/data/storage/mysql/create_costdetails_tables.sql +++ b/data/storage/mysql/create_costdetails_tables.sql @@ -10,7 +10,7 @@ CREATE TABLE `cost_details` ( `accid` varchar(64) NOT NULL, `direction` varchar(8) NOT NULL, `tenant` varchar(128) NOT NULL, - `tor` varchar(32) NOT NULL, + `category` varchar(32) NOT NULL, `account` varchar(128) NOT NULL, `subject` varchar(128) NOT NULL, `destination` varchar(128) NOT NULL, diff --git a/data/storage/mysql/create_tariffplan_tables.sql b/data/storage/mysql/create_tariffplan_tables.sql index 9a4872938..f1d04b29c 100644 --- a/data/storage/mysql/create_tariffplan_tables.sql +++ b/data/storage/mysql/create_tariffplan_tables.sql @@ -100,7 +100,7 @@ CREATE TABLE `tp_rating_profiles` ( `tpid` varchar(64) NOT NULL, `loadid` varchar(64) NOT NULL, `tenant` varchar(64) NOT NULL, - `tor` varchar(16) NOT NULL, + `category` varchar(16) NOT NULL, `direction` varchar(8) NOT NULL, `subject` varchar(64) NOT NULL, `activation_time` varchar(24) NOT NULL, @@ -108,7 +108,7 @@ CREATE TABLE `tp_rating_profiles` ( `fallback_subjects` varchar(64), PRIMARY KEY (`tbid`), KEY `tpid_loadid` (`tpid`, `loadid`), - UNIQUE KEY `tpid_loadid_tenant_tor_dir_subj_atime` (`tpid`,`loadid`, `tenant`,`tor`,`direction`,`subject`,`activation_time`) + UNIQUE KEY `tpid_loadid_tenant_category_dir_subj_atime` (`tpid`,`loadid`, `tenant`,`category`,`direction`,`subject`,`activation_time`) ); -- @@ -215,14 +215,14 @@ CREATE TABLE `tp_account_actions` ( -- Table structure for table `tp_lcrs` -- -DROP TABLE IF EXISTS tp_lcrs; -CREATE TABLE tp_lcrs ( +DROP TABLE IF EXISTS tp_lcr_rules; +CREATE TABLE tp_lcr ( `tbid` int(11) NOT NULL AUTO_INCREMENT, `tpid` varchar(64) NOT NULL, `direction` varchar(8) NOT NULL, `tenant` varchar(64) NOT NULL, `customer` varchar(64) NOT NULL, - `destination`_id varchar(64) NOT NULL, + `destination_id` varchar(64) NOT NULL, `category` varchar(16) NOT NULL, `strategy` varchar(16) NOT NULL, `suppliers` varchar(64) NOT NULL, @@ -250,12 +250,12 @@ CREATE TABLE tp_derived_chargers ( `reqtype_field` varchar(24) NOT NULL, `direction_field` varchar(24) NOT NULL, `tenant_field` varchar(24) NOT NULL, - `tor_field` varchar(24) NOT NULL, + `category_field` varchar(24) NOT NULL, `account_field` varchar(24) NOT NULL, `subject_field` varchar(24) NOT NULL, `destination_field` varchar(24) NOT NULL, - `setup_time`_field varchar(24) NOT NULL, - `answer_time`_field varchar(24) NOT NULL, + `setup_time_field` varchar(24) NOT NULL, + `answer_time_field` varchar(24) NOT NULL, `duration_field` varchar(24) NOT NULL, PRIMARY KEY (`tbid`), KEY `tpid` (`tpid`) diff --git a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv index 9e3929114..cbae2ced0 100644 --- a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv +++ b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv @@ -1,4 +1,4 @@ -#Tag,BalanceType,Direction,ThresholdType,ThresholdValue,DestinationTag,ActionsTag,Weight +#Tag,BalanceType,Direction,ThresholdType,ThresholdValue,Recurrent,DestinationTag,ActionsTag,Weight STANDARD_TRIGGERS,*monetary,*out,*min_balance,2,false,,LOG_BALANCE,10 STANDARD_TRIGGERS,*monetary,*out,*max_balance,20,false,,LOG_BALANCE,10 STANDARD_TRIGGERS,*monetary,*out,*max_counter,15,false,FS_USERS,LOG_BALANCE,10 diff --git a/engine/loader_csv.go b/engine/loader_csv.go index fafd09d6c..96e503197 100644 --- a/engine/loader_csv.go +++ b/engine/loader_csv.go @@ -161,8 +161,10 @@ func (csvr *CSVReader) ShowStatistics() { } // actions log.Print("Actions: ", len(csvr.actions)) - // action timings + // action plans log.Print("Action plans: ", len(csvr.actionsTimings)) + // action trigers + log.Print("Action trigers: ", len(csvr.actionsTriggers)) // account actions log.Print("Account actions: ", len(csvr.accountActions)) // derivedChargers diff --git a/engine/loader_helpers.go b/engine/loader_helpers.go index 2926233b6..b463a110d 100644 --- a/engine/loader_helpers.go +++ b/engine/loader_helpers.go @@ -210,8 +210,8 @@ var FileValidators = map[string]*FileLineRegexValidator{ regexp.MustCompile(`(?:\w+\s*,\s*){3}(?:\d+\.?\d*){1}`), "Tag([0-9A-Za-z_]),ActionsTag([0-9A-Za-z_]),TimingTag([0-9A-Za-z_]),Weight([0-9.])"}, utils.ACTION_TRIGGERS_CSV: &FileLineRegexValidator{utils.ACTION_TRIGGERS_NRCOLS, - regexp.MustCompile(`(?:\w+),(?:\*\w+),(?:\*out),(?:\*\w+),(?:\d+\.?\d*),(?:\w+|\*any)?,(?:\w+),(?:\d+\.?\d*)$`), - "Tag([0-9A-Za-z_]),BalanceType(*[a-z_]),Direction(*out),ThresholdType(*[a-z_]),ThresholdValue([0-9]+),DestinationTag([0-9A-Za-z_]|*all),ActionsTag([0-9A-Za-z_]),Weight([0-9]+)"}, + regexp.MustCompile(`(?:\w+),(?:\*\w+),(?:\*out),(?:\*\w+),(?:\d+\.?\d*),(?:true|false)?,(?:\w+|\*any)?,(?:\w+),(?:\d+\.?\d*)$`), + "Tag([0-9A-Za-z_]),BalanceType(*[a-z_]),Direction(*out),ThresholdType(*[a-z_]),ThresholdValue([0-9]+),Recurrent(true|false),DestinationTag([0-9A-Za-z_]|*all),ActionsTag([0-9A-Za-z_]),Weight([0-9]+)"}, utils.ACCOUNT_ACTIONS_CSV: &FileLineRegexValidator{utils.ACCOUNT_ACTIONS_NRCOLS, regexp.MustCompile(`(?:\w+\s*),(?:(\w+;?)+\s*),(?:\*out\s*),(?:\w+\s*),(?:\w+\s*)$`), "Tenant([0-9A-Za-z_]),Account([0-9A-Za-z_.]),Direction(*out),ActionTimingsTag([0-9A-Za-z_]),ActionTriggersTag([0-9A-Za-z_])"}, diff --git a/engine/loader_helpers_test.go b/engine/loader_helpers_test.go index c7884c7fe..41bc0e580 100644 --- a/engine/loader_helpers_test.go +++ b/engine/loader_helpers_test.go @@ -73,9 +73,9 @@ DUMMY,INVALID;DATA ` var actionTriggersSample = `#Tag,BalanceType,Direction,ThresholdType,ThresholdValue,DestinationTag,ActionsTag,Weight -STANDARD_TRIGGERS,*monetary,*out,*min_balance,2,,LOG_BALANCE,10 -STANDARD_TRIGGERS,*monetary,*out,*max_balance,20,,LOG_BALANCE,10 -STANDARD_TRIGGERS,*monetary,*out,*max_counter,15,FS_USERS,LOG_BALANCE,10 +STANDARD_TRIGGERS,*monetary,*out,*min_balance,2,false,,LOG_BALANCE,10 +STANDARD_TRIGGERS,*monetary,*out,*max_balance,20,false,,LOG_BALANCE,10 +STANDARD_TRIGGERS,*monetary,*out,*max_counter,15,false,FS_USERS,LOG_BALANCE,10 DUMMY,INVALID;DATA ` diff --git a/engine/loader_local_test.go b/engine/loader_local_test.go index 98cc5da5b..220754699 100644 --- a/engine/loader_local_test.go +++ b/engine/loader_local_test.go @@ -20,6 +20,7 @@ package engine import ( "flag" + "fmt" "path" "testing" @@ -223,6 +224,7 @@ func TestLoadFromStorDb(t *testing.T) { t.Error("Failed loading action triggers: ", err.Error()) } if err := loader.LoadAccountActions(); err != nil { + fmt.Printf("Have actionTriggers loaded :%v\n", loader.actionsTriggers) t.Error("Failed loading account actions: ", err.Error()) } if err := loader.WriteToDatabase(true, false); err != nil { diff --git a/engine/storage_interface.go b/engine/storage_interface.go index 048885a52..8de3fd905 100644 --- a/engine/storage_interface.go +++ b/engine/storage_interface.go @@ -110,7 +110,7 @@ type AccountingStorage interface { type CdrStorage interface { Storage - SetCdr(utils.RawCDR) error + SetCdr(*utils.StoredCdr) error SetRatedCdr(*utils.StoredCdr, string) error GetStoredCdrs([]string, []string, []string, []string, []string, []string, []string, []string, []string, []string, []string, int64, int64, time.Time, time.Time, bool, bool) ([]*utils.StoredCdr, error) diff --git a/engine/storage_sql.go b/engine/storage_sql.go index 7745fd625..5cdade5b3 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -128,7 +128,7 @@ func (self *SQLStorage) RemTPData(table, tpid string, args ...string) error { q := fmt.Sprintf("DELETE FROM %s WHERE tpid='%s' AND id='%s'", table, tpid, args[0]) switch table { case utils.TBL_TP_RATE_PROFILES: - q = fmt.Sprintf("DELETE FROM %s WHERE tpid='%s' AND loadid='%s' AND tenant='%s' AND tor='%s' AND direction='%s' AND subject='%s'", + q = fmt.Sprintf("DELETE FROM %s WHERE tpid='%s' AND loadid='%s' AND tenant='%s' AND category='%s' AND direction='%s' AND subject='%s'", table, tpid, args[0], args[1], args[2], args[3], args[4]) case utils.TBL_TP_ACCOUNT_ACTIONS: q = fmt.Sprintf("DELETE FROM %s WHERE tpid='%s' AND loadid='%s' AND tenant='%s' AND account='%s' AND direction='%s'", @@ -261,7 +261,7 @@ func (self *SQLStorage) SetTPRatingProfiles(tpid string, rps map[string]*utils.T return nil //Nothing to set } var buffer bytes.Buffer - buffer.WriteString(fmt.Sprintf("INSERT INTO %s (tpid,loadid,tenant,tor,direction,subject,activation_time,rating_plan_id,fallback_subjects) VALUES ", + buffer.WriteString(fmt.Sprintf("INSERT INTO %s (tpid,loadid,tenant,category,direction,subject,activation_time,rating_plan_id,fallback_subjects) VALUES ", utils.TBL_TP_RATE_PROFILES)) i := 0 for _, rp := range rps { @@ -442,7 +442,7 @@ func (self *SQLStorage) SetTPActionTriggers(tpid string, ats map[string][]*utils return nil //Nothing to set } var buffer bytes.Buffer - buffer.WriteString(fmt.Sprintf("INSERT INTO %s (tpid,id,balance_type,direction,threshold_type,threshold_value,destination_id,actions_id,weight) VALUES ", + buffer.WriteString(fmt.Sprintf("INSERT INTO %s (tpid,id,balance_type,direction,threshold_type,threshold_value,recurrent,destination_id,actions_id,weight) VALUES ", utils.TBL_TP_ACTION_TRIGGERS)) i := 0 for atId, atRows := range ats { @@ -450,13 +450,13 @@ func (self *SQLStorage) SetTPActionTriggers(tpid string, ats map[string][]*utils if i != 0 { //Consecutive values after the first will be prefixed with "," as separator buffer.WriteRune(',') } - buffer.WriteString(fmt.Sprintf("('%s','%s','%s','%s','%s', %f, '%s','%s',%f)", + buffer.WriteString(fmt.Sprintf("('%s','%s','%s','%s','%s', %f, %t, '%s','%s',%f)", tpid, atId, atsRow.BalanceType, atsRow.Direction, atsRow.ThresholdType, - atsRow.ThresholdValue, atsRow.DestinationId, atsRow.ActionsId, atsRow.Weight)) + atsRow.ThresholdValue, atsRow.Recurrent, atsRow.DestinationId, atsRow.ActionsId, atsRow.Weight)) i++ } } - buffer.WriteString(" ON DUPLICATE KEY UPDATE weight=values(weight)") + buffer.WriteString(" ON DUPLICATE KEY UPDATE recurrent=values(recurrent), weight=values(weight)") if _, err := self.Db.Exec(buffer.String()); err != nil { return err } @@ -496,7 +496,7 @@ func (self *SQLStorage) LogCallCost(cgrid, source, runid string, cc *CallCost) ( if err != nil { Logger.Err(fmt.Sprintf("Error marshalling timespans to json: %v", err)) } - _, err = self.Db.Exec(fmt.Sprintf("INSERT INTO %s (cgrid, direction, tenant, tor, account, subject, destination, cost, timespans, source, runid, cost_time)VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', %f, '%s','%s','%s',now()) ON DUPLICATE KEY UPDATE direction=values(direction), tenant=values(tenant), tor=values(tor), account=values(account), subject=values(subject), destination=values(destination), cost=values(cost), timespans=values(timespans), source=values(source), cost_time=now()", + _, err = self.Db.Exec(fmt.Sprintf("INSERT INTO %s (cgrid, direction, tenant, category, account, subject, destination, cost, timespans, source, runid, cost_time) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', %f, '%s','%s','%s',now()) ON DUPLICATE KEY UPDATE direction=values(direction), tenant=values(tenant), category=values(category), account=values(account), subject=values(subject), destination=values(destination), cost=values(cost), timespans=values(timespans), source=values(source), cost_time=now()", utils.TBL_COST_DETAILS, cgrid, cc.Direction, @@ -516,7 +516,7 @@ func (self *SQLStorage) LogCallCost(cgrid, source, runid string, cc *CallCost) ( } func (self *SQLStorage) GetCallCostLog(cgrid, source, runid string) (cc *CallCost, err error) { - qry := fmt.Sprintf("SELECT cgrid, direction, tenant, tor, account, subject, destination, cost, timespans, source FROM %s WHERE cgrid='%s' AND runid='%s'", + qry := fmt.Sprintf("SELECT cgrid, direction, tenant, category, account, subject, destination, cost, timespans, source FROM %s WHERE cgrid='%s' AND runid='%s'", utils.TBL_COST_DETAILS, cgrid, runid) if len(source) != 0 { qry += fmt.Sprintf(" AND source='%s'", source) @@ -544,38 +544,34 @@ func (self *SQLStorage) LogActionTiming(source string, at *ActionTiming, as Acti } func (self *SQLStorage) LogError(uuid, source, runid, errstr string) (err error) { return } -func (self *SQLStorage) SetCdr(cdr utils.RawCDR) (err error) { - // map[account:1001 direction:out orig_ip:172.16.1.1 tor:call accid:accid23 answer_time:2013-02-03 19:54:00 cdrsource:freeswitch_csv destination:+4986517174963 duration:62 reqtype:prepaid subject:1001 supplier:supplier1 tenant:cgrates.org] - setupTime, _ := cdr.GetSetupTime() // Ignore errors, we want to store the cdr no matter what - answerTime, _ := cdr.GetAnswerTime() // Ignore errors, we want to store the cdr no matter what - dur, _ := cdr.GetDuration() +func (self *SQLStorage) SetCdr(cdr *utils.StoredCdr) (err error) { _, err = self.Db.Exec(fmt.Sprintf("INSERT INTO %s VALUES (NULL,'%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s', %d)", utils.TBL_CDRS_PRIMARY, - cdr.GetCgrId(), - cdr.GetAccId(), - cdr.GetCdrHost(), - cdr.GetCdrSource(), - cdr.GetReqType(), - cdr.GetDirection(), - cdr.GetTenant(), - cdr.GetCategory(), - cdr.GetAccount(), - cdr.GetSubject(), - cdr.GetDestination(), - setupTime, - answerTime, - dur, + cdr.CgrId, + cdr.AccId, + cdr.CdrHost, + cdr.CdrSource, + cdr.ReqType, + cdr.Direction, + cdr.Tenant, + cdr.Category, + cdr.Account, + cdr.Subject, + cdr.Destination, + cdr.SetupTime, + cdr.AnswerTime, + cdr.Duration, )) if err != nil { Logger.Err(fmt.Sprintf("failed to execute cdr insert statement: %v", err)) } - extraFields, err := json.Marshal(cdr.GetExtraFields()) + extraFields, err := json.Marshal(cdr.ExtraFields) if err != nil { Logger.Err(fmt.Sprintf("Error marshalling cdr extra fields to json: %v", err)) } _, err = self.Db.Exec(fmt.Sprintf("INSERT INTO %s VALUES ('NULL','%s', '%s')", utils.TBL_CDRS_EXTRA, - cdr.GetCgrId(), + cdr.CgrId, extraFields, )) if err != nil { @@ -602,10 +598,10 @@ func (self *SQLStorage) SetRatedCdr(storedCdr *utils.StoredCdr, extraInfo string // Return a slice of CDRs from storDb using optional filters.a // ignoreErr - do not consider cdrs with rating errors // ignoreRated - do not consider cdrs which were already rated, including here the ones with errors -func (self *SQLStorage) GetStoredCdrs(cgrIds, runIds, cdrHosts, cdrSources, reqTypes, directions, tenants, tors, accounts, subjects, destPrefixes []string, orderIdStart, orderIdEnd int64, +func (self *SQLStorage) GetStoredCdrs(cgrIds, runIds, cdrHosts, cdrSources, reqTypes, directions, tenants, categories, accounts, subjects, destPrefixes []string, orderIdStart, orderIdEnd int64, timeStart, timeEnd time.Time, ignoreErr, ignoreRated bool) ([]*utils.StoredCdr, error) { var cdrs []*utils.StoredCdr - q := bytes.NewBufferString(fmt.Sprintf("SELECT %s.cgrid,%s.tbid,accid,cdrhost,cdrsource,reqtype,direction,tenant,tor,account,%s.subject,destination,setup_time,answer_time,duration,extra_fields,runid,cost FROM %s LEFT JOIN %s ON %s.cgrid=%s.cgrid LEFT JOIN %s ON %s.cgrid=%s.cgrid", utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_EXTRA, utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_EXTRA, utils.TBL_RATED_CDRS, utils.TBL_CDRS_PRIMARY, utils.TBL_RATED_CDRS)) + q := bytes.NewBufferString(fmt.Sprintf("SELECT %s.cgrid,%s.tbid,accid,cdrhost,cdrsource,reqtype,direction,tenant,category,account,%s.subject,destination,setup_time,answer_time,duration,extra_fields,runid,cost FROM %s LEFT JOIN %s ON %s.cgrid=%s.cgrid LEFT JOIN %s ON %s.cgrid=%s.cgrid", utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_EXTRA, utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_EXTRA, utils.TBL_RATED_CDRS, utils.TBL_CDRS_PRIMARY, utils.TBL_RATED_CDRS)) fltr := new(bytes.Buffer) if len(cgrIds) != 0 { qIds := bytes.NewBufferString(" (") @@ -705,13 +701,13 @@ func (self *SQLStorage) GetStoredCdrs(cgrIds, runIds, cdrHosts, cdrSources, reqT } fltr.Write(qIds.Bytes()) } - if len(tors) != 0 { + if len(categories) != 0 { qIds := bytes.NewBufferString(" (") - for idx, tor := range tors { + for idx, category := range categories { if idx != 0 { qIds.WriteString(" OR") } - qIds.WriteString(fmt.Sprintf(" tor='%s'", tor)) + qIds.WriteString(fmt.Sprintf(" category='%s'", category)) } qIds.WriteString(" )") if fltr.Len() != 0 { @@ -809,14 +805,14 @@ func (self *SQLStorage) GetStoredCdrs(cgrIds, runIds, cdrHosts, cdrSources, reqT } defer rows.Close() for rows.Next() { - var cgrid, accid, cdrhost, cdrsrc, reqtype, direction, tenant, tor, account, subject, destination string + var cgrid, accid, cdrhost, cdrsrc, reqtype, direction, tenant, category, account, subject, destination string var extraFields []byte var setupTime, answerTime time.Time var runid sql.NullString // So we can export unmediated CDRs var orderid, duration int64 var cost sql.NullFloat64 // So we can export unmediated CDRs var extraFieldsMp map[string]string - if err := rows.Scan(&cgrid, &orderid, &accid, &cdrhost, &cdrsrc, &reqtype, &direction, &tenant, &tor, &account, &subject, &destination, &setupTime, &answerTime, &duration, + if err := rows.Scan(&cgrid, &orderid, &accid, &cdrhost, &cdrsrc, &reqtype, &direction, &tenant, &category, &account, &subject, &destination, &setupTime, &answerTime, &duration, &extraFields, &runid, &cost); err != nil { return nil, err } @@ -825,7 +821,7 @@ func (self *SQLStorage) GetStoredCdrs(cgrIds, runIds, cdrHosts, cdrSources, reqT } storCdr := &utils.StoredCdr{ CgrId: cgrid, OrderId: orderid, AccId: accid, CdrHost: cdrhost, CdrSource: cdrsrc, ReqType: reqtype, Direction: direction, Tenant: tenant, - Category: tor, Account: account, Subject: subject, Destination: destination, SetupTime: setupTime, AnswerTime: answerTime, Duration: time.Duration(duration), + Category: category, Account: account, Subject: subject, Destination: destination, SetupTime: setupTime, AnswerTime: answerTime, Duration: time.Duration(duration), ExtraFields: extraFieldsMp, MediationRunId: runid.String, Cost: cost.Float64, } cdrs = append(cdrs, storCdr) @@ -1030,7 +1026,7 @@ func (self *SQLStorage) GetTpRatingPlans(tpid, tag string) (map[string][]*utils. } func (self *SQLStorage) GetTpRatingProfiles(qryRpf *utils.TPRatingProfile) (map[string]*utils.TPRatingProfile, error) { - q := fmt.Sprintf("SELECT loadid,direction,tenant,tor,subject,activation_time,rating_plan_id,fallback_subjects FROM %s WHERE tpid='%s'", + q := fmt.Sprintf("SELECT loadid,direction,tenant,category,subject,activation_time,rating_plan_id,fallback_subjects FROM %s WHERE tpid='%s'", utils.TBL_TP_RATE_PROFILES, qryRpf.TPid) if len(qryRpf.LoadId) != 0 { q += fmt.Sprintf(" AND loadid='%s'", qryRpf.LoadId) @@ -1039,7 +1035,7 @@ func (self *SQLStorage) GetTpRatingProfiles(qryRpf *utils.TPRatingProfile) (map[ q += fmt.Sprintf(" AND tenant='%s'", qryRpf.Tenant) } if len(qryRpf.Category) != 0 { - q += fmt.Sprintf(" AND tor='%s'", qryRpf.Category) + q += fmt.Sprintf(" AND category='%s'", qryRpf.Category) } if len(qryRpf.Direction) != 0 { q += fmt.Sprintf(" AND direction='%s'", qryRpf.Direction) @@ -1054,11 +1050,11 @@ func (self *SQLStorage) GetTpRatingProfiles(qryRpf *utils.TPRatingProfile) (map[ defer rows.Close() rpfs := make(map[string]*utils.TPRatingProfile) for rows.Next() { - var rcvLoadId, tenant, tor, direction, subject, fallback_subjects, rating_plan_tag, activation_time string - if err := rows.Scan(&rcvLoadId, &tenant, &tor, &direction, &subject, &activation_time, &rating_plan_tag, &fallback_subjects); err != nil { + var rcvLoadId, tenant, category, direction, subject, fallback_subjects, rating_plan_tag, activation_time string + if err := rows.Scan(&rcvLoadId, &tenant, &category, &direction, &subject, &activation_time, &rating_plan_tag, &fallback_subjects); err != nil { return nil, err } - rp := &utils.TPRatingProfile{TPid: qryRpf.TPid, LoadId: rcvLoadId, Tenant: tenant, Category: tor, Direction: direction, Subject: subject} + rp := &utils.TPRatingProfile{TPid: qryRpf.TPid, LoadId: rcvLoadId, Tenant: tenant, Category: category, Direction: direction, Subject: subject} if existingRp, has := rpfs[rp.KeyId()]; !has { rp.RatingPlanActivations = []*utils.TPRatingActivation{ &utils.TPRatingActivation{ActivationTime: activation_time, RatingPlanId: rating_plan_tag, FallbackSubjects: fallback_subjects}} @@ -1203,7 +1199,7 @@ func (self *SQLStorage) GetTpActions(tpid, tag string) (map[string][]*utils.TPAc func (self *SQLStorage) GetTpActionTriggers(tpid, tag string) (map[string][]*utils.TPActionTrigger, error) { ats := make(map[string][]*utils.TPActionTrigger) - q := fmt.Sprintf("SELECT tpid,id,balance_type,direction,threshold_type,threshold_value,destination_id,actions_id,weight FROM %s WHERE tpid='%s'", + q := fmt.Sprintf("SELECT tpid,id,balance_type,direction,threshold_type,threshold_value,recurrent,destination_id,actions_id,weight FROM %s WHERE tpid='%s'", utils.TBL_TP_ACTION_TRIGGERS, tpid) if tag != "" { q += fmt.Sprintf(" AND id='%s'", tag) @@ -1226,6 +1222,7 @@ func (self *SQLStorage) GetTpActionTriggers(tpid, tag string) (map[string][]*uti Direction: direction, ThresholdType: thresholdType, ThresholdValue: threshold, + Recurrent: recurrent, DestinationId: destinations_tag, ActionsId: actions_tag, Weight: weight, diff --git a/engine/storage_sql_local_test.go b/engine/storage_sql_local_test.go index 3c5b58207..829227d2b 100644 --- a/engine/storage_sql_local_test.go +++ b/engine/storage_sql_local_test.go @@ -1,6 +1,6 @@ /* -Rating system designed to be used in VoIP Carriers World -Copyright (C) 2013 ITsysCOM +Real-time Charging System for Telecom & ISP environments +Copyright (C) 2012-2014 ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -153,7 +153,7 @@ func TestSetCdr(t *testing.T) { "account": "1002", "subject": "1002", "destination": "+4986517174963", "setup_time": "2013-11-07T08:42:25Z", "answer_time": "2013-11-07T08:42:26Z", "duration": "15s", "field_extr1": "val_extr1", "fieldextr2": "valextr2", "cdrsource": TEST_SQL} for _, cdr := range []*utils.CgrCdr{cgrCdr1, cgrCdr2, cgrCdr3, cgrCdr4, cgrCdr5} { - if err := mysql.SetCdr(cdr); err != nil { + if err := mysql.SetCdr(cdr.AsStoredCdr()); err != nil { t.Error(err.Error()) } } @@ -470,3 +470,28 @@ func TestRemStoredCdrs(t *testing.T) { t.Error("Unexpected number of StoredCdrs returned: ", storedCdrs) } } + +func TestSetGetTPActionTriggers(t *testing.T) { + if !*testLocal { + return + } + atrg := &utils.TPActionTrigger{ + BalanceType: "*monetary", + Direction: "*out", + ThresholdType: "*min_balance", + ThresholdValue: 2.0, + Recurrent: true, + DestinationId: "*any", + Weight: 10.0, + ActionsId: "LOG_BALANCE", + } + mpAtrgs := map[string][]*utils.TPActionTrigger{TEST_SQL: []*utils.TPActionTrigger{atrg}} + if err := mysql.SetTPActionTriggers(TEST_SQL, mpAtrgs); err != nil { + t.Error("Unexpected error: ", err.Error()) + } + if rcvMpAtrgs, err := mysql.GetTpActionTriggers(TEST_SQL, TEST_SQL); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if !reflect.DeepEqual(mpAtrgs, rcvMpAtrgs) { + t.Errorf("Expecting: %v, received: %v", mpAtrgs, rcvMpAtrgs) + } +} diff --git a/engine/tpimporter_csv.go b/engine/tpimporter_csv.go index 616112a2d..d96d6a8aa 100644 --- a/engine/tpimporter_csv.go +++ b/engine/tpimporter_csv.go @@ -414,7 +414,7 @@ func (self *TPCSVImporter) importActionTriggers(fn string) error { } continue } - tag, balanceType, direction, thresholdType, destinationTag, actionsTag := record[0], record[1], record[2], record[3], record[5], record[6] + tag, balanceType, direction, thresholdType, destinationTag, actionsTag := record[0], record[1], record[2], record[3], record[6], record[7] threshold, err := strconv.ParseFloat(record[4], 64) if err != nil { if self.Verbose { @@ -422,7 +422,11 @@ func (self *TPCSVImporter) importActionTriggers(fn string) error { } continue } - weight, err := strconv.ParseFloat(record[7], 64) + recurrent, err := strconv.ParseBool(record[5]) + if err != nil { + log.Printf("Ignoring line %d, warning: <%s>", lineNr, err.Error()) + } + weight, err := strconv.ParseFloat(record[8], 64) if err != nil { if self.Verbose { log.Printf("Ignoring line %d, warning: <%s> ", lineNr, err.Error()) @@ -434,6 +438,7 @@ func (self *TPCSVImporter) importActionTriggers(fn string) error { Direction: direction, ThresholdType: thresholdType, ThresholdValue: threshold, + Recurrent: recurrent, DestinationId: destinationTag, Weight: weight, ActionsId: actionsTag, diff --git a/general_tests/fsevcorelate_test.go b/general_tests/fsevcorelate_test.go index 18821870c..addee97b6 100644 --- a/general_tests/fsevcorelate_test.go +++ b/general_tests/fsevcorelate_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/cgrates/cgrates/cdrs" + "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/sessionmanager" ) @@ -213,18 +214,20 @@ variable_sip_local_sdp_str: v%3D0%0Ao%3DFreeSWITCH%201396951687%201396951689%20I var jsonCdr = []byte(`{"core-uuid":"feef0b51-7fdf-4c4a-878e-aff233752de2","channel_data":{"state":"CS_REPORTING","direction":"inbound","state_number":"11","flags":"0=1;1=1;3=1;36=1;37=1;39=1;42=1;47=1;52=1;73=1;75=1;94=1","caps":"1=1;2=1;3=1;4=1;5=1;6=1"},"variables":{"direction":"inbound","uuid":"86cfd6e2-dbda-45a3-b59d-f683ec368e8b","session_id":"5","sip_from_user":"1001","sip_from_uri":"1001@192.168.56.74","sip_from_host":"192.168.56.74","channel_name":"sofia/internal/1001@192.168.56.74","sip_local_network_addr":"192.168.56.74","sip_network_ip":"192.168.56.1","sip_network_port":"5060","sip_received_ip":"192.168.56.1","sip_received_port":"5060","sip_via_protocol":"udp","sip_authorized":"true","Event-Name":"REQUEST_PARAMS","Core-UUID":"feef0b51-7fdf-4c4a-878e-aff233752de2","FreeSWITCH-Hostname":"CGRTest","FreeSWITCH-Switchname":"CGRTest","FreeSWITCH-IPv4":"192.168.178.32","FreeSWITCH-IPv6":"::1","Event-Date-Local":"2014-04-08 21:10:21","Event-Date-GMT":"Tue, 08 Apr 2014 19:10:21 GMT","Event-Date-Timestamp":"1396984221278217","Event-Calling-File":"sofia.c","Event-Calling-Function":"sofia_handle_sip_i_invite","Event-Calling-Line-Number":"8076","Event-Sequence":"1423","sip_number_alias":"1001","sip_auth_username":"1001","sip_auth_realm":"192.168.56.74","number_alias":"1001","requested_domain_name":"192.168.56.66","record_stereo":"true","default_gateway":"example.com","default_areacode":"918","transfer_fallback_extension":"operator","toll_allow":"domestic,international,local","accountcode":"1001","user_context":"default","effective_caller_id_name":"Extension 1001","effective_caller_id_number":"1001","outbound_caller_id_name":"FreeSWITCH","outbound_caller_id_number":"0000000000","callgroup":"techsupport","user_name":"1001","domain_name":"192.168.56.66","sip_from_user_stripped":"1001","sofia_profile_name":"internal","recovery_profile_name":"internal","sip_req_user":"1002","sip_req_uri":"1002@192.168.56.74","sip_req_host":"192.168.56.74","sip_to_user":"1002","sip_to_uri":"1002@192.168.56.74","sip_to_host":"192.168.56.74","sip_contact_params":"transport=udp;registering_acc=192_168_56_74","sip_contact_user":"1001","sip_contact_port":"5060","sip_contact_uri":"1001@192.168.56.1:5060","sip_contact_host":"192.168.56.1","sip_via_host":"192.168.56.1","sip_via_port":"5060","presence_id":"1001@192.168.56.74","ep_codec_string":"G722@8000h@20i@64000b,PCMU@8000h@20i@64000b,PCMA@8000h@20i@64000b,GSM@8000h@20i@13200b","cgr_notify":"+AUTH_OK","max_forwards":"69","transfer_history":"1396984221:caefc538-5da4-4245-8716-112c706383d8:bl_xfer:1002/default/XML","transfer_source":"1396984221:caefc538-5da4-4245-8716-112c706383d8:bl_xfer:1002/default/XML","DP_MATCH":"ARRAY::1002|:1002","call_uuid":"86cfd6e2-dbda-45a3-b59d-f683ec368e8b","RFC2822_DATE":"Tue, 08 Apr 2014 21:10:21 +0200","dialed_extension":"1002","export_vars":"RFC2822_DATE,RFC2822_DATE,dialed_extension","ringback":"%(2000,4000,440,480)","transfer_ringback":"local_stream://moh","call_timeout":"30","hangup_after_bridge":"true","continue_on_fail":"true","called_party_callgroup":"techsupport","current_application_data":"user/1002@192.168.56.66","current_application":"bridge","dialed_user":"1002","dialed_domain":"192.168.56.66","inherit_codec":"true","originated_legs":"ARRAY::402f0929-fa14-4a5f-9642-3a1311bb4ddd;Outbound Call;1002|:402f0929-fa14-4a5f-9642-3a1311bb4ddd;Outbound Call;1002","rtp_use_codec_string":"G722,PCMU,PCMA,GSM","sip_use_codec_name":"G722","sip_use_codec_rate":"8000","sip_use_codec_ptime":"20","write_codec":"G722","write_rate":"16000","video_possible":"true","local_media_ip":"192.168.56.74","local_media_port":"32534","advertised_media_ip":"192.168.56.74","sip_use_pt":"9","rtp_use_ssrc":"1431080133","zrtp_secure_media_confirmed_audio":"true","zrtp_sas1_string_audio":"j6ff","switch_m_sdp":"v=0\r\no=1002 0 0 IN IP4 192.168.56.1\r\ns=-\r\nc=IN IP4 192.168.56.1\r\nt=0 0\r\nm=audio 5020 RTP/AVP 9 0 8 3 101\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:3 GSM/8000\r\na=rtpmap:101 telephone-event/8000\r\n","read_codec":"G722","read_rate":"16000","endpoint_disposition":"ANSWER","originate_causes":"ARRAY::402f0929-fa14-4a5f-9642-3a1311bb4ddd;NONE|:402f0929-fa14-4a5f-9642-3a1311bb4ddd;NONE","originate_disposition":"SUCCESS","DIALSTATUS":"SUCCESS","last_bridge_to":"402f0929-fa14-4a5f-9642-3a1311bb4ddd","bridge_channel":"sofia/internal/sip:1002@192.168.56.1:5060","bridge_uuid":"402f0929-fa14-4a5f-9642-3a1311bb4ddd","signal_bond":"402f0929-fa14-4a5f-9642-3a1311bb4ddd","last_sent_callee_id_name":"Outbound Call","last_sent_callee_id_number":"1002","cgr_reqtype":"prepaid","sip_reinvite_sdp":"v=0\r\no=1001 0 1 IN IP4 192.168.56.1\r\ns=-\r\nc=IN IP4 192.168.56.1\r\nt=0 0\r\nm=audio 5016 RTP/AVP 96 97 98 9 100 102 0 8 103 3 104 101\r\na=sendonly\r\na=rtpmap:96 opus/48000/2\r\na=fmtp:96 usedtx=1\r\na=rtpmap:97 SILK/24000\r\na=rtpmap:98 SILK/16000\r\na=rtpmap:9 G722/8000\r\na=rtpmap:100 speex/32000\r\na=rtpmap:102 speex/16000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:103 iLBC/8000\r\na=rtpmap:3 GSM/8000\r\na=rtpmap:104 speex/8000\r\na=rtpmap:101 telephone-event/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=zrtp-hash:1.10 722d57097aaabea2749ea8938472478f8d88645b23521fa5f8005a7a2bed8286\r\nm=video 0 RTP/AVP 105 99\r\n","switch_r_sdp":"v=0\r\no=1001 0 1 IN IP4 192.168.56.1\r\ns=-\r\nc=IN IP4 192.168.56.1\r\nt=0 0\r\nm=audio 5016 RTP/AVP 96 97 98 9 100 102 0 8 103 3 104 101\r\na=rtpmap:96 opus/48000/2\r\na=fmtp:96 usedtx=1\r\na=rtpmap:97 SILK/24000\r\na=rtpmap:98 SILK/16000\r\na=rtpmap:9 G722/8000\r\na=rtpmap:100 speex/32000\r\na=rtpmap:102 speex/16000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:103 iLBC/8000\r\na=rtpmap:3 GSM/8000\r\na=rtpmap:104 speex/8000\r\na=rtpmap:101 telephone-event/8000\r\na=sendonly\r\na=extmap:1 urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=zrtp-hash:1.10 722d57097aaabea2749ea8938472478f8d88645b23521fa5f8005a7a2bed8286\r\nm=video 0 RTP/AVP 105 99\r\n","r_sdp_audio_zrtp_hash":"1.10 722d57097aaabea2749ea8938472478f8d88645b23521fa5f8005a7a2bed8286","remote_media_ip":"192.168.56.1","remote_media_port":"5016","sip_audio_recv_pt":"9","dtmf_type":"rfc2833","sip_2833_send_payload":"101","sip_2833_recv_payload":"101","sip_local_sdp_str":"v=0\no=FreeSWITCH 1396951687 1396951690 IN IP4 192.168.56.74\ns=FreeSWITCH\nc=IN IP4 192.168.56.74\nt=0 0\nm=audio 32534 RTP/AVP 9 101\na=rtpmap:9 G722/8000\na=rtpmap:101 telephone-event/8000\na=fmtp:101 0-16\na=ptime:20\na=sendrecv\n","sip_to_tag":"rXc9vZpv9eFaF","sip_from_tag":"1afc7eca","sip_cseq":"3","sip_call_id":"6691dbf8ffdc02bdacee02bc305d5c71@0:0:0:0:0:0:0:0","sip_full_via":"SIP/2.0/UDP 192.168.56.1:5060;branch=z9hG4bK-323133-5d083abc0d3f327b9101586e71b5fce4","sip_from_display":"1001","sip_full_from":"\"1001\" ;tag=1afc7eca","sip_full_to":";tag=rXc9vZpv9eFaF","sip_term_status":"200","proto_specific_hangup_cause":"sip:200","sip_term_cause":"16","last_bridge_role":"originator","sip_user_agent":"Jitsi2.5.5065Linux","sip_hangup_disposition":"recv_bye","bridge_hangup_cause":"NORMAL_CLEARING","hangup_cause":"NORMAL_CLEARING","hangup_cause_q850":"16","digits_dialed":"none","start_stamp":"2014-04-08 21:10:21","profile_start_stamp":"2014-04-08 21:10:21","answer_stamp":"2014-04-08 21:10:27","bridge_stamp":"2014-04-08 21:10:27","hold_stamp":"2014-04-08 21:10:27","progress_stamp":"2014-04-08 21:10:21","progress_media_stamp":"2014-04-08 21:10:21","hold_events":"{{1396984227824182,1396984242247995}}","end_stamp":"2014-04-08 21:10:42","start_epoch":"1396984221","start_uepoch":"1396984221278217","profile_start_epoch":"1396984221","profile_start_uepoch":"1396984221377035","answer_epoch":"1396984227","answer_uepoch":"1396984227717006","bridge_epoch":"1396984227","bridge_uepoch":"1396984227737268","last_hold_epoch":"1396984227","last_hold_uepoch":"1396984227824167","hold_accum_seconds":"14","hold_accum_usec":"14423816","hold_accum_ms":"14423","resurrect_epoch":"0","resurrect_uepoch":"0","progress_epoch":"1396984221","progress_uepoch":"1396984221497331","progress_media_epoch":"1396984221","progress_media_uepoch":"1396984221517042","end_epoch":"1396984242","end_uepoch":"1396984242257026","last_app":"bridge","last_arg":"user/1002@192.168.56.66","caller_id":"\"1001\" <1001>","duration":"21","billsec":"15","progresssec":"0","answersec":"6","waitsec":"6","progress_mediasec":"0","flow_billsec":"21","mduration":"20979","billmsec":"14540","progressmsec":"219","answermsec":"6439","waitmsec":"6459","progress_mediamsec":"239","flow_billmsec":"20979","uduration":"20978809","billusec":"14540020","progressusec":"219114","answerusec":"6438789","waitusec":"6459051","progress_mediausec":"238825","flow_billusec":"20978809","rtp_audio_in_raw_bytes":"181360","rtp_audio_in_media_bytes":"180304","rtp_audio_in_packet_count":"1031","rtp_audio_in_media_packet_count":"1025","rtp_audio_in_skip_packet_count":"45","rtp_audio_in_jb_packet_count":"0","rtp_audio_in_dtmf_packet_count":"0","rtp_audio_in_cng_packet_count":"0","rtp_audio_in_flush_packet_count":"6","rtp_audio_in_largest_jb_size":"0","rtp_audio_out_raw_bytes":"165780","rtp_audio_out_media_bytes":"165780","rtp_audio_out_packet_count":"942","rtp_audio_out_media_packet_count":"942","rtp_audio_out_skip_packet_count":"0","rtp_audio_out_dtmf_packet_count":"0","rtp_audio_out_cng_packet_count":"0","rtp_audio_rtcp_packet_count":"0","rtp_audio_rtcp_octet_count":"0"},"app_log":{"applications":[{"app_name":"hash","app_data":"insert/192.168.56.66-spymap/1001/86cfd6e2-dbda-45a3-b59d-f683ec368e8b"},{"app_name":"hash","app_data":"insert/192.168.56.66-last_dial/1001/1002"},{"app_name":"hash","app_data":"insert/192.168.56.66-last_dial/global/86cfd6e2-dbda-45a3-b59d-f683ec368e8b"},{"app_name":"export","app_data":"RFC2822_DATE=Tue, 08 Apr 2014 21:10:21 +0200"},{"app_name":"park","app_data":""},{"app_name":"hash","app_data":"insert/192.168.56.66-spymap/1001/86cfd6e2-dbda-45a3-b59d-f683ec368e8b"},{"app_name":"hash","app_data":"insert/192.168.56.66-last_dial/1001/1002"},{"app_name":"hash","app_data":"insert/192.168.56.66-last_dial/global/86cfd6e2-dbda-45a3-b59d-f683ec368e8b"},{"app_name":"export","app_data":"RFC2822_DATE=Tue, 08 Apr 2014 21:10:21 +0200"},{"app_name":"export","app_data":"dialed_extension=1002"},{"app_name":"bind_meta_app","app_data":"1 b s execute_extension::dx XML features"},{"app_name":"bind_meta_app","app_data":"2 b s record_session::/var/lib/freeswitch/recordings/1001.2014-04-08-21-10-21.wav"},{"app_name":"bind_meta_app","app_data":"3 b s execute_extension::cf XML features"},{"app_name":"bind_meta_app","app_data":"4 b s execute_extension::att_xfer XML features"},{"app_name":"set","app_data":"ringback=%(2000,4000,440,480)"},{"app_name":"set","app_data":"transfer_ringback=local_stream://moh"},{"app_name":"set","app_data":"call_timeout=30"},{"app_name":"set","app_data":"hangup_after_bridge=true"},{"app_name":"set","app_data":"continue_on_fail=true"},{"app_name":"hash","app_data":"insert/192.168.56.66-call_return/1002/1001"},{"app_name":"hash","app_data":"insert/192.168.56.66-last_dial_ext/1002/86cfd6e2-dbda-45a3-b59d-f683ec368e8b"},{"app_name":"set","app_data":"called_party_callgroup=techsupport"},{"app_name":"hash","app_data":"insert/192.168.56.66-last_dial_ext/techsupport/86cfd6e2-dbda-45a3-b59d-f683ec368e8b"},{"app_name":"hash","app_data":"insert/192.168.56.66-last_dial_ext/global/86cfd6e2-dbda-45a3-b59d-f683ec368e8b"},{"app_name":"hash","app_data":"insert/192.168.56.66-last_dial/techsupport/86cfd6e2-dbda-45a3-b59d-f683ec368e8b"},{"app_name":"bridge","app_data":"user/1002@192.168.56.66"}]},"callflow":{"dialplan":"XML","profile_index":"2","extension":{"name":"global","number":"1002","applications":[{"app_name":"hash","app_data":"insert/${domain_name}-spymap/${caller_id_number}/${uuid}"},{"app_name":"hash","app_data":"insert/${domain_name}-last_dial/${caller_id_number}/${destination_number}"},{"app_name":"hash","app_data":"insert/${domain_name}-last_dial/global/${uuid}"},{"app_name":"export","app_data":"RFC2822_DATE=${strftime(%a, %d %b %Y %T %z)}"},{"app_name":"export","app_data":"dialed_extension=1002"},{"app_name":"bind_meta_app","app_data":"1 b s execute_extension::dx XML features"},{"app_name":"bind_meta_app","app_data":"2 b s record_session::/var/lib/freeswitch/recordings/${caller_id_number}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"},{"app_name":"bind_meta_app","app_data":"3 b s execute_extension::cf XML features"},{"app_name":"bind_meta_app","app_data":"4 b s execute_extension::att_xfer XML features"},{"app_name":"set","app_data":"ringback=${us-ring}"},{"app_name":"set","app_data":"transfer_ringback=local_stream://moh"},{"app_name":"set","app_data":"call_timeout=30"},{"app_name":"set","app_data":"hangup_after_bridge=true"},{"app_name":"set","app_data":"continue_on_fail=true"},{"app_name":"hash","app_data":"insert/${domain_name}-call_return/${dialed_extension}/${caller_id_number}"},{"app_name":"hash","app_data":"insert/${domain_name}-last_dial_ext/${dialed_extension}/${uuid}"},{"app_name":"set","app_data":"called_party_callgroup=${user_data(${dialed_extension}@${domain_name} var callgroup)}"},{"app_name":"hash","app_data":"insert/${domain_name}-last_dial_ext/${called_party_callgroup}/${uuid}"},{"app_name":"hash","app_data":"insert/${domain_name}-last_dial_ext/global/${uuid}"},{"app_name":"hash","app_data":"insert/${domain_name}-last_dial/${called_party_callgroup}/${uuid}"},{"app_name":"bridge","app_data":"user/${dialed_extension}@${domain_name}"},{"last_executed":"true","app_name":"answer","app_data":""},{"app_name":"sleep","app_data":"1000"},{"app_name":"bridge","app_data":"loopback/app=voicemail:default ${domain_name} ${dialed_extension}"}],"current_app":"answer"},"caller_profile":{"username":"1001","dialplan":"XML","caller_id_name":"1001","ani":"1001","aniii":"","caller_id_number":"1001","network_addr":"192.168.56.1","rdnis":"1002","destination_number":"1002","uuid":"86cfd6e2-dbda-45a3-b59d-f683ec368e8b","source":"mod_sofia","context":"default","chan_name":"sofia/internal/1001@192.168.56.74","originatee":{"originatee_caller_profiles":[{"username":"1001","dialplan":"XML","caller_id_name":"Extension 1001","ani":"1001","aniii":"","caller_id_number":"1001","network_addr":"192.168.56.1","rdnis":"1002","destination_number":"1002","uuid":"402f0929-fa14-4a5f-9642-3a1311bb4ddd","source":"mod_sofia","context":"default","chan_name":"sofia/internal/sip:1002@192.168.56.1:5060"},{"username":"1001","dialplan":"XML","caller_id_name":"Extension 1001","ani":"1001","aniii":"","caller_id_number":"1001","network_addr":"192.168.56.1","rdnis":"1002","destination_number":"1002","uuid":"402f0929-fa14-4a5f-9642-3a1311bb4ddd","source":"mod_sofia","context":"default","chan_name":"sofia/internal/sip:1002@192.168.56.1:5060"}]}},"times":{"created_time":"1396984221278217","profile_created_time":"1396984221377035","progress_time":"1396984221497331","progress_media_time":"1396984221517042","answered_time":"1396984227717006","hangup_time":"1396984242257026","resurrect_time":"0","transfer_time":"0"}},"callflow":{"dialplan":"XML","profile_index":"1","extension":{"name":"global","number":"1002","applications":[{"app_name":"hash","app_data":"insert/${domain_name}-spymap/${caller_id_number}/${uuid}"},{"app_name":"hash","app_data":"insert/${domain_name}-last_dial/${caller_id_number}/${destination_number}"},{"app_name":"hash","app_data":"insert/${domain_name}-last_dial/global/${uuid}"},{"app_name":"export","app_data":"RFC2822_DATE=${strftime(%a, %d %b %Y %T %z)}"},{"app_name":"park","app_data":""}]},"caller_profile":{"username":"1001","dialplan":"XML","caller_id_name":"1001","ani":"1001","aniii":"","caller_id_number":"1001","network_addr":"192.168.56.1","rdnis":"","destination_number":"1002","uuid":"86cfd6e2-dbda-45a3-b59d-f683ec368e8b","source":"mod_sofia","context":"default","chan_name":"sofia/internal/1001@192.168.56.74"},"times":{"created_time":"1396984221278217","profile_created_time":"1396984221278217","progress_time":"0","progress_media_time":"0","answered_time":"0","hangup_time":"0","resurrect_time":"0","transfer_time":"1396984221377035"}}}`) func TestEvCorelate(t *testing.T) { + cfg, _ := config.NewDefaultCGRConfig() + cdrs.New(nil, nil, cfg) // So we can set the package cfg answerEv := new(sessionmanager.FSEvent).New(answerEvent) if answerEv.GetName() != "CHANNEL_ANSWER" { t.Error("Event not parsed correctly: ", answerEv) } - cdrEv, err := new(cdrs.FSCdr).New(jsonCdr) + cdrEv, err := cdrs.NewFSCdr(jsonCdr) if err != nil { t.Errorf("Error loading cdr: %v", err.Error()) - } else if acntId := cdrEv.GetAccId(); acntId != "86cfd6e2-dbda-45a3-b59d-f683ec368e8b" { - t.Error("Unexpected acntId received", acntId) + } else if cdrEv.AsStoredCdr().AccId != "86cfd6e2-dbda-45a3-b59d-f683ec368e8b" { + t.Error("Unexpected acntId received", cdrEv.AsStoredCdr().AccId) } - if answerEv.GetCgrId() != cdrEv.GetCgrId() { - t.Error("CgrIds do not match", answerEv.GetCgrId(), cdrEv.GetCgrId()) + if answerEv.GetCgrId() != cdrEv.AsStoredCdr().CgrId { + t.Error("CgrIds do not match", answerEv.GetCgrId(), cdrEv.AsStoredCdr().CgrId) } } diff --git a/mediator/mediator.go b/mediator/mediator.go index 7afcce21d..d01b7a313 100644 --- a/mediator/mediator.go +++ b/mediator/mediator.go @@ -46,10 +46,10 @@ type Mediator struct { } // Retrive the cost from logging database -func (self *Mediator) getCostsFromDB(cgrid string) (cc *engine.CallCost, err error) { +func (self *Mediator) getCostsFromDB(cgrid, runId string) (cc *engine.CallCost, err error) { for i := 0; i < 3; i++ { // Mechanism to avoid concurrency between SessionManager writing the costs and mediator picking them up - cc, err = self.logDb.GetCallCostLog(cgrid, engine.SESSION_MANAGER_SOURCE, utils.DEFAULT_RUNID) //ToDo: What are we getting when there is no log? - if cc != nil { // There were no errors, chances are that we got what we are looking for + cc, err = self.logDb.GetCallCostLog(cgrid, engine.SESSION_MANAGER_SOURCE, runId) //ToDo: What are we getting when there is no log? + if cc != nil { // There were no errors, chances are that we got what we are looking for break } time.Sleep(time.Duration(i) * time.Second) @@ -58,88 +58,86 @@ func (self *Mediator) getCostsFromDB(cgrid string) (cc *engine.CallCost, err err } // Retrive the cost from engine -func (self *Mediator) getCostsFromRater(cdr *utils.StoredCdr) (*engine.CallCost, error) { +func (self *Mediator) getCostsFromRater(storedCdr *utils.StoredCdr) (*engine.CallCost, error) { cc := &engine.CallCost{} var err error - if cdr.Duration == time.Duration(0) { // failed call, returning empty callcost, no error + if storedCdr.Duration == time.Duration(0) { // failed call, returning empty callcost, no error return cc, nil } cd := engine.CallDescriptor{ Direction: "*out", //record[m.directionFields[runIdx]] TODO: fix me - Tenant: cdr.Tenant, - Category: cdr.Category, - Subject: cdr.Subject, - Account: cdr.Account, - Destination: cdr.Destination, - TimeStart: cdr.AnswerTime, - TimeEnd: cdr.AnswerTime.Add(cdr.Duration), + Tenant: storedCdr.Tenant, + Category: storedCdr.Category, + Subject: storedCdr.Subject, + Account: storedCdr.Account, + Destination: storedCdr.Destination, + TimeStart: storedCdr.AnswerTime, + TimeEnd: storedCdr.AnswerTime.Add(storedCdr.Duration), LoopIndex: 0, - DurationIndex: cdr.Duration, + DurationIndex: storedCdr.Duration, } - if cdr.ReqType == utils.PSEUDOPREPAID { + if storedCdr.ReqType == utils.PSEUDOPREPAID { err = self.connector.Debit(cd, cc) } else { err = self.connector.GetCost(cd, cc) } if err != nil { - self.logDb.LogError(cdr.CgrId, engine.MEDIATOR_SOURCE, cdr.MediationRunId, err.Error()) + self.logDb.LogError(storedCdr.CgrId, engine.MEDIATOR_SOURCE, storedCdr.MediationRunId, err.Error()) } else { // If the mediator calculated a price it will write it to logdb - self.logDb.LogCallCost(utils.Sha1(cdr.AccId, cdr.SetupTime.String()), engine.MEDIATOR_SOURCE, cdr.MediationRunId, cc) + self.logDb.LogCallCost(storedCdr.CgrId, engine.MEDIATOR_SOURCE, storedCdr.MediationRunId, cc) } return cc, err } -func (self *Mediator) rateCDR(cdr *utils.StoredCdr) error { +func (self *Mediator) rateCDR(storedCdr *utils.StoredCdr) error { var qryCC *engine.CallCost var errCost error - if cdr.ReqType == utils.PREPAID || cdr.ReqType == utils.POSTPAID { + if storedCdr.ReqType == utils.PREPAID || storedCdr.ReqType == utils.POSTPAID { // Should be previously calculated and stored in DB - qryCC, errCost = self.getCostsFromDB(cdr.CgrId) + qryCC, errCost = self.getCostsFromDB(storedCdr.CgrId, storedCdr.MediationRunId) } else { - qryCC, errCost = self.getCostsFromRater(cdr) + qryCC, errCost = self.getCostsFromRater(storedCdr) } if errCost != nil { return errCost } else if qryCC == nil { return errors.New("No cost returned from rater") } - cdr.Cost = qryCC.Cost + storedCdr.Cost = qryCC.Cost return nil } -// Forks original CDR based on original request plus runIds for extra mediation -func (self *Mediator) RateCdr(dbcdr utils.RawCDR) error { - rtCdr, err := utils.NewStoredCdrFromRawCDR(dbcdr) - if err != nil { - return err - } - cdrs := []*utils.StoredCdr{rtCdr} // Start with initial dbcdr, will add here all to be mediated - attrsDC := utils.AttrDerivedChargers{Tenant: rtCdr.Tenant, Category: rtCdr.Category, Direction: rtCdr.Direction, - Account: rtCdr.Account, Subject: rtCdr.Subject} +func (self *Mediator) RateCdr(storedCdr *utils.StoredCdr) error { + cdrs := []*utils.StoredCdr{storedCdr} // Start with initial storCdr, will add here all to be mediated + attrsDC := utils.AttrDerivedChargers{Tenant: storedCdr.Tenant, Category: storedCdr.Category, Direction: storedCdr.Direction, + Account: storedCdr.Account, Subject: storedCdr.Subject} var dcs utils.DerivedChargers if err := self.connector.GetDerivedChargers(attrsDC, &dcs); err != nil { - errText := fmt.Sprintf("Could not get derived charging for cgrid %s, error: %s", rtCdr.CgrId, err.Error()) + errText := fmt.Sprintf("Could not get derived charging for cgrid %s, error: %s", storedCdr.CgrId, err.Error()) engine.Logger.Err(errText) return errors.New(errText) } for _, dc := range dcs { - forkedCdr, err := dbcdr.ForkCdr(dc.RunId, dc.ReqTypeField, dc.DirectionField, - dc.TenantField, dc.CategoryField, dc.AccountField, dc.SubjectField, dc.DestinationField, dc.SetupTimeField, dc.AnswerTimeField, dc.DurationField, []string{}, true) + forkedCdr, err := storedCdr.ForkCdr(dc.RunId, &utils.RSRField{Id: dc.ReqTypeField}, &utils.RSRField{Id: dc.DirectionField}, + &utils.RSRField{Id: dc.TenantField}, &utils.RSRField{Id: dc.CategoryField}, &utils.RSRField{Id: dc.AccountField}, + &utils.RSRField{Id: dc.SubjectField}, &utils.RSRField{Id: dc.DestinationField}, &utils.RSRField{Id: dc.SetupTimeField}, + &utils.RSRField{Id: dc.AnswerTimeField}, &utils.RSRField{Id: dc.DurationField}, []*utils.RSRField{}, true) engine.Logger.Debug(fmt.Sprintf("Forked CDR for dc: %v, is: %v", dc, forkedCdr)) if err != nil { // Errors on fork, cannot calculate further, write that into db for later analysis - self.cdrDb.SetRatedCdr(&utils.StoredCdr{CgrId: dbcdr.GetCgrId(), MediationRunId: dc.RunId, Cost: -1.0}, err.Error()) // Cannot fork CDR, important just runid and error + self.cdrDb.SetRatedCdr(&utils.StoredCdr{CgrId: storedCdr.CgrId, CdrSource: utils.FORKED_CDR, MediationRunId: dc.RunId, Cost: -1}, + err.Error()) // Cannot fork CDR, important just runid and error continue } cdrs = append(cdrs, forkedCdr) } for _, cdr := range cdrs { extraInfo := "" - if err = self.rateCDR(cdr); err != nil { + if err := self.rateCDR(cdr); err != nil { extraInfo = err.Error() } if err := self.cdrDb.SetRatedCdr(cdr, extraInfo); err != nil { - engine.Logger.Err(fmt.Sprintf(" Could not record cost for cgrid: <%s>, err: <%s>, cost: %f, extraInfo: %s", + engine.Logger.Err(fmt.Sprintf(" Could not record cost for cgrid: <%s>, ERROR: <%s>, cost: %f, extraInfo: %s", cdr.CgrId, err.Error(), cdr.Cost, extraInfo)) } } diff --git a/mediator/mediator_local_test.go b/mediator/mediator_local_test.go index 5029190ed..0615e7b1b 100644 --- a/mediator/mediator_local_test.go +++ b/mediator/mediator_local_test.go @@ -174,7 +174,7 @@ func TestInjectCdrs(t *testing.T) { "tenant": "cgrates.org", "tor": "call", "account": "dan", "subject": "dan", "destination": "+4986517173964", "answer_time": "2013-11-07T09:42:26Z", "duration": "20"} for _, cdr := range []utils.CgrCdr{cgrCdr1, cgrCdr2} { - if err := cdrStor.SetCdr(cdr); err != nil { + if err := cdrStor.SetCdr(cdr.AsStoredCdr()); err != nil { t.Error(err) } } diff --git a/utils/cgrcdr.go b/utils/cgrcdr.go index b0d60d509..598daf637 100644 --- a/utils/cgrcdr.go +++ b/utils/cgrcdr.go @@ -19,10 +19,7 @@ along with this program. If not, see package utils import ( - "errors" - "fmt" "net/http" - "strings" "time" ) @@ -78,7 +75,7 @@ func (cgrCdr CgrCdr) GetDestination() string { } func (cgrCdr CgrCdr) GetCategory() string { - return cgrCdr[Category] + return cgrCdr[CATEGORY] } func (cgrCdr CgrCdr) GetTenant() string { @@ -108,133 +105,23 @@ func (cgrCdr CgrCdr) GetDuration() (time.Duration, error) { return ParseDurationWithSecs(cgrCdr[DURATION]) } -// Used in mediation, fieldsMandatory marks whether missing field out of request represents error or can be ignored -// If the fields in parameters start with ^ their value is considered instead of dynamically retrieving it from CDR -func (cgrCdr CgrCdr) ForkCdr(runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, setupTimeFld, answerTimeFld, durationFld string, extraFlds []string, fieldsMandatory bool) (*StoredCdr, error) { - if IsSliceMember([]string{runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, answerTimeFld, durationFld}, "") { - return nil, errors.New(fmt.Sprintf("%s:FieldName", ERR_MANDATORY_IE_MISSING)) // All input field names are mandatory - } - var err error - var hasKey bool - var sTimeStr, aTimeStr, durStr string - rtCdr := new(StoredCdr) - rtCdr.MediationRunId = runId - rtCdr.Cost = -1.0 // Default for non-rated CDR - if rtCdr.AccId, hasKey = cgrCdr[ACCID]; !hasKey { - if fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, ACCID)) - } - } - // MetaDefault will automatically be converted to their standard values - if reqTypeFld == META_DEFAULT { - reqTypeFld = REQTYPE - } - if directionFld == META_DEFAULT { - directionFld = DIRECTION - } - if tenantFld == META_DEFAULT { - tenantFld = TENANT - } - if torFld == META_DEFAULT { - torFld = Category - } - if accountFld == META_DEFAULT { - accountFld = ACCOUNT - } - if subjectFld == META_DEFAULT { - subjectFld = SUBJECT - } - if destFld == META_DEFAULT { - destFld = DESTINATION - } - if setupTimeFld == META_DEFAULT { - setupTimeFld = SETUP_TIME - } - if answerTimeFld == META_DEFAULT { - answerTimeFld = ANSWER_TIME - } - if durationFld == META_DEFAULT { - durationFld = DURATION - } - if rtCdr.CdrHost, hasKey = cgrCdr[CDRHOST]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, CDRHOST)) - } - if rtCdr.CdrSource, hasKey = cgrCdr[CDRSOURCE]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, CDRSOURCE)) - } - if strings.HasPrefix(reqTypeFld, STATIC_VALUE_PREFIX) { // Values starting with prefix are not dynamically populated - rtCdr.ReqType = reqTypeFld[1:] - } else if rtCdr.ReqType, hasKey = cgrCdr[reqTypeFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, reqTypeFld)) - } - if strings.HasPrefix(directionFld, STATIC_VALUE_PREFIX) { - rtCdr.Direction = directionFld[1:] - } else if rtCdr.Direction, hasKey = cgrCdr[directionFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, directionFld)) - } - if strings.HasPrefix(tenantFld, STATIC_VALUE_PREFIX) { - rtCdr.Tenant = tenantFld[1:] - } else if rtCdr.Tenant, hasKey = cgrCdr[tenantFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, tenantFld)) - } - if strings.HasPrefix(torFld, STATIC_VALUE_PREFIX) { - rtCdr.Category = torFld[1:] - } else if rtCdr.Category, hasKey = cgrCdr[torFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, torFld)) - } - if strings.HasPrefix(accountFld, STATIC_VALUE_PREFIX) { - rtCdr.Account = accountFld[1:] - } else if rtCdr.Account, hasKey = cgrCdr[accountFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, accountFld)) - } - if strings.HasPrefix(subjectFld, STATIC_VALUE_PREFIX) { - rtCdr.Subject = subjectFld[1:] - } else if rtCdr.Subject, hasKey = cgrCdr[subjectFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, subjectFld)) - } - if strings.HasPrefix(destFld, STATIC_VALUE_PREFIX) { - rtCdr.Destination = destFld[1:] - } else if rtCdr.Destination, hasKey = cgrCdr[destFld]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, destFld)) - } - if sTimeStr, hasKey = cgrCdr[setupTimeFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(setupTimeFld, STATIC_VALUE_PREFIX) { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, setupTimeFld)) - } else { - if strings.HasPrefix(setupTimeFld, STATIC_VALUE_PREFIX) { - sTimeStr = setupTimeFld[1:] - } - if rtCdr.SetupTime, err = ParseTimeDetectLayout(sTimeStr); err != nil && fieldsMandatory { - return nil, err - } - } - if aTimeStr, hasKey = cgrCdr[answerTimeFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(answerTimeFld, STATIC_VALUE_PREFIX) { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, answerTimeFld)) - } else { - if strings.HasPrefix(answerTimeFld, STATIC_VALUE_PREFIX) { - aTimeStr = answerTimeFld[1:] - } - if rtCdr.AnswerTime, err = ParseTimeDetectLayout(aTimeStr); err != nil && fieldsMandatory { - return nil, err - } - } - if durStr, hasKey = cgrCdr[durationFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(durationFld, STATIC_VALUE_PREFIX) { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, durationFld)) - } else { - if strings.HasPrefix(durationFld, STATIC_VALUE_PREFIX) { - durStr = durationFld[1:] - } - if rtCdr.Duration, err = ParseDurationWithSecs(durStr); err != nil && fieldsMandatory { - return nil, err - } - } - rtCdr.ExtraFields = make(map[string]string, len(extraFlds)) - for _, fldName := range extraFlds { - if fldVal, hasKey := cgrCdr[fldName]; !hasKey && fieldsMandatory { - return nil, errors.New(fmt.Sprintf("%s:%s", ERR_MANDATORY_IE_MISSING, fldName)) - } else { - rtCdr.ExtraFields[fldName] = fldVal - } - } - rtCdr.CgrId = Sha1(rtCdr.AccId, rtCdr.SetupTime.String()) - return rtCdr, nil +func (cgrCdr CgrCdr) AsStoredCdr() *StoredCdr { + storCdr := new(StoredCdr) + storCdr.CgrId = cgrCdr.GetCgrId() + storCdr.AccId = cgrCdr.GetAccId() + storCdr.CdrHost = cgrCdr.GetCdrHost() + storCdr.CdrSource = cgrCdr.GetCdrSource() + storCdr.ReqType = cgrCdr.GetReqType() + storCdr.Direction = cgrCdr.GetDirection() + storCdr.Tenant = cgrCdr.GetTenant() + storCdr.Category = cgrCdr.GetCategory() + storCdr.Account = cgrCdr.GetAccount() + storCdr.Subject = cgrCdr.GetSubject() + storCdr.Destination = cgrCdr.GetDestination() + storCdr.SetupTime, _ = cgrCdr.GetSetupTime() // Not interested to process errors, should do them if necessary in a previous step + storCdr.AnswerTime, _ = cgrCdr.GetAnswerTime() + storCdr.Duration, _ = cgrCdr.GetDuration() + storCdr.ExtraFields = cgrCdr.GetExtraFields() + storCdr.Cost = -1 + return storCdr } diff --git a/utils/cgrcdr_test.go b/utils/cgrcdr_test.go index 888ed49d2..d59105b13 100644 --- a/utils/cgrcdr_test.go +++ b/utils/cgrcdr_test.go @@ -28,9 +28,13 @@ import ( curl --data "accid=asbfdsaf&cdrhost=192.168.1.1&reqtype=rated&direction=*out&tenant=cgrates.org&tor=call&account=1001&subject=1001&destination=1002&time_answer=1383813746&duration=10&field_extr1=val_extr1&fieldextr2=valextr2" http://ipbxdev:2080/cgr */ +func TestCgrCdrInterfaces(t *testing.T) { + var _ RawCdr = make(CgrCdr) +} + func TestCgrCdrFields(t *testing.T) { - cgrCdr := CgrCdr{"accid": "dsafdsaf", "cdrhost": "192.168.1.1", "reqtype": "rated", "direction": "*out", "tenant": "cgrates.org", "tor": "call", - "account": "1001", "subject": "1001", "destination": "1002", "setup_time": "2013-11-07T08:42:20Z", "answer_time": "2013-11-07T08:42:26Z", "duration": "10", + cgrCdr := CgrCdr{ACCID: "dsafdsaf", CDRHOST: "192.168.1.1", REQTYPE: "rated", DIRECTION: "*out", TENANT: "cgrates.org", CATEGORY: "call", + ACCOUNT: "1001", SUBJECT: "1001", DESTINATION: "1002", SETUP_TIME: "2013-11-07T08:42:20Z", ANSWER_TIME: "2013-11-07T08:42:26Z", DURATION: "10", "field_extr1": "val_extr1", "fieldextr2": "valextr2"} setupTime, _ := ParseTimeDetectLayout("2013-11-07T08:42:20Z") if cgrCdr.GetCgrId() != Sha1("dsafdsaf", setupTime.String()) { @@ -85,76 +89,16 @@ func TestCgrCdrFields(t *testing.T) { } } -func TestCgrCdrForkCdr(t *testing.T) { - sampleCdr1 := &CgrCdr{"accid": "dsafdsaf", "cdrhost": "192.168.1.1", "cdrsource": "source_test", "reqtype": "rated", "direction": "*out", "tenant": "cgrates.org", "tor": "call", - "account": "1001", "subject": "1001", "destination": "1002", "setup_time": "2013-11-07T08:42:24Z", "answer_time": "2013-11-07T08:42:26Z", "duration": "10", +func TestCgrCdrAsStoredCdr(t *testing.T) { + cgrCdr := CgrCdr{ACCID: "dsafdsaf", CDRHOST: "192.168.1.1", CDRSOURCE: "internal_test", REQTYPE: "rated", DIRECTION: "*out", TENANT: "cgrates.org", CATEGORY: "call", + ACCOUNT: "1001", SUBJECT: "1001", DESTINATION: "1002", SETUP_TIME: "2013-11-07T08:42:20Z", ANSWER_TIME: "2013-11-07T08:42:26Z", DURATION: "10", "field_extr1": "val_extr1", "fieldextr2": "valextr2"} - rtSampleCdrOut, err := sampleCdr1.ForkCdr("sample_run1", "reqtype", "direction", "tenant", "tor", "account", "subject", "destination", "setup_time", "answer_time", "duration", - []string{}, true) - if err != nil { - t.Error("Unexpected error received", err) - } - setupTime1 := time.Date(2013, 11, 7, 8, 42, 24, 0, time.UTC) - expctSplRatedCdr := &StoredCdr{CgrId: Sha1("dsafdsaf", setupTime1.String()), AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: "source_test", ReqType: "rated", - Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", - SetupTime: setupTime1, AnswerTime: time.Unix(1383813746, 0).UTC(), - Duration: 10000000000, ExtraFields: map[string]string{}, MediationRunId: "sample_run1", Cost: -1} - if !reflect.DeepEqual(expctSplRatedCdr, rtSampleCdrOut) { - t.Errorf("Expected: %v, received: %v", expctSplRatedCdr, rtSampleCdrOut) - } - cgrCdr := &CgrCdr{"accid": "dsafdsaf", "cdrhost": "192.168.1.1", "cdrsource": "source_test", "reqtype": "rated", "direction": "*out", "tenant": "cgrates.org", "tor": "call", - "account": "1001", "subject": "1001", "destination": "1002", "setup_time": "2013-11-07T08:42:24Z", "answer_time": "2013-11-07T08:42:26Z", "duration": "10", - "field_extr1": "val_extr1", "fieldextr2": "valextr2"} - rtCdrOut, err := cgrCdr.ForkCdr("wholesale_run", "reqtype", "direction", "tenant", "tor", "account", "subject", "destination", "setup_time", "answer_time", "duration", - []string{"field_extr1", "fieldextr2"}, true) - if err != nil { - t.Error("Unexpected error received", err) - } - setupTime, _ := ParseTimeDetectLayout("2013-11-07T08:42:24Z") - expctRatedCdr := &StoredCdr{CgrId: Sha1("dsafdsaf", setupTime.String()), AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: "source_test", ReqType: "rated", - Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", - SetupTime: time.Unix(1383813744, 0).UTC(), AnswerTime: time.Unix(1383813746, 0).UTC(), - Duration: 10000000000, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, MediationRunId: "wholesale_run", Cost: -1} - if !reflect.DeepEqual(rtCdrOut, expctRatedCdr) { - t.Errorf("Received: %v, expected: %v", rtCdrOut, expctRatedCdr) - } - rtCdrOut2, err := cgrCdr.ForkCdr("wholesale_run", "^postpaid", "^*in", "^cgrates.com", "^premium_call", "^first_account", "^first_subject", "destination", - "^2013-12-07T08:42:24Z", "^2013-12-07T08:42:26Z", "^12s", []string{"field_extr1", "fieldextr2"}, true) - if err != nil { - t.Error("Unexpected error received", err) - } - expctRatedCdr2 := &StoredCdr{CgrId: Sha1("dsafdsaf", time.Date(2013, 12, 7, 8, 42, 24, 0, time.UTC).String()), AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: "source_test", ReqType: "postpaid", - Direction: "*in", Tenant: "cgrates.com", Category: "premium_call", Account: "first_account", Subject: "first_subject", Destination: "1002", - SetupTime: time.Date(2013, 12, 7, 8, 42, 24, 0, time.UTC), - AnswerTime: time.Date(2013, 12, 7, 8, 42, 26, 0, time.UTC), Duration: time.Duration(12) * time.Second, - ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, MediationRunId: "wholesale_run", Cost: -1} - if !reflect.DeepEqual(rtCdrOut2, expctRatedCdr2) { - t.Errorf("Received: %v, expected: %v", rtCdrOut2, expctRatedCdr2) - } - _, err = cgrCdr.ForkCdr("wholesale_run", "dummy_header", "direction", "tenant", "tor", "account", "subject", "destination", "setup_time", "answer_time", "duration", - []string{"field_extr1", "fieldextr2"}, true) - if err == nil { - t.Error("Failed to detect missing header") - } -} - -func TestCgrCdrForkCdrFromMetaDefaults(t *testing.T) { - cgrCdr := &CgrCdr{"accid": "dsafdsaf", "cdrhost": "192.168.1.1", "cdrsource": "source_test", "reqtype": "rated", "direction": "*out", "tenant": "cgrates.org", "tor": "call", - "account": "1001", "subject": "1001", "destination": "1002", "setup_time": "2013-11-07T08:42:24Z", "answer_time": "2013-11-07T08:42:26Z", "duration": "10", - "field_extr1": "val_extr1", "fieldextr2": "valextr2"} - setupTime := time.Date(2013, 11, 7, 8, 42, 24, 0, time.UTC) - expctCdr := &StoredCdr{CgrId: Sha1("dsafdsaf", setupTime.String()), AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: "source_test", ReqType: "rated", - Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", - SetupTime: setupTime, AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), - Duration: time.Duration(10) * time.Second, - ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, MediationRunId: "wholesale_run", Cost: -1} - cdrOut, err := cgrCdr.ForkCdr("wholesale_run", META_DEFAULT, META_DEFAULT, META_DEFAULT, META_DEFAULT, META_DEFAULT, META_DEFAULT, META_DEFAULT, - META_DEFAULT, META_DEFAULT, META_DEFAULT, []string{"field_extr1", "fieldextr2"}, true) - if err != nil { - t.Fatal("Unexpected error received", err) - } - - if !reflect.DeepEqual(expctCdr, cdrOut) { - t.Errorf("Expected: %v, received: %v", expctCdr, cdrOut) + setupTime, _ := ParseTimeDetectLayout(cgrCdr["setup_time"]) + expctRtCdr := &StoredCdr{CgrId: Sha1(cgrCdr["accid"], setupTime.String()), AccId: cgrCdr["accid"], CdrHost: cgrCdr["cdrhost"], CdrSource: cgrCdr["cdrsource"], ReqType: cgrCdr["reqtype"], + Direction: cgrCdr[DIRECTION], Tenant: cgrCdr["tenant"], Category: cgrCdr[CATEGORY], Account: cgrCdr["account"], Subject: cgrCdr["subject"], + Destination: cgrCdr["destination"], SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), Duration: time.Duration(10) * time.Second, + ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: -1} + if storedCdr := cgrCdr.AsStoredCdr(); !reflect.DeepEqual(expctRtCdr, storedCdr) { + t.Errorf("Expecting %v, received: %v", expctRtCdr, storedCdr) } } diff --git a/utils/consts.go b/utils/consts.go index 4db9ee727..291b49be4 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -81,7 +81,7 @@ const ( REQTYPE = "reqtype" DIRECTION = "direction" TENANT = "tenant" - Category = "tor" + CATEGORY = "category" ACCOUNT = "account" SUBJECT = "subject" DESTINATION = "destination" @@ -92,7 +92,7 @@ const ( COST = "cost" DEFAULT_RUNID = "default" STATIC_VALUE_PREFIX = "^" - CDRE_CSV = "csv" + CSV = "csv" CDRE_DRYRUN = "dry_run" INTERNAL = "internal" ZERO_RATING_SUBJECT_PREFIX = "*zero" @@ -104,8 +104,11 @@ const ( CONCATENATED_KEY_SEP = ":" META_DEFAULT = "*default" FORKED_CDR = "forked_cdr" + UNIT_TEST = "UNIT_TEST" + HDR_VAL_SEP = "/" ) var ( - CdreCdrFormats = []string{CDRE_CSV, CDRE_DRYRUN, CDRE_FIXED_WIDTH} + CdreCdrFormats = []string{CSV, CDRE_DRYRUN, CDRE_FIXED_WIDTH} + PrimaryCdrFields = []string{ACCID, CDRHOST, CDRSOURCE, REQTYPE, DIRECTION, TENANT, CATEGORY, ACCOUNT, SUBJECT, DESTINATION, SETUP_TIME, ANSWER_TIME, DURATION} ) diff --git a/utils/rawcdr.go b/utils/rawcdr.go index 533467cec..d797ac05a 100644 --- a/utils/rawcdr.go +++ b/utils/rawcdr.go @@ -18,28 +18,7 @@ along with this program. If not, see package utils -import ( - "time" -) - -var PrimaryCdrFields []string = []string{ACCID, CDRHOST, CDRSOURCE, REQTYPE, DIRECTION, TENANT, Category, ACCOUNT, SUBJECT, DESTINATION, SETUP_TIME, ANSWER_TIME, DURATION} - -// RawCDR is the type containing all the original CDR fields, needs it as it is for later usage -type RawCDR interface { - GetCgrId() string - GetAccId() string - GetCdrHost() string - GetCdrSource() string - GetDirection() string - GetSubject() string - GetAccount() string - GetDestination() string - GetCategory() string - GetTenant() string - GetReqType() string - GetSetupTime() (time.Time, error) // Time when the call was set-up - GetAnswerTime() (time.Time, error) // Time when the call was answered - GetDuration() (time.Duration, error) - GetExtraFields() map[string]string //Stores extra CDR Fields - ForkCdr(string, string, string, string, string, string, string, string, string, string, string, []string, bool) (*StoredCdr, error) // Based on fields queried will return a particular instance of RatedCDR +// RawCDR is the original CDR received from external sources (eg: FreeSWITCH) +type RawCdr interface { + AsStoredCdr() *StoredCdr // Convert the inbound Cdr into internally used one, CgrCdr } diff --git a/utils/rsrfield.go b/utils/rsrfield.go index ed5c11695..aed9133cc 100644 --- a/utils/rsrfield.go +++ b/utils/rsrfield.go @@ -28,6 +28,15 @@ func NewRSRField(fldStr string) (*RSRField, error) { if len(fldStr) == 0 { return nil, nil } + if strings.HasPrefix(fldStr, STATIC_VALUE_PREFIX) { // Special case when RSR is defined as static header/value + var staticHdr, staticVal string + if splt := strings.Split(fldStr, HDR_VAL_SEP); len(splt) == 2 { // Using | as separator since ':' is often use in date/time fields + staticHdr, staticVal = splt[0][1:], splt[1] + } else { + staticHdr, staticVal = splt[0][1:], splt[0][1:] // If no split, header will remain as original, value as header without the prefix + } + return &RSRField{Id: staticHdr, staticValue: staticVal}, nil + } if !strings.HasPrefix(fldStr, REGEXP_PREFIX) { return &RSRField{Id: fldStr}, nil } @@ -53,12 +62,16 @@ func NewRSRField(fldStr string) (*RSRField, error) { } type RSRField struct { - Id string // Identifier - RSRules []*ReSearchReplace // Rules to use when processing field value + Id string // Identifier + RSRules []*ReSearchReplace // Rules to use when processing field value + staticValue string // If defined, enforces parsing always to this value } // Parse the field value from a string func (rsrf *RSRField) ParseValue(value string) string { + if len(rsrf.staticValue) != 0 { // Enforce parsing of static values + return rsrf.staticValue + } if len(value) == 0 { return value } diff --git a/utils/rsrfield_test.go b/utils/rsrfield_test.go index 4b0e407ed..539b319ce 100644 --- a/utils/rsrfield_test.go +++ b/utils/rsrfield_test.go @@ -80,3 +80,20 @@ func TestConvertPlusNationalAnd00(t *testing.T) { t.Errorf("Expecting: 003186517174963, received: %s", parsedVal) } } + +func TestRSRParseStatic(t *testing.T) { + if rsrField, err := NewRSRField("^static_header/static_value"); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(rsrField, &RSRField{Id: "static_header", staticValue: "static_value"}) { + t.Errorf("Unexpected RSRField received: %v", rsrField) + } else if parsed := rsrField.ParseValue("dynamic_value"); parsed != "static_value" { + t.Errorf("Expected: %s, received: %s", "static_value", parsed) + } + if rsrField, err := NewRSRField(`^static_hdrvalue`); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(rsrField, &RSRField{Id: "static_hdrvalue", staticValue: "static_hdrvalue"}) { + t.Errorf("Unexpected RSRField received: %v", rsrField) + } else if parsed := rsrField.ParseValue("dynamic_value"); parsed != "static_hdrvalue" { + t.Errorf("Expected: %s, received: %s", "static_hdrvalue", parsed) + } +} diff --git a/utils/storedcdr.go b/utils/storedcdr.go index 0d799567c..f517b4123 100644 --- a/utils/storedcdr.go +++ b/utils/storedcdr.go @@ -19,40 +19,15 @@ along with this program. If not, see package utils import ( + "errors" + "fmt" "math" "net/url" "strconv" "time" ) -func NewStoredCdrFromRawCDR(rawcdr RawCDR) (*StoredCdr, error) { - var err error - strCdr := new(StoredCdr) - strCdr.CgrId = rawcdr.GetCgrId() - strCdr.AccId = rawcdr.GetAccId() - strCdr.CdrHost = rawcdr.GetCdrHost() - strCdr.CdrSource = rawcdr.GetCdrSource() - strCdr.ReqType = rawcdr.GetReqType() - strCdr.Direction = rawcdr.GetDirection() - strCdr.Tenant = rawcdr.GetTenant() - strCdr.Category = rawcdr.GetCategory() - strCdr.Account = rawcdr.GetAccount() - strCdr.Subject = rawcdr.GetSubject() - strCdr.Destination = rawcdr.GetDestination() - if strCdr.SetupTime, err = rawcdr.GetSetupTime(); err != nil { - return nil, err - } - if strCdr.AnswerTime, err = rawcdr.GetAnswerTime(); err != nil { - return nil, err - } - strCdr.Duration, _ = rawcdr.GetDuration() - strCdr.ExtraFields = rawcdr.GetExtraFields() - strCdr.MediationRunId = DEFAULT_RUNID - strCdr.Cost = -1 - return strCdr, nil -} - -// Rated CDR as extracted from StorDb. Kinda standard of internal CDR, complies to CDR interface also +// Kinda standard of internal CDR, complies to CDR interface also type StoredCdr struct { CgrId string OrderId int64 // Stor order id used as export order id @@ -74,68 +49,6 @@ type StoredCdr struct { Cost float64 } -// Methods maintaining RawCDR interface - -func (storedCdr *StoredCdr) GetCgrId() string { - return storedCdr.CgrId -} - -func (storedCdr *StoredCdr) GetAccId() string { - return storedCdr.AccId -} - -func (storedCdr *StoredCdr) GetCdrHost() string { - return storedCdr.CdrHost -} - -func (storedCdr *StoredCdr) GetCdrSource() string { - return storedCdr.CdrSource -} - -func (storedCdr *StoredCdr) GetDirection() string { - return storedCdr.Direction -} - -func (storedCdr *StoredCdr) GetSubject() string { - return storedCdr.Subject -} - -func (storedCdr *StoredCdr) GetAccount() string { - return storedCdr.Account -} - -func (storedCdr *StoredCdr) GetDestination() string { - return storedCdr.Destination -} - -func (storedCdr *StoredCdr) GetCategory() string { - return storedCdr.Category -} - -func (storedCdr *StoredCdr) GetTenant() string { - return storedCdr.Tenant -} - -func (storedCdr *StoredCdr) GetReqType() string { - return storedCdr.ReqType -} - -func (storedCdr *StoredCdr) GetSetupTime() (time.Time, error) { - return storedCdr.SetupTime, nil -} - -func (storedCdr *StoredCdr) GetAnswerTime() (time.Time, error) { - return storedCdr.AnswerTime, nil -} - -func (storedCdr *StoredCdr) GetDuration() (time.Duration, error) { - return storedCdr.Duration, nil -} - -func (storedCdr *StoredCdr) GetExtraFields() map[string]string { - return storedCdr.ExtraFields -} - // Return cost as string, formated with number of decimals configured func (storedCdr *StoredCdr) FormatCost(shiftDecimals, roundDecimals int) string { cost := storedCdr.Cost @@ -145,81 +58,165 @@ func (storedCdr *StoredCdr) FormatCost(shiftDecimals, roundDecimals int) string return strconv.FormatFloat(cost, 'f', roundDecimals, 64) } -// Converts part of the rated Cdr as httpForm used to post remotely to CDRS -func (storedCdr *StoredCdr) AsRawCdrHttpForm() url.Values { +// Used to retrieve fields as string, primary fields are const labeled +func (storedCdr *StoredCdr) FieldAsString(rsrFld *RSRField) string { + switch rsrFld.Id { + case CGRID: + return rsrFld.ParseValue(storedCdr.CgrId) + case ORDERID: + return rsrFld.ParseValue(strconv.FormatInt(storedCdr.OrderId, 10)) + case ACCID: + return rsrFld.ParseValue(storedCdr.AccId) + case CDRHOST: + return rsrFld.ParseValue(storedCdr.CdrHost) + case CDRSOURCE: + return rsrFld.ParseValue(storedCdr.CdrSource) + case REQTYPE: + return rsrFld.ParseValue(storedCdr.ReqType) + case DIRECTION: + return rsrFld.ParseValue(storedCdr.Direction) + case TENANT: + return rsrFld.ParseValue(storedCdr.Tenant) + case CATEGORY: + return rsrFld.ParseValue(storedCdr.Category) + case ACCOUNT: + return rsrFld.ParseValue(storedCdr.Account) + case SUBJECT: + return rsrFld.ParseValue(storedCdr.Subject) + case DESTINATION: + return rsrFld.ParseValue(storedCdr.Destination) + case SETUP_TIME: + return rsrFld.ParseValue(storedCdr.SetupTime.String()) + case ANSWER_TIME: + return rsrFld.ParseValue(storedCdr.AnswerTime.String()) + case DURATION: + return rsrFld.ParseValue(strconv.FormatFloat(storedCdr.Duration.Seconds(), 'f', -1, 64)) + case MEDI_RUNID: + return rsrFld.ParseValue(storedCdr.MediationRunId) + case COST: + return rsrFld.ParseValue(strconv.FormatFloat(storedCdr.Cost, 'f', -1, 64)) // Recommended to use FormatCost + default: + return rsrFld.ParseValue(storedCdr.ExtraFields[rsrFld.Id]) + } +} + +func (storedCdr *StoredCdr) AsStoredCdr() *StoredCdr { + return storedCdr +} + +// Ability to send the CgrCdr remotely to another CDR server +func (storedCdr *StoredCdr) AsHttpForm() url.Values { v := url.Values{} + for fld, val := range storedCdr.ExtraFields { + v.Set(fld, val) + } v.Set(ACCID, storedCdr.AccId) v.Set(CDRHOST, storedCdr.CdrHost) v.Set(CDRSOURCE, storedCdr.CdrSource) v.Set(REQTYPE, storedCdr.ReqType) v.Set(DIRECTION, storedCdr.Direction) v.Set(TENANT, storedCdr.Tenant) - v.Set(Category, storedCdr.Category) + v.Set(CATEGORY, storedCdr.Category) v.Set(ACCOUNT, storedCdr.Account) v.Set(SUBJECT, storedCdr.Subject) v.Set(DESTINATION, storedCdr.Destination) v.Set(SETUP_TIME, storedCdr.SetupTime.String()) v.Set(ANSWER_TIME, storedCdr.AnswerTime.String()) v.Set(DURATION, strconv.FormatFloat(storedCdr.Duration.Seconds(), 'f', -1, 64)) - for fld, val := range storedCdr.ExtraFields { - v.Set(fld, val) - } return v } -// Used to export fields as string, primary fields are const labeled -func (storedCdr *StoredCdr) ExportFieldValue(fldName string) string { - switch fldName { - case CGRID: - return storedCdr.CgrId - case ORDERID: - return strconv.FormatInt(storedCdr.OrderId, 10) - case ACCID: - return storedCdr.AccId - case CDRHOST: - return storedCdr.CdrHost - case CDRSOURCE: - return storedCdr.CdrSource - case REQTYPE: - return storedCdr.ReqType - case DIRECTION: - return storedCdr.Direction - case TENANT: - return storedCdr.Tenant - case Category: - return storedCdr.Category - case ACCOUNT: - return storedCdr.Account - case SUBJECT: - return storedCdr.Subject - case DESTINATION: - return storedCdr.Destination - case SETUP_TIME: - return storedCdr.SetupTime.String() - case ANSWER_TIME: - return storedCdr.AnswerTime.String() - case DURATION: - return strconv.FormatFloat(storedCdr.Duration.Seconds(), 'f', -1, 64) - case MEDI_RUNID: - return storedCdr.MediationRunId - case COST: - return strconv.FormatFloat(storedCdr.Cost, 'f', -1, 64) // Recommended to use FormatCost - default: - return storedCdr.ExtraFields[fldName] +// Used in mediation, primaryMandatory marks whether missing field out of request represents error or can be ignored +func (storedCdr *StoredCdr) ForkCdr(runId string, reqTypeFld, directionFld, tenantFld, categFld, accountFld, subjectFld, destFld, setupTimeFld, answerTimeFld, durationFld *RSRField, + extraFlds []*RSRField, primaryMandatory bool) (*StoredCdr, error) { + // MetaDefault will automatically be converted to their standard values + if reqTypeFld.Id == META_DEFAULT { + reqTypeFld.Id = REQTYPE } -} - -// Converts to CgrCdr, so we can fork with less code -func (storedCdr *StoredCdr) AsCgrCdr() CgrCdr { - cgrCdr := make(CgrCdr) - for _, fldName := range PrimaryCdrFields { - cgrCdr[fldName] = storedCdr.ExportFieldValue(fldName) + if directionFld.Id == META_DEFAULT { + directionFld.Id = DIRECTION } - return cgrCdr -} - -func (storedCdr *StoredCdr) ForkCdr(runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, - setupTimeFld, answerTimeFld, durationFld string, extraFlds []string, fieldsMandatory bool) (*StoredCdr, error) { - return storedCdr.AsCgrCdr().ForkCdr(runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, - setupTimeFld, answerTimeFld, durationFld, extraFlds, fieldsMandatory) + if tenantFld.Id == META_DEFAULT { + tenantFld.Id = TENANT + } + if categFld.Id == META_DEFAULT { + categFld.Id = CATEGORY + } + if accountFld.Id == META_DEFAULT { + accountFld.Id = ACCOUNT + } + if subjectFld.Id == META_DEFAULT { + subjectFld.Id = SUBJECT + } + if destFld.Id == META_DEFAULT { + destFld.Id = DESTINATION + } + if setupTimeFld.Id == META_DEFAULT { + setupTimeFld.Id = SETUP_TIME + } + if answerTimeFld.Id == META_DEFAULT { + answerTimeFld.Id = ANSWER_TIME + } + if durationFld.Id == META_DEFAULT { + durationFld.Id = DURATION + } + var err error + frkStorCdr := new(StoredCdr) + frkStorCdr.CgrId = storedCdr.CgrId + frkStorCdr.MediationRunId = runId + frkStorCdr.Cost = -1.0 // Default for non-rated CDR + frkStorCdr.AccId = storedCdr.AccId + frkStorCdr.CdrHost = storedCdr.CdrHost + frkStorCdr.CdrSource = storedCdr.CdrSource + frkStorCdr.ReqType = storedCdr.FieldAsString(reqTypeFld) + if primaryMandatory && len(frkStorCdr.ReqType) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, REQTYPE, reqTypeFld.Id)) + } + frkStorCdr.Direction = storedCdr.FieldAsString(directionFld) + if primaryMandatory && len(frkStorCdr.Direction) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, DIRECTION, directionFld.Id)) + } + frkStorCdr.Tenant = storedCdr.FieldAsString(tenantFld) + if primaryMandatory && len(frkStorCdr.Tenant) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, TENANT, tenantFld.Id)) + } + frkStorCdr.Category = storedCdr.FieldAsString(categFld) + if primaryMandatory && len(frkStorCdr.Category) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, CATEGORY, categFld.Id)) + } + frkStorCdr.Account = storedCdr.FieldAsString(accountFld) + if primaryMandatory && len(frkStorCdr.Account) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, ACCOUNT, accountFld.Id)) + } + frkStorCdr.Subject = storedCdr.FieldAsString(subjectFld) + if primaryMandatory && len(frkStorCdr.Subject) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, SUBJECT, subjectFld.Id)) + } + frkStorCdr.Destination = storedCdr.FieldAsString(destFld) + if primaryMandatory && len(frkStorCdr.Destination) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, DESTINATION, destFld.Id)) + } + sTimeStr := storedCdr.FieldAsString(setupTimeFld) + if primaryMandatory && len(sTimeStr) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, SETUP_TIME, setupTimeFld.Id)) + } else if frkStorCdr.SetupTime, err = ParseTimeDetectLayout(sTimeStr); err != nil { + return nil, err + } + aTimeStr := storedCdr.FieldAsString(answerTimeFld) + if primaryMandatory && len(aTimeStr) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, ANSWER_TIME, answerTimeFld.Id)) + } else if frkStorCdr.AnswerTime, err = ParseTimeDetectLayout(aTimeStr); err != nil { + return nil, err + } + durStr := storedCdr.FieldAsString(durationFld) + if primaryMandatory && len(durStr) == 0 { + return nil, errors.New(fmt.Sprintf("%s:%s:%s", ERR_MANDATORY_IE_MISSING, DURATION, durationFld.Id)) + } else if frkStorCdr.Duration, err = ParseDurationWithSecs(durStr); err != nil { + return nil, err + } + frkStorCdr.ExtraFields = make(map[string]string, len(extraFlds)) + for _, fld := range extraFlds { + frkStorCdr.ExtraFields[fld.Id] = storedCdr.FieldAsString(fld) + } + return frkStorCdr, nil } diff --git a/utils/storedcdr_test.go b/utils/storedcdr_test.go index 99a50367b..59b68796a 100644 --- a/utils/storedcdr_test.go +++ b/utils/storedcdr_test.go @@ -25,166 +25,54 @@ import ( ) func TestStoredCdrInterfaces(t *testing.T) { - ratedCdr := new(StoredCdr) - var _ RawCDR = ratedCdr + storedCdr := new(StoredCdr) + var _ RawCdr = storedCdr } -func TestNewStoredCdrFromRawCDR(t *testing.T) { - cgrCdr := CgrCdr{"accid": "dsafdsaf", "cdrhost": "192.168.1.1", "cdrsource": "internal_test", "reqtype": "rated", "direction": "*out", "tenant": "cgrates.org", "tor": "call", - "account": "1001", "subject": "1001", "destination": "1002", "setup_time": "2013-11-07T08:42:20Z", "answer_time": "2013-11-07T08:42:26Z", "duration": "10", - "field_extr1": "val_extr1", "fieldextr2": "valextr2"} - setupTime, _ := ParseTimeDetectLayout(cgrCdr["setup_time"]) - expctRtCdr := &StoredCdr{CgrId: Sha1(cgrCdr["accid"], setupTime.String()), AccId: cgrCdr["accid"], CdrHost: cgrCdr["cdrhost"], CdrSource: cgrCdr["cdrsource"], ReqType: cgrCdr["reqtype"], - Direction: cgrCdr["direction"], Tenant: cgrCdr["tenant"], Category: cgrCdr["tor"], Account: cgrCdr["account"], Subject: cgrCdr["subject"], - Destination: cgrCdr["destination"], SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), Duration: time.Duration(10) * time.Second, - ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, MediationRunId: DEFAULT_RUNID, Cost: -1} - if rt, err := NewStoredCdrFromRawCDR(cgrCdr); err != nil { - t.Error(err) - } else if !reflect.DeepEqual(rt, expctRtCdr) { - t.Errorf("Received %v, expected: %v", rt, expctRtCdr) - } -} - -func TestStoredCdrFields(t *testing.T) { - ratedCdr := StoredCdr{CgrId: Sha1("dsafdsaf", time.Unix(1383813746, 0).String()), AccId: "dsafdsaf", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", - Category: "call", Account: "1001", Subject: "1001", Destination: "1002", SetupTime: time.Unix(1383813746, 0), AnswerTime: time.Unix(1383813746, 0), Duration: 10, - ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, - } - if ratedCdr.GetCgrId() != Sha1("dsafdsaf", time.Unix(1383813746, 0).String()) { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetAccId() != "dsafdsaf" { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetCdrHost() != "192.168.1.1" { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetDirection() != "*out" { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetSubject() != "1001" { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetAccount() != "1001" { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetDestination() != "1002" { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetCategory() != "call" { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetTenant() != "cgrates.org" { - t.Error("Error parsing cdr: ", ratedCdr) - } - if ratedCdr.GetReqType() != RATED { - t.Error("Error parsing cdr: ", ratedCdr) - } - setupTime, _ := ratedCdr.GetSetupTime() - expectedSTime, _ := time.Parse(time.RFC3339, "2013-11-07T08:42:26Z") - if setupTime.UTC() != expectedSTime { - t.Error("Error parsing cdr: ", ratedCdr) - } - answerTime, _ := ratedCdr.GetAnswerTime() - expectedATime, _ := time.Parse(time.RFC3339, "2013-11-07T08:42:26Z") - if answerTime.UTC() != expectedATime { - t.Error("Error parsing cdr: ", ratedCdr) - } - dur, _ := ratedCdr.GetDuration() - if dur != 10 { - t.Error("Error parsing cdr: ", ratedCdr) - } - extraFields := ratedCdr.GetExtraFields() - if len(extraFields) != 2 { - t.Error("Error parsing extra fields: ", extraFields) - } - if extraFields["field_extr1"] != "val_extr1" { - t.Error("Error parsing extra fields: ", extraFields) - } - if ratedCdr.Cost != 1.01 { - t.Error("Error parsing cdr: ", ratedCdr) - } -} - -func TestAsRawCdrHttpForm(t *testing.T) { - ratedCdr := StoredCdr{CgrId: Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: "test", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", - Category: "call", Account: "1001", Subject: "1001", Destination: "1002", SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), - Duration: time.Duration(10) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, - } - cdrForm := ratedCdr.AsRawCdrHttpForm() - if cdrForm.Get(ACCID) != ratedCdr.AccId { - t.Errorf("Expected: %s, received: %s", ratedCdr.AccId, cdrForm.Get(ACCID)) - } - if cdrForm.Get(CDRHOST) != ratedCdr.CdrHost { - t.Errorf("Expected: %s, received: %s", ratedCdr.CdrHost, cdrForm.Get(CDRHOST)) - } - if cdrForm.Get(CDRSOURCE) != ratedCdr.CdrSource { - t.Errorf("Expected: %s, received: %s", ratedCdr.CdrSource, cdrForm.Get(CDRSOURCE)) - } - if cdrForm.Get(REQTYPE) != ratedCdr.ReqType { - t.Errorf("Expected: %s, received: %s", ratedCdr.ReqType, cdrForm.Get(REQTYPE)) - } - if cdrForm.Get(DIRECTION) != ratedCdr.Direction { - t.Errorf("Expected: %s, received: %s", ratedCdr.Direction, cdrForm.Get(DIRECTION)) - } - if cdrForm.Get(TENANT) != ratedCdr.Tenant { - t.Errorf("Expected: %s, received: %s", ratedCdr.Tenant, cdrForm.Get(TENANT)) - } - if cdrForm.Get(Category) != ratedCdr.Category { - t.Errorf("Expected: %s, received: %s", ratedCdr.Category, cdrForm.Get(Category)) - } - if cdrForm.Get(ACCOUNT) != ratedCdr.Account { - t.Errorf("Expected: %s, received: %s", ratedCdr.Account, cdrForm.Get(ACCOUNT)) - } - if cdrForm.Get(SUBJECT) != ratedCdr.Subject { - t.Errorf("Expected: %s, received: %s", ratedCdr.Subject, cdrForm.Get(SUBJECT)) - } - if cdrForm.Get(DESTINATION) != ratedCdr.Destination { - t.Errorf("Expected: %s, received: %s", ratedCdr.Destination, cdrForm.Get(DESTINATION)) - } - if cdrForm.Get(SETUP_TIME) != "2013-11-07 08:42:20 +0000 UTC" { - t.Errorf("Expected: %s, received: %s", "2013-11-07 08:42:26 +0000 UTC", cdrForm.Get(SETUP_TIME)) - } - if cdrForm.Get(ANSWER_TIME) != "2013-11-07 08:42:26 +0000 UTC" { - t.Errorf("Expected: %s, received: %s", "2013-11-07 08:42:26 +0000 UTC", cdrForm.Get(ANSWER_TIME)) - } - if cdrForm.Get(DURATION) != "10" { - t.Errorf("Expected: %s, received: %s", "10", cdrForm.Get(DURATION)) - } - if cdrForm.Get("field_extr1") != ratedCdr.ExtraFields["field_extr1"] { - t.Errorf("Expected: %s, received: %s", ratedCdr.ExtraFields["field_extr1"], cdrForm.Get("field_extr1")) - } - if cdrForm.Get("fieldextr2") != ratedCdr.ExtraFields["fieldextr2"] { - t.Errorf("Expected: %s, received: %s", ratedCdr.ExtraFields["fieldextr2"], cdrForm.Get("fieldextr2")) - } -} - -func TestExportFieldValue(t *testing.T) { +func TestFieldAsString(t *testing.T) { cdr := StoredCdr{CgrId: Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), OrderId: 123, AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: "test", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", SetupTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), MediationRunId: DEFAULT_RUNID, Duration: time.Duration(10) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, } - if cdr.ExportFieldValue(CGRID) != cdr.CgrId || - cdr.ExportFieldValue(ORDERID) != "123" || - cdr.ExportFieldValue(ACCID) != cdr.AccId || - cdr.ExportFieldValue(CDRHOST) != cdr.CdrHost || - cdr.ExportFieldValue(CDRSOURCE) != cdr.CdrSource || - cdr.ExportFieldValue(REQTYPE) != cdr.ReqType || - cdr.ExportFieldValue(DIRECTION) != cdr.Direction || - cdr.ExportFieldValue(TENANT) != cdr.Tenant || - cdr.ExportFieldValue(Category) != cdr.Category || - cdr.ExportFieldValue(ACCOUNT) != cdr.Account || - cdr.ExportFieldValue(SUBJECT) != cdr.Subject || - cdr.ExportFieldValue(DESTINATION) != cdr.Destination || - cdr.ExportFieldValue(SETUP_TIME) != cdr.SetupTime.String() || - cdr.ExportFieldValue(ANSWER_TIME) != cdr.AnswerTime.String() || - cdr.ExportFieldValue(DURATION) != "10" || - cdr.ExportFieldValue(MEDI_RUNID) != cdr.MediationRunId || - cdr.ExportFieldValue(COST) != "1.01" || - cdr.ExportFieldValue("field_extr1") != cdr.ExtraFields["field_extr1"] || - cdr.ExportFieldValue("fieldextr2") != cdr.ExtraFields["fieldextr2"] || - cdr.ExportFieldValue("dummy_field") != "" { - t.Error("Unexpected filed value received") + if cdr.FieldAsString(&RSRField{Id: CGRID}) != cdr.CgrId || + cdr.FieldAsString(&RSRField{Id: ORDERID}) != "123" || + cdr.FieldAsString(&RSRField{Id: ACCID}) != cdr.AccId || + cdr.FieldAsString(&RSRField{Id: CDRHOST}) != cdr.CdrHost || + cdr.FieldAsString(&RSRField{Id: CDRSOURCE}) != cdr.CdrSource || + cdr.FieldAsString(&RSRField{Id: REQTYPE}) != cdr.ReqType || + cdr.FieldAsString(&RSRField{Id: DIRECTION}) != cdr.Direction || + cdr.FieldAsString(&RSRField{Id: CATEGORY}) != cdr.Category || + cdr.FieldAsString(&RSRField{Id: ACCOUNT}) != cdr.Account || + cdr.FieldAsString(&RSRField{Id: SUBJECT}) != cdr.Subject || + cdr.FieldAsString(&RSRField{Id: DESTINATION}) != cdr.Destination || + cdr.FieldAsString(&RSRField{Id: SETUP_TIME}) != cdr.SetupTime.String() || + cdr.FieldAsString(&RSRField{Id: ANSWER_TIME}) != cdr.AnswerTime.String() || + cdr.FieldAsString(&RSRField{Id: DURATION}) != "10" || + cdr.FieldAsString(&RSRField{Id: MEDI_RUNID}) != cdr.MediationRunId || + cdr.FieldAsString(&RSRField{Id: COST}) != "1.01" || + cdr.FieldAsString(&RSRField{Id: "field_extr1"}) != cdr.ExtraFields["field_extr1"] || + cdr.FieldAsString(&RSRField{Id: "fieldextr2"}) != cdr.ExtraFields["fieldextr2"] || + cdr.FieldAsString(&RSRField{Id: "dummy_field"}) != "" { + t.Error("Unexpected filed value received", + cdr.FieldAsString(&RSRField{Id: CGRID}) != cdr.CgrId, + cdr.FieldAsString(&RSRField{Id: ORDERID}) != "123", + cdr.FieldAsString(&RSRField{Id: ACCID}) != cdr.AccId, + cdr.FieldAsString(&RSRField{Id: CDRHOST}) != cdr.CdrHost, + cdr.FieldAsString(&RSRField{Id: CDRSOURCE}) != cdr.CdrSource, + cdr.FieldAsString(&RSRField{Id: REQTYPE}) != cdr.ReqType, + cdr.FieldAsString(&RSRField{Id: DIRECTION}) != cdr.Direction, + cdr.FieldAsString(&RSRField{Id: CATEGORY}) != cdr.Category, + cdr.FieldAsString(&RSRField{Id: ACCOUNT}) != cdr.Account, + cdr.FieldAsString(&RSRField{Id: SUBJECT}) != cdr.Subject, + cdr.FieldAsString(&RSRField{Id: DESTINATION}) != cdr.Destination, + cdr.FieldAsString(&RSRField{Id: SETUP_TIME}) != cdr.SetupTime.String(), + cdr.FieldAsString(&RSRField{Id: ANSWER_TIME}) != cdr.AnswerTime.String(), + cdr.FieldAsString(&RSRField{Id: DURATION}) != "10", + cdr.FieldAsString(&RSRField{Id: MEDI_RUNID}) != cdr.MediationRunId, + cdr.FieldAsString(&RSRField{Id: COST}) != "1.01", + cdr.FieldAsString(&RSRField{Id: "field_extr1"}) != cdr.ExtraFields["field_extr1"], + cdr.FieldAsString(&RSRField{Id: "fieldextr2"}) != cdr.ExtraFields["fieldextr2"], + cdr.FieldAsString(&RSRField{Id: "dummy_field"}) != "") } } @@ -207,3 +95,138 @@ func TestFormatCost(t *testing.T) { t.Error("Unexpected format of the cost: ", cdr.FormatCost(2, 3)) } } + +func TestStoredCdrAsHttpForm(t *testing.T) { + storCdr := StoredCdr{CgrId: Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), OrderId: 123, AccId: "dsafdsaf", CdrHost: "192.168.1.1", + CdrSource: UNIT_TEST, ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", + SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), MediationRunId: DEFAULT_RUNID, + Duration: time.Duration(10) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, + } + cdrForm := storCdr.AsHttpForm() + if cdrForm.Get(ACCID) != "dsafdsaf" { + t.Errorf("Expected: %s, received: %s", "dsafdsaf", cdrForm.Get(ACCID)) + } + if cdrForm.Get(CDRHOST) != "192.168.1.1" { + t.Errorf("Expected: %s, received: %s", "192.168.1.1", cdrForm.Get(CDRHOST)) + } + if cdrForm.Get(CDRSOURCE) != UNIT_TEST { + t.Errorf("Expected: %s, received: %s", UNIT_TEST, cdrForm.Get(CDRSOURCE)) + } + if cdrForm.Get(REQTYPE) != "rated" { + t.Errorf("Expected: %s, received: %s", "rated", cdrForm.Get(REQTYPE)) + } + if cdrForm.Get(DIRECTION) != "*out" { + t.Errorf("Expected: %s, received: %s", "*out", cdrForm.Get(DIRECTION)) + } + if cdrForm.Get(TENANT) != "cgrates.org" { + t.Errorf("Expected: %s, received: %s", "cgrates.org", cdrForm.Get(TENANT)) + } + if cdrForm.Get(CATEGORY) != "call" { + t.Errorf("Expected: %s, received: %s", "call", cdrForm.Get(CATEGORY)) + } + if cdrForm.Get(ACCOUNT) != "1001" { + t.Errorf("Expected: %s, received: %s", "1001", cdrForm.Get(ACCOUNT)) + } + if cdrForm.Get(SUBJECT) != "1001" { + t.Errorf("Expected: %s, received: %s", "1001", cdrForm.Get(SUBJECT)) + } + if cdrForm.Get(DESTINATION) != "1002" { + t.Errorf("Expected: %s, received: %s", "1002", cdrForm.Get(DESTINATION)) + } + if cdrForm.Get(SETUP_TIME) != "2013-11-07 08:42:20 +0000 UTC" { + t.Errorf("Expected: %s, received: %s", "2013-11-07 08:42:20 +0000 UTC", cdrForm.Get(SETUP_TIME)) + } + if cdrForm.Get(ANSWER_TIME) != "2013-11-07 08:42:26 +0000 UTC" { + t.Errorf("Expected: %s, received: %s", "2013-11-07 08:42:26 +0000 UTC", cdrForm.Get(ANSWER_TIME)) + } + if cdrForm.Get(DURATION) != "10" { + t.Errorf("Expected: %s, received: %s", "10", cdrForm.Get(DURATION)) + } + if cdrForm.Get("field_extr1") != "val_extr1" { + t.Errorf("Expected: %s, received: %s", "val_extr1", cdrForm.Get("field_extr1")) + } + if cdrForm.Get("fieldextr2") != "valextr2" { + t.Errorf("Expected: %s, received: %s", "valextr2", cdrForm.Get("fieldextr2")) + } +} + +func TestStoredCdrForkCdr(t *testing.T) { + storCdr := StoredCdr{CgrId: Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), OrderId: 123, AccId: "dsafdsaf", CdrHost: "192.168.1.1", + CdrSource: UNIT_TEST, ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", + SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), MediationRunId: DEFAULT_RUNID, + Duration: time.Duration(10) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "field_extr2": "valextr2"}, Cost: 1.01, + } + rtSampleCdrOut, err := storCdr.ForkCdr("sample_run1", &RSRField{Id: REQTYPE}, &RSRField{Id: DIRECTION}, &RSRField{Id: TENANT}, &RSRField{Id: CATEGORY}, + &RSRField{Id: ACCOUNT}, &RSRField{Id: SUBJECT}, &RSRField{Id: DESTINATION}, &RSRField{Id: SETUP_TIME}, &RSRField{Id: ANSWER_TIME}, &RSRField{Id: DURATION}, + []*RSRField{&RSRField{Id: "field_extr1"}, &RSRField{Id: "field_extr2"}}, true) + if err != nil { + t.Error("Unexpected error received", err) + } + expctSplRatedCdr := &StoredCdr{CgrId: storCdr.CgrId, AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: UNIT_TEST, ReqType: "rated", + Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", + SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), + Duration: time.Duration(10) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "field_extr2": "valextr2"}, MediationRunId: "sample_run1", Cost: -1} + if !reflect.DeepEqual(expctSplRatedCdr, rtSampleCdrOut) { + t.Errorf("Expected: %v, received: %v", expctSplRatedCdr, rtSampleCdrOut) + } +} + +func TestStoredCdrForkCdrStaticVals(t *testing.T) { + storCdr := StoredCdr{CgrId: Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderId: 123, AccId: "dsafdsaf", CdrHost: "192.168.1.1", + CdrSource: UNIT_TEST, ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", + SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), MediationRunId: DEFAULT_RUNID, + Duration: time.Duration(10) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, + } + rsrStPostpaid, _ := NewRSRField("^postpaid") + rsrStIn, _ := NewRSRField("^*in") + rsrStCgr, _ := NewRSRField("^cgrates.com") + rsrStPC, _ := NewRSRField("^premium_call") + rsrStFA, _ := NewRSRField("^first_account") + rsrStFS, _ := NewRSRField("^first_subject") + rsrStST, _ := NewRSRField("^2013-12-07T08:42:24Z") + rsrStAT, _ := NewRSRField("^2013-12-07T08:42:26Z") + rsrStDur, _ := NewRSRField("^12s") + rtCdrOut2, err := storCdr.ForkCdr("wholesale_run", rsrStPostpaid, rsrStIn, rsrStCgr, rsrStPC, rsrStFA, rsrStFS, &RSRField{Id: "destination"}, rsrStST, rsrStAT, rsrStDur, + []*RSRField{}, true) + + if err != nil { + t.Error("Unexpected error received", err) + } + expctRatedCdr2 := &StoredCdr{CgrId: storCdr.CgrId, AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: UNIT_TEST, ReqType: "postpaid", + Direction: "*in", Tenant: "cgrates.com", Category: "premium_call", Account: "first_account", Subject: "first_subject", Destination: "1002", + SetupTime: time.Date(2013, 12, 7, 8, 42, 24, 0, time.UTC), + AnswerTime: time.Date(2013, 12, 7, 8, 42, 26, 0, time.UTC), Duration: time.Duration(12) * time.Second, + ExtraFields: map[string]string{}, MediationRunId: "wholesale_run", Cost: -1} + if !reflect.DeepEqual(rtCdrOut2, expctRatedCdr2) { + t.Errorf("Received: %v, expected: %v", rtCdrOut2, expctRatedCdr2) + } + _, err = storCdr.ForkCdr("wholesale_run", &RSRField{Id: "dummy_header"}, &RSRField{Id: "direction"}, &RSRField{Id: "tenant"}, &RSRField{Id: "tor"}, &RSRField{Id: "account"}, + &RSRField{Id: "subject"}, &RSRField{Id: "destination"}, &RSRField{Id: "setup_time"}, &RSRField{Id: "answer_time"}, &RSRField{Id: "duration"}, + []*RSRField{}, true) + if err == nil { + t.Error("Failed to detect missing header") + } +} + +func TestStoredCdrForkCdrFromMetaDefaults(t *testing.T) { + storCdr := StoredCdr{CgrId: Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderId: 123, AccId: "dsafdsaf", CdrHost: "192.168.1.1", + CdrSource: UNIT_TEST, ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", + SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), MediationRunId: DEFAULT_RUNID, + Duration: time.Duration(10) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, + } + expctCdr := &StoredCdr{CgrId: storCdr.CgrId, AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: UNIT_TEST, ReqType: "rated", + Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", + SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), + Duration: time.Duration(10) * time.Second, + ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, MediationRunId: "wholesale_run", Cost: -1} + cdrOut, err := storCdr.ForkCdr("wholesale_run", &RSRField{Id: META_DEFAULT}, &RSRField{Id: META_DEFAULT}, &RSRField{Id: META_DEFAULT}, &RSRField{Id: META_DEFAULT}, + &RSRField{Id: META_DEFAULT}, &RSRField{Id: META_DEFAULT}, &RSRField{Id: META_DEFAULT}, + &RSRField{Id: META_DEFAULT}, &RSRField{Id: META_DEFAULT}, &RSRField{Id: META_DEFAULT}, []*RSRField{&RSRField{Id: "field_extr1"}, &RSRField{Id: "fieldextr2"}}, true) + if err != nil { + t.Fatal("Unexpected error received", err) + } + + if !reflect.DeepEqual(expctCdr, cdrOut) { + t.Errorf("Expected: %v, received: %v", expctCdr, cdrOut) + } +}