mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
Merge branch 'master' of https://github.com/cgrates/cgrates
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/"},
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -148,6 +148,7 @@ const (
|
||||
HDR_VAL_SEP = "/"
|
||||
MONETARY = "*monetary"
|
||||
SMS = "*sms"
|
||||
MMS = "*mms"
|
||||
GENERIC = "*generic"
|
||||
DATA = "*data"
|
||||
VOICE = "*voice"
|
||||
|
||||
Reference in New Issue
Block a user