diff --git a/apier/v1/cdre.go b/apier/v1/cdre.go index fe77a03d1..224058cfc 100644 --- a/apier/v1/cdre.go +++ b/apier/v1/cdre.go @@ -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) } diff --git a/apier/v2/cdre.go b/apier/v2/cdre.go index d4e0d81bb..31ea8f43e 100644 --- a/apier/v2/cdre.go +++ b/apier/v2/cdre.go @@ -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) } diff --git a/cdre/cdrexporter.go b/cdre/cdrexporter.go index 00fbe67d9..a98a26c95 100644 --- a/cdre/cdrexporter.go +++ b/cdre/cdrexporter.go @@ -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 } diff --git a/cdre/cdrexporter_test.go b/cdre/cdrexporter_test.go index 0960c8b8f..045fc0e4b 100644 --- a/cdre/cdrexporter_test.go +++ b/cdre/cdrexporter_test.go @@ -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) } diff --git a/cdre/csv_test.go b/cdre/csv_test.go index 0990c7a80..c69c140b9 100644 --- a/cdre/csv_test.go +++ b/cdre/csv_test.go @@ -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) } diff --git a/cdre/fixedwidth_test.go b/cdre/fixedwidth_test.go index ffdd1f747..ef5f5137e 100644 --- a/cdre/fixedwidth_test.go +++ b/cdre/fixedwidth_test.go @@ -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) } diff --git a/config/cdreconfig.go b/config/cdreconfig.go index 55a98c3b5..f55bad28e 100644 --- a/config/cdreconfig.go +++ b/config/cdreconfig.go @@ -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 diff --git a/config/cfg_data.json b/config/cfg_data.json index 77f20a7da..f5eb4a811 100644 --- a/config/cfg_data.json +++ b/config/cfg_data.json @@ -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/"}, ], diff --git a/config/config_defaults.go b/config/config_defaults.go index 837c8ecd0..0cedd84f0 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -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 diff --git a/config/config_json_test.go b/config/config_json_test.go index c7317f93d..2548ec5c4 100644 --- a/config/config_json_test.go +++ b/config/config_json_test.go @@ -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), diff --git a/config/libconfig_json.go b/config/libconfig_json.go index fdf261f00..fba619846 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -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 diff --git a/engine/account.go b/engine/account.go index d733af1c8..16493a6cb 100644 --- a/engine/account.go +++ b/engine/account.go @@ -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 { diff --git a/engine/actions_test.go b/engine/actions_test.go index 12adecf9d..4eaaec183 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -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) { diff --git a/engine/cdr.go b/engine/cdr.go index ca0893832..de741ed3e 100644 --- a/engine/cdr.go +++ b/engine/cdr.go @@ -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 { diff --git a/utils/apitpdata.go b/utils/apitpdata.go index dd3c1c246..9a2c63d31 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -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 diff --git a/utils/cond_loader.go b/utils/cond_loader.go index 8fdfca867..5ff35350c 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -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 } diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index c0e9930ff..996d12d27 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -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) { diff --git a/utils/consts.go b/utils/consts.go index 345408b29..6f413f1f3 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -148,6 +148,7 @@ const ( HDR_VAL_SEP = "/" MONETARY = "*monetary" SMS = "*sms" + MMS = "*mms" GENERIC = "*generic" DATA = "*data" VOICE = "*voice"