Refactoring CDRs to support RSRFields

This commit is contained in:
DanB
2014-05-05 20:14:40 +02:00
parent 7bda45fcce
commit f7abbacfe5
39 changed files with 848 additions and 1205 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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("<Cdrc> 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("<Cdrc> 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("<Cdrc> 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("<Cdrc> Failed posting CDR, error: %s", err.Error()))
continue
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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())
}

View File

@@ -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
}

File diff suppressed because one or more lines are too long

View File

@@ -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. <csv>
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 <http_cgr>
CdrcRunDelay time.Duration // Sleep interval between consecutive runs, 0 to use automation via inotify
CdrcCdrType string // CDR file format <csv>.
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. <csv>
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 <http_cgr>
CdrcRunDelay time.Duration // Sleep interval between consecutive runs, 0 to use automation via inotify
CdrcCdrType string // CDR file format <csv>.
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")

View File

@@ -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

View File

@@ -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: <label_extrafield_1>:<index_extrafield_1>
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
}

View File

@@ -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)
}
}

View File

@@ -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: <same|postgres|mongo|redis>
stordb_type = test # Log/scategoryed database type to use: <same|postgres|mongo|redis>
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: <msgpack|json>
dbdata_encoding = test # The encoding used to scategorye object data in strings: <msgpack|json>
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: <true|false>.
[cdrs]
enabled = true # Start the CDR Server service: <true|false>.
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 <csv>
@@ -61,24 +61,24 @@ cdrs = test # Address where to reach CDR server
cdrs_method = test # Mechanism to use when posting CDRs on server <http_cgr>
run_delay = 99 # Period to sleep between two runs, 0 to use automation via inotify
cdr_type = test # CDR file format <csv>.
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: <true|false>.
enabled = true # Starts Mediacategory service: <true|false>.
rater = test # Address where to reach the Rater: <internal|x.y.z.y:1234>
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 <true|false>.
[history_server]
enabled = true # Starts History service: <true|false>.
history_dir = test # Location on disk where to store history files.
enabled = true # Starts Hiscategoryy service: <true|false>.
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: <true|false>.
server = test # Address where to reach the master history server: <internal|x.y.z.y:1234>
enabled = true # Starts Hiscategoryy as a client: <true|false>.
server = test # Address where to reach the master hiscategoryy server: <internal|x.y.z.y:1234>
[mailer]
server = test # The server to use when sending emails out

View File

@@ -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>.

View File

@@ -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,

View File

@@ -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,

View File

@@ -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`)

View File

@@ -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
1 #Tag,BalanceType,Direction,ThresholdType,ThresholdValue,DestinationTag,ActionsTag,Weight #Tag BalanceType Direction ThresholdType ThresholdValue Recurrent DestinationTag ActionsTag Weight
2 STANDARD_TRIGGERS,*monetary,*out,*min_balance,2,false,,LOG_BALANCE,10 STANDARD_TRIGGERS *monetary *out *min_balance 2 false LOG_BALANCE 10
3 STANDARD_TRIGGERS,*monetary,*out,*max_balance,20,false,,LOG_BALANCE,10 STANDARD_TRIGGERS *monetary *out *max_balance 20 false LOG_BALANCE 10
4 STANDARD_TRIGGERS,*monetary,*out,*max_counter,15,false,FS_USERS,LOG_BALANCE,10 STANDARD_TRIGGERS *monetary *out *max_counter 15 false FS_USERS LOG_BALANCE 10

View File

@@ -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

View File

@@ -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_])"},

View File

@@ -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
`

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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,

File diff suppressed because one or more lines are too long

View File

@@ -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("<Mediator> Could not record cost for cgrid: <%s>, err: <%s>, cost: %f, extraInfo: %s",
engine.Logger.Err(fmt.Sprintf("<Mediator> Could not record cost for cgrid: <%s>, ERROR: <%s>, cost: %f, extraInfo: %s",
cdr.CgrId, err.Error(), cdr.Cost, extraInfo))
}
}

View File

@@ -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)
}
}

View File

@@ -19,10 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
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
}

View File

@@ -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)
}
}

View File

@@ -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}
)

View File

@@ -18,28 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
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
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -19,40 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
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
}

View File

@@ -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)
}
}