This commit is contained in:
DanB
2016-01-20 13:50:06 +01:00
18 changed files with 359 additions and 37 deletions

View File

@@ -144,6 +144,10 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E
if attr.SmsUsageMultiplyFactor != nil && *attr.SmsUsageMultiplyFactor != 0.0 {
smsUsageMultiplyFactor = *attr.SmsUsageMultiplyFactor
}
mmsUsageMultiplyFactor := exportTemplate.MMSUsageMultiplyFactor
if attr.MmsUsageMultiplyFactor != nil && *attr.MmsUsageMultiplyFactor != 0.0 {
mmsUsageMultiplyFactor = *attr.MmsUsageMultiplyFactor
}
genericUsageMultiplyFactor := exportTemplate.GenericUsageMultiplyFactor
if attr.GenericUsageMultiplyFactor != nil && *attr.GenericUsageMultiplyFactor != 0.0 {
genericUsageMultiplyFactor = *attr.GenericUsageMultiplyFactor
@@ -179,8 +183,7 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E
*reply = utils.ExportedFileCdrs{ExportedFilePath: ""}
return nil
}
cdrexp, err := cdre.NewCdrExporter(cdrs, self.CdrDb, exportTemplate, cdrFormat, fieldSep, exportId, dataUsageMultiplyFactor, smsUsageMultiplyFactor, genericUsageMultiplyFactor,
costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify, self.Config.DefaultTimezone)
cdrexp, err := cdre.NewCdrExporter(cdrs, self.CdrDb, exportTemplate, cdrFormat, fieldSep, exportId, dataUsageMultiplyFactor, smsUsageMultiplyFactor, mmsUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify, self.Config.DefaultTimezone)
if err != nil {
return utils.NewErrServerError(err)
}

View File

@@ -80,6 +80,10 @@ func (self *ApierV2) ExportCdrsToFile(attr utils.AttrExportCdrsToFile, reply *ut
if attr.SMSUsageMultiplyFactor != nil && *attr.SMSUsageMultiplyFactor != 0.0 {
SMSUsageMultiplyFactor = *attr.SMSUsageMultiplyFactor
}
MMSUsageMultiplyFactor := exportTemplate.MMSUsageMultiplyFactor
if attr.MMSUsageMultiplyFactor != nil && *attr.MMSUsageMultiplyFactor != 0.0 {
MMSUsageMultiplyFactor = *attr.MMSUsageMultiplyFactor
}
genericUsageMultiplyFactor := exportTemplate.GenericUsageMultiplyFactor
if attr.GenericUsageMultiplyFactor != nil && *attr.GenericUsageMultiplyFactor != 0.0 {
genericUsageMultiplyFactor = *attr.GenericUsageMultiplyFactor
@@ -115,8 +119,7 @@ func (self *ApierV2) ExportCdrsToFile(attr utils.AttrExportCdrsToFile, reply *ut
*reply = utils.ExportedFileCdrs{ExportedFilePath: ""}
return nil
}
cdrexp, err := cdre.NewCdrExporter(cdrs, self.CdrDb, exportTemplate, cdrFormat, fieldSep, ExportID, dataUsageMultiplyFactor, SMSUsageMultiplyFactor, genericUsageMultiplyFactor,
costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify, self.Config.DefaultTimezone)
cdrexp, err := cdre.NewCdrExporter(cdrs, self.CdrDb, exportTemplate, cdrFormat, fieldSep, ExportID, dataUsageMultiplyFactor, SMSUsageMultiplyFactor, MMSUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify, self.Config.DefaultTimezone)
if err != nil {
return utils.NewErrServerError(err)
}

View File

@@ -43,6 +43,7 @@ const (
META_NRCDRS = "*cdrs_number"
META_DURCDRS = "*cdrs_duration"
META_SMSUSAGE = "*sms_usage"
META_MMSUSAGE = "*mms_usage"
META_GENERICUSAGE = "*generic_usage"
META_DATAUSAGE = "*data_usage"
META_COSTCDRS = "*cdrs_cost"
@@ -53,7 +54,7 @@ const (
var err error
func NewCdrExporter(cdrs []*engine.CDR, cdrDb engine.CdrStorage, exportTpl *config.CdreConfig, cdrFormat string, fieldSeparator rune, exportId string,
dataUsageMultiplyFactor, smsUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor float64,
dataUsageMultiplyFactor, smsUsageMultiplyFactor, mmsUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor float64,
costShiftDigits, roundDecimals, cgrPrecision int, maskDestId string, maskLen int, httpSkipTlsCheck bool, timezone string) (*CdrExporter, error) {
if len(cdrs) == 0 { // Nothing to export
return nil, nil
@@ -66,7 +67,7 @@ func NewCdrExporter(cdrs []*engine.CDR, cdrDb engine.CdrStorage, exportTpl *conf
fieldSeparator: fieldSeparator,
exportId: exportId,
dataUsageMultiplyFactor: dataUsageMultiplyFactor,
smsUsageMultiplyFactor: smsUsageMultiplyFactor,
mmsUsageMultiplyFactor: mmsUsageMultiplyFactor,
costMultiplyFactor: costMultiplyFactor,
costShiftDigits: costShiftDigits,
roundDecimals: roundDecimals,
@@ -92,18 +93,19 @@ type CdrExporter struct {
exportId string // Unique identifier or this export
dataUsageMultiplyFactor,
smsUsageMultiplyFactor, // Multiply the SMS usage (eg: some billing systems billing them as minutes)
mmsUsageMultiplyFactor,
genericUsageMultiplyFactor,
costMultiplyFactor float64
costShiftDigits, roundDecimals, cgrPrecision int
maskDestId string
maskLen int
httpSkipTlsCheck bool
timezone string
header, trailer []string // Header and Trailer fields
content [][]string // Rows of cdr fields
firstCdrATime, lastCdrATime time.Time
numberOfRecords int
totalDuration, totalDataUsage, totalSmsUsage, totalGenericUsage time.Duration
costShiftDigits, roundDecimals, cgrPrecision int
maskDestId string
maskLen int
httpSkipTlsCheck bool
timezone string
header, trailer []string // Header and Trailer fields
content [][]string // Rows of cdr fields
firstCdrATime, lastCdrATime time.Time
numberOfRecords int
totalDuration, totalDataUsage, totalSmsUsage, totalMmsUsage, totalGenericUsage time.Duration
totalCost float64
firstExpOrderId, lastExpOrderId int64
@@ -234,6 +236,9 @@ func (cdre *CdrExporter) metaHandler(tag, arg string) (string, error) {
case META_SMSUSAGE:
emulatedCdr := &engine.CDR{ToR: utils.SMS, Usage: cdre.totalSmsUsage}
return emulatedCdr.FormatUsage(arg), nil
case META_MMSUSAGE:
emulatedCdr := &engine.CDR{ToR: utils.MMS, Usage: cdre.totalMmsUsage}
return emulatedCdr.FormatUsage(arg), nil
case META_GENERICUSAGE:
emulatedCdr := &engine.CDR{ToR: utils.GENERIC, Usage: cdre.totalGenericUsage}
return emulatedCdr.FormatUsage(arg), nil
@@ -322,6 +327,8 @@ func (cdre *CdrExporter) processCdr(cdr *engine.CDR) error {
cdr.UsageMultiply(cdre.dataUsageMultiplyFactor, cdre.cgrPrecision)
} else if cdre.smsUsageMultiplyFactor != 0 && cdr.ToR == utils.SMS {
cdr.UsageMultiply(cdre.smsUsageMultiplyFactor, cdre.cgrPrecision)
} else if cdre.mmsUsageMultiplyFactor != 0 && cdr.ToR == utils.MMS {
cdr.UsageMultiply(cdre.mmsUsageMultiplyFactor, cdre.cgrPrecision)
} else if cdre.genericUsageMultiplyFactor != 0 && cdr.ToR == utils.GENERIC {
cdr.UsageMultiply(cdre.genericUsageMultiplyFactor, cdre.cgrPrecision)
}
@@ -392,6 +399,9 @@ func (cdre *CdrExporter) processCdr(cdr *engine.CDR) error {
if cdr.ToR == utils.SMS { // Count usage for SMS
cdre.totalSmsUsage += cdr.Usage
}
if cdr.ToR == utils.MMS { // Count usage for MMS
cdre.totalMmsUsage += cdr.Usage
}
if cdr.ToR == utils.GENERIC { // Count usage for GENERIC
cdre.totalGenericUsage += cdr.Usage
}

View File

@@ -52,7 +52,7 @@ func TestCdreGetCombimedCdrFieldVal(t *testing.T) {
Usage: time.Duration(10) * time.Second, RunID: "RETAIL1", Cost: 5.01},
}
cdre, err := NewCdrExporter(cdrs, nil, cfg.CdreProfiles["*default"], cfg.CdreProfiles["*default"].CdrFormat, cfg.CdreProfiles["*default"].FieldSeparator,
"firstexport", 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "")
"firstexport", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "")
if err != nil {
t.Error("Unexpected error received: ", err)
}

View File

@@ -40,7 +40,7 @@ func TestCsvCdrWriter(t *testing.T) {
Usage: time.Duration(10) * time.Second, RunID: utils.DEFAULT_RUNID,
ExtraFields: map[string]string{"extra1": "val_extra1", "extra2": "val_extra2", "extra3": "val_extra3"}, Cost: 1.01,
}
cdre, err := NewCdrExporter([]*engine.CDR{storedCdr1}, nil, cfg.CdreProfiles["*default"], utils.CSV, ',', "firstexport", 0.0, 0.0, 0.0, 0.0, 0, 4,
cdre, err := NewCdrExporter([]*engine.CDR{storedCdr1}, nil, cfg.CdreProfiles["*default"], utils.CSV, ',', "firstexport", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4,
cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "")
if err != nil {
t.Error("Unexpected error received: ", err)
@@ -69,7 +69,7 @@ func TestAlternativeFieldSeparator(t *testing.T) {
ExtraFields: map[string]string{"extra1": "val_extra1", "extra2": "val_extra2", "extra3": "val_extra3"}, Cost: 1.01,
}
cdre, err := NewCdrExporter([]*engine.CDR{storedCdr1}, nil, cfg.CdreProfiles["*default"], utils.CSV, '|',
"firstexport", 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "")
"firstexport", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "")
if err != nil {
t.Error("Unexpected error received: ", err)
}

View File

@@ -127,7 +127,7 @@ func TestWriteCdr(t *testing.T) {
Usage: time.Duration(10) * time.Second, RunID: utils.DEFAULT_RUNID, Cost: 2.34567,
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
}
cdre, err := NewCdrExporter([]*engine.CDR{cdr}, nil, cdreCfg, utils.CDRE_FIXED_WIDTH, ',', "fwv_1", 0.0, 0.0, 0.0, 0.0, 0, 4,
cdre, err := NewCdrExporter([]*engine.CDR{cdr}, nil, cdreCfg, utils.CDRE_FIXED_WIDTH, ',', "fwv_1", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4,
cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify, "")
if err != nil {
t.Error(err)
@@ -203,7 +203,7 @@ func TestWriteCdrs(t *testing.T) {
}
cfg, _ := config.NewDefaultCGRConfig()
cdre, err := NewCdrExporter([]*engine.CDR{cdr1, cdr2, cdr3, cdr4}, nil, cdreCfg, utils.CDRE_FIXED_WIDTH, ',',
"fwv_1", 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify, "")
"fwv_1", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify, "")
if err != nil {
t.Error(err)
}

View File

@@ -24,6 +24,7 @@ type CdreConfig struct {
FieldSeparator rune
DataUsageMultiplyFactor float64
SMSUsageMultiplyFactor float64
MMSUsageMultiplyFactor float64
GenericUsageMultiplyFactor float64
CostMultiplyFactor float64
CostRoundingDecimals int
@@ -54,6 +55,9 @@ func (self *CdreConfig) loadFromJsonCfg(jsnCfg *CdreJsonCfg) error {
if jsnCfg.Sms_usage_multiply_factor != nil {
self.SMSUsageMultiplyFactor = *jsnCfg.Sms_usage_multiply_factor
}
if jsnCfg.Mms_usage_multiply_factor != nil {
self.MMSUsageMultiplyFactor = *jsnCfg.Mms_usage_multiply_factor
}
if jsnCfg.Generic_usage_multiply_factor != nil {
self.GenericUsageMultiplyFactor = *jsnCfg.Generic_usage_multiply_factor
}
@@ -100,6 +104,7 @@ func (self *CdreConfig) Clone() *CdreConfig {
clnCdre.FieldSeparator = self.FieldSeparator
clnCdre.DataUsageMultiplyFactor = self.DataUsageMultiplyFactor
clnCdre.SMSUsageMultiplyFactor = self.SMSUsageMultiplyFactor
clnCdre.MMSUsageMultiplyFactor = self.MMSUsageMultiplyFactor
clnCdre.GenericUsageMultiplyFactor = self.GenericUsageMultiplyFactor
clnCdre.CostMultiplyFactor = self.CostMultiplyFactor
clnCdre.CostRoundingDecimals = self.CostRoundingDecimals

View File

@@ -33,7 +33,7 @@
"run_delay": 1,
"cdr_source_id": "csv2", // free form field, tag identifying the source of the CDRs within CDRS database
"content_fields":[ // import template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value
{"field_id": "ToR", "value": "~7:s/^(voice|data|sms|generic)$/*$1/"},
{"field_id": "ToR", "value": "~7:s/^(voice|data|sms|mms|generic)$/*$1/"},
{"field_id": "AnswerTime", "value": "1"},
{"field_id": "Usage", "value": "~9:s/^(\\d+)$/${1}s/"},
],

View File

@@ -132,6 +132,7 @@ const CGRATES_CFG_JSON = `
"field_separator": ",",
"data_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from KBytes to Bytes)
"sms_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from SMS unit to call duration in some billing systems)
"mms_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from MMS unit to call duration in some billing systems)
"generic_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from GENERIC unit to call duration in some billing systems)
"cost_multiply_factor": 1, // multiply cost before export, eg: add VAT
"cost_rounding_decimals": -1, // rounding decimals for Cost values. -1 to disable rounding

View File

@@ -249,6 +249,7 @@ func TestDfCdreJsonCfgs(t *testing.T) {
Field_separator: utils.StringPointer(","),
Data_usage_multiply_factor: utils.Float64Pointer(1.0),
Sms_usage_multiply_factor: utils.Float64Pointer(1.0),
Mms_usage_multiply_factor: utils.Float64Pointer(1.0),
Generic_usage_multiply_factor: utils.Float64Pointer(1.0),
Cost_multiply_factor: utils.Float64Pointer(1.0),
Cost_rounding_decimals: utils.IntPointer(-1),
@@ -591,7 +592,7 @@ func TestNewCgrJsonCfgFromFile(t *testing.T) {
t.Error("Received: ", gCfg)
}
cdrFields := []*CdrFieldJsonCfg{
&CdrFieldJsonCfg{Field_id: utils.StringPointer(utils.TOR), Value: utils.StringPointer("~7:s/^(voice|data|sms|generic)$/*$1/")},
&CdrFieldJsonCfg{Field_id: utils.StringPointer(utils.TOR), Value: utils.StringPointer("~7:s/^(voice|data|sms|mms|generic)$/*$1/")},
&CdrFieldJsonCfg{Field_id: utils.StringPointer(utils.ANSWER_TIME), Value: utils.StringPointer("1")},
&CdrFieldJsonCfg{Field_id: utils.StringPointer(utils.USAGE), Value: utils.StringPointer(`~9:s/^(\d+)$/${1}s/`)},
}
@@ -615,7 +616,7 @@ func TestNewCgrJsonCfgFromFile(t *testing.T) {
if cfg, err := cgrJsonCfg.CdrcJsonCfg(); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(eCfgCdrc, cfg) {
t.Error("Received: ", cfg["CDRC-CSV2"])
t.Error("Received: ", utils.ToIJSON(cfg["CDRC-CSV2"]))
}
eCfgSmFs := &SmFsJsonCfg{
Enabled: utils.BoolPointer(true),

View File

@@ -127,6 +127,7 @@ type CdreJsonCfg struct {
Field_separator *string
Data_usage_multiply_factor *float64
Sms_usage_multiply_factor *float64
Mms_usage_multiply_factor *float64
Generic_usage_multiply_factor *float64
Cost_multiply_factor *float64
Cost_rounding_decimals *int

View File

@@ -699,7 +699,9 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance
func (acc *Account) matchConditions(condition string) (bool, error) {
cl := &utils.CondLoader{}
cl.Parse(condition)
if err := cl.Parse(condition); err != nil {
return false, err
}
for balanceType, balanceChain := range acc.BalanceMap {
for _, b := range balanceChain {
check, err := cl.Check(&struct {

View File

@@ -1568,6 +1568,197 @@ func TestActionTransferMonetaryDefaultFilter(t *testing.T) {
}
}
func TestActionConditionalTopup(t *testing.T) {
err := accountingStorage.SetAccount(
&Account{
Id: "cgrates.org:cond",
BalanceMap: map[string]BalanceChain{
utils.MONETARY: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Id: utils.META_DEFAULT,
Value: 10,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 3,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 1,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 6,
Weight: 20,
},
},
},
})
if err != nil {
t.Errorf("error setting account: %v", err)
}
a := &Action{
ActionType: CONDITIONAL_TOPUP,
BalanceType: utils.MONETARY,
ExtraParameters: `{"Type":"*monetary","Value":1,"Weight":10}`,
Balance: &Balance{
Value: 11,
Weight: 30,
},
}
at := &ActionTiming{
accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}},
actions: Actions{a},
}
at.Execute()
afterUb, err := accountingStorage.GetAccount("cgrates.org:cond")
if err != nil {
t.Error("account not found: ", err, afterUb)
}
if len(afterUb.BalanceMap[utils.MONETARY]) != 5 ||
afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 31 ||
afterUb.BalanceMap[utils.MONETARY][4].Value != 11 {
for _, b := range afterUb.BalanceMap[utils.MONETARY] {
t.Logf("B: %+v", b)
}
t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue())
}
}
func TestActionConditionalTopupNoMatch(t *testing.T) {
err := accountingStorage.SetAccount(
&Account{
Id: "cgrates.org:cond",
BalanceMap: map[string]BalanceChain{
utils.MONETARY: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Id: utils.META_DEFAULT,
Value: 10,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 3,
Weight: 20,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 1,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 6,
Weight: 20,
},
},
},
})
if err != nil {
t.Errorf("error setting account: %v", err)
}
a := &Action{
ActionType: CONDITIONAL_TOPUP,
BalanceType: utils.MONETARY,
ExtraParameters: `{"Type":"*monetary","Value":2,"Weight":10}`,
Balance: &Balance{
Value: 11,
Weight: 30,
},
}
at := &ActionTiming{
accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}},
actions: Actions{a},
}
at.Execute()
afterUb, err := accountingStorage.GetAccount("cgrates.org:cond")
if err != nil {
t.Error("account not found: ", err, afterUb)
}
if len(afterUb.BalanceMap[utils.MONETARY]) != 4 ||
afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 20 {
for _, b := range afterUb.BalanceMap[utils.MONETARY] {
t.Logf("B: %+v", b)
}
t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue())
}
}
func TestActionConditionalTopupExistingBalance(t *testing.T) {
err := accountingStorage.SetAccount(
&Account{
Id: "cgrates.org:cond",
BalanceMap: map[string]BalanceChain{
utils.MONETARY: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Value: 1,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 6,
Weight: 20,
},
},
utils.VOICE: BalanceChain{
&Balance{
Uuid: utils.GenUUID(),
Value: 10,
Weight: 10,
},
&Balance{
Uuid: utils.GenUUID(),
Value: 100,
Weight: 20,
},
},
},
})
if err != nil {
t.Errorf("error setting account: %v", err)
}
a := &Action{
ActionType: CONDITIONAL_TOPUP,
BalanceType: utils.MONETARY,
ExtraParameters: `{"Type":"*voice","Value":{"*gte":100}}`,
Balance: &Balance{
Value: 11,
Weight: 10,
},
}
at := &ActionTiming{
accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}},
actions: Actions{a},
}
at.Execute()
afterUb, err := accountingStorage.GetAccount("cgrates.org:cond")
if err != nil {
t.Error("account not found: ", err, afterUb)
}
if len(afterUb.BalanceMap[utils.MONETARY]) != 2 ||
afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 18 {
for _, b := range afterUb.BalanceMap[utils.MONETARY] {
t.Logf("B: %+v", b)
}
t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue())
}
}
/**************** Benchmarks ********************************/
func BenchmarkUUID(b *testing.B) {

View File

@@ -126,7 +126,7 @@ func (cdr *CDR) FormatCost(shiftDecimals, roundDecimals int) string {
// Formats usage on export
func (cdr *CDR) FormatUsage(layout string) string {
if utils.IsSliceMember([]string{utils.DATA, utils.SMS, utils.GENERIC}, cdr.ToR) {
if utils.IsSliceMember([]string{utils.DATA, utils.SMS, utils.MMS, utils.GENERIC}, cdr.ToR) {
return strconv.FormatFloat(utils.Round(cdr.Usage.Seconds(), 0, utils.ROUNDING_MIDDLE), 'f', -1, 64)
}
switch layout {

View File

@@ -597,6 +597,7 @@ type AttrExpFileCdrs struct {
ExportTemplate *string // Exported fields template <""|fld1,fld2|*xml:instance_name>
DataUsageMultiplyFactor *float64 // Multiply data usage before export (eg: convert from KBytes to Bytes)
SmsUsageMultiplyFactor *float64 // Multiply sms usage before export (eg: convert from SMS unit to call duration for some billing systems)
MmsUsageMultiplyFactor *float64 // Multiply mms usage before export (eg: convert from MMS unit to call duration for some billing systems)
GenericUsageMultiplyFactor *float64 // Multiply generic usage before export (eg: convert from GENERIC unit to call duration for some billing systems)
CostMultiplyFactor *float64 // Multiply the cost before export, eg: apply VAT
CostShiftDigits *int // If defined it will shift cost digits before applying rouding (eg: convert from Eur->cents), -1 to use general config ones
@@ -1089,6 +1090,7 @@ type AttrExportCdrsToFile struct {
ExportTemplate *string // Exported fields template <""|fld1,fld2|*xml:instance_name>
DataUsageMultiplyFactor *float64 // Multiply data usage before export (eg: convert from KBytes to Bytes)
SMSUsageMultiplyFactor *float64 // Multiply sms usage before export (eg: convert from SMS unit to call duration for some billing systems)
MMSUsageMultiplyFactor *float64 // Multiply mms usage before export (eg: convert from MMS unit to call duration for some billing systems)
GenericUsageMultiplyFactor *float64 // Multiply generic usage before export (eg: convert from GENERIC unit to call duration for some billing systems)
CostMultiplyFactor *float64 // Multiply the cost before export, eg: apply VAT
CostShiftDigits *int // If defined it will shift cost digits before applying rouding (eg: convert from Eur->cents), -1 to use general config ones

View File

@@ -1,10 +1,36 @@
package utils
/*
When an action is using *conditional_ form before the execution the engine is checking the ExtraParameters field for condition filter, loads it and checks all the balances in the account for one that is satisfying the condition. If one is fond the action is executed, otherwise it will do nothing for this account.
The condition syntax is a json encoded document similar to mongodb query language.
Examples:
- {"Weight":{"*gt":50}} checks for a balance with weight greater than 50
- {"*or":[{"Value":{"*eq":0}},{"Value":{"*gte":100}}] checks for a balance with value equal to 0 or equal or highr than 100
Available operators:
- *eq: equal
- *gt: greater than
- *gte: greater or equal than
- *lt: less then
- *lte: less or equal than
- *exp: expired
- *or: logical or
- *and: logical and
- *has: receives a list of elements and checks that the elements are present in the specified field (also a list)
Equal (*eq) and local and (*and) operators are implicit for shortcuts. In this way:
{"*and":[{"Value":{"*eq":3}},{"Weight":{"*eq":10}}]} is equivalent to: {"Value":3, "Weight":10}.
*/
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
)
const (
@@ -110,6 +136,24 @@ func (ov *operatorValue) checkStruct(o interface{}) (bool, error) {
}
return true, nil
}
// date conversion
if ov.operator == CondEXP {
var expDate time.Time
var ok bool
if expDate, ok = o.(time.Time); !ok {
return false, NewErrInvalidArgument(o)
}
var expired bool
if expired, ok = ov.value.(bool); !ok {
return false, NewErrInvalidArgument(ov.value)
}
if expired { // check for expiration
return !expDate.IsZero() && expDate.Before(time.Now()), nil
} else { // check not expired
return expDate.IsZero() || expDate.After(time.Now()), nil
}
}
// float conversion
var of, vf float64
var ok bool
@@ -148,6 +192,13 @@ func (kv *keyValue) checkStruct(o interface{}) (bool, error) {
return value.Interface() == kv.value, nil
}
type trueElement struct{}
func (te *trueElement) addChild(condElement) error { return ErrNotImplemented }
func (te *trueElement) checkStruct(o interface{}) (bool, error) {
return true, nil
}
func isOperator(s string) bool {
return strings.HasPrefix(s, "*")
}
@@ -186,18 +237,27 @@ func (cp *CondLoader) load(a map[string]interface{}, parentElement condElement)
if parentElement != nil {
parentElement.addChild(currentElement)
} else {
return currentElement, nil
if len(a) > 1 {
parentElement = &operatorSlice{operator: CondAND}
parentElement.addChild(currentElement)
} else {
return currentElement, nil
}
}
}
return nil, nil
return parentElement, nil
}
func (cp *CondLoader) Parse(s string) (err error) {
a := make(map[string]interface{})
if err := json.Unmarshal([]byte([]byte(s)), &a); err != nil {
return err
if len(s) != 0 {
if err := json.Unmarshal([]byte([]byte(s)), &a); err != nil {
return err
}
cp.rootElement, err = cp.load(a, nil)
} else {
cp.rootElement = &trueElement{}
}
cp.rootElement, err = cp.load(a, nil)
return
}

View File

@@ -3,6 +3,7 @@ package utils
import (
"strings"
"testing"
"time"
)
func TestCondLoader(t *testing.T) {
@@ -20,17 +21,23 @@ func TestCondLoader(t *testing.T) {
if err != nil {
t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err)
}
err = cl.Parse(``)
if err != nil {
t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err)
}
}
func TestCondKeyValue(t *testing.T) {
o := struct {
Test string
Field float64
Other bool
Test string
Field float64
Other bool
ExpDate time.Time
}{
Test: "test",
Field: 6.0,
Other: true,
Test: "test",
Field: 6.0,
Other: true,
ExpDate: time.Date(2016, 1, 19, 20, 47, 0, 0, time.UTC),
}
cl := &CondLoader{}
err := cl.Parse(`{"Test":"test"}`)
@@ -68,6 +75,41 @@ func TestCondKeyValue(t *testing.T) {
if check, err := cl.Check(o); check || err != nil {
t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement))
}
err = cl.Parse(`{"Field":6, "Other":false}`)
if err != nil {
t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err)
}
if check, err := cl.Check(o); check || err != nil {
t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement))
}
err = cl.Parse(`{"Other":true, "Field":{"*gt":5}}`)
if err != nil {
t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err)
}
if check, err := cl.Check(o); !check || err != nil {
t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement))
}
err = cl.Parse(``)
if err != nil {
t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err)
}
if check, err := cl.Check(o); !check || err != nil {
t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement))
}
err = cl.Parse(`{"ExpDate":{"*exp":true}}`)
if err != nil {
t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err)
}
if check, err := cl.Check(o); !check || err != nil {
t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement))
}
err = cl.Parse(`{"ExpDate":{"*exp":false}}`)
if err != nil {
t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err)
}
if check, err := cl.Check(o); check || err != nil {
t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement))
}
}
func TestCondKeyValuePointer(t *testing.T) {

View File

@@ -148,6 +148,7 @@ const (
HDR_VAL_SEP = "/"
MONETARY = "*monetary"
SMS = "*sms"
MMS = "*mms"
GENERIC = "*generic"
DATA = "*data"
VOICE = "*voice"