diff --git a/apier/cdre.go b/apier/cdre.go index fe39c8a1d..41e29ec7c 100644 --- a/apier/cdre.go +++ b/apier/cdre.go @@ -53,7 +53,11 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E if len(exportId) == 0 { exportId = strconv.FormatInt(time.Now().Unix(), 10) } - roundDecimals := attr.RoundingDecimals + costShiftDigits := attr.CostShiftDigits + if costShiftDigits != 0 { + costShiftDigits = self.Config.CdreCostShiftDigits + } + roundDecimals := attr.RoundDecimals if roundDecimals == 0 { roundDecimals = self.Config.RoundingDecimals } @@ -99,7 +103,7 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E return err } defer fileOut.Close() - csvWriter := cdre.NewCsvCdrWriter(fileOut, roundDecimals, maskDestId, maskLen, exportedFields) + csvWriter := cdre.NewCsvCdrWriter(fileOut, costShiftDigits, roundDecimals, maskDestId, maskLen, exportedFields) exportedIds := make([]string, 0) unexportedIds := make(map[string]string) for _, cdr := range cdrs { @@ -132,7 +136,7 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E return err } defer fileOut.Close() - fww, _ := cdre.NewFWCdrWriter(self.LogDb, fileOut, exportTemplate, exportId, roundDecimals, maskDestId, maskLen) + fww, _ := cdre.NewFWCdrWriter(self.LogDb, fileOut, exportTemplate, exportId, costShiftDigits, roundDecimals, maskDestId, maskLen) exportedIds := make([]string, 0) unexportedIds := make(map[string]string) for _, cdr := range cdrs { diff --git a/cdre/csv.go b/cdre/csv.go index 64597e1e2..b2c3f7cb0 100644 --- a/cdre/csv.go +++ b/cdre/csv.go @@ -26,15 +26,15 @@ import ( ) type CsvCdrWriter struct { - writer *csv.Writer - roundDecimals int // Round floats like Cost using this number of decimals - maskDestId string - maskLen int - exportedFields []*utils.RSRField // The fields exported, order important + writer *csv.Writer + costShiftDigits, roundDecimals int // Round floats like Cost using this number of decimals + maskDestId string + maskLen int + exportedFields []*utils.RSRField // The fields exported, order important } -func NewCsvCdrWriter(writer io.Writer, roundDecimals int, maskDestId string, maskLen int, exportedFields []*utils.RSRField) *CsvCdrWriter { - return &CsvCdrWriter{csv.NewWriter(writer), roundDecimals, maskDestId, maskLen, exportedFields} +func NewCsvCdrWriter(writer io.Writer, costShiftDigits, roundDecimals int, maskDestId string, maskLen int, exportedFields []*utils.RSRField) *CsvCdrWriter { + return &CsvCdrWriter{csv.NewWriter(writer), costShiftDigits, roundDecimals, maskDestId, maskLen, exportedFields} } func (csvwr *CsvCdrWriter) WriteCdr(cdr *utils.StoredCdr) error { @@ -42,7 +42,7 @@ func (csvwr *CsvCdrWriter) WriteCdr(cdr *utils.StoredCdr) error { for idx, fld := range csvwr.exportedFields { var fldVal string if fld.Id == utils.COST { - fldVal = cdr.FormatCost(csvwr.roundDecimals) + fldVal = cdr.FormatCost(csvwr.costShiftDigits, csvwr.roundDecimals) } else if fld.Id == utils.DESTINATION { fldVal = cdr.ExportFieldValue(utils.DESTINATION) if len(csvwr.maskDestId) != 0 && csvwr.maskLen > 0 && engine.CachedDestHasPrefix(csvwr.maskDestId, fldVal) { diff --git a/cdre/csv_test.go b/cdre/csv_test.go index e64627eb6..16d66163d 100644 --- a/cdre/csv_test.go +++ b/cdre/csv_test.go @@ -31,7 +31,7 @@ func TestCsvCdrWriter(t *testing.T) { writer := &bytes.Buffer{} cfg, _ := config.NewDefaultCGRConfig() exportedFields := append(cfg.CdreExportedFields, &utils.RSRField{Id: "extra3"}, &utils.RSRField{Id: "dummy_extra"}, &utils.RSRField{Id: "extra1"}) - csvCdrWriter := NewCsvCdrWriter(writer, 4, "", -1, exportedFields) + csvCdrWriter := NewCsvCdrWriter(writer, 0, 4, "", -1, exportedFields) ratedCdr := &utils.StoredCdr{CgrId: utils.FSCgrId("dsafdsaf"), AccId: "dsafdsaf", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", TOR: "call", Account: "1001", Subject: "1001", Destination: "1002", SetupTime: time.Unix(1383813745, 0).UTC(), AnswerTime: time.Unix(1383813746, 0).UTC(), Duration: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, diff --git a/cdre/fixedwidth.go b/cdre/fixedwidth.go index b9a750978..aaecf5b03 100644 --- a/cdre/fixedwidth.go +++ b/cdre/fixedwidth.go @@ -47,38 +47,40 @@ const ( META_DURCDRS = "cdrs_duration" META_COSTCDRS = "cdrs_cost" META_MASKDESTINATION = "mask_destination" + META_FORMATCOST = "format_cost" ) var err error func NewFWCdrWriter(logDb engine.LogStorage, outFile *os.File, exportTpl *config.CgrXmlCdreFwCfg, exportId string, - roundDecimals int, maskDestId string, maskLen int) (*FixedWidthCdrWriter, error) { + costShiftDigits, roundDecimals int, maskDestId string, maskLen int) (*FixedWidthCdrWriter, error) { return &FixedWidthCdrWriter{ - logDb: logDb, - writer: outFile, - exportTemplate: exportTpl, - exportId: exportId, - roundDecimals: roundDecimals, - maskDestId: maskDestId, - maskLen: maskLen, - header: &bytes.Buffer{}, - content: &bytes.Buffer{}, - trailer: &bytes.Buffer{}}, nil + logDb: logDb, + writer: outFile, + exportTemplate: exportTpl, + exportId: exportId, + costShiftDigits: costShiftDigits, + roundDecimals: roundDecimals, + maskDestId: maskDestId, + maskLen: maskLen, + header: &bytes.Buffer{}, + content: &bytes.Buffer{}, + trailer: &bytes.Buffer{}}, nil } type FixedWidthCdrWriter struct { - logDb engine.LogStorage // Used to extract cost_details if these are requested - writer io.Writer - exportTemplate *config.CgrXmlCdreFwCfg - exportId string // Unique identifier or this export - roundDecimals int - maskDestId string - maskLen int - header, content, trailer *bytes.Buffer - firstCdrATime, lastCdrATime time.Time - numberOfRecords int - totalDuration time.Duration - totalCost float64 + logDb engine.LogStorage // Used to extract cost_details if these are requested + writer io.Writer + exportTemplate *config.CgrXmlCdreFwCfg + exportId string // Unique identifier or this export + costShiftDigits, roundDecimals int + maskDestId string + maskLen int + header, content, trailer *bytes.Buffer + firstCdrATime, lastCdrATime time.Time + numberOfRecords int + totalDuration time.Duration + totalCost float64 } // Return Json marshaled callCost attached to @@ -116,7 +118,7 @@ func (fwv *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layo return "", err } case utils.COST: - cdrVal = cdr.FormatCost(fwv.roundDecimals) + cdrVal = cdr.FormatCost(fwv.costShiftDigits, fwv.roundDecimals) case utils.SETUP_TIME: cdrVal = cdr.SetupTime.Format(layout) case utils.ANSWER_TIME: // Format time based on layout diff --git a/config/config.go b/config/config.go index bc88048c5..8dd5d5c16 100644 --- a/config/config.go +++ b/config/config.go @@ -93,6 +93,7 @@ type CGRConfig struct { CdreCdrFormat string // Format of the exported CDRs. CdreMaskDestId string // Id of the destination list to be masked in CDRs CdreMaskLength int // Number of digits to mask in the destination suffix if destination is in the MaskDestinationdsId + CdreCostShiftDigits int // Shift digits in the cost on export (eg: convert from EUR to cents) CdreDir string // Path towards exported cdrs directory CdreExportedFields []*utils.RSRField // List of fields in the exported CDRs CdreFWXmlTemplate *CgrXmlCdreFwCfg // Use this configuration as export template in case of fixed fields length @@ -201,6 +202,7 @@ func (self *CGRConfig) setDefaults() error { self.CdreCdrFormat = "csv" self.CdreMaskDestId = "" self.CdreMaskLength = 0 + self.CdreCostShiftDigits = 0 self.CdreDir = "/var/log/cgrates/cdr/cdre" self.CdrcEnabled = false self.CdrcCdrs = utils.INTERNAL @@ -498,6 +500,9 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) { if hasOpt = c.HasOption("cdre", "mask_length"); hasOpt { cfg.CdreMaskLength, _ = c.GetInt("cdre", "mask_length") } + if hasOpt = c.HasOption("cdre", "cost_shift_digits"); hasOpt { + cfg.CdreCostShiftDigits, _ = c.GetInt("cdre", "cost_shift_digits") + } if hasOpt = c.HasOption("cdre", "export_template"); hasOpt { // Load configs for csv normally from template, fixed_width from xml file exportTemplate, _ := c.GetString("cdre", "export_template") if cfg.CdreCdrFormat != utils.CDRE_FIXED_WIDTH { // Csv most likely diff --git a/config/config_test.go b/config/config_test.go index 942d16c15..a992e89c0 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -84,6 +84,7 @@ func TestDefaults(t *testing.T) { eCfg.CdreCdrFormat = "csv" eCfg.CdreMaskDestId = "" eCfg.CdreMaskLength = 0 + eCfg.CdreCostShiftDigits = 0 eCfg.CdreDir = "/var/log/cgrates/cdr/cdre" eCfg.CdrcEnabled = false eCfg.CdrcCdrs = utils.INTERNAL @@ -240,6 +241,7 @@ func TestConfigFromFile(t *testing.T) { eCfg.CdreCdrFormat = "test" eCfg.CdreMaskDestId = "test" eCfg.CdreMaskLength = 99 + eCfg.CdreCostShiftDigits = 99 eCfg.CdreExportedFields = []*utils.RSRField{&utils.RSRField{Id: "test"}} eCfg.CdreDir = "test" eCfg.CdrcEnabled = true diff --git a/config/test_data.txt b/config/test_data.txt index 515e2d664..21ad69960 100644 --- a/config/test_data.txt +++ b/config/test_data.txt @@ -51,6 +51,7 @@ mediator = test # Address where to reach the Mediator. Empty for disabling me cdr_format = test # Exported CDRs format mask_destination_id = test # Destination id containing called addresses to be masked on export mask_length = 99 # Length of the destination suffix to be masked +cost_shift_digits = 99 # Shift the number of cost export_dir = test # Path where the exported CDRs will be placed export_template = test # List of fields in the exported CDRs diff --git a/data/conf/cgrates.cfg b/data/conf/cgrates.cfg index 2af875892..1333a6892 100644 --- a/data/conf/cgrates.cfg +++ b/data/conf/cgrates.cfg @@ -54,6 +54,7 @@ # cdr_format = csv # Exported CDRs format # mask_destination_id = # Destination id containing called addresses to be masked on export # 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/cdr/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 # Exported fields template <""|fld1,fld2|*xml:instance_name> diff --git a/engine/storage_sql.go b/engine/storage_sql.go index e90210a6a..aa1aa30d3 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -784,7 +784,7 @@ func (self *SQLStorage) GetStoredCdrs(cgrIds, runIds, cdrHosts, cdrSources, reqT return nil, err } if err := json.Unmarshal(extraFields, &extraFieldsMp); err != nil { - return nil, err + return nil, fmt.Errorf("JSON unmarshal error for cgrid: %s, runid: %s, error: %s", cgrid, runid, err.Error()) } storCdr := &utils.StoredCdr{ CgrId: cgrid, AccId: accid, CdrHost: cdrhost, CdrSource: cdrsrc, ReqType: reqtype, Direction: direction, Tenant: tenant, diff --git a/utils/apitpdata.go b/utils/apitpdata.go index 7ea5395ef..13ce692c2 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -320,7 +320,8 @@ type AttrExpFileCdrs struct { ExportId string // Optional exportid ExportFileName string // If provided the output filename will be set to this ExportTemplate string // Exported fields template <""|fld1,fld2|*xml:instance_name> - RoundingDecimals int // Overwrite configured roundDecimals with this dynamically + CostShiftDigits int // If defined it will shift cost digits before applying rouding (eg: convert from Eur->cents) + RoundDecimals int // Overwrite configured roundDecimals with this dynamically MaskDestinationId string // Overwrite configured MaskDestId MaskLength int // Overwrite configured MaskLength CgrIds []string // If provided, it will filter based on the cgrids present in list diff --git a/utils/storedcdr.go b/utils/storedcdr.go index 60dd46a09..fa841b728 100644 --- a/utils/storedcdr.go +++ b/utils/storedcdr.go @@ -19,6 +19,7 @@ along with this program. If not, see package utils import ( + "math" "net/url" "strconv" "time" @@ -135,8 +136,12 @@ func (storedCdr *StoredCdr) GetExtraFields() map[string]string { } // Return cost as string, formated with number of decimals configured -func (storedCdr *StoredCdr) FormatCost(roundDecimals int) string { - return strconv.FormatFloat(storedCdr.Cost, 'f', roundDecimals, 64) +func (storedCdr *StoredCdr) FormatCost(shiftDecimals, roundDecimals int) string { + cost := storedCdr.Cost + if shiftDecimals != 0 { + cost = cost * math.Pow10(shiftDecimals) + } + return strconv.FormatFloat(cost, 'f', roundDecimals, 64) } func (storedCdr *StoredCdr) AsStoredCdr(runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, setupTimeFld, answerTimeFld, durationFld string, extraFlds []string, fieldsMandatory bool) (*StoredCdr, error) { diff --git a/utils/storedcdr_test.go b/utils/storedcdr_test.go index 6fad3e290..edfa8e3b0 100644 --- a/utils/storedcdr_test.go +++ b/utils/storedcdr_test.go @@ -179,11 +179,15 @@ func TestExportFieldValue(t *testing.T) { func TestFormatCost(t *testing.T) { cdr := StoredCdr{Cost: 1.01} - if cdr.FormatCost(4) != "1.0100" { - t.Error("Unexpected format of the cost: ", cdr.FormatCost(4)) + if cdr.FormatCost(0, 4) != "1.0100" { + t.Error("Unexpected format of the cost: ", cdr.FormatCost(0, 4)) } cdr = StoredCdr{Cost: 1.01001} - if cdr.FormatCost(4) != "1.0100" { - t.Error("Unexpected format of the cost: ", cdr.FormatCost(4)) + if cdr.FormatCost(0, 4) != "1.0100" { + t.Error("Unexpected format of the cost: ", cdr.FormatCost(0, 4)) + } + cdr = StoredCdr{Cost: 1.01001} + if cdr.FormatCost(2, 0) != "101" { + t.Error("Unexpected format of the cost: ", cdr.FormatCost(2, 0)) } }