From 13834eefbecc92cc987f33ed9fa0e1d513c4cf92 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 12 Jan 2016 22:19:12 +0200 Subject: [PATCH 01/38] started balance blocker flag --- engine/account.go | 8 ++++++++ engine/balances.go | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/engine/account.go b/engine/account.go index b3dcb777b..e529a7966 100644 --- a/engine/account.go +++ b/engine/account.go @@ -316,6 +316,10 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo return } } + // check for blocker + if dryRun && balance.Blocker { + return // don't go to next balances + } } } @@ -354,6 +358,10 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo return } } + // check for blocker + if dryRun && balance.Blocker { + return // don't go to next balances + } } } //log.Printf("END CD: %+v", cd) diff --git a/engine/balances.go b/engine/balances.go index d667eaf76..167bc0cca 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -46,6 +46,7 @@ type Balance struct { TimingIDs utils.StringMap Disabled bool Factor ValueFactor + Blocker bool precision int account *Account // used to store ub reference for shared balances dirty bool @@ -67,7 +68,9 @@ func (b *Balance) Equal(o *Balance) bool { b.RatingSubject == o.RatingSubject && b.Categories.Equal(o.Categories) && b.SharedGroups.Equal(o.SharedGroups) && - b.Disabled == o.Disabled + b.Disabled == o.Disabled && + + b.Blocker == o.Blocker } func (b *Balance) MatchFilter(o *Balance, skipIds bool) bool { From 7c6d2eb09a6fccdee8e829474f61b7ffb99b0cdc Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 12 Jan 2016 22:57:17 +0200 Subject: [PATCH 02/38] added blocker flag to models --- engine/action.go | 2 +- engine/action_trigger.go | 10 +++++--- engine/loader_csv_test.go | 44 ++++++++++++++++++------------------ engine/model_converters.go | 3 +++ engine/model_helpers.go | 3 +++ engine/model_helpers_test.go | 10 ++++---- engine/models.go | 14 +++++++----- utils/apitpdata.go | 3 +++ 8 files changed, 53 insertions(+), 36 deletions(-) diff --git a/engine/action.go b/engine/action.go index 734b86c64..2353173d4 100644 --- a/engine/action.go +++ b/engine/action.go @@ -42,7 +42,7 @@ type Action struct { ActionType string BalanceType string ExtraParameters string - ExpirationString string + ExpirationString string // must stay as string because it can have relative values like 1month Weight float64 Balance *Balance } diff --git a/engine/action_trigger.go b/engine/action_trigger.go index 286736c81..fb3dc6e65 100644 --- a/engine/action_trigger.go +++ b/engine/action_trigger.go @@ -46,7 +46,8 @@ type ActionTrigger struct { BalanceRatingSubject string // filter for balance BalanceCategories utils.StringMap // filter for balance BalanceSharedGroups utils.StringMap // filter for balance - BalanceDisabled bool // filter for balance + BalanceBlocker bool + BalanceDisabled bool // filter for balance Weight float64 ActionsId string MinQueuedItems int // Trigger actions only if this number is hit (stats only) @@ -117,7 +118,7 @@ func (at *ActionTrigger) Match(a *Action) bool { return match } id := a.BalanceType == "" || at.BalanceType == a.BalanceType - thresholdType, thresholdValue, direction, destinationId, weight, ratingSubject, categories, sharedGroup, timings, disabled := true, true, true, true, true, true, true, true, true, true + thresholdType, thresholdValue, direction, destinationId, weight, ratingSubject, categories, sharedGroup, timings, blocker, disabled := true, true, true, true, true, true, true, true, true, true, true if a.ExtraParameters != "" { t := struct { ThresholdType string @@ -129,6 +130,7 @@ func (at *ActionTrigger) Match(a *Action) bool { BalanceCategories string BalanceSharedGroups string BalanceTimingTags string + BalanceBlocker bool BalanceDisabled bool }{} json.Unmarshal([]byte(a.ExtraParameters), &t) @@ -141,9 +143,10 @@ func (at *ActionTrigger) Match(a *Action) bool { sharedGroup = len(t.BalanceSharedGroups) == 0 || at.BalanceSharedGroups.Equal(utils.ParseStringMap(t.BalanceSharedGroups)) weight = t.BalanceWeight == 0 || at.BalanceWeight == t.BalanceWeight ratingSubject = t.BalanceRatingSubject == "" || at.BalanceRatingSubject == t.BalanceRatingSubject + blocker = at.BalanceBlocker == t.BalanceBlocker disabled = at.BalanceDisabled == t.BalanceDisabled } - return id && direction && thresholdType && thresholdValue && destinationId && weight && ratingSubject && categories && sharedGroup && timings && disabled + return id && direction && thresholdType && thresholdValue && destinationId && weight && ratingSubject && categories && sharedGroup && timings && blocker && disabled } // makes a shallow copy of the receiver @@ -163,6 +166,7 @@ func (at *ActionTrigger) CreateBalance() *Balance { Categories: at.BalanceCategories, SharedGroups: at.BalanceSharedGroups, TimingIDs: at.BalanceTimingTags, + Blocker: at.BalanceBlocker, Disabled: at.BalanceDisabled, Weight: at.BalanceWeight, } diff --git a/engine/loader_csv_test.go b/engine/loader_csv_test.go index fc8d985dc..359e762c6 100644 --- a/engine/loader_csv_test.go +++ b/engine/loader_csv_test.go @@ -159,18 +159,18 @@ SG3,*any,*lowest, *in,cgrates.org,call,*any,*any,*any,LCR_STANDARD,*lowest_cost,,2012-01-01T00:00:00Z,20 ` actions = ` -MINI,*topup_reset,,,*monetary,*out,,,,,*unlimited,,10,10,false,10 -MINI,*topup,,,*voice,*out,,NAT,test,,*unlimited,,100,10,false,10 -SHARED,*topup,,,*monetary,*out,,,,SG1,*unlimited,,100,10,false,10 -TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10 -SE0,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,0,10,false,10 -SE10,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,10,5,false,10 -SE10,*topup,,,*monetary,*out,,,,,*unlimited,,10,10,false,10 -EE0,*topup_reset,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,10 -EE0,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,10 -DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,false,10 -NEG,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,10 +MINI,*topup_reset,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10 +MINI,*topup,,,*voice,*out,,NAT,test,,*unlimited,,100,10,false,false,10 +SHARED,*topup,,,*monetary,*out,,,,SG1,*unlimited,,100,10,false,false,10 +TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10 +SE0,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,0,10,false,false,10 +SE10,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,10,5,false,false,10 +SE10,*topup,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10 +EE0,*topup_reset,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,false,10 +EE0,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 +DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,false,false,10 +NEG,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 ` actionPlans = ` MORE_MINUTES,MINI,ONE_TIME_RUN,10 @@ -184,16 +184,16 @@ POST_AT,NEG,*asap,10 ` actionTriggers = ` -STANDARD_TRIGGER,st0,*min_event_counter,10,false,0,,*voice,*out,,GERMANY_O2,,,,,,,,SOME_1,10 -STANDARD_TRIGGER,st1,*max_balance,200,false,0,,*voice,*out,,GERMANY,,,,,,,,SOME_2,10 -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,LOG_WARNING,10 -CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +STANDARD_TRIGGER,st0,*min_event_counter,10,false,0,,*voice,*out,,GERMANY_O2,,,,,,,,,SOME_1,10 +STANDARD_TRIGGER,st1,*max_balance,200,false,0,,*voice,*out,,GERMANY,,,,,,,,,SOME_2,10 +STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10 +CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 ` accountActions = ` vdf,minitsboy,MORE_MINUTES,STANDARD_TRIGGER,, diff --git a/engine/model_converters.go b/engine/model_converters.go index 058ea73f8..bc81870da 100644 --- a/engine/model_converters.go +++ b/engine/model_converters.go @@ -179,6 +179,8 @@ func APItoModelAction(as *utils.TPActions) (result []TpAction) { Categories: a.Categories, SharedGroups: a.SharedGroups, BalanceWeight: a.BalanceWeight, + BalanceBlocker: a.BalanceBlocker, + BalanceDisabled: a.BalanceDisabled, ExtraParameters: a.ExtraParameters, Weight: a.Weight, }) @@ -231,6 +233,7 @@ func APItoModelActionTrigger(ats *utils.TPActionTriggers) (result []TpActionTrig BalanceRatingSubject: at.BalanceRatingSubject, BalanceCategories: at.BalanceCategories, BalanceSharedGroups: at.BalanceSharedGroups, + BalanceBlocker: at.BalanceBlocker, BalanceDisabled: at.BalanceDisabled, MinQueuedItems: at.MinQueuedItems, ActionsTag: at.ActionsId, diff --git a/engine/model_helpers.go b/engine/model_helpers.go index 0d452e0e5..e6d58373b 100644 --- a/engine/model_helpers.go +++ b/engine/model_helpers.go @@ -396,6 +396,8 @@ func (tps TpActions) GetActions() (map[string][]*utils.TPAction, error) { Categories: tpAc.Categories, SharedGroups: tpAc.SharedGroups, BalanceWeight: tpAc.BalanceWeight, + BalanceBlocker: tpAc.BalanceBlocker, + BalanceDisabled: tpAc.BalanceDisabled, ExtraParameters: tpAc.ExtraParameters, Weight: tpAc.Weight, } @@ -437,6 +439,7 @@ func (tps TpActionTriggers) GetActionTriggers() (map[string][]*utils.TPActionTri BalanceRatingSubject: tpAt.BalanceRatingSubject, BalanceCategories: tpAt.BalanceCategories, BalanceSharedGroups: tpAt.BalanceSharedGroups, + BalanceBlocker: tpAt.BalanceBlocker, BalanceDisabled: tpAt.BalanceDisabled, Weight: tpAt.Weight, ActionsId: tpAt.ActionsTag, diff --git a/engine/model_helpers_test.go b/engine/model_helpers_test.go index d8edf26cb..3621b6818 100644 --- a/engine/model_helpers_test.go +++ b/engine/model_helpers_test.go @@ -265,8 +265,8 @@ func TestTPActionsAsExportSlice(t *testing.T) { }, } expectedSlc := [][]string{ - []string{"TEST_ACTIONS", "*topup_reset", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "10"}, - []string{"TEST_ACTIONS", "*http_post", "http://localhost/¶m1=value1", "", "", "", "", "", "", "", "", "", "0", "0", "false", "20"}, + []string{"TEST_ACTIONS", "*topup_reset", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "false", "10"}, + []string{"TEST_ACTIONS", "*http_post", "http://localhost/¶m1=value1", "", "", "", "", "", "", "", "", "", "0", "0", "false", "false", "20"}, } ms := APItoModelAction(tpActs) @@ -567,6 +567,7 @@ func TestTPActionPlanAsExportSlice(t *testing.T) { BalanceRatingSubject: "special1", BalanceCategories: "call", BalanceSharedGroups: "SHARED_1", + BalanceBlocker: false, BalanceDisabled: false, MinQueuedItems: 0, ActionsId: "LOG_WARNING", @@ -588,6 +589,7 @@ func TestTPActionPlanAsExportSlice(t *testing.T) { BalanceRatingSubject: "special1", BalanceCategories: "call", BalanceSharedGroups: "SHARED_1", + BalanceBlocker: false, BalanceDisabled: false, MinQueuedItems: 0, ActionsId: "LOG_WARNING", @@ -595,8 +597,8 @@ func TestTPActionPlanAsExportSlice(t *testing.T) { }, } expectedSlc := [][]string{ - []string{"STANDARD_TRIGGERS", "1", "*min_balance", "2", "false", "0", "b1", "*monetary", "*out", "call", "", "special1", "SHARED_1", "*never", "T1", "0", "false", "0", "LOG_WARNING", "10"}, - []string{"STANDARD_TRIGGERS", "2", "*max_event_counter", "5", "false", "0", "b2", "*monetary", "*out", "call", "FS_USERS", "special1", "SHARED_1", "*never", "T1", "0", "false", "0", "LOG_WARNING", "10"}, + []string{"STANDARD_TRIGGERS", "1", "*min_balance", "2", "false", "0", "b1", "*monetary", "*out", "call", "", "special1", "SHARED_1", "*never", "T1", "0", "false", "false", "0", "LOG_WARNING", "10"}, + []string{"STANDARD_TRIGGERS", "2", "*max_event_counter", "5", "false", "0", "b2", "*monetary", "*out", "call", "FS_USERS", "special1", "SHARED_1", "*never", "T1", "0", "false", "false", "0", "LOG_WARNING", "10"}, } ms := APItoModelActionTrigger(at) var slc [][]string diff --git a/engine/models.go b/engine/models.go index ead7492dc..312873962 100644 --- a/engine/models.go +++ b/engine/models.go @@ -167,8 +167,9 @@ type TpAction struct { TimingTags string `index:"11" re:"[0-9A-Za-z_;]*|\*any"` Units float64 `index:"12" re:"\d+\s*"` BalanceWeight float64 `index:"13" re:"\d+\.?\d*\s*"` - BalanceDisabled bool `index:"14" re:""` - Weight float64 `index:"15" re:"\d+\.?\d*\s*"` + BalanceBlocker bool `index:"14" re:""` + BalanceDisabled bool `index:"15" re:""` + Weight float64 `index:"16" re:"\d+\.?\d*\s*"` CreatedAt time.Time } @@ -201,10 +202,11 @@ type TpActionTrigger struct { BalanceExpiryTime string `index:"13" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` BalanceTimingTags string `index:"14" re:"[0-9A-Za-z_;]*|\*any"` BalanceWeight float64 `index:"15" re:"\d+\.?\d*"` - BalanceDisabled bool `index:"16" re:""` - MinQueuedItems int `index:"17" re:"\d+"` - ActionsTag string `index:"18" re:"\w+"` - Weight float64 `index:"19" re:"\d+\.?\d*"` + BalanceBlocker bool `index:"16" re:""` + BalanceDisabled bool `index:"17" re:""` + MinQueuedItems int `index:"18" re:"\d+"` + ActionsTag string `index:"19" re:"\w+"` + Weight float64 `index:"20" re:"\d+\.?\d*"` CreatedAt time.Time } diff --git a/utils/apitpdata.go b/utils/apitpdata.go index f4246bd3a..dd3c1c246 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -286,6 +286,8 @@ type TPAction struct { SharedGroups string // Reference to a shared group BalanceWeight float64 // Balance weight ExtraParameters string + BalanceBlocker bool + BalanceDisabled bool Weight float64 // Action's weight } @@ -487,6 +489,7 @@ type TPActionTrigger struct { BalanceRatingSubject string // filter for balance BalanceCategories string // filter for balance BalanceSharedGroups string // filter for balance + BalanceBlocker bool // filter for balance BalanceDisabled bool // filter for balance MinQueuedItems int // Trigger actions only if this number is hit (stats only) ActionsId string // Actions which will execute on threshold reached From 51f33d2283377341f518d06376aab6006f189092 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 12 Jan 2016 23:15:21 +0200 Subject: [PATCH 03/38] blocker flag in tariffplans and sql scripts --- .../mysql/create_tariffplan_tables.sql | 2 ++ .../postgres/create_tariffplan_tables.sql | 2 ++ data/tariffplans/cdrstats/ActionTriggers.csv | 12 +++++----- data/tariffplans/cdrstats/Actions.csv | 2 +- .../prepaid1centpsec/ActionTriggers.csv | 16 ++++++------- data/tariffplans/prepaid1centpsec/Actions.csv | 12 +++++----- data/tariffplans/tutorial/ActionTriggers.csv | 24 +++++++++---------- data/tariffplans/tutorial/Actions.csv | 20 ++++++++-------- general_tests/acntacts_test.go | 6 ++--- general_tests/auth_test.go | 2 +- general_tests/ddazmbl1_test.go | 4 ++-- general_tests/ddazmbl2_test.go | 4 ++-- general_tests/ddazmbl3_test.go | 2 +- 13 files changed, 56 insertions(+), 52 deletions(-) diff --git a/data/storage/mysql/create_tariffplan_tables.sql b/data/storage/mysql/create_tariffplan_tables.sql index 14feeb608..efc2e81ae 100644 --- a/data/storage/mysql/create_tariffplan_tables.sql +++ b/data/storage/mysql/create_tariffplan_tables.sql @@ -160,6 +160,7 @@ CREATE TABLE `tp_actions` ( `categories` varchar(32) NOT NULL, `shared_groups` varchar(64) NOT NULL, `balance_weight` DECIMAL(8,2) NOT NULL, + `balance_blocker` BOOLEAN NOT NULL, `balance_disabled` BOOLEAN NOT NULL, `extra_parameters` varchar(256) NOT NULL, `weight` DECIMAL(8,2) NOT NULL, @@ -211,6 +212,7 @@ CREATE TABLE `tp_action_triggers` ( `balance_expiry_time` varchar(24) NOT NULL, `balance_timing_tags` varchar(128) NOT NULL, `balance_weight` DECIMAL(8,2) NOT NULL, + `balance_blocker` BOOL NOT NULL, `balance_disabled` BOOL NOT NULL, `min_queued_items` int(11) NOT NULL, `actions_tag` varchar(64) NOT NULL, diff --git a/data/storage/postgres/create_tariffplan_tables.sql b/data/storage/postgres/create_tariffplan_tables.sql index 8ecdffc73..de3a163e7 100644 --- a/data/storage/postgres/create_tariffplan_tables.sql +++ b/data/storage/postgres/create_tariffplan_tables.sql @@ -155,6 +155,7 @@ CREATE TABLE tp_actions ( categories VARCHAR(32) NOT NULL, shared_groups VARCHAR(64) NOT NULL, balance_weight NUMERIC(8,2) NOT NULL, + balance_blocker BOOLEAN NOT NULL, balance_disabled BOOLEAN NOT NULL, extra_parameters VARCHAR(256) NOT NULL, weight NUMERIC(8,2) NOT NULL, @@ -206,6 +207,7 @@ CREATE TABLE tp_action_triggers ( balance_expiry_time VARCHAR(24) NOT NULL, balance_timing_tags VARCHAR(128) NOT NULL, balance_weight NUMERIC(8,2) NOT NULL, + balance_blocker BOOL NOT NULL, balance_disabled BOOL NOT NULL, min_queued_items INTEGER NOT NULL, actions_tag VARCHAR(64) NOT NULL, diff --git a/data/tariffplans/cdrstats/ActionTriggers.csv b/data/tariffplans/cdrstats/ActionTriggers.csv index 061f0d531..6081b9d67 100644 --- a/data/tariffplans/cdrstats/ActionTriggers.csv +++ b/data/tariffplans/cdrstats/ActionTriggers.csv @@ -1,6 +1,6 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceDisabled[16],StatsMinQueuedItems[17],ActionsTag[18],Weight[19] -CDRST3_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,3,CDRST_LOG,10 -CDRST3_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST3_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST4_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST4_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,2,CDRST_LOG,10 +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsTag[19],Weight[20] +CDRST3_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_LOG,10 +CDRST3_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST3_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST4_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST4_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,2,CDRST_LOG,10 diff --git a/data/tariffplans/cdrstats/Actions.csv b/data/tariffplans/cdrstats/Actions.csv index 09e0c5a14..72fcd27bf 100644 --- a/data/tariffplans/cdrstats/Actions.csv +++ b/data/tariffplans/cdrstats/Actions.csv @@ -1,2 +1,2 @@ #ActionsTag,Action,BalanceTag,BalanceType,Directions,Units,ExpiryTime,TimingTags,DestinationTags,RatingSubject,Categories,BalanceWeight,SharedGroup,ExtraParameters,Weight -CDRST_LOG,*log,,,,,,,,,,,,,false,10 +CDRST_LOG,*log,,,,,,,,,,,,,false,false,10 diff --git a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv index 1c8e2803b..90385f02d 100644 --- a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv +++ b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv @@ -1,9 +1,9 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceDisabled[16],StatsMinQueuedItems[17],ActionsTag[18],Weight[19] -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,LOG_BALANCE,10 +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsTag[19],Weight[20] +STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,LOG_BALANCE,10 -STANDARD_TRIGGERS,,*max_event_counter,15,false,0,,*monetary,*out,,FS_USERS,,,,,,,,LOG_BALANCE,10 -CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,2,CDRST_LOG,10 +STANDARD_TRIGGERS,,*max_event_counter,15,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_BALANCE,10 +CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,2,CDRST_LOG,10 diff --git a/data/tariffplans/prepaid1centpsec/Actions.csv b/data/tariffplans/prepaid1centpsec/Actions.csv index 6680eccaa..712d74f9d 100644 --- a/data/tariffplans/prepaid1centpsec/Actions.csv +++ b/data/tariffplans/prepaid1centpsec/Actions.csv @@ -1,6 +1,6 @@ -#ActionsTag[0],Action[1],ActionExtraParameters[2],BalanceTag[3],BalanceType[4],Directions[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingTags[11],Units[12],BalanceWeight[13],BalanceDisabled[14],Weight[15] -PREPAID_10,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,10 -BONUS_1,*topup,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,10 -LOG_BALANCE,*log,,,,,,,,,,,,,false,10 -CDRST_WARN_HTTP,*call_url,http://localhost:8080,,,,,,,,,,,,false,10 -CDRST_LOG,*log,,,,,,,,,,,,,false,10 +#ActionsTag[0],Action[1],ActionExtraParameters[2],BalanceTag[3],BalanceType[4],Directions[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingTags[11],Units[12],BalanceWeight[13],BalanceBlocker[14],BalanceDisabled[15],Weight[16] +PREPAID_10,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +BONUS_1,*topup,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 +LOG_BALANCE,*log,,,,,,,,,,,,,false,false,10 +CDRST_WARN_HTTP,*call_url,http://localhost:8080,,,,,,,,,,,,false,false,10 +CDRST_LOG,*log,,,,,,,,,,,,,false,false,10 diff --git a/data/tariffplans/tutorial/ActionTriggers.csv b/data/tariffplans/tutorial/ActionTriggers.csv index 15209486f..e68fbdf34 100644 --- a/data/tariffplans/tutorial/ActionTriggers.csv +++ b/data/tariffplans/tutorial/ActionTriggers.csv @@ -1,12 +1,12 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationIds[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingIds[14],BalanceWeight[15],BalanceDisabled[16],StatsMinQueuedItems[17],ActionsId[18],Weight[19] -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,100,false,0,,*monetary,*out,,,,,,,,,,DISABLE_AND_LOG,10 -CDRST1_WARN,,*min_asr,45,true,1m,,,,,,,,,,,,3,LOG_WARNING,10 -CDRST1_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1_WARN,,*max_acc,10,true,1m,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1001_WARN,,*min_asr,65,true,1m,,,,,,,,,,,,3,LOG_WARNING,10 -CDRST1001_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1001_WARN,,*max_acc,5,true,1m,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST3_WARN,,*min_acd,60,false,1m,,,,,,,,,,,,5,LOG_WARNING,10 +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationIds[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingIds[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsId[19],Weight[20] +STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,100,false,0,,*monetary,*out,,,,,,,,,,,DISABLE_AND_LOG,10 +CDRST1_WARN,,*min_asr,45,true,1m,,,,,,,,,,,,,3,LOG_WARNING,10 +CDRST1_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1_WARN,,*max_acc,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1001_WARN,,*min_asr,65,true,1m,,,,,,,,,,,,,3,LOG_WARNING,10 +CDRST1001_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1001_WARN,,*max_acc,5,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST3_WARN,,*min_acd,60,false,1m,,,,,,,,,,,,,5,LOG_WARNING,10 diff --git a/data/tariffplans/tutorial/Actions.csv b/data/tariffplans/tutorial/Actions.csv index ede6ee591..b2ac178b6 100644 --- a/data/tariffplans/tutorial/Actions.csv +++ b/data/tariffplans/tutorial/Actions.csv @@ -1,10 +1,10 @@ -#ActionsId[0],Action[1],ExtraParameters[2],BalanceId[3],BalanceType[4],Directions[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingIds[11],Units[12],BalanceWeight[13],BalanceDisabled[14],Weight[15] -TOPUP_RST_10,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,10 -TOPUP_RST_5,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,5,20,false,10 -TOPUP_RST_5,*topup_reset,,,*voice,*out,,DST_1002,SPECIAL_1002,,*unlimited,,90,20,false,10 -TOPUP_120_DST1003,*topup_reset,,,*voice,*out,,DST_1003,,,*unlimited,,120,20,false,10 -TOPUP_RST_SHARED_5,*topup,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,5,10,false,10 -SHARED_A_0,*topup_reset,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,0,10,false,10 -LOG_WARNING,*log,,,,,,,,,,,,,false,10 -DISABLE_AND_LOG,*log,,,,,,,,,,,,,false,10 -DISABLE_AND_LOG,*disable_account,,,,,,,,,,,,,false,10 +#ActionsId[0],Action[1],ExtraParameters[2],BalanceId[3],BalanceType[4],Directions[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingIds[11],Units[12],BalanceWeight[13],BalanceBlocker[14],BalanceDisabled[15],Weight[16] +TOPUP_RST_10,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +TOPUP_RST_5,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,5,20,false,false,10 +TOPUP_RST_5,*topup_reset,,,*voice,*out,,DST_1002,SPECIAL_1002,,*unlimited,,90,20,false,false,10 +TOPUP_120_DST1003,*topup_reset,,,*voice,*out,,DST_1003,,,*unlimited,,120,20,false,false,10 +TOPUP_RST_SHARED_5,*topup,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,5,10,false,false,10 +SHARED_A_0,*topup_reset,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,0,10,false,false,10 +LOG_WARNING,*log,,,,,,,,,,,,,false,false,10 +DISABLE_AND_LOG,*log,,,,,,,,,,,,,false,false,10 +DISABLE_AND_LOG,*disable_account,,,,,,,,,,,,,false,false,10 diff --git a/general_tests/acntacts_test.go b/general_tests/acntacts_test.go index d61797377..aea15334c 100644 --- a/general_tests/acntacts_test.go +++ b/general_tests/acntacts_test.go @@ -44,9 +44,9 @@ func TestAcntActsLoadCsv(t *testing.T) { ratingProfiles := `` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*voice,*out,,*any,,,*unlimited,,10,10,false,10 -DISABLE_ACNT,*disable_account,,,,,,,,,,,,,false,10 -ENABLE_ACNT,*enable_account,,,,,,,,,,,,,false,10` + actions := `TOPUP10_AC,*topup_reset,,,*voice,*out,,*any,,,*unlimited,,10,10,false,false,10 +DISABLE_ACNT,*disable_account,,,,,,,,,,,,,false,false,10 +ENABLE_ACNT,*enable_account,,,,,,,,,,,,,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10` actionTriggers := `` accountActions := `cgrates.org,1,TOPUP10_AT,,,` diff --git a/general_tests/auth_test.go b/general_tests/auth_test.go index b9069716e..aa0836217 100644 --- a/general_tests/auth_test.go +++ b/general_tests/auth_test.go @@ -56,7 +56,7 @@ RP_ANY,DR_ANY_1CNT,*any,10` *out,cgrates.org,call,*any,2013-01-06T00:00:00Z,RP_ANY,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,10` + actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,*asap,10` actionTriggers := `` accountActions := `cgrates.org,testauthpostpaid1,TOPUP10_AT,,,` diff --git a/general_tests/ddazmbl1_test.go b/general_tests/ddazmbl1_test.go index afc603bb8..fe0ef141e 100644 --- a/general_tests/ddazmbl1_test.go +++ b/general_tests/ddazmbl1_test.go @@ -53,8 +53,8 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10` + actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10 TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` diff --git a/general_tests/ddazmbl2_test.go b/general_tests/ddazmbl2_test.go index cd371fe7a..2e7df3d3b 100644 --- a/general_tests/ddazmbl2_test.go +++ b/general_tests/ddazmbl2_test.go @@ -53,8 +53,8 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10` + actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10 TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` diff --git a/general_tests/ddazmbl3_test.go b/general_tests/ddazmbl3_test.go index 2df5c03e3..40ed39897 100644 --- a/general_tests/ddazmbl3_test.go +++ b/general_tests/ddazmbl3_test.go @@ -53,7 +53,7 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10` + actions := `TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` accountActions := `cgrates.org,12346,TOPUP10_AT,,,` From 2b3fbbdf96841b6fc5397fc31930b39d5180e4dd Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 13 Jan 2016 10:25:59 +0200 Subject: [PATCH 04/38] dmtagent test fix --- agents/dmtagent_it_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agents/dmtagent_it_test.go b/agents/dmtagent_it_test.go index 9a449b0b0..a3d1cbd83 100644 --- a/agents/dmtagent_it_test.go +++ b/agents/dmtagent_it_test.go @@ -434,7 +434,7 @@ func TestDmtAgentCdrs(t *testing.T) { if cdrs[0].Usage != "610" { t.Errorf("Unexpected CDR Usage received, cdr: %+v ", cdrs[0]) } - if cdrs[0].Cost != 0.795 { + if cdrs[0].Cost != 0.5349 { t.Errorf("Unexpected CDR Cost received, cdr: %+v ", cdrs[0]) } } From fed3844163715d650ffb13e81859ded500791f61 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 13 Jan 2016 11:42:33 +0200 Subject: [PATCH 05/38] fix for tariffplan line --- data/tariffplans/prepaid1centpsec/ActionTriggers.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv index 90385f02d..c50ff9a7c 100644 --- a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv +++ b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv @@ -1,6 +1,6 @@ #Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsTag[19],Weight[20] STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,LOG_BALANCE,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 STANDARD_TRIGGERS,,*max_event_counter,15,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_BALANCE,10 CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 From 2905d4d089901cdaefdfae788d24ed71831459ae Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 13 Jan 2016 18:51:00 +0200 Subject: [PATCH 06/38] fixes and tests for blocker flag, fixes #274 --- engine/account.go | 10 ++++---- engine/balances.go | 1 + engine/calldesc.go | 1 - engine/calldesc_test.go | 52 +++++++++++++++++++++++++++++++++++++++ engine/loader_csv_test.go | 35 +++++++++++++++++--------- engine/storage_test.go | 2 +- engine/tp_reader.go | 15 +++++++++++ 7 files changed, 97 insertions(+), 19 deletions(-) diff --git a/engine/account.go b/engine/account.go index e529a7966..46b033eaf 100644 --- a/engine/account.go +++ b/engine/account.go @@ -318,11 +318,11 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo } // check for blocker if dryRun && balance.Blocker { + //log.Print("BLOCKER!") return // don't go to next balances } } } - // debit money moneyBalanceChecker := true for moneyBalanceChecker { @@ -336,14 +336,13 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo return nil, debitErr } //utils.Logger.Info(fmt.Sprintf("CD AFTER MONEY: %+v", cd)) - //log.Printf("partCC: %+v", partCC) if partCC != nil { cc.Timespans = append(cc.Timespans, partCC.Timespans...) cc.negativeConnectFee = partCC.negativeConnectFee - //for i, ts := range cc.Timespans { - //log.Printf("cc.times[an[%d]: %+v\n", i, ts) - //} + /*for i, ts := range cc.Timespans { + log.Printf("cc.times[an[%d]: %+v\n", i, ts) + }*/ cd.TimeStart = cc.GetEndTime() //log.Printf("CD: %+v", cd) //log.Printf("CD: %+v - %+v", cd.TimeStart, cd.TimeEnd) @@ -360,6 +359,7 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo } // check for blocker if dryRun && balance.Blocker { + //log.Print("BLOCKER!") return // don't go to next balances } } diff --git a/engine/balances.go b/engine/balances.go index 167bc0cca..8f7e9ca39 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -234,6 +234,7 @@ func (b *Balance) Clone() *Balance { SharedGroups: b.SharedGroups, TimingIDs: b.TimingIDs, Timings: b.Timings, // should not be a problem with aliasing + Blocker: b.Blocker, Disabled: b.Disabled, dirty: b.dirty, } diff --git a/engine/calldesc.go b/engine/calldesc.go index 335252161..7ceb823f2 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -555,7 +555,6 @@ func (origCD *CallDescriptor) getMaxSessionDuration(origAcc *Account) (time.Dura //utils.Logger.Debug("ACCOUNT: " + utils.ToJSON(account)) //utils.Logger.Debug("DEFAULT_BALANCE: " + utils.ToJSON(defaultBalance)) - // cc, err := cd.debit(account, true, false) //utils.Logger.Debug("CC: " + utils.ToJSON(cc)) //log.Print("CC: ", utils.ToIJSON(cc)) diff --git a/engine/calldesc_test.go b/engine/calldesc_test.go index b33807f38..f470910ae 100644 --- a/engine/calldesc_test.go +++ b/engine/calldesc_test.go @@ -537,6 +537,57 @@ func TestMaxSessionTimeWithMaxCost(t *testing.T) { } } +func TestGetMaxSessiontWithBlocker(t *testing.T) { + ap, _ := ratingStorage.GetActionPlan("BLOCK_AT", false) + for _, at := range ap.ActionTimings { + at.accountIDs = ap.AccountIDs + at.Execute() + } + acc, err := accountingStorage.GetAccount("cgrates.org:block") + if err != nil { + t.Error("error getting account: ", err) + } + if len(acc.BalanceMap[utils.MONETARY]) != 2 || + acc.BalanceMap[utils.MONETARY][0].Blocker != true { + for _, b := range acc.BalanceMap[utils.MONETARY] { + t.Logf("B: %+v", b) + } + t.Error("Error executing action plan on account: ", acc.BalanceMap[utils.MONETARY]) + } + cd := &CallDescriptor{ + Direction: "*out", + Category: "call", + Tenant: "cgrates.org", + Subject: "block", + Account: "block", + Destination: "0723", + TimeStart: time.Date(2016, 1, 13, 14, 0, 0, 0, time.UTC), + TimeEnd: time.Date(2016, 1, 13, 14, 30, 0, 0, time.UTC), + MaxCostSoFar: 0, + } + result, err := cd.GetMaxSessionDuration() + expected := 985 * time.Second + if result != expected || err != nil { + t.Errorf("Expected %v was %v (%v)", expected, result, err) + } + cd = &CallDescriptor{ + Direction: "*out", + Category: "call", + Tenant: "cgrates.org", + Subject: "block", + Account: "block", + Destination: "444", + TimeStart: time.Date(2016, 1, 13, 14, 0, 0, 0, time.UTC), + TimeEnd: time.Date(2016, 1, 13, 14, 30, 0, 0, time.UTC), + MaxCostSoFar: 0, + } + result, err = cd.GetMaxSessionDuration() + expected = 30 * time.Minute + if result != expected || err != nil { + t.Errorf("Expected %v was %v (%v)", expected, result, err) + } +} + func TestGetCostWithMaxCost(t *testing.T) { ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false) for _, at := range ap.ActionTimings { @@ -560,6 +611,7 @@ func TestGetCostWithMaxCost(t *testing.T) { t.Errorf("Expected %v was %v", expected, cc.Cost) } } + func TestGetCostRoundingIssue(t *testing.T) { ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false) for _, at := range ap.ActionTimings { diff --git a/engine/loader_csv_test.go b/engine/loader_csv_test.go index 359e762c6..f4b1e507b 100644 --- a/engine/loader_csv_test.go +++ b/engine/loader_csv_test.go @@ -147,6 +147,7 @@ DY_PLAN,RT_DY,*any,10 *in,cgrates.org,LCR_STANDARD,max,2013-03-23T00:00:00Z,RP_MX,, *out,cgrates.org,call,money,2015-02-28T00:00:00Z,EVENING,, *out,cgrates.org,call,dy,2015-02-28T00:00:00Z,DY_PLAN,, +*out,cgrates.org,call,block,2015-02-28T00:00:00Z,DY_PLAN,, ` sharedGroups = ` SG1,*any,*lowest, @@ -171,6 +172,8 @@ EE0,*topup_reset,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,false,10 EE0,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,false,false,10 NEG,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 +BLOCK,*topup,,bblocker,*monetary,*out,,NAT,,,*unlimited,,10,20,true,false,20 +BLOCK,*topup,,bfree,*monetary,*out,,,,,*unlimited,,20,10,false,false,10 ` actionPlans = ` MORE_MINUTES,MINI,ONE_TIME_RUN,10 @@ -181,6 +184,7 @@ TOPUP_SHARED0_AT,SE0,*asap,10 TOPUP_SHARED10_AT,SE10,*asap,10 TOPUP_EMPTY_AT,EE0,*asap,10 POST_AT,NEG,*asap,10 +BLOCK_AT,BLOCK,*asap,10 ` actionTriggers = ` @@ -207,6 +211,7 @@ vdf,emptyX,TOPUP_EMPTY_AT,,, vdf,emptyY,TOPUP_EMPTY_AT,,, vdf,post,POST_AT,,, cgrates.org,alodis,TOPUP_EMPTY_AT,,true,true +cgrates.org,block,BLOCK_AT,,false,false ` derivedCharges = ` @@ -790,7 +795,7 @@ func TestLoadRatingPlans(t *testing.T) { } func TestLoadRatingProfiles(t *testing.T) { - if len(csvr.ratingProfiles) != 22 { + if len(csvr.ratingProfiles) != 23 { t.Error("Failed to load rating profiles: ", len(csvr.ratingProfiles), csvr.ratingProfiles) } rp := csvr.ratingProfiles["*out:test:0:trp"] @@ -809,7 +814,7 @@ func TestLoadRatingProfiles(t *testing.T) { } func TestLoadActions(t *testing.T) { - if len(csvr.actions) != 9 { + if len(csvr.actions) != 10 { t.Error("Failed to load actions: ", len(csvr.actions)) } as1 := csvr.actions["MINI"] @@ -822,12 +827,14 @@ func TestLoadActions(t *testing.T) { ExtraParameters: "", Weight: 10, Balance: &Balance{ - Uuid: as1[0].Balance.Uuid, - Directions: utils.NewStringMap(utils.OUT), - Value: 10, - Weight: 10, - TimingIDs: utils.StringMap{}, - SharedGroups: utils.StringMap{}, + Uuid: as1[0].Balance.Uuid, + Directions: utils.NewStringMap(utils.OUT), + Value: 10, + Weight: 10, + DestinationIds: utils.StringMap{}, + TimingIDs: utils.StringMap{}, + SharedGroups: utils.StringMap{}, + Categories: utils.StringMap{}, }, }, &Action{ @@ -846,10 +853,11 @@ func TestLoadActions(t *testing.T) { DestinationIds: utils.NewStringMap("NAT"), TimingIDs: utils.StringMap{}, SharedGroups: utils.StringMap{}, + Categories: utils.StringMap{}, }, }, } - if !reflect.DeepEqual(as1[1], expected[1]) { + if !reflect.DeepEqual(as1, expected) { t.Errorf("Error loading action1: %+v", as1[0].Balance) } as2 := csvr.actions["SHARED"] @@ -868,10 +876,11 @@ func TestLoadActions(t *testing.T) { Weight: 10, SharedGroups: utils.NewStringMap("SG1"), TimingIDs: utils.StringMap{}, + Categories: utils.StringMap{}, }, }, } - if !reflect.DeepEqual(as2[0], expected[0]) { + if !reflect.DeepEqual(as2, expected) { t.Errorf("Error loading action: %+v", as2[0].Balance) } as3 := csvr.actions["DEFEE"] @@ -886,7 +895,9 @@ func TestLoadActions(t *testing.T) { Directions: utils.StringMap{}, DestinationIds: utils.StringMap{}, TimingIDs: utils.StringMap{}, + Categories: utils.StringMap{}, SharedGroups: utils.StringMap{}, + Blocker: false, }, }, } @@ -982,7 +993,7 @@ func TestLoadLCRs(t *testing.T) { } func TestLoadActionTimings(t *testing.T) { - if len(csvr.actionPlans) != 6 { + if len(csvr.actionPlans) != 7 { t.Error("Failed to load action timings: ", len(csvr.actionPlans)) } atm := csvr.actionPlans["MORE_MINUTES"] @@ -1070,7 +1081,7 @@ func TestLoadActionTriggers(t *testing.T) { } func TestLoadAccountActions(t *testing.T) { - if len(csvr.accountActions) != 11 { + if len(csvr.accountActions) != 12 { t.Error("Failed to load account actions: ", len(csvr.accountActions)) } aa := csvr.accountActions["vdf:minitsboy"] diff --git a/engine/storage_test.go b/engine/storage_test.go index 375979fa9..98a312739 100644 --- a/engine/storage_test.go +++ b/engine/storage_test.go @@ -274,7 +274,7 @@ func TestDifferentUuid(t *testing.T) { func TestStorageTask(t *testing.T) { // clean previous unused tasks - for i := 0; i < 16; i++ { + for i := 0; i < 18; i++ { ratingStorage.PopTask() } diff --git a/engine/tp_reader.go b/engine/tp_reader.go index 5f22f0a2a..b0982699f 100644 --- a/engine/tp_reader.go +++ b/engine/tp_reader.go @@ -523,6 +523,8 @@ func (tpr *TpReader) LoadActions() (err error) { DestinationIds: utils.ParseStringMap(tpact.DestinationIds), SharedGroups: utils.ParseStringMap(tpact.SharedGroups), TimingIDs: utils.ParseStringMap(tpact.TimingTags), + Blocker: tpact.BalanceBlocker, + Disabled: tpact.BalanceDisabled, }, } // load action timings from tags @@ -640,6 +642,8 @@ func (tpr *TpReader) LoadActionTriggers() (err error) { BalanceRatingSubject: atr.BalanceRatingSubject, BalanceCategories: utils.ParseStringMap(atr.BalanceCategories), BalanceSharedGroups: utils.ParseStringMap(atr.BalanceSharedGroups), + BalanceBlocker: atr.BalanceBlocker, + BalanceDisabled: atr.BalanceDisabled, Weight: atr.Weight, ActionsId: atr.ActionsId, MinQueuedItems: atr.MinQueuedItems, @@ -788,9 +792,12 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error BalanceDestinationIds: utils.ParseStringMap(apiAtr.BalanceDestinationIds), BalanceWeight: apiAtr.BalanceWeight, BalanceExpirationDate: expTime, + BalanceTimingTags: utils.ParseStringMap(apiAtr.BalanceTimingTags), BalanceRatingSubject: apiAtr.BalanceRatingSubject, BalanceCategories: utils.ParseStringMap(apiAtr.BalanceCategories), BalanceSharedGroups: utils.ParseStringMap(apiAtr.BalanceSharedGroups), + BalanceBlocker: apiAtr.BalanceBlocker, + BalanceDisabled: apiAtr.BalanceDisabled, Weight: apiAtr.Weight, ActionsId: apiAtr.ActionsId, } @@ -831,13 +838,17 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, Balance: &Balance{ + Id: tpact.BalanceId, Value: tpact.Units, Weight: tpact.BalanceWeight, RatingSubject: tpact.RatingSubject, + Categories: utils.ParseStringMap(tpact.Categories), Directions: utils.ParseStringMap(tpact.Directions), DestinationIds: utils.ParseStringMap(tpact.DestinationIds), SharedGroups: utils.ParseStringMap(tpact.SharedGroups), TimingIDs: utils.ParseStringMap(tpact.TimingTags), + Blocker: tpact.BalanceBlocker, + Disabled: tpact.BalanceDisabled, }, } } @@ -1056,13 +1067,17 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) { ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, Balance: &Balance{ + Id: tpact.BalanceId, Value: tpact.Units, Weight: tpact.BalanceWeight, RatingSubject: tpact.RatingSubject, + Categories: utils.ParseStringMap(tpact.Categories), Directions: utils.ParseStringMap(tpact.Directions), DestinationIds: utils.ParseStringMap(tpact.DestinationIds), SharedGroups: utils.ParseStringMap(tpact.SharedGroups), TimingIDs: utils.ParseStringMap(tpact.TimingTags), + Blocker: tpact.BalanceBlocker, + Disabled: tpact.BalanceDisabled, }, } } From f453040b72eaf2c190321313511b955a764e40a7 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 13 Jan 2016 19:26:42 +0200 Subject: [PATCH 07/38] include blcoker in balance match --- engine/account.go | 4 +++- engine/balances.go | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/engine/account.go b/engine/account.go index 46b033eaf..d65c8ab4d 100644 --- a/engine/account.go +++ b/engine/account.go @@ -176,9 +176,11 @@ func (ub *Account) enableDisableBalanceAction(a *Action) error { } found := false id := a.BalanceType + disabled := a.Balance.Disabled + a.Balance.Disabled = !disabled // match for the opposite for _, b := range ub.BalanceMap[id] { if b.MatchFilter(a.Balance, false) { - b.Disabled = a.Balance.Disabled + b.Disabled = disabled b.dirty = true found = true } diff --git a/engine/balances.go b/engine/balances.go index 8f7e9ca39..967289a8b 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -69,7 +69,6 @@ func (b *Balance) Equal(o *Balance) bool { b.Categories.Equal(o.Categories) && b.SharedGroups.Equal(o.SharedGroups) && b.Disabled == o.Disabled && - b.Blocker == o.Blocker } @@ -88,6 +87,8 @@ func (b *Balance) MatchFilter(o *Balance, skipIds bool) bool { } return (o.ExpirationDate.IsZero() || b.ExpirationDate.Equal(o.ExpirationDate)) && (o.Weight == 0 || b.Weight == o.Weight) && + (b.Blocker == o.Blocker) && + (b.Disabled == o.Disabled) && (len(o.DestinationIds) == 0 || b.DestinationIds.Includes(o.DestinationIds)) && (len(o.Directions) == 0 || b.Directions.Includes(o.Directions)) && (len(o.Categories) == 0 || b.Categories.Includes(o.Categories)) && From 269ada2d00df33d25d53ee78e69120b6cbc7027d Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 13 Jan 2016 19:58:23 +0200 Subject: [PATCH 08/38] started conditional actions --- engine/account.go | 13 ++++++++++++ engine/action.go | 52 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/engine/account.go b/engine/account.go index d65c8ab4d..64add9128 100644 --- a/engine/account.go +++ b/engine/account.go @@ -697,6 +697,19 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance } } +func (acc *Account) matchConditions(condition string) (bool, error) { + condMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(condition), condMap); err != nil { + return false, err + } + operator, found := condMap["Operator"] + if !found { + return false, errors.New("operator not found") + } + _ = operator + return true, nil +} + // used in some api for transition func (acc *Account) AsOldStructure() interface{} { type Balance struct { diff --git a/engine/action.go b/engine/action.go index 2353173d4..3cbf06cbb 100644 --- a/engine/action.go +++ b/engine/action.go @@ -72,6 +72,10 @@ const ( CDRLOG = "*cdrlog" SET_DDESTINATIONS = "*set_ddestinations" TRANSFER_MONETARY_DEFAULT = "*transfer_monetary_default" + CONDITIONAL_TOPUP = "*conditional_topup" + CONDITIONAL_TOPUP_RESET = "*conditional_topup_reset" + CONDITIONAL_DEBIT = "*conditional_debit" + CONDITIONAL_DEBIT_RESET = "*conditional_debit_reset" ) func (a *Action) Clone() *Action { @@ -131,11 +135,19 @@ func getActionFunc(typ string) (actionTypeFunc, bool) { case SET_DDESTINATIONS: return setddestinations, true case REMOVE_ACCOUNT: - return removeAccount, true + return removeAccountAction, true case REMOVE_BALANCE: - return removeBalance, true + return removeBalanceAction, true case TRANSFER_MONETARY_DEFAULT: return transferMonetaryDefault, true + case CONDITIONAL_DEBIT: + return conditionalDebitAction, true + case CONDITIONAL_DEBIT_RESET: + return conditionalDebitResetAction, true + case CONDITIONAL_TOPUP: + return conditionalTopupAction, true + case CONDITIONAL_TOPUP_RESET: + return conditionalTopupResetAction, true } return nil, false } @@ -522,7 +534,7 @@ func setddestinations(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actio return nil } -func removeAccount(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { +func removeAccountAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { var accID string if ub != nil { accID = ub.Id @@ -568,7 +580,7 @@ func removeAccount(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) return nil } -func removeBalance(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { +func removeBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { if _, exists := ub.BalanceMap[a.BalanceType]; !exists { return utils.ErrNotFound } @@ -610,6 +622,38 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a return accountingStorage.SetAccount(acc) } +func conditionalDebitAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { + return debitAction(acc, sq, a, acs) + } else { + return err + } +} + +func conditionalDebitResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { + return debitResetAction(acc, sq, a, acs) + } else { + return err + } +} + +func conditionalTopupAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { + return topupAction(acc, sq, a, acs) + } else { + return err + } +} + +func conditionalTopupResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { + return topupResetAction(acc, sq, a, acs) + } else { + return err + } +} + // Structure to store actions according to weight type Actions []*Action From 0406433d48e450cf87f8fd6b052210fb3649ae1b Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Thu, 14 Jan 2016 16:00:05 +0200 Subject: [PATCH 09/38] added filter to *transfer_monetary_default action --- engine/action.go | 3 ++- engine/actions_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++ engine/balances.go | 3 +++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/engine/action.go b/engine/action.go index 3cbf06cbb..cfce5fe18 100644 --- a/engine/action.go +++ b/engine/action.go @@ -611,7 +611,8 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a bChain := acc.BalanceMap[utils.MONETARY] for _, balance := range bChain { if balance.Uuid != defaultBalance.Uuid && - balance.Id != defaultBalance.Id { // extra caution + balance.Id != defaultBalance.Id && // extra caution + balance.MatchFilter(a.Balance, false) { if balance.Value > 0 { defaultBalance.Value += balance.Value balance.Value = 0 diff --git a/engine/actions_test.go b/engine/actions_test.go index bccf168c6..12adecf9d 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -1507,6 +1507,67 @@ func TestActionTransferMonetaryDefault(t *testing.T) { } } +func TestActionTransferMonetaryDefaultFilter(t *testing.T) { + err := accountingStorage.SetAccount( + &Account{ + Id: "cgrates.org:trans", + 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: TRANSFER_MONETARY_DEFAULT, + Balance: &Balance{Weight: 20}, + } + + at := &ActionTiming{ + accountIDs: map[string]struct{}{"cgrates.org:trans": struct{}{}}, + actions: Actions{a}, + } + at.Execute() + + afterUb, err := accountingStorage.GetAccount("cgrates.org:trans") + if err != nil { + t.Error("account not found: ", err, afterUb) + } + if afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 20 || + afterUb.BalanceMap[utils.MONETARY][0].Value != 19 || + afterUb.BalanceMap[utils.MONETARY][1].Value != 0 || + afterUb.BalanceMap[utils.MONETARY][2].Value != 1 || + afterUb.BalanceMap[utils.MONETARY][3].Value != 0 { + 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/balances.go b/engine/balances.go index 967289a8b..bc0ad33f8 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -73,6 +73,9 @@ func (b *Balance) Equal(o *Balance) bool { } func (b *Balance) MatchFilter(o *Balance, skipIds bool) bool { + if o == nil { + return true + } if !skipIds && o.Uuid != "" { return b.Uuid == o.Uuid } From cba3c25b0114112556b4f65f1712188d83ab9b94 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Fri, 15 Jan 2016 19:05:32 +0200 Subject: [PATCH 10/38] condition string parser class --- engine/account.go | 2 +- utils/cond_loader.go | 101 ++++++++++++++++++++++++++++++++++++++ utils/cond_loader_test.go | 11 +++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 utils/cond_loader.go create mode 100644 utils/cond_loader_test.go diff --git a/engine/account.go b/engine/account.go index 64add9128..ec486c756 100644 --- a/engine/account.go +++ b/engine/account.go @@ -704,7 +704,7 @@ func (acc *Account) matchConditions(condition string) (bool, error) { } operator, found := condMap["Operator"] if !found { - return false, errors.New("operator not found") + operator = utils.COND_EQ } _ = operator return true, nil diff --git a/utils/cond_loader.go b/utils/cond_loader.go new file mode 100644 index 000000000..e9b375ee5 --- /dev/null +++ b/utils/cond_loader.go @@ -0,0 +1,101 @@ +package utils + +import ( + "encoding/json" + "strings" +) + +const ( + COND_EQ = "*eq" + COND_GT = "*gt" + COND_LT = "*lt" + COND_EXP = "*exp" +) + +type CondElement interface { + AddChild(CondElement) + CheckStruct(interface{}) (bool, error) +} + +type OperatorSlice struct { + Operator string + Slice []CondElement +} + +func (os *OperatorSlice) AddChild(ce CondElement) { + os.Slice = append(os.Slice, ce) +} +func (os *OperatorSlice) CheckStruct(o interface{}) (bool, error) { return true, nil } + +type KeyStruct struct { + Key string + Struct CondElement +} + +func (ks *KeyStruct) AddChild(ce CondElement) { + ks.Struct = ce +} +func (ks *KeyStruct) CheckStruct(o interface{}) (bool, error) { return true, nil } + +type OperatorValue struct { + Operator string + Value interface{} +} + +func (ov *OperatorValue) AddChild(CondElement) {} +func (ov *OperatorValue) CheckStruct(o interface{}) (bool, error) { return true, nil } + +type KeyValue struct { + Key string + Value interface{} +} + +func (os *KeyValue) AddChild(CondElement) {} +func (os *KeyValue) CheckStruct(o interface{}) (bool, error) { return true, nil } + +func isOperator(s string) bool { + return strings.HasPrefix(s, "*") +} + +type CondLoader struct { + RootElement CondElement +} + +func (cp *CondLoader) Load(a map[string]interface{}, parentElement CondElement) (CondElement, error) { + for key, value := range a { + var currentElement CondElement + switch t := value.(type) { + case []interface{}: + currentElement = &OperatorSlice{Operator: key} + for _, e := range t { + cp.Load(e.(map[string]interface{}), currentElement) + } + case map[string]interface{}: + currentElement = &KeyStruct{Key: key} + //log.Print("map: ", t) + cp.Load(t, currentElement) + case interface{}: + if isOperator(key) { + currentElement = &OperatorValue{Operator: key, Value: t} + } else { + currentElement = &KeyValue{Key: key, Value: t} + } + //log.Print("generic interface: ", t) + default: + return nil, ErrParserError + } + if parentElement != nil { + parentElement.AddChild(currentElement) + } else { + return currentElement, nil + } + } + return nil, nil +} + +func (cp *CondLoader) Parse(s string) (err error) { + a := make(map[string]interface{}) + json.Unmarshal([]byte([]byte(s)), &a) + cp.RootElement, err = cp.Load(a, nil) + return +} diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go new file mode 100644 index 000000000..5d4a251ed --- /dev/null +++ b/utils/cond_loader_test.go @@ -0,0 +1,11 @@ +package utils + +import "testing" + +func TestCondLoader(t *testing.T) { + cl := &CondLoader{} + err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) + if err != nil || cl.RootElement == nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.RootElement), err) + } +} From 250254b213708a679179a4780ec9f7e2e0cc1e2f Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Mon, 18 Jan 2016 13:36:40 +0200 Subject: [PATCH 11/38] return root object instead of storing it --- utils/cond_loader.go | 13 ++++++------- utils/cond_loader_test.go | 6 +++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/utils/cond_loader.go b/utils/cond_loader.go index e9b375ee5..a7bcd617d 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -57,9 +57,7 @@ func isOperator(s string) bool { return strings.HasPrefix(s, "*") } -type CondLoader struct { - RootElement CondElement -} +type CondLoader struct{} func (cp *CondLoader) Load(a map[string]interface{}, parentElement CondElement) (CondElement, error) { for key, value := range a { @@ -93,9 +91,10 @@ func (cp *CondLoader) Load(a map[string]interface{}, parentElement CondElement) return nil, nil } -func (cp *CondLoader) Parse(s string) (err error) { +func (cp *CondLoader) Parse(s string) (root CondElement, err error) { a := make(map[string]interface{}) - json.Unmarshal([]byte([]byte(s)), &a) - cp.RootElement, err = cp.Load(a, nil) - return + if err := json.Unmarshal([]byte([]byte(s)), &a); err != nil { + return nil, err + } + return cp.Load(a, nil) } diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index 5d4a251ed..70b0382f8 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -4,8 +4,8 @@ import "testing" func TestCondLoader(t *testing.T) { cl := &CondLoader{} - err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) - if err != nil || cl.RootElement == nil { - t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.RootElement), err) + root, err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) + if err != nil || root == nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(root), err) } } From f097ac86469dee2f2593f426cb9c688480029b2a Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Mon, 18 Jan 2016 13:40:56 +0200 Subject: [PATCH 12/38] no account protection (*transfer_monetary_default) --- engine/action.go | 3 +++ utils/consts.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/engine/action.go b/engine/action.go index cfce5fe18..79e0df5df 100644 --- a/engine/action.go +++ b/engine/action.go @@ -604,6 +604,9 @@ func removeBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Ac } func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + if acc == nil { + return utils.NewErrAccountNotFound("", "*transfer_monetary_default") + } if _, exists := acc.BalanceMap[utils.MONETARY]; !exists { return utils.ErrNotFound } diff --git a/utils/consts.go b/utils/consts.go index 75530494f..5eddb8d00 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -13,6 +13,10 @@ func NewErrServerError(err error) error { return fmt.Errorf("SERVER_ERROR: %s", err) } +func NewErrAccountNotFound(accID, context string) error { + return fmt.Errorf("ACCOUNT_NOT_FOUND: %s in %s", accID, context) +} + var ( ErrNotImplemented = errors.New("NOT_IMPLEMENTED") ErrNotFound = errors.New("NOT_FOUND") From e0c8ea02c5225b32c974fee3f80126084e6ae4fa Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Mon, 18 Jan 2016 17:22:18 +0200 Subject: [PATCH 13/38] refined error messages --- console/scheduler_queue.go | 2 +- engine/action.go | 3 ++- engine/calldesc.go | 6 +++++- utils/consts.go | 7 ++----- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/console/scheduler_queue.go b/console/scheduler_queue.go index 079575373..c91eb92a7 100644 --- a/console/scheduler_queue.go +++ b/console/scheduler_queue.go @@ -22,7 +22,7 @@ import "github.com/cgrates/cgrates/apier/v1" func init() { c := &CmdGetScheduledActions{ - name: "scheduler_queue", + name: "2", rpcMethod: "ApierV1.GetScheduledActions", rpcParams: &v1.AttrsGetScheduledActions{}, } diff --git a/engine/action.go b/engine/action.go index 79e0df5df..fdf0eaff1 100644 --- a/engine/action.go +++ b/engine/action.go @@ -605,7 +605,8 @@ func removeBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Ac func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { if acc == nil { - return utils.NewErrAccountNotFound("", "*transfer_monetary_default") + utils.Logger.Err("*transfer_monetary_default called without account") + return utils.ErrAccountNotFound } if _, exists := acc.BalanceMap[utils.MONETARY]; !exists { return utils.ErrNotFound diff --git a/engine/calldesc.go b/engine/calldesc.go index 7ceb823f2..46771baa4 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -195,7 +195,11 @@ func (cd *CallDescriptor) LoadRatingPlans() (err error) { err, _ = cd.getRatingPlansForPrefix(cd.GetKey(FALLBACK_SUBJECT), 1) } //load the rating plans - if err != nil || !cd.continousRatingInfos() { + if err != nil { + utils.Logger.Err(fmt.Sprintf("Rating plan not found for destination %s and account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject))) + err = utils.ErrRatingPlanNotFound + } + if !cd.continousRatingInfos() { utils.Logger.Err(fmt.Sprintf("Destination %s not authorized for account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject))) err = utils.ErrUnauthorizedDestination } diff --git a/utils/consts.go b/utils/consts.go index 5eddb8d00..345408b29 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -13,10 +13,6 @@ func NewErrServerError(err error) error { return fmt.Errorf("SERVER_ERROR: %s", err) } -func NewErrAccountNotFound(accID, context string) error { - return fmt.Errorf("ACCOUNT_NOT_FOUND: %s in %s", accID, context) -} - var ( ErrNotImplemented = errors.New("NOT_IMPLEMENTED") ErrNotFound = errors.New("NOT_FOUND") @@ -30,7 +26,8 @@ var ( ErrInvalidPath = errors.New("INVALID_PATH") ErrInvalidKey = errors.New("INVALID_KEY") ErrUnauthorizedDestination = errors.New("UNAUTHORIZED_DESTINATION") - ErrAccountNotFound = errors.New("AccountNotFound") + ErrRatingPlanNotFound = errors.New("RATING_PLAN_NOT_FOUND") + ErrAccountNotFound = errors.New("ACCOUNT_NOT_FOUND") ) const ( From 5e7bdbcd974d6836eda0eacb448f12798dd06c00 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Mon, 18 Jan 2016 22:03:11 +0200 Subject: [PATCH 14/38] condition language parser and tests --- engine/account.go | 23 ++-- engine/action.go | 8 +- utils/cond_loader.go | 173 ++++++++++++++++++------- utils/cond_loader_test.go | 258 +++++++++++++++++++++++++++++++++++++- 4 files changed, 399 insertions(+), 63 deletions(-) diff --git a/engine/account.go b/engine/account.go index ec486c756..e539721f0 100644 --- a/engine/account.go +++ b/engine/account.go @@ -697,17 +697,20 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance } } -func (acc *Account) matchConditions(condition string) (bool, error) { - condMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(condition), condMap); err != nil { - return false, err +func (acc *Account) matchConditions(condition, balanceType string) (bool, error) { + + cl := &utils.CondLoader{} + cl.Parse(condition) + for _, b := range acc.BalanceMap[balanceType] { + check, err := cl.Check(b) + if err != nil { + return false, err + } + if check { + return true, nil + } } - operator, found := condMap["Operator"] - if !found { - operator = utils.COND_EQ - } - _ = operator - return true, nil + return false, nil } // used in some api for transition diff --git a/engine/action.go b/engine/action.go index fdf0eaff1..97a1b807b 100644 --- a/engine/action.go +++ b/engine/action.go @@ -628,7 +628,7 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a } func conditionalDebitAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { + if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { return debitAction(acc, sq, a, acs) } else { return err @@ -636,7 +636,7 @@ func conditionalDebitAction(acc *Account, sq *StatsQueueTriggered, a *Action, ac } func conditionalDebitResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { + if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { return debitResetAction(acc, sq, a, acs) } else { return err @@ -644,7 +644,7 @@ func conditionalDebitResetAction(acc *Account, sq *StatsQueueTriggered, a *Actio } func conditionalTopupAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { + if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { return topupAction(acc, sq, a, acs) } else { return err @@ -652,7 +652,7 @@ func conditionalTopupAction(acc *Account, sq *StatsQueueTriggered, a *Action, ac } func conditionalTopupResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { + if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { return topupResetAction(acc, sq, a, acs) } else { return err diff --git a/utils/cond_loader.go b/utils/cond_loader.go index a7bcd617d..08695f8f1 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -2,88 +2,164 @@ package utils import ( "encoding/json" + "errors" + "reflect" "strings" ) const ( - COND_EQ = "*eq" - COND_GT = "*gt" - COND_LT = "*lt" - COND_EXP = "*exp" + CondEQ = "*eq" + CondGT = "*gt" + CondGTE = "*gte" + CondLT = "*lt" + CondLTE = "*lte" + CondEXP = "*exp" + CondOR = "*or" + CondAND = "*and" ) -type CondElement interface { - AddChild(CondElement) - CheckStruct(interface{}) (bool, error) +var ( + ErrNotNumerical = errors.New("NOT_NUMERICAL") +) + +type condElement interface { + addChild(condElement) error + checkStruct(interface{}) (bool, error) } -type OperatorSlice struct { - Operator string - Slice []CondElement +type operatorSlice struct { + operator string + slice []condElement } -func (os *OperatorSlice) AddChild(ce CondElement) { - os.Slice = append(os.Slice, ce) +func (os *operatorSlice) addChild(ce condElement) error { + os.slice = append(os.slice, ce) + return nil } -func (os *OperatorSlice) CheckStruct(o interface{}) (bool, error) { return true, nil } - -type KeyStruct struct { - Key string - Struct CondElement +func (os *operatorSlice) checkStruct(o interface{}) (bool, error) { + switch os.operator { + case CondOR: + for _, cond := range os.slice { + check, err := cond.checkStruct(o) + if err != nil { + return false, err + } + if check { + return true, nil + } + } + case CondAND: + accumulator := true + for _, cond := range os.slice { + check, err := cond.checkStruct(o) + if err != nil { + return false, err + } + accumulator = accumulator && check + } + return accumulator, nil + } + return false, nil } -func (ks *KeyStruct) AddChild(ce CondElement) { - ks.Struct = ce -} -func (ks *KeyStruct) CheckStruct(o interface{}) (bool, error) { return true, nil } - -type OperatorValue struct { - Operator string - Value interface{} +type keyStruct struct { + key string + elem condElement } -func (ov *OperatorValue) AddChild(CondElement) {} -func (ov *OperatorValue) CheckStruct(o interface{}) (bool, error) { return true, nil } - -type KeyValue struct { - Key string - Value interface{} +func (ks *keyStruct) addChild(ce condElement) error { + ks.elem = ce + return nil +} +func (ks *keyStruct) checkStruct(o interface{}) (bool, error) { + obj := reflect.ValueOf(o) + if obj.Kind() == reflect.Ptr { + obj = obj.Elem() + } + value := obj.FieldByName(ks.key) + return ks.elem.checkStruct(value.Interface()) } -func (os *KeyValue) AddChild(CondElement) {} -func (os *KeyValue) CheckStruct(o interface{}) (bool, error) { return true, nil } +type operatorValue struct { + operator string + value interface{} +} + +func (ov *operatorValue) addChild(condElement) error { return ErrNotImplemented } +func (ov *operatorValue) checkStruct(o interface{}) (bool, error) { + if ov.operator == CondEQ { + return ov.value == o, nil + } + var of, vf float64 + var ok bool + if of, ok = o.(float64); !ok { + return false, ErrNotNumerical + } + if vf, ok = ov.value.(float64); !ok { + return false, ErrNotNumerical + } + switch ov.operator { + case CondGT: + return of > vf, nil + case CondGTE: + return of >= vf, nil + case CondLT: + return of < vf, nil + case CondLTE: + return of <= vf, nil + + } + return true, nil +} + +type keyValue struct { + key string + value interface{} +} + +func (kv *keyValue) addChild(condElement) error { return ErrNotImplemented } +func (kv *keyValue) checkStruct(o interface{}) (bool, error) { + obj := reflect.ValueOf(o) + if obj.Kind() == reflect.Ptr { + obj = obj.Elem() + } + value := obj.FieldByName(kv.key) + return value.Interface() == kv.value, nil +} func isOperator(s string) bool { return strings.HasPrefix(s, "*") } -type CondLoader struct{} +type CondLoader struct { + rootElement condElement +} -func (cp *CondLoader) Load(a map[string]interface{}, parentElement CondElement) (CondElement, error) { +func (cp *CondLoader) load(a map[string]interface{}, parentElement condElement) (condElement, error) { for key, value := range a { - var currentElement CondElement + var currentElement condElement switch t := value.(type) { case []interface{}: - currentElement = &OperatorSlice{Operator: key} + currentElement = &operatorSlice{operator: key} for _, e := range t { - cp.Load(e.(map[string]interface{}), currentElement) + cp.load(e.(map[string]interface{}), currentElement) } case map[string]interface{}: - currentElement = &KeyStruct{Key: key} + currentElement = &keyStruct{key: key} //log.Print("map: ", t) - cp.Load(t, currentElement) + cp.load(t, currentElement) case interface{}: if isOperator(key) { - currentElement = &OperatorValue{Operator: key, Value: t} + currentElement = &operatorValue{operator: key, value: t} } else { - currentElement = &KeyValue{Key: key, Value: t} + currentElement = &keyValue{key: key, value: t} } //log.Print("generic interface: ", t) default: return nil, ErrParserError } if parentElement != nil { - parentElement.AddChild(currentElement) + parentElement.addChild(currentElement) } else { return currentElement, nil } @@ -91,10 +167,15 @@ func (cp *CondLoader) Load(a map[string]interface{}, parentElement CondElement) return nil, nil } -func (cp *CondLoader) Parse(s string) (root CondElement, err error) { +func (cp *CondLoader) Parse(s string) (err error) { a := make(map[string]interface{}) if err := json.Unmarshal([]byte([]byte(s)), &a); err != nil { - return nil, err + return err } - return cp.Load(a, nil) + cp.rootElement, err = cp.load(a, nil) + return +} + +func (cp *CondLoader) Check(o interface{}) (bool, error) { + return cp.rootElement.checkStruct(o) } diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index 70b0382f8..e5ef1c42f 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -4,8 +4,260 @@ import "testing" func TestCondLoader(t *testing.T) { cl := &CondLoader{} - root, err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) - if err != nil || root == nil { - t.Errorf("Error loading structure: %+v (%v)", ToIJSON(root), err) + err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) + 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: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"Test":"test"}`) + 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(`{"Field":6}`) + 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}`) + 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) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"Test":"test"}`) + 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(`{"Field":6}`) + 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}`) + 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 TestCondOperatorValue(t *testing.T) { + root := &operatorValue{operator: "*gt", value: 3.4} + if check, err := root.checkStruct(3.5); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) + } + root = &operatorValue{operator: "*eq", value: 3.4} + if check, err := root.checkStruct(3.5); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) + } + root = &operatorValue{operator: "*eq", value: 3.4} + if check, err := root.checkStruct(3.4); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) + } + root = &operatorValue{operator: "*eq", value: "zinc"} + if check, err := root.checkStruct("zinc"); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) + } +} + +func TestCondKeyStruct(t *testing.T) { + o := struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"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(`{"Test":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); check || err != ErrNotNumerical { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*gte": 6}}`) + 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(`{"Field":{"*lt": 7}}`) + 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(`{"Field":{"*lte": 6}}`) + 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(`{"Field":{"*eq": 6}}`) + 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(`{"Test":{"*eq": "test"}}`) + 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 TestCondKeyStructPointer(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"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(`{"Test":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); check || err != ErrNotNumerical { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*gte": 6}}`) + 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(`{"Field":{"*lt": 7}}`) + 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(`{"Field":{"*lte": 6}}`) + 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(`{"Field":{"*eq": 6}}`) + 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(`{"Test":{"*eq": "test"}}`) + 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 TestCondOperatorSlice(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"*or":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":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(`{"*or":[{"Test":"test"},{"Field":{"*gt":7}},{"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(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":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(`{"*and":[{"Test":"test"},{"Field":{"*gt":7}},{"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)) } } From 6b863d65c671e0cad9c73132b44948e8349e8994 Mon Sep 17 00:00:00 2001 From: DanB Date: Tue, 19 Jan 2016 14:01:52 +0100 Subject: [PATCH 15/38] Diameter default Result-Code handling fix --- agents/dmtagent.go | 43 +++++++++++++++++++++++++++++++------- agents/dmtagent_it_test.go | 5 +++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/agents/dmtagent.go b/agents/dmtagent.go index fc1afc19b..8477dc4eb 100644 --- a/agents/dmtagent.go +++ b/agents/dmtagent.go @@ -20,6 +20,7 @@ package agents import ( "fmt" + "strconv" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" @@ -82,14 +83,22 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro cca := NewBareCCAFromCCR(ccr, self.cgrCfg.DiameterAgentCfg().OriginHost, self.cgrCfg.DiameterAgentCfg().OriginRealm) smgEv, err := ccr.AsSMGenericEvent(reqProcessor.CCRFields) if err != nil { - cca.ResultCode = DiameterRatingFailed + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA reply-code, error: %s", ccr.diamMessage, err)) + return nil + } utils.Logger.Err(fmt.Sprintf(" Processing message: %+v AsSMGenericEvent, error: %s", ccr.diamMessage, err)) return cca } var maxUsage float64 if reqProcessor.DryRun { // DryRun does not send over network utils.Logger.Info(fmt.Sprintf(" SMGenericEvent: %+v", smgEv)) - cca.ResultCode = diam.LimitedSuccess + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.LimitedSuccess), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } } else { // Find out maxUsage over APIs switch ccr.CCRequestType { case 1: @@ -110,19 +119,39 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro } } if err != nil { - cca.ResultCode = DiameterRatingFailed + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } utils.Logger.Err(fmt.Sprintf(" Processing message: %+v, API error: %s", ccr.diamMessage, err)) return cca } if ccr.CCRequestType != 3 && maxUsage == 0 { // Not enough balance, RFC demands 4012 - cca.ResultCode = 4012 + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, "4012", + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } } else { - cca.ResultCode = diam.Success + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.Success), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } + } + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Granted-Service-Unit", "CC-Time"}, strconv.FormatFloat(maxUsage, 'f', 0, 64), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Granted-Service-Unit, error: %s", ccr.diamMessage, err)) + return nil } - cca.GrantedServiceUnit.CCTime = int(maxUsage) } if err := cca.SetProcessorAVPs(reqProcessor, maxUsage); err != nil { - cca.ResultCode = DiameterRatingFailed + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } utils.Logger.Err(fmt.Sprintf(" CCA SetProcessorAVPs for message: %+v, error: %s", ccr.diamMessage, err)) return cca } diff --git a/agents/dmtagent_it_test.go b/agents/dmtagent_it_test.go index a3d1cbd83..3ec68ef73 100644 --- a/agents/dmtagent_it_test.go +++ b/agents/dmtagent_it_test.go @@ -213,6 +213,11 @@ func TestDmtAgentSendCCRInit(t *testing.T) { } else if strCCTime := avpValAsString(avps[0]); strCCTime != "300" { t.Errorf("Expecting 300, received: %s", strCCTime) } + if result, err := msg.FindAVP("Result-Code", dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if resultStr := avpValAsString(result); resultStr != "2001" { + t.Errorf("Expecting 2001, received: %s", resultStr) + } var acnt *engine.Account attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} eAcntVal := 9.484 From 2812eafc3e34a89b6080e6a5b205419cd1a1cf9e Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 19 Jan 2016 16:12:21 +0200 Subject: [PATCH 16/38] implemented *has operator and added Type field --- engine/account.go | 25 +++++++---- engine/action.go | 8 ++-- utils/cond_loader.go | 46 ++++++++++++++++---- utils/cond_loader_test.go | 88 +++++++++++++++++++++++++++++++++++---- 4 files changed, 138 insertions(+), 29 deletions(-) diff --git a/engine/account.go b/engine/account.go index e539721f0..d733af1c8 100644 --- a/engine/account.go +++ b/engine/account.go @@ -697,17 +697,24 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance } } -func (acc *Account) matchConditions(condition, balanceType string) (bool, error) { - +func (acc *Account) matchConditions(condition string) (bool, error) { cl := &utils.CondLoader{} cl.Parse(condition) - for _, b := range acc.BalanceMap[balanceType] { - check, err := cl.Check(b) - if err != nil { - return false, err - } - if check { - return true, nil + for balanceType, balanceChain := range acc.BalanceMap { + for _, b := range balanceChain { + check, err := cl.Check(&struct { + Type string + *Balance + }{ + Type: balanceType, + Balance: b, + }) + if err != nil { + return false, err + } + if check { + return true, nil + } } } return false, nil diff --git a/engine/action.go b/engine/action.go index 97a1b807b..fdf0eaff1 100644 --- a/engine/action.go +++ b/engine/action.go @@ -628,7 +628,7 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a } func conditionalDebitAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { return debitAction(acc, sq, a, acs) } else { return err @@ -636,7 +636,7 @@ func conditionalDebitAction(acc *Account, sq *StatsQueueTriggered, a *Action, ac } func conditionalDebitResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { return debitResetAction(acc, sq, a, acs) } else { return err @@ -644,7 +644,7 @@ func conditionalDebitResetAction(acc *Account, sq *StatsQueueTriggered, a *Actio } func conditionalTopupAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { return topupAction(acc, sq, a, acs) } else { return err @@ -652,7 +652,7 @@ func conditionalTopupAction(acc *Account, sq *StatsQueueTriggered, a *Action, ac } func conditionalTopupResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { return topupResetAction(acc, sq, a, acs) } else { return err diff --git a/utils/cond_loader.go b/utils/cond_loader.go index 08695f8f1..8fdfca867 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -2,7 +2,7 @@ package utils import ( "encoding/json" - "errors" + "fmt" "reflect" "strings" ) @@ -16,11 +16,12 @@ const ( CondEXP = "*exp" CondOR = "*or" CondAND = "*and" + CondHAS = "*has" ) -var ( - ErrNotNumerical = errors.New("NOT_NUMERICAL") -) +func NewErrInvalidArgument(arg interface{}) error { + return fmt.Errorf("INVALID_ARGUMENT: %v", arg) +} type condElement interface { addChild(condElement) error @@ -87,16 +88,36 @@ type operatorValue struct { func (ov *operatorValue) addChild(condElement) error { return ErrNotImplemented } func (ov *operatorValue) checkStruct(o interface{}) (bool, error) { + // no conversion if ov.operator == CondEQ { return ov.value == o, nil } + // StringMap conversion + if ov.operator == CondHAS { + var strMap StringMap + var ok bool + if strMap, ok = o.(StringMap); !ok { + return false, NewErrInvalidArgument(o) + } + var strSlice []interface{} + if strSlice, ok = ov.value.([]interface{}); !ok { + return false, NewErrInvalidArgument(ov.value) + } + for _, str := range strSlice { + if !strMap[str.(string)] { + return false, nil + } + } + return true, nil + } + // float conversion var of, vf float64 var ok bool if of, ok = o.(float64); !ok { - return false, ErrNotNumerical + return false, NewErrInvalidArgument(o) } if vf, ok = ov.value.(float64); !ok { - return false, ErrNotNumerical + return false, NewErrInvalidArgument(ov.value) } switch ov.operator { case CondGT: @@ -140,9 +161,13 @@ func (cp *CondLoader) load(a map[string]interface{}, parentElement condElement) var currentElement condElement switch t := value.(type) { case []interface{}: - currentElement = &operatorSlice{operator: key} - for _, e := range t { - cp.load(e.(map[string]interface{}), currentElement) + if key == CondHAS { + currentElement = &operatorValue{operator: key, value: t} + } else { + currentElement = &operatorSlice{operator: key} + for _, e := range t { + cp.load(e.(map[string]interface{}), currentElement) + } } case map[string]interface{}: currentElement = &keyStruct{key: key} @@ -177,5 +202,8 @@ func (cp *CondLoader) Parse(s string) (err error) { } func (cp *CondLoader) Check(o interface{}) (bool, error) { + if cp.rootElement == nil { + return false, ErrParserError + } return cp.rootElement.checkStruct(o) } diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index e5ef1c42f..c0e9930ff 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -1,6 +1,9 @@ package utils -import "testing" +import ( + "strings" + "testing" +) func TestCondLoader(t *testing.T) { cl := &CondLoader{} @@ -8,6 +11,15 @@ func TestCondLoader(t *testing.T) { if err != nil { t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) } + + err = cl.Parse(`{"*has":["NAT","RET","EUR"]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + err = cl.Parse(`{"Field":7, "Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } } func TestCondKeyValue(t *testing.T) { @@ -42,6 +54,20 @@ 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":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(`{"Field":7, "Other":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)) + } } func TestCondKeyValuePointer(t *testing.T) { @@ -79,22 +105,26 @@ func TestCondKeyValuePointer(t *testing.T) { } func TestCondOperatorValue(t *testing.T) { - root := &operatorValue{operator: "*gt", value: 3.4} + root := &operatorValue{operator: CondGT, value: 3.4} if check, err := root.checkStruct(3.5); !check || err != nil { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) } - root = &operatorValue{operator: "*eq", value: 3.4} + root = &operatorValue{operator: CondEQ, value: 3.4} if check, err := root.checkStruct(3.5); check || err != nil { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) } - root = &operatorValue{operator: "*eq", value: 3.4} + root = &operatorValue{operator: CondEQ, value: 3.4} if check, err := root.checkStruct(3.4); !check || err != nil { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) } - root = &operatorValue{operator: "*eq", value: "zinc"} + root = &operatorValue{operator: CondEQ, value: "zinc"} if check, err := root.checkStruct("zinc"); !check || err != nil { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) } + root = &operatorValue{operator: CondHAS, value: []interface{}{"NAT", "RET", "EUR"}} + if check, err := root.checkStruct(StringMap{"WOR": true, "EUR": true, "NAT": true, "RET": true, "ROM": true}); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, ToIJSON(root)) + } } func TestCondKeyStruct(t *testing.T) { @@ -119,7 +149,7 @@ func TestCondKeyStruct(t *testing.T) { if err != nil { t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) } - if check, err := cl.Check(o); check || err != ErrNotNumerical { + if check, err := cl.Check(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) } err = cl.Parse(`{"Field":{"*gte": 6}}`) @@ -181,7 +211,7 @@ func TestCondKeyStructPointer(t *testing.T) { if err != nil { t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) } - if check, err := cl.Check(o); check || err != ErrNotNumerical { + if check, err := cl.Check(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) } err = cl.Parse(`{"Field":{"*gte": 6}}`) @@ -261,3 +291,47 @@ func TestCondOperatorSlice(t *testing.T) { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) } } + +func TestCondMixed(t *testing.T) { + o := &struct { + Test string + Field float64 + Categories StringMap + Other bool + }{ + Test: "test", + Field: 6.0, + Categories: StringMap{"call": true, "data": true, "voice": true}, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true},{"Categories":{"*has":["data", "call"]}}]}`) + 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 TestCondBalanceType(t *testing.T) { + type Balance struct { + Value float64 + } + + o := &struct { + BalanceType string + Balance + }{ + BalanceType: MONETARY, + Balance: Balance{Value: 10}, + } + cl := &CondLoader{} + err := cl.Parse(`{"BalanceType":"*monetary","Value":10}`) + 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)) + } +} From 239619241556ff346f03514a6fb80e6d3241bcc6 Mon Sep 17 00:00:00 2001 From: DanB Date: Tue, 19 Jan 2016 18:52:20 +0100 Subject: [PATCH 17/38] SMGeneric.SessionEnd handling usage instead of endTime, fixes #353 --- sessionmanager/smgeneric.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sessionmanager/smgeneric.go b/sessionmanager/smgeneric.go index c9b3f2289..8dc40851a 100644 --- a/sessionmanager/smgeneric.go +++ b/sessionmanager/smgeneric.go @@ -106,7 +106,7 @@ func (self *SMGeneric) sessionStart(evStart SMGenericEvent, connId string) error } // End a session from outside -func (self *SMGeneric) sessionEnd(sessionId string, endTime time.Time) error { +func (self *SMGeneric) sessionEnd(sessionId string, usage time.Duration) error { _, err := self.guard.Guard(func() (interface{}, error) { // Lock it on UUID level ss := self.getSession(sessionId) if len(ss) == 0 { // Not handled by us @@ -119,7 +119,12 @@ func (self *SMGeneric) sessionEnd(sessionId string, endTime time.Time) error { if idx == 0 && s.stopDebit != nil { close(s.stopDebit) // Stop automatic debits } - if err := s.close(endTime); err != nil { + aTime, err := s.eventStart.GetAnswerTime(utils.META_DEFAULT, self.cgrCfg.DefaultTimezone) + if err != nil || aTime.IsZero() { + utils.Logger.Err(fmt.Sprintf(" Could not retrieve answer time for session: %s, runId: %s, aTime: %+v, error: %s", + sessionId, s.runId, aTime, err.Error())) + } + if err := s.close(aTime.Add(usage)); err != nil { utils.Logger.Err(fmt.Sprintf(" Could not close session: %s, runId: %s, error: %s", sessionId, s.runId, err.Error())) } if err := s.saveOperations(); err != nil { @@ -189,11 +194,11 @@ func (self *SMGeneric) SessionStart(gev SMGenericEvent, clnt *rpc2.Client) (time // Called on session end, should stop debit loop func (self *SMGeneric) SessionEnd(gev SMGenericEvent, clnt *rpc2.Client) error { - endTime, err := gev.GetEndTime(utils.META_DEFAULT, self.timezone) + usage, err := gev.GetUsage(utils.META_DEFAULT) if err != nil { return err } - if err := self.sessionEnd(gev.GetUUID(), endTime); err != nil { + if err := self.sessionEnd(gev.GetUUID(), usage); err != nil { return err } return nil @@ -248,7 +253,7 @@ func (self *SMGeneric) Connect() error { // System shutdown func (self *SMGeneric) Shutdown() error { for ssId := range self.getSessions() { // Force sessions shutdown - self.sessionEnd(ssId, time.Now()) + self.sessionEnd(ssId, time.Duration(self.cgrCfg.MaxCallDuration)) } return nil } From 0607c4d6062baad9b290489aaf7d6ee7376b4796 Mon Sep 17 00:00:00 2001 From: DanB Date: Tue, 19 Jan 2016 18:57:17 +0100 Subject: [PATCH 18/38] Diameter Granted-Service-AVP not in terminate requests --- agents/dmtagent.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/agents/dmtagent.go b/agents/dmtagent.go index 8477dc4eb..db26b69b7 100644 --- a/agents/dmtagent.go +++ b/agents/dmtagent.go @@ -140,10 +140,12 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro return nil } } - if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Granted-Service-Unit", "CC-Time"}, strconv.FormatFloat(maxUsage, 'f', 0, 64), - false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Granted-Service-Unit, error: %s", ccr.diamMessage, err)) - return nil + if ccr.CCRequestType != 3 { // For terminate, we don't add granted-service-unit AVP + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Granted-Service-Unit", "CC-Time"}, strconv.FormatFloat(maxUsage, 'f', 0, 64), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Granted-Service-Unit, error: %s", ccr.diamMessage, err)) + return nil + } } } if err := cca.SetProcessorAVPs(reqProcessor, maxUsage); err != nil { From 4e784b6ef59d205cda6682068f498f80833f481d Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 19 Jan 2016 21:24:13 +0200 Subject: [PATCH 19/38] conditional topup tests and improvements fixes #275 --- engine/account.go | 4 +- engine/actions_test.go | 191 ++++++++++++++++++++++++++++++++++++++ utils/cond_loader.go | 26 +++++- utils/cond_loader_test.go | 25 +++++ 4 files changed, 240 insertions(+), 6 deletions(-) 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/utils/cond_loader.go b/utils/cond_loader.go index 8fdfca867..6af762673 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -148,6 +148,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 +193,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..908c29b19 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -20,6 +20,10 @@ 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) { @@ -68,6 +72,27 @@ 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)) + } } func TestCondKeyValuePointer(t *testing.T) { From 54f6d5b02f6739ceab6091e350d6fca8aada5f17 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 19 Jan 2016 22:01:03 +0200 Subject: [PATCH 20/38] added mms tor type --- apier/v1/cdre.go | 7 +++++-- apier/v2/cdre.go | 7 +++++-- cdre/cdrexporter.go | 34 ++++++++++++++++++++++------------ cdre/cdrexporter_test.go | 2 +- cdre/csv_test.go | 4 ++-- cdre/fixedwidth_test.go | 4 ++-- config/cdreconfig.go | 5 +++++ config/cfg_data.json | 2 +- config/config_defaults.go | 1 + config/config_json_test.go | 5 +++-- config/libconfig_json.go | 1 + engine/cdr.go | 2 +- utils/apitpdata.go | 2 ++ utils/consts.go | 1 + 14 files changed, 52 insertions(+), 25 deletions(-) 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/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/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" From 0f2c803f028b03b0944ae82ed5d0e099e0854f51 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 19 Jan 2016 22:35:19 +0200 Subject: [PATCH 21/38] conditional language documentation --- utils/cond_loader.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/utils/cond_loader.go b/utils/cond_loader.go index 6af762673..bebc35aef 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -1,5 +1,29 @@ 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 +- *or: logical or +- *and: logical and +- *has: logical has + +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" @@ -13,7 +37,7 @@ const ( CondGTE = "*gte" CondLT = "*lt" CondLTE = "*lte" - CondEXP = "*exp" + //CondEXP = "*exp" CondOR = "*or" CondAND = "*and" CondHAS = "*has" From 327d1b6de7d6b00e83ba187aef05b84d13d46e77 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 19 Jan 2016 22:50:35 +0200 Subject: [PATCH 22/38] added *exp condition operator (expired) --- utils/cond_loader.go | 21 ++++++++++++++++++++- utils/cond_loader_test.go | 29 +++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/utils/cond_loader.go b/utils/cond_loader.go index bebc35aef..18e3be116 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -29,6 +29,7 @@ import ( "fmt" "reflect" "strings" + "time" ) const ( @@ -37,7 +38,7 @@ const ( CondGTE = "*gte" CondLT = "*lt" CondLTE = "*lte" - //CondEXP = "*exp" + CondEXP = "*exp" CondOR = "*or" CondAND = "*and" CondHAS = "*has" @@ -134,6 +135,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 diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index 908c29b19..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) { @@ -28,13 +29,15 @@ func TestCondLoader(t *testing.T) { 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"}`) @@ -93,6 +96,20 @@ 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(`{"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) { From 38e79d5ec9b707d9089dff5dbeb6303d4d4b883b Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Tue, 19 Jan 2016 22:51:40 +0200 Subject: [PATCH 23/38] added *exp to the documentation --- utils/cond_loader.go | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/cond_loader.go b/utils/cond_loader.go index 18e3be116..e03cba8d3 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -15,6 +15,7 @@ Available operators: - *gte: greater or equal than - *lt: less then - *lte: less or equal than +- *exp: expired - *or: logical or - *and: logical and - *has: logical has From 31302da05546b1b7e9b4a38ba9abc08d5951d9ae Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 20 Jan 2016 12:36:19 +0200 Subject: [PATCH 24/38] small comment change --- utils/cond_loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/cond_loader.go b/utils/cond_loader.go index e03cba8d3..5ff35350c 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -18,7 +18,7 @@ Available operators: - *exp: expired - *or: logical or - *and: logical and -- *has: logical has +- *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: From e106deaef3039ec59094d819cfe082dd8bca7fd8 Mon Sep 17 00:00:00 2001 From: DanB Date: Wed, 20 Jan 2016 13:18:58 +0100 Subject: [PATCH 25/38] MaxDebit for ChargeEvent, fixes #350 --- sessionmanager/smgeneric.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/sessionmanager/smgeneric.go b/sessionmanager/smgeneric.go index 8dc40851a..69ae61088 100644 --- a/sessionmanager/smgeneric.go +++ b/sessionmanager/smgeneric.go @@ -212,13 +212,33 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) error } else if len(sessionRuns) == 0 { return nil } - var withErrors bool + var err error for _, sR := range sessionRuns { cc := new(engine.CallCost) - if err := self.rater.Debit(sR.CallDescriptor, cc); err != nil { - withErrors = true + if err = self.rater.MaxDebit(sR.CallDescriptor, cc); err != nil { utils.Logger.Err(fmt.Sprintf(" Could not Debit CD: %+v, RunID: %s, error: %s", sR.CallDescriptor, sR.DerivedCharger.RunID, err.Error())) - continue + break + } + sR.CallCosts = append(sR.CallCosts, cc) // Save it so we can revert on issues + if cc.GetDuration() == 0 { + err = errors.New("INSUFFICIENT_FUNDS") + break + } + } + if err != nil { // Refund the ones already taken since we have error on one of the debits + for _, sR := range sessionRuns { + for _, cc := range sR.CallCosts { + utils.Logger.Debug(fmt.Sprintf("%+v", cc)) + continue // Refund here + } + } + return err + } + var withErrors bool + for _, sR := range sessionRuns { + var cc *engine.CallCost + for _, ccSR := range sR.CallCosts { + cc.Merge(ccSR) } var reply string if err := self.cdrsrv.LogCallCost(&engine.CallCostLog{ From 1dfbb86fb3e2520add7ec7cc4841d54fcad5b5b0 Mon Sep 17 00:00:00 2001 From: DanB Date: Wed, 20 Jan 2016 13:49:53 +0100 Subject: [PATCH 26/38] ChargeEvent returning maxUsage --- agents/dmtagent.go | 4 ++-- apier/v1/smgenericbirpcv1.go | 7 ++++--- apier/v1/smgenericv1.go | 9 +++++---- sessionmanager/smgeneric.go | 21 +++++++++++---------- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/agents/dmtagent.go b/agents/dmtagent.go index db26b69b7..e1c0b6c6d 100644 --- a/agents/dmtagent.go +++ b/agents/dmtagent.go @@ -110,7 +110,7 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro if ccr.CCRequestType == 3 { err = self.smg.Call("SMGenericV1.SessionEnd", smgEv, &rpl) } else if ccr.CCRequestType == 4 { - err = self.smg.Call("SMGenericV1.ChargeEvent", smgEv, &rpl) + err = self.smg.Call("SMGenericV1.ChargeEvent", smgEv, &maxUsage) } if self.cgrCfg.DiameterAgentCfg().CreateCDR { if errCdr := self.smg.Call("SMGenericV1.ProcessCdr", smgEv, &rpl); errCdr != nil { @@ -127,7 +127,7 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro utils.Logger.Err(fmt.Sprintf(" Processing message: %+v, API error: %s", ccr.diamMessage, err)) return cca } - if ccr.CCRequestType != 3 && maxUsage == 0 { // Not enough balance, RFC demands 4012 + if ccr.CCRequestType != 3 && ccr.CCRequestType != 4 && maxUsage == 0 { // Not enough balance, RFC demands 4012 if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, "4012", false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) diff --git a/apier/v1/smgenericbirpcv1.go b/apier/v1/smgenericbirpcv1.go index b80bcf960..035d55b86 100644 --- a/apier/v1/smgenericbirpcv1.go +++ b/apier/v1/smgenericbirpcv1.go @@ -100,11 +100,12 @@ func (self *SMGenericBiRpcV1) SessionEnd(clnt *rpc2.Client, ev sessionmanager.SM } // Called on individual Events (eg SMS) -func (self *SMGenericBiRpcV1) ChargeEvent(clnt *rpc2.Client, ev sessionmanager.SMGenericEvent, reply *string) error { - if err := self.sm.ChargeEvent(ev, clnt); err != nil { +func (self *SMGenericBiRpcV1) ChargeEvent(clnt *rpc2.Client, ev sessionmanager.SMGenericEvent, maxUsage *float64) error { + if minMaxUsage, err := self.sm.ChargeEvent(ev, clnt); err != nil { return utils.NewErrServerError(err) + } else { + *maxUsage = minMaxUsage.Seconds() } - *reply = utils.OK return nil } diff --git a/apier/v1/smgenericv1.go b/apier/v1/smgenericv1.go index 834ebbe9f..d56c1d431 100644 --- a/apier/v1/smgenericv1.go +++ b/apier/v1/smgenericv1.go @@ -71,11 +71,12 @@ func (self *SMGenericV1) SessionEnd(ev sessionmanager.SMGenericEvent, reply *str } // Called on individual Events (eg SMS) -func (self *SMGenericV1) ChargeEvent(ev sessionmanager.SMGenericEvent, reply *string) error { - if err := self.sm.ChargeEvent(ev, nil); err != nil { +func (self *SMGenericV1) ChargeEvent(ev sessionmanager.SMGenericEvent, maxUsage *float64) error { + if minMaxUsage, err := self.sm.ChargeEvent(ev, nil); err != nil { return utils.NewErrServerError(err) + } else { + *maxUsage = minMaxUsage.Seconds() } - *reply = utils.OK return nil } @@ -146,7 +147,7 @@ func (self *SMGenericV1) Call(serviceMethod string, args interface{}, reply inte if !canConvert { return rpcclient.ErrWrongArgsType } - replyConverted, canConvert := reply.(*string) + replyConverted, canConvert := reply.(*float64) if !canConvert { return rpcclient.ErrWrongReplyType } diff --git a/sessionmanager/smgeneric.go b/sessionmanager/smgeneric.go index 69ae61088..dd3cf163f 100644 --- a/sessionmanager/smgeneric.go +++ b/sessionmanager/smgeneric.go @@ -205,14 +205,13 @@ func (self *SMGeneric) SessionEnd(gev SMGenericEvent, clnt *rpc2.Client) error { } // Processes one time events (eg: SMS) -func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) error { +func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) (maxDur time.Duration, err error) { var sessionRuns []*engine.SessionRun if err := self.rater.GetSessionRuns(gev.AsStoredCdr(self.cgrCfg, self.timezone), &sessionRuns); err != nil { - return err + return nilDuration, err } else if len(sessionRuns) == 0 { - return nil + return nilDuration, nil } - var err error for _, sR := range sessionRuns { cc := new(engine.CallCost) if err = self.rater.MaxDebit(sR.CallDescriptor, cc); err != nil { @@ -220,19 +219,21 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) error break } sR.CallCosts = append(sR.CallCosts, cc) // Save it so we can revert on issues - if cc.GetDuration() == 0 { + if ccDur := cc.GetDuration(); ccDur == 0 { err = errors.New("INSUFFICIENT_FUNDS") break + } else if ccDur < maxDur { + maxDur = ccDur } } if err != nil { // Refund the ones already taken since we have error on one of the debits for _, sR := range sessionRuns { for _, cc := range sR.CallCosts { - utils.Logger.Debug(fmt.Sprintf("%+v", cc)) - continue // Refund here + utils.Logger.Debug(fmt.Sprintf("%+v", cc)) // Refund here + continue } } - return err + return nilDuration, err } var withErrors bool for _, sR := range sessionRuns { @@ -253,9 +254,9 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) error } } if withErrors { - return ErrPartiallyExecuted + return nilDuration, ErrPartiallyExecuted } - return nil + return maxDur, nil } func (self *SMGeneric) ProcessCdr(gev SMGenericEvent) error { From 4d6be144c32972a0a599ec1a28c738d1bc1b79d0 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 20 Jan 2016 15:59:27 +0200 Subject: [PATCH 27/38] added RatedUsage to call cost --- engine/callcost.go | 7 +++++++ engine/callcost_test.go | 4 ++++ engine/calldesc.go | 2 ++ engine/cdrs.go | 1 + 4 files changed, 14 insertions(+) diff --git a/engine/callcost.go b/engine/callcost.go index 767efc8f1..391b2a33a 100644 --- a/engine/callcost.go +++ b/engine/callcost.go @@ -29,6 +29,7 @@ type CallCost struct { Direction, Category, Tenant, Subject, Account, Destination, TOR string Cost float64 Timespans TimeSpans + RatedUsage float64 deductConnectFee bool negativeConnectFee bool // the connect fee went negative on default balance maxCostDisconect bool @@ -61,6 +62,12 @@ func (cc *CallCost) GetDuration() (td time.Duration) { return } +func (cc *CallCost) UpdateRatedUsage() time.Duration { + totalDuration := cc.GetDuration() + cc.RatedUsage = totalDuration.Seconds() + return totalDuration +} + func (cc *CallCost) GetConnectFee() float64 { if len(cc.Timespans) == 0 || cc.Timespans[0].RateInterval == nil || diff --git a/engine/callcost_test.go b/engine/callcost_test.go index c9ce24f01..55b0eced8 100644 --- a/engine/callcost_test.go +++ b/engine/callcost_test.go @@ -50,6 +50,10 @@ func TestSingleResultMerge(t *testing.T) { if cc1.Cost != 122 { t.Errorf("Exdpected 120 was %v", cc1.Cost) } + d := cc1.UpdateRatedUsage() + if d != 2*time.Minute || cc1.RatedUsage != 120.0 { + t.Errorf("error updating rating usage: %v, %v", d, cc1.RatedUsage) + } } func TestMultipleResultMerge(t *testing.T) { diff --git a/engine/calldesc.go b/engine/calldesc.go index 46771baa4..c38b3be18 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -526,6 +526,7 @@ func (cd *CallDescriptor) getCost() (*CallCost, error) { cc.Cost = utils.Round(cc.Cost, roundingDecimals, roundingMethod) //utils.Logger.Info(fmt.Sprintf(" Get Cost: %s => %v", cd.GetKey(), cc)) cc.Timespans.Compress() + cc.UpdateRatedUsage() return cc, err } @@ -668,6 +669,7 @@ func (cd *CallDescriptor) debit(account *Account, dryRun bool, goNegative bool) return nil, err } cc.updateCost() + cc.UpdateRatedUsage() cc.Timespans.Compress() //log.Printf("OUT CC: ", cc) return diff --git a/engine/cdrs.go b/engine/cdrs.go index 457a5fcfd..51a9249e5 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -107,6 +107,7 @@ func (self *CdrServer) ProcessExternalCdr(eCDR *ExternalCDR) error { // RPC method, used to log callcosts to db func (self *CdrServer) LogCallCost(ccl *CallCostLog) error { + ccl.CallCost.UpdateRatedUsage() // make sure rated usage is updated if ccl.CheckDuplicate { _, err := self.guard.Guard(func() (interface{}, error) { cc, err := self.cdrDb.GetCallCostLog(ccl.CgrId, ccl.RunId) From 12c09575e33bf943938a43246f90468d032ddfa3 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 20 Jan 2016 16:04:14 +0200 Subject: [PATCH 28/38] fix for smggenreic nil pointer --- sessionmanager/smgeneric.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sessionmanager/smgeneric.go b/sessionmanager/smgeneric.go index dd3cf163f..7254a7620 100644 --- a/sessionmanager/smgeneric.go +++ b/sessionmanager/smgeneric.go @@ -237,9 +237,14 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) (maxDu } var withErrors bool for _, sR := range sessionRuns { - var cc *engine.CallCost - for _, ccSR := range sR.CallCosts { - cc.Merge(ccSR) + if len(sR.CallCosts) == 0 { + continue + } + cc := sR.CallCosts[0] + if len(sR.CallCosts) > 1 { + for _, ccSR := range sR.CallCosts[1:] { + cc.Merge(ccSR) + } } var reply string if err := self.cdrsrv.LogCallCost(&engine.CallCostLog{ From 3c88c9248852c636e1bc32005956c19a5892522f Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 20 Jan 2016 16:21:40 +0200 Subject: [PATCH 29/38] session runs refund --- sessionmanager/smg_session.go | 2 +- sessionmanager/smgeneric.go | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/sessionmanager/smg_session.go b/sessionmanager/smg_session.go index 84eb2c8aa..18a86a9b5 100644 --- a/sessionmanager/smg_session.go +++ b/sessionmanager/smg_session.go @@ -157,7 +157,7 @@ func (self *SMGSession) refund(refundDuration time.Duration) error { Increments: refundIncrements, } cd.Increments.Compress() - utils.Logger.Info(fmt.Sprintf("Refunding duration %v with cd: %+v", initialRefundDuration, utils.ToJSON(cd))) + utils.Logger.Info(fmt.Sprintf("Refunding duration %v with cd: %s", initialRefundDuration, utils.ToJSON(cd))) var response float64 err := self.rater.RefundIncrements(cd, &response) if err != nil { diff --git a/sessionmanager/smgeneric.go b/sessionmanager/smgeneric.go index 7254a7620..59ac17337 100644 --- a/sessionmanager/smgeneric.go +++ b/sessionmanager/smgeneric.go @@ -228,10 +228,41 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) (maxDu } if err != nil { // Refund the ones already taken since we have error on one of the debits for _, sR := range sessionRuns { - for _, cc := range sR.CallCosts { - utils.Logger.Debug(fmt.Sprintf("%+v", cc)) // Refund here + if len(sR.CallCosts) == 0 { continue } + cc := sR.CallCosts[0] + if len(sR.CallCosts) > 1 { + for _, ccSR := range sR.CallCosts { + cc.Merge(ccSR) + } + } + // collect increments + var refundIncrements engine.Increments + cc.Timespans.Decompress() + for _, ts := range cc.Timespans { + refundIncrements = append(refundIncrements, ts.Increments...) + } + // refund cc + if len(refundIncrements) > 0 { + cd := &engine.CallDescriptor{ + Direction: cc.Direction, + Tenant: cc.Tenant, + Category: cc.Category, + Subject: cc.Subject, + Account: cc.Account, + Destination: cc.Destination, + TOR: cc.TOR, + Increments: refundIncrements, + } + cd.Increments.Compress() + utils.Logger.Info(fmt.Sprintf("Refunding session run callcost: %s", utils.ToJSON(cd))) + var response float64 + err := self.rater.RefundIncrements(cd, &response) + if err != nil { + return nilDuration, err + } + } } return nilDuration, err } From 7d833beb9a042f19cec84743851acc109d0d724f Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 20 Jan 2016 16:57:16 +0200 Subject: [PATCH 30/38] one more cond test --- utils/cond_loader_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index 996d12d27..34b01cbe3 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -89,6 +89,13 @@ 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(`{"Other":true, "Field":{"*gt":7}}`) + 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) From cde9e7e54e12f76b479e0317dcccfaca6935e822 Mon Sep 17 00:00:00 2001 From: DanB Date: Wed, 20 Jan 2016 16:58:47 +0100 Subject: [PATCH 31/38] Diameter - Do not add GrantedAVP when result code not authorizing the request --- agents/dmtagent.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/agents/dmtagent.go b/agents/dmtagent.go index e1c0b6c6d..86cbff4b7 100644 --- a/agents/dmtagent.go +++ b/agents/dmtagent.go @@ -127,20 +127,20 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro utils.Logger.Err(fmt.Sprintf(" Processing message: %+v, API error: %s", ccr.diamMessage, err)) return cca } + var notAuthrizedResultCode bool if ccr.CCRequestType != 3 && ccr.CCRequestType != 4 && maxUsage == 0 { // Not enough balance, RFC demands 4012 if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, "4012", false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) return nil } - } else { - if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.Success), - false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { - utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) - return nil - } + notAuthrizedResultCode = true + } else if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.Success), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil } - if ccr.CCRequestType != 3 { // For terminate, we don't add granted-service-unit AVP + if ccr.CCRequestType != 3 && !notAuthrizedResultCode { // For terminate, we don't add granted-service-unit AVP if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Granted-Service-Unit", "CC-Time"}, strconv.FormatFloat(maxUsage, 'f', 0, 64), false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Granted-Service-Unit, error: %s", ccr.diamMessage, err)) From c3e8901b55e57776f61949f366e27a6aa8e52bb0 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 20 Jan 2016 18:41:45 +0200 Subject: [PATCH 32/38] more updates on RatedUsage, fixes #337 --- engine/callcost.go | 3 +++ engine/cdrs.go | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/engine/callcost.go b/engine/callcost.go index 391b2a33a..e786318a7 100644 --- a/engine/callcost.go +++ b/engine/callcost.go @@ -63,6 +63,9 @@ func (cc *CallCost) GetDuration() (td time.Duration) { } func (cc *CallCost) UpdateRatedUsage() time.Duration { + if cc == nil { + return 0 + } totalDuration := cc.GetDuration() cc.RatedUsage = totalDuration.Seconds() return totalDuration diff --git a/engine/cdrs.go b/engine/cdrs.go index 51a9249e5..0d11122a3 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -175,6 +175,9 @@ func (self *CdrServer) processCdr(cdr *CDR) (err error) { cdr.RunID = utils.MetaRaw } if self.cgrCfg.CDRSStoreCdrs { // Store RawCDRs, this we do sync so we can reply with the status + if cdr.CostDetails != nil { + cdr.CostDetails.UpdateRatedUsage() + } if err := self.cdrDb.SetCDR(cdr, false); err != nil { // Only original CDR stored in primary table, no derived utils.Logger.Err(fmt.Sprintf(" Storing primary CDR %+v, got error: %s", cdr, err.Error())) return err // Error is propagated back and we don't continue processing the CDR if we cannot store it @@ -231,6 +234,9 @@ func (self *CdrServer) rateStoreStatsReplicate(cdr *CDR, sendToStats bool) error } if self.cgrCfg.CDRSStoreCdrs { // Store CDRs // Store RatedCDR + if cdr.CostDetails != nil { + cdr.CostDetails.UpdateRatedUsage() + } if err := self.cdrDb.SetCDR(cdr, true); err != nil { utils.Logger.Err(fmt.Sprintf(" Storing rated CDR %+v, got error: %s", cdr, err.Error())) } From 4b629cc15eac78ed19649abfba5c08c79882ae9a Mon Sep 17 00:00:00 2001 From: DanB Date: Wed, 20 Jan 2016 18:00:50 +0100 Subject: [PATCH 33/38] No CC-Time for SMS --- agents/dmtagent.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agents/dmtagent.go b/agents/dmtagent.go index 86cbff4b7..651f3b5ae 100644 --- a/agents/dmtagent.go +++ b/agents/dmtagent.go @@ -127,20 +127,20 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro utils.Logger.Err(fmt.Sprintf(" Processing message: %+v, API error: %s", ccr.diamMessage, err)) return cca } - var notAuthrizedResultCode bool + var unauthorizedResultCode bool if ccr.CCRequestType != 3 && ccr.CCRequestType != 4 && maxUsage == 0 { // Not enough balance, RFC demands 4012 if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, "4012", false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) return nil } - notAuthrizedResultCode = true + unauthorizedResultCode = true } else if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.Success), false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) return nil } - if ccr.CCRequestType != 3 && !notAuthrizedResultCode { // For terminate, we don't add granted-service-unit AVP + if ccr.CCRequestType != 3 && ccr.CCRequestType != 4 && !unauthorizedResultCode { // For terminate or previously marked unauthorized, we don't add granted-service-unit AVP if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Granted-Service-Unit", "CC-Time"}, strconv.FormatFloat(maxUsage, 'f', 0, 64), false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Granted-Service-Unit, error: %s", ccr.diamMessage, err)) From 107c6b2853cec50fc4082844979174e04b808962 Mon Sep 17 00:00:00 2001 From: DanB Date: Wed, 20 Jan 2016 19:01:00 +0100 Subject: [PATCH 34/38] Fix docs regarding http_cdr address --- docs/cdrserver.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cdrserver.rst b/docs/cdrserver.rst index 686ffb510..4d6c9133c 100644 --- a/docs/cdrserver.rst +++ b/docs/cdrserver.rst @@ -11,7 +11,7 @@ CDR-CGR Available as handler within http server. -To feed CDRs in via this interface, one must use url of the form: . +To feed CDRs in via this interface, one must use url of the form: . The CDR fields are received via http form (although for simplicity we support inserting them within query parameters as well) and are expected to be urlencoded in order to transport special characters reliably. All fields are expected by CGRateS as string, particular conversions being done on processing each CDR. The fields received are split into two different categories based on CGRateS interest in them: @@ -38,7 +38,7 @@ Extra fields: any field coming in via the http request and not a member of prima Example of sample CDR generated simply using curl: :: - curl --data "curl --data "tor=*voice&accid=iiaasbfdsaf&cdrhost=192.168.1.1&cdrsource=curl_cdr&reqtype=rated&direction=*out&tenant=192.168.56.66&category=call&account=dan&subject=dan&destination=%2B4986517174963&answer_time=1383813746&usage=1&sip_user=Jitsi&subject2=1003" http://127.0.0.1:2080/cgr + curl --data "curl --data "tor=*voice&accid=iiaasbfdsaf&cdrhost=192.168.1.1&cdrsource=curl_cdr&reqtype=rated&direction=*out&tenant=192.168.56.66&category=call&account=dan&subject=dan&destination=%2B4986517174963&answer_time=1383813746&usage=1&sip_user=Jitsi&subject2=1003" http://127.0.0.1:2080/cdr_http CDR-FS_JSON From da8ab844d41bc0b9ae6e53311f88e238ba77e919 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Wed, 20 Jan 2016 21:09:20 +0200 Subject: [PATCH 35/38] fix and test for wrong field name crash --- utils/cond_loader.go | 7 +++++++ utils/cond_loader_test.go | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/utils/cond_loader.go b/utils/cond_loader.go index 5ff35350c..da7123d73 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -104,6 +104,9 @@ func (ks *keyStruct) checkStruct(o interface{}) (bool, error) { obj = obj.Elem() } value := obj.FieldByName(ks.key) + if !value.IsValid() { + return false, NewErrInvalidArgument(ks.key) + } return ks.elem.checkStruct(value.Interface()) } @@ -203,6 +206,10 @@ func isOperator(s string) bool { return strings.HasPrefix(s, "*") } +func notEmpty(x interface{}) bool { + return !reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) +} + type CondLoader struct { rootElement condElement } diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index 34b01cbe3..7352b3cc9 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -117,6 +117,20 @@ 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(`{"*and":[{"Field":{"*gte":50}},{"Test":{"*eq":"test"}}]}`) + 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(`{"WrongFieldName":{"*eq":1}}`) + 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) { From 511f5002d4d89fc12301788200d64f95124e511c Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Thu, 21 Jan 2016 11:27:09 +0200 Subject: [PATCH 36/38] cond lang refactor for better design --- apier/v2/accounts.go | 98 +++++++++++++++++++++++ utils/cond_loader.go | 184 +++++++++++++++++++++++++------------------ 2 files changed, 205 insertions(+), 77 deletions(-) diff --git a/apier/v2/accounts.go b/apier/v2/accounts.go index 73a8f9fe7..d1c49b5fd 100644 --- a/apier/v2/accounts.go +++ b/apier/v2/accounts.go @@ -77,3 +77,101 @@ func (self *ApierV2) GetAccount(attr *utils.AttrGetAccount, reply *engine.Accoun *reply = *account return nil } + +type AttrSetAccount struct { + Tenant string + Account string + ActionPlanId string + ActionTriggersId string + AllowNegative *bool + Disabled *bool + ReloadScheduler bool +} + +func (self *ApierV2) SetAccount(attr AttrSetAccount, reply *string) error { + if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + var schedulerReloadNeeded = false + accID := utils.AccountKey(attr.Tenant, attr.Account) + var ub *engine.Account + _, err := engine.Guardian.Guard(func() (interface{}, error) { + if bal, _ := self.AccountDb.GetAccount(accID); bal != nil { + ub = bal + } else { // Not found in db, create it here + ub = &engine.Account{ + Id: accID, + } + } + if len(attr.ActionPlanId) != 0 { + _, err := engine.Guardian.Guard(func() (interface{}, error) { + var ap *engine.ActionPlan + var err error + ap, err = self.RatingDb.GetActionPlan(attr.ActionPlanId, false) + if err != nil { + return 0, err + } + if _, exists := ap.AccountIDs[accID]; !exists { + if ap.AccountIDs == nil { + ap.AccountIDs = make(map[string]struct{}) + } + ap.AccountIDs[accID] = struct{}{} + schedulerReloadNeeded = true + // create tasks + for _, at := range ap.ActionTimings { + if at.IsASAP() { + t := &engine.Task{ + Uuid: utils.GenUUID(), + AccountID: accID, + ActionsID: at.ActionsID, + } + if err = self.RatingDb.PushTask(t); err != nil { + return 0, err + } + } + } + if err := self.RatingDb.SetActionPlan(attr.ActionPlanId, ap); err != nil { + return 0, err + } + // update cache + self.RatingDb.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: []string{utils.ACTION_PLAN_PREFIX + attr.ActionPlanId}}) + } + return 0, nil + }, 0, utils.ACTION_PLAN_PREFIX) + if err != nil { + return 0, err + } + } + + if len(attr.ActionTriggersId) != 0 { + atrs, err := self.RatingDb.GetActionTriggers(attr.ActionTriggersId) + if err != nil { + return 0, err + } + ub.ActionTriggers = atrs + ub.InitCounters() + } + if attr.AllowNegative != nil { + ub.AllowNegative = *attr.AllowNegative + } + if attr.Disabled != nil { + ub.Disabled = *attr.Disabled + } + // All prepared, save account + if err := self.AccountDb.SetAccount(ub); err != nil { + return 0, err + } + return 0, nil + }, 0, accID) + if err != nil { + return utils.NewErrServerError(err) + } + if attr.ReloadScheduler && schedulerReloadNeeded { + // reload scheduler + if self.Sched != nil { + self.Sched.Reload(true) + } + } + *reply = utils.OK // This will mark saving of the account, error still can show up in actionTimingsId + return nil +} diff --git a/utils/cond_loader.go b/utils/cond_loader.go index da7123d73..f14e49607 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -45,21 +45,108 @@ const ( CondHAS = "*has" ) +var operatorMap = map[string]func(field, value interface{}) (bool, error){ + CondEQ: func(field, value interface{}) (bool, error) { + return value == field, nil + }, + CondGT: func(field, value interface{}) (bool, error) { + var of, vf float64 + var ok bool + if of, ok = field.(float64); !ok { + return false, NewErrInvalidArgument(field) + } + if vf, ok = value.(float64); !ok { + return false, NewErrInvalidArgument(value) + } + return of > vf, nil + }, + CondGTE: func(field, value interface{}) (bool, error) { + var of, vf float64 + var ok bool + if of, ok = field.(float64); !ok { + return false, NewErrInvalidArgument(field) + } + if vf, ok = value.(float64); !ok { + return false, NewErrInvalidArgument(value) + } + return of >= vf, nil + }, + CondLT: func(field, value interface{}) (bool, error) { + var of, vf float64 + var ok bool + if of, ok = field.(float64); !ok { + return false, NewErrInvalidArgument(field) + } + if vf, ok = value.(float64); !ok { + return false, NewErrInvalidArgument(value) + } + return of < vf, nil + }, + CondLTE: func(field, value interface{}) (bool, error) { + var of, vf float64 + var ok bool + if of, ok = field.(float64); !ok { + return false, NewErrInvalidArgument(field) + } + if vf, ok = value.(float64); !ok { + return false, NewErrInvalidArgument(value) + } + return of <= vf, nil + }, + CondEXP: func(field, value interface{}) (bool, error) { + var expDate time.Time + var ok bool + if expDate, ok = field.(time.Time); !ok { + return false, NewErrInvalidArgument(field) + } + var expired bool + if expired, ok = value.(bool); !ok { + return false, NewErrInvalidArgument(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 + } + }, + CondHAS: func(field, value interface{}) (bool, error) { + var strMap StringMap + var ok bool + if strMap, ok = field.(StringMap); !ok { + return false, NewErrInvalidArgument(field) + } + var strSlice []interface{} + if strSlice, ok = value.([]interface{}); !ok { + return false, NewErrInvalidArgument(value) + } + for _, str := range strSlice { + if !strMap[str.(string)] { + return false, nil + } + } + return true, nil + }, +} + func NewErrInvalidArgument(arg interface{}) error { return fmt.Errorf("INVALID_ARGUMENT: %v", arg) } -type condElement interface { - addChild(condElement) error +type compositeElement interface { + element + addChild(element) error +} + +type element interface { checkStruct(interface{}) (bool, error) } type operatorSlice struct { operator string - slice []condElement + slice []element } -func (os *operatorSlice) addChild(ce condElement) error { +func (os *operatorSlice) addChild(ce element) error { os.slice = append(os.slice, ce) return nil } @@ -91,10 +178,10 @@ func (os *operatorSlice) checkStruct(o interface{}) (bool, error) { type keyStruct struct { key string - elem condElement + elem element } -func (ks *keyStruct) addChild(ce condElement) error { +func (ks *keyStruct) addChild(ce element) error { ks.elem = ce return nil } @@ -115,69 +202,11 @@ type operatorValue struct { value interface{} } -func (ov *operatorValue) addChild(condElement) error { return ErrNotImplemented } func (ov *operatorValue) checkStruct(o interface{}) (bool, error) { - // no conversion - if ov.operator == CondEQ { - return ov.value == o, nil + if f, ok := operatorMap[ov.operator]; ok { + return f(o, ov.value) } - // StringMap conversion - if ov.operator == CondHAS { - var strMap StringMap - var ok bool - if strMap, ok = o.(StringMap); !ok { - return false, NewErrInvalidArgument(o) - } - var strSlice []interface{} - if strSlice, ok = ov.value.([]interface{}); !ok { - return false, NewErrInvalidArgument(ov.value) - } - for _, str := range strSlice { - if !strMap[str.(string)] { - return false, nil - } - } - 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 - if of, ok = o.(float64); !ok { - return false, NewErrInvalidArgument(o) - } - if vf, ok = ov.value.(float64); !ok { - return false, NewErrInvalidArgument(ov.value) - } - switch ov.operator { - case CondGT: - return of > vf, nil - case CondGTE: - return of >= vf, nil - case CondLT: - return of < vf, nil - case CondLTE: - return of <= vf, nil - - } - return true, nil + return false, nil } type keyValue struct { @@ -185,19 +214,20 @@ type keyValue struct { value interface{} } -func (kv *keyValue) addChild(condElement) error { return ErrNotImplemented } func (kv *keyValue) checkStruct(o interface{}) (bool, error) { obj := reflect.ValueOf(o) if obj.Kind() == reflect.Ptr { obj = obj.Elem() } value := obj.FieldByName(kv.key) + if !value.IsValid() { + return false, NewErrInvalidArgument(kv.key) + } 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 } @@ -211,12 +241,12 @@ func notEmpty(x interface{}) bool { } type CondLoader struct { - rootElement condElement + rootElement element } -func (cp *CondLoader) load(a map[string]interface{}, parentElement condElement) (condElement, error) { +func (cp *CondLoader) load(a map[string]interface{}, parentElement compositeElement) (element, error) { for key, value := range a { - var currentElement condElement + var currentElement element switch t := value.(type) { case []interface{}: if key == CondHAS { @@ -224,13 +254,13 @@ func (cp *CondLoader) load(a map[string]interface{}, parentElement condElement) } else { currentElement = &operatorSlice{operator: key} for _, e := range t { - cp.load(e.(map[string]interface{}), currentElement) + cp.load(e.(map[string]interface{}), currentElement.(compositeElement)) } } case map[string]interface{}: currentElement = &keyStruct{key: key} //log.Print("map: ", t) - cp.load(t, currentElement) + cp.load(t, currentElement.(compositeElement)) case interface{}: if isOperator(key) { currentElement = &operatorValue{operator: key, value: t} @@ -241,13 +271,13 @@ func (cp *CondLoader) load(a map[string]interface{}, parentElement condElement) default: return nil, ErrParserError } - if parentElement != nil { + if parentElement != nil { // normal recurrent action parentElement.addChild(currentElement) } else { - if len(a) > 1 { + if len(a) > 1 { // we have more keys in the map parentElement = &operatorSlice{operator: CondAND} parentElement.addChild(currentElement) - } else { + } else { // it was only one key value return currentElement, nil } } From 9daa473da12b20ea6dcf93c7d42c7fb9fdc39ec9 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Thu, 21 Jan 2016 12:00:00 +0200 Subject: [PATCH 37/38] fix type in the scheduler queue console command --- console/scheduler_queue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/scheduler_queue.go b/console/scheduler_queue.go index c91eb92a7..079575373 100644 --- a/console/scheduler_queue.go +++ b/console/scheduler_queue.go @@ -22,7 +22,7 @@ import "github.com/cgrates/cgrates/apier/v1" func init() { c := &CmdGetScheduledActions{ - name: "2", + name: "scheduler_queue", rpcMethod: "ApierV1.GetScheduledActions", rpcParams: &v1.AttrsGetScheduledActions{}, } From 862795ef063a3f3f808e8c12099c648e1242d4db Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Thu, 21 Jan 2016 17:00:25 +0200 Subject: [PATCH 38/38] all actons can be conditional with Filter field --- apier/v1/apier.go | 4 +- .../mysql/create_tariffplan_tables.sql | 1 + .../postgres/create_tariffplan_tables.sql | 1 + data/tariffplans/cdrstats/Actions.csv | 3 +- data/tariffplans/prepaid1centpsec/Actions.csv | 12 +- data/tariffplans/tutorial/Actions.csv | 20 +- engine/account.go | 9 +- engine/action.go | 45 +- engine/action_plan.go | 10 + engine/action_trigger.go | 11 + engine/actions_test.go | 18 +- engine/loader_csv_test.go | 28 +- engine/model_converters.go | 1 + engine/model_helpers.go | 1 + engine/model_helpers_test.go | 4 +- engine/models.go | 29 +- engine/tp_reader.go | 3 + general_tests/acntacts_test.go | 6 +- general_tests/auth_test.go | 2 +- general_tests/ddazmbl1_test.go | 4 +- general_tests/ddazmbl2_test.go | 4 +- general_tests/ddazmbl3_test.go | 2 +- glide.lock | 6 +- glide.yaml | 1 + utils/apitpdata.go | 1 + utils/cond_loader.go | 306 -------------- utils/cond_loader_test.go | 400 ------------------ 27 files changed, 109 insertions(+), 823 deletions(-) delete mode 100644 utils/cond_loader.go delete mode 100644 utils/cond_loader_test.go diff --git a/apier/v1/apier.go b/apier/v1/apier.go index 140c6eb1c..45fb0ca96 100644 --- a/apier/v1/apier.go +++ b/apier/v1/apier.go @@ -509,6 +509,7 @@ func (self *ApierV1) SetActions(attrs utils.AttrSetActions, reply *string) error Weight: apiAct.Weight, ExpirationString: apiAct.ExpiryTime, ExtraParameters: apiAct.ExtraParameters, + Filter: apiAct.Filter, Balance: &engine.Balance{ Uuid: utils.GenUUID(), Id: apiAct.BalanceId, @@ -545,6 +546,7 @@ func (self *ApierV1) GetActions(actsId string, reply *[]*utils.TPAction) error { BalanceType: engAct.BalanceType, ExpiryTime: engAct.ExpirationString, ExtraParameters: engAct.ExtraParameters, + Filter: engAct.Filter, Weight: engAct.Weight, } if engAct.Balance != nil { @@ -738,7 +740,7 @@ type AttrResetTriggeredAction struct { func (self *ApierV1) ResetTriggeredActions(attr AttrResetTriggeredAction, reply *string) error { var a *engine.Action if attr.Id != "" { - // we can identify the trigge by the id + // we can identify the trigger by the id a = &engine.Action{Id: attr.Id} } else { extraParameters, err := json.Marshal(struct { diff --git a/data/storage/mysql/create_tariffplan_tables.sql b/data/storage/mysql/create_tariffplan_tables.sql index efc2e81ae..15783c927 100644 --- a/data/storage/mysql/create_tariffplan_tables.sql +++ b/data/storage/mysql/create_tariffplan_tables.sql @@ -163,6 +163,7 @@ CREATE TABLE `tp_actions` ( `balance_blocker` BOOLEAN NOT NULL, `balance_disabled` BOOLEAN NOT NULL, `extra_parameters` varchar(256) NOT NULL, + `filter` varchar(256) NOT NULL, `weight` DECIMAL(8,2) NOT NULL, `created_at` TIMESTAMP, PRIMARY KEY (`id`), diff --git a/data/storage/postgres/create_tariffplan_tables.sql b/data/storage/postgres/create_tariffplan_tables.sql index de3a163e7..857fb956f 100644 --- a/data/storage/postgres/create_tariffplan_tables.sql +++ b/data/storage/postgres/create_tariffplan_tables.sql @@ -158,6 +158,7 @@ CREATE TABLE tp_actions ( balance_blocker BOOLEAN NOT NULL, balance_disabled BOOLEAN NOT NULL, extra_parameters VARCHAR(256) NOT NULL, + filter VARCHAR(256) NOT NULL, weight NUMERIC(8,2) NOT NULL, created_at TIMESTAMP, UNIQUE (tpid, tag, action, balance_tag, balance_type, directions, expiry_time, timing_tags, destination_tags, shared_groups, balance_weight, weight) diff --git a/data/tariffplans/cdrstats/Actions.csv b/data/tariffplans/cdrstats/Actions.csv index 72fcd27bf..a87f15e9b 100644 --- a/data/tariffplans/cdrstats/Actions.csv +++ b/data/tariffplans/cdrstats/Actions.csv @@ -1,2 +1 @@ -#ActionsTag,Action,BalanceTag,BalanceType,Directions,Units,ExpiryTime,TimingTags,DestinationTags,RatingSubject,Categories,BalanceWeight,SharedGroup,ExtraParameters,Weight -CDRST_LOG,*log,,,,,,,,,,,,,false,false,10 +CDRST_LOG,*log,,,,,,,,,,,,,,false,false,10 diff --git a/data/tariffplans/prepaid1centpsec/Actions.csv b/data/tariffplans/prepaid1centpsec/Actions.csv index 712d74f9d..2729b8c77 100644 --- a/data/tariffplans/prepaid1centpsec/Actions.csv +++ b/data/tariffplans/prepaid1centpsec/Actions.csv @@ -1,6 +1,6 @@ -#ActionsTag[0],Action[1],ActionExtraParameters[2],BalanceTag[3],BalanceType[4],Directions[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingTags[11],Units[12],BalanceWeight[13],BalanceBlocker[14],BalanceDisabled[15],Weight[16] -PREPAID_10,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 -BONUS_1,*topup,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 -LOG_BALANCE,*log,,,,,,,,,,,,,false,false,10 -CDRST_WARN_HTTP,*call_url,http://localhost:8080,,,,,,,,,,,,false,false,10 -CDRST_LOG,*log,,,,,,,,,,,,,false,false,10 +#ActionsTag[0],Action[1],ActionExtraParameters[2],Filter[3],BalanceTag[4],BalanceType[5],Directions[6],Categories[7],DestinationIds[8],RatingSubject[9],SharedGroup[10],ExpiryTime[11],TimingTags[12],Units[13],BalanceWeight[14],BalanceBlocker[15],BalanceDisabled[16],Weight[17] +PREPAID_10,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +BONUS_1,*topup,,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 +LOG_BALANCE,*log,,,,,,,,,,,,,,false,false,10 +CDRST_WARN_HTTP,*call_url,http://localhost:8080,,,,,,,,,,,,,false,false,10 +CDRST_LOG,*log,,,,,,,,,,,,,,false,false,10 diff --git a/data/tariffplans/tutorial/Actions.csv b/data/tariffplans/tutorial/Actions.csv index b2ac178b6..93a3faa1f 100644 --- a/data/tariffplans/tutorial/Actions.csv +++ b/data/tariffplans/tutorial/Actions.csv @@ -1,10 +1,10 @@ -#ActionsId[0],Action[1],ExtraParameters[2],BalanceId[3],BalanceType[4],Directions[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingIds[11],Units[12],BalanceWeight[13],BalanceBlocker[14],BalanceDisabled[15],Weight[16] -TOPUP_RST_10,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 -TOPUP_RST_5,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,5,20,false,false,10 -TOPUP_RST_5,*topup_reset,,,*voice,*out,,DST_1002,SPECIAL_1002,,*unlimited,,90,20,false,false,10 -TOPUP_120_DST1003,*topup_reset,,,*voice,*out,,DST_1003,,,*unlimited,,120,20,false,false,10 -TOPUP_RST_SHARED_5,*topup,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,5,10,false,false,10 -SHARED_A_0,*topup_reset,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,0,10,false,false,10 -LOG_WARNING,*log,,,,,,,,,,,,,false,false,10 -DISABLE_AND_LOG,*log,,,,,,,,,,,,,false,false,10 -DISABLE_AND_LOG,*disable_account,,,,,,,,,,,,,false,false,10 +#ActionsId[0],Action[1],ExtraParameters[2],Filter[3],BalanceId[4],BalanceType[5],Directions[6],Categories[7],DestinationIds[8],RatingSubject[9],SharedGroup[10],ExpiryTime[11],TimingIds[12],Units[13],BalanceWeight[14],BalanceBlocker[15],BalanceDisabled[16],Weight[17] +TOPUP_RST_10,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +TOPUP_RST_5,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,5,20,false,false,10 +TOPUP_RST_5,*topup_reset,,,,*voice,*out,,DST_1002,SPECIAL_1002,,*unlimited,,90,20,false,false,10 +TOPUP_120_DST1003,*topup_reset,,,,*voice,*out,,DST_1003,,,*unlimited,,120,20,false,false,10 +TOPUP_RST_SHARED_5,*topup,,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,5,10,false,false,10 +SHARED_A_0,*topup_reset,,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,0,10,false,false,10 +LOG_WARNING,*log,,,,,,,,,,,,,,false,false,10 +DISABLE_AND_LOG,*log,,,,,,,,,,,,,,false,false,10 +DISABLE_AND_LOG,*disable_account,,,,,,,,,,,,,,false,false,10 diff --git a/engine/account.go b/engine/account.go index 16493a6cb..7ae6b8083 100644 --- a/engine/account.go +++ b/engine/account.go @@ -26,6 +26,7 @@ import ( "github.com/cgrates/cgrates/cache2go" "github.com/cgrates/cgrates/utils" + "github.com/cgrates/structmatcher" "strings" ) @@ -697,14 +698,14 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance } } -func (acc *Account) matchConditions(condition string) (bool, error) { - cl := &utils.CondLoader{} - if err := cl.Parse(condition); err != nil { +func (acc *Account) matchActionFilter(condition string) (bool, error) { + sm, err := structmatcher.NewStructMatcher(condition) + if err != nil { return false, err } for balanceType, balanceChain := range acc.BalanceMap { for _, b := range balanceChain { - check, err := cl.Check(&struct { + check, err := sm.Match(&struct { Type string *Balance }{ diff --git a/engine/action.go b/engine/action.go index fdf0eaff1..147b176a7 100644 --- a/engine/action.go +++ b/engine/action.go @@ -42,6 +42,7 @@ type Action struct { ActionType string BalanceType string ExtraParameters string + Filter string ExpirationString string // must stay as string because it can have relative values like 1month Weight float64 Balance *Balance @@ -72,10 +73,6 @@ const ( CDRLOG = "*cdrlog" SET_DDESTINATIONS = "*set_ddestinations" TRANSFER_MONETARY_DEFAULT = "*transfer_monetary_default" - CONDITIONAL_TOPUP = "*conditional_topup" - CONDITIONAL_TOPUP_RESET = "*conditional_topup_reset" - CONDITIONAL_DEBIT = "*conditional_debit" - CONDITIONAL_DEBIT_RESET = "*conditional_debit_reset" ) func (a *Action) Clone() *Action { @@ -140,14 +137,6 @@ func getActionFunc(typ string) (actionTypeFunc, bool) { return removeBalanceAction, true case TRANSFER_MONETARY_DEFAULT: return transferMonetaryDefault, true - case CONDITIONAL_DEBIT: - return conditionalDebitAction, true - case CONDITIONAL_DEBIT_RESET: - return conditionalDebitResetAction, true - case CONDITIONAL_TOPUP: - return conditionalTopupAction, true - case CONDITIONAL_TOPUP_RESET: - return conditionalTopupResetAction, true } return nil, false } @@ -627,38 +616,6 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a return accountingStorage.SetAccount(acc) } -func conditionalDebitAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { - return debitAction(acc, sq, a, acs) - } else { - return err - } -} - -func conditionalDebitResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { - return debitResetAction(acc, sq, a, acs) - } else { - return err - } -} - -func conditionalTopupAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { - return topupAction(acc, sq, a, acs) - } else { - return err - } -} - -func conditionalTopupResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { - return topupResetAction(acc, sq, a, acs) - } else { - return err - } -} - // Structure to store actions according to weight type Actions []*Action diff --git a/engine/action_plan.go b/engine/action_plan.go index 1acea3887..163ca4595 100644 --- a/engine/action_plan.go +++ b/engine/action_plan.go @@ -291,6 +291,16 @@ func (at *ActionTiming) Execute() (err error) { transactionFailed := false removeAccountActionFound := false for _, a := range aac { + // check action filter + if len(a.Filter) > 0 { + matched, err := ub.matchActionFilter(a.Filter) + if err != nil { + return 0, err + } + if !matched { + continue + } + } if ub.Disabled && a.ActionType != ENABLE_ACCOUNT { continue // disabled acocunts are not removed from action plan //return 0, fmt.Errorf("Account %s is disabled", accID) diff --git a/engine/action_trigger.go b/engine/action_trigger.go index fb3dc6e65..9fa9ace1d 100644 --- a/engine/action_trigger.go +++ b/engine/action_trigger.go @@ -76,6 +76,17 @@ func (at *ActionTrigger) Execute(ub *Account, sq *StatsQueueTriggered) (err erro transactionFailed := false removeAccountActionFound := false for _, a := range aac { + // check action filter + if len(a.Filter) > 0 { + matched, err := ub.matchActionFilter(a.Filter) + if err != nil { + return err + } + if !matched { + continue + } + } + if a.Balance == nil { a.Balance = &Balance{} } diff --git a/engine/actions_test.go b/engine/actions_test.go index 4eaaec183..604b85447 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -1603,9 +1603,9 @@ func TestActionConditionalTopup(t *testing.T) { } a := &Action{ - ActionType: CONDITIONAL_TOPUP, - BalanceType: utils.MONETARY, - ExtraParameters: `{"Type":"*monetary","Value":1,"Weight":10}`, + ActionType: TOPUP, + BalanceType: utils.MONETARY, + Filter: `{"Type":"*monetary","Value":1,"Weight":10}`, Balance: &Balance{ Value: 11, Weight: 30, @@ -1667,9 +1667,9 @@ func TestActionConditionalTopupNoMatch(t *testing.T) { } a := &Action{ - ActionType: CONDITIONAL_TOPUP, - BalanceType: utils.MONETARY, - ExtraParameters: `{"Type":"*monetary","Value":2,"Weight":10}`, + ActionType: TOPUP, + BalanceType: utils.MONETARY, + Filter: `{"Type":"*monetary","Value":2,"Weight":10}`, Balance: &Balance{ Value: 11, Weight: 30, @@ -1731,9 +1731,9 @@ func TestActionConditionalTopupExistingBalance(t *testing.T) { } a := &Action{ - ActionType: CONDITIONAL_TOPUP, - BalanceType: utils.MONETARY, - ExtraParameters: `{"Type":"*voice","Value":{"*gte":100}}`, + ActionType: TOPUP, + BalanceType: utils.MONETARY, + Filter: `{"Type":"*voice","Value":{"*gte":100}}`, Balance: &Balance{ Value: 11, Weight: 10, diff --git a/engine/loader_csv_test.go b/engine/loader_csv_test.go index f4b1e507b..c13bac58d 100644 --- a/engine/loader_csv_test.go +++ b/engine/loader_csv_test.go @@ -160,20 +160,20 @@ SG3,*any,*lowest, *in,cgrates.org,call,*any,*any,*any,LCR_STANDARD,*lowest_cost,,2012-01-01T00:00:00Z,20 ` actions = ` -MINI,*topup_reset,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10 -MINI,*topup,,,*voice,*out,,NAT,test,,*unlimited,,100,10,false,false,10 -SHARED,*topup,,,*monetary,*out,,,,SG1,*unlimited,,100,10,false,false,10 -TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10 -SE0,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,0,10,false,false,10 -SE10,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,10,5,false,false,10 -SE10,*topup,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10 -EE0,*topup_reset,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,false,10 -EE0,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 -DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,false,false,10 -NEG,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 -BLOCK,*topup,,bblocker,*monetary,*out,,NAT,,,*unlimited,,10,20,true,false,20 -BLOCK,*topup,,bfree,*monetary,*out,,,,,*unlimited,,20,10,false,false,10 +MINI,*topup_reset,,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10 +MINI,*topup,,,,*voice,*out,,NAT,test,,*unlimited,,100,10,false,false,10 +SHARED,*topup,,,,*monetary,*out,,,,SG1,*unlimited,,100,10,false,false,10 +TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10 +SE0,*topup_reset,,,,*monetary,*out,,,,SG2,*unlimited,,0,10,false,false,10 +SE10,*topup_reset,,,,*monetary,*out,,,,SG2,*unlimited,,10,5,false,false,10 +SE10,*topup,,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10 +EE0,*topup_reset,,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,false,10 +EE0,*allow_negative,,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 +DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,,false,false,10 +NEG,*allow_negative,,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 +BLOCK,*topup,,,bblocker,*monetary,*out,,NAT,,,*unlimited,,10,20,true,false,20 +BLOCK,*topup,,,bfree,*monetary,*out,,,,,*unlimited,,20,10,false,false,10 ` actionPlans = ` MORE_MINUTES,MINI,ONE_TIME_RUN,10 diff --git a/engine/model_converters.go b/engine/model_converters.go index bc81870da..d3035772e 100644 --- a/engine/model_converters.go +++ b/engine/model_converters.go @@ -173,6 +173,7 @@ func APItoModelAction(as *utils.TPActions) (result []TpAction) { Directions: a.Directions, Units: a.Units, ExpiryTime: a.ExpiryTime, + Filter: a.Filter, TimingTags: a.TimingTags, DestinationTags: a.DestinationIds, RatingSubject: a.RatingSubject, diff --git a/engine/model_helpers.go b/engine/model_helpers.go index e6d58373b..0d2d0a648 100644 --- a/engine/model_helpers.go +++ b/engine/model_helpers.go @@ -390,6 +390,7 @@ func (tps TpActions) GetActions() (map[string][]*utils.TPAction, error) { Directions: tpAc.Directions, Units: tpAc.Units, ExpiryTime: tpAc.ExpiryTime, + Filter: tpAc.Filter, TimingTags: tpAc.TimingTags, DestinationIds: tpAc.DestinationTags, RatingSubject: tpAc.RatingSubject, diff --git a/engine/model_helpers_test.go b/engine/model_helpers_test.go index 3621b6818..7d0413c9a 100644 --- a/engine/model_helpers_test.go +++ b/engine/model_helpers_test.go @@ -265,8 +265,8 @@ func TestTPActionsAsExportSlice(t *testing.T) { }, } expectedSlc := [][]string{ - []string{"TEST_ACTIONS", "*topup_reset", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "false", "10"}, - []string{"TEST_ACTIONS", "*http_post", "http://localhost/¶m1=value1", "", "", "", "", "", "", "", "", "", "0", "0", "false", "false", "20"}, + []string{"TEST_ACTIONS", "*topup_reset", "", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "false", "10"}, + []string{"TEST_ACTIONS", "*http_post", "http://localhost/¶m1=value1", "", "", "", "", "", "", "", "", "", "", "0", "0", "false", "false", "20"}, } ms := APItoModelAction(tpActs) diff --git a/engine/models.go b/engine/models.go index 312873962..2fb2ffc56 100644 --- a/engine/models.go +++ b/engine/models.go @@ -156,20 +156,21 @@ type TpAction struct { Tag string `index:"0" re:"\w+\s*"` Action string `index:"1" re:"\*\w+\s*"` ExtraParameters string `index:"2" re:"\S+\s*"` - BalanceTag string `index:"3" re:"\w+\s*"` - BalanceType string `index:"4" re:"\*\w+\s*"` - Directions string `index:"5" re:""` - Categories string `index:"6" re:""` - DestinationTags string `index:"7" re:"\*any|\w+\s*"` - RatingSubject string `index:"8" re:"\w+\s*"` - SharedGroups string `index:"9" re:"[0-9A-Za-z_;]*"` - ExpiryTime string `index:"10" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` - TimingTags string `index:"11" re:"[0-9A-Za-z_;]*|\*any"` - Units float64 `index:"12" re:"\d+\s*"` - BalanceWeight float64 `index:"13" re:"\d+\.?\d*\s*"` - BalanceBlocker bool `index:"14" re:""` - BalanceDisabled bool `index:"15" re:""` - Weight float64 `index:"16" re:"\d+\.?\d*\s*"` + Filter string `index:"3" re:"\S+\s*"` + BalanceTag string `index:"4" re:"\w+\s*"` + BalanceType string `index:"5" re:"\*\w+\s*"` + Directions string `index:"6" re:""` + Categories string `index:"7" re:""` + DestinationTags string `index:"8" re:"\*any|\w+\s*"` + RatingSubject string `index:"9" re:"\w+\s*"` + SharedGroups string `index:"10" re:"[0-9A-Za-z_;]*"` + ExpiryTime string `index:"11" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` + TimingTags string `index:"12" re:"[0-9A-Za-z_;]*|\*any"` + Units float64 `index:"13" re:"\d+\s*"` + BalanceWeight float64 `index:"14" re:"\d+\.?\d*\s*"` + BalanceBlocker bool `index:"15" re:""` + BalanceDisabled bool `index:"16" re:""` + Weight float64 `index:"17" re:"\d+\.?\d*\s*"` CreatedAt time.Time } diff --git a/engine/tp_reader.go b/engine/tp_reader.go index b0982699f..517606d95 100644 --- a/engine/tp_reader.go +++ b/engine/tp_reader.go @@ -513,6 +513,7 @@ func (tpr *TpReader) LoadActions() (err error) { Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, + Filter: tpact.Filter, Balance: &Balance{ Id: tpact.BalanceId, Value: tpact.Units, @@ -837,6 +838,7 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, + Filter: tpact.Filter, Balance: &Balance{ Id: tpact.BalanceId, Value: tpact.Units, @@ -1066,6 +1068,7 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) { Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, + Filter: tpact.Filter, Balance: &Balance{ Id: tpact.BalanceId, Value: tpact.Units, diff --git a/general_tests/acntacts_test.go b/general_tests/acntacts_test.go index aea15334c..572f60180 100644 --- a/general_tests/acntacts_test.go +++ b/general_tests/acntacts_test.go @@ -44,9 +44,9 @@ func TestAcntActsLoadCsv(t *testing.T) { ratingProfiles := `` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*voice,*out,,*any,,,*unlimited,,10,10,false,false,10 -DISABLE_ACNT,*disable_account,,,,,,,,,,,,,false,false,10 -ENABLE_ACNT,*enable_account,,,,,,,,,,,,,false,false,10` + actions := `TOPUP10_AC,*topup_reset,,,,*voice,*out,,*any,,,*unlimited,,10,10,false,false,10 +DISABLE_ACNT,*disable_account,,,,,,,,,,,,,,false,false,10 +ENABLE_ACNT,*enable_account,,,,,,,,,,,,,,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10` actionTriggers := `` accountActions := `cgrates.org,1,TOPUP10_AT,,,` diff --git a/general_tests/auth_test.go b/general_tests/auth_test.go index aa0836217..81a683305 100644 --- a/general_tests/auth_test.go +++ b/general_tests/auth_test.go @@ -56,7 +56,7 @@ RP_ANY,DR_ANY_1CNT,*any,10` *out,cgrates.org,call,*any,2013-01-06T00:00:00Z,RP_ANY,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,false,10` + actions := `TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,*asap,10` actionTriggers := `` accountActions := `cgrates.org,testauthpostpaid1,TOPUP10_AT,,,` diff --git a/general_tests/ddazmbl1_test.go b/general_tests/ddazmbl1_test.go index fe0ef141e..1884d24eb 100644 --- a/general_tests/ddazmbl1_test.go +++ b/general_tests/ddazmbl1_test.go @@ -53,8 +53,8 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` + actions := `TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10 TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` diff --git a/general_tests/ddazmbl2_test.go b/general_tests/ddazmbl2_test.go index 2e7df3d3b..18b7ba4de 100644 --- a/general_tests/ddazmbl2_test.go +++ b/general_tests/ddazmbl2_test.go @@ -53,8 +53,8 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` + actions := `TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10 TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` diff --git a/general_tests/ddazmbl3_test.go b/general_tests/ddazmbl3_test.go index 40ed39897..541584982 100644 --- a/general_tests/ddazmbl3_test.go +++ b/general_tests/ddazmbl3_test.go @@ -53,7 +53,7 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` + actions := `TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` accountActions := `cgrates.org,12346,TOPUP10_AT,,,` diff --git a/glide.lock b/glide.lock index a7791b5bd..ac52fee0e 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 330fc999239d5766f033409be2335338cfd172d0dcf18ad752b8613f45e9f451 -updated: 2016-01-06T13:35:12.445693385+02:00 +hash: 855bc23b0e58452edf1d31f228430476a2602b79d225397d33b805b252cebb83 +updated: 2016-01-21T12:30:17.296295607+02:00 imports: - name: github.com/cenkalti/hub version: 57d753b5f4856e77b3cf8ecce78c97215a7d324d @@ -13,6 +13,8 @@ imports: version: 3d6beed663452471dec3ca194137a30d379d9e8f - name: github.com/cgrates/rpcclient version: 79661b1e514823a9ac93b2b9e97e037ee190ba47 +- name: github.com/cgrates/structmatcher + version: 98feee0bab15ce165540fe5f0fa006db2e9f898c - name: github.com/DisposaBoy/JsonConfigReader version: 33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4 - name: github.com/fiorix/go-diameter diff --git a/glide.yaml b/glide.yaml index d69b5ba46..d1d548281 100644 --- a/glide.yaml +++ b/glide.yaml @@ -6,6 +6,7 @@ import: - package: github.com/cgrates/kamevapi - package: github.com/cgrates/osipsdagram - package: github.com/cgrates/rpcclient +- package: github.com/cgrates/structmatcher - package: github.com/fiorix/go-diameter subpackages: - /diam diff --git a/utils/apitpdata.go b/utils/apitpdata.go index 9a2c63d31..f235c33ed 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -279,6 +279,7 @@ type TPAction struct { Directions string // Balance direction Units float64 // Number of units to add/deduct ExpiryTime string // Time when the units will expire + Filter string // The condition on balances that is checked before the action TimingTags string // Timing when balance is active DestinationIds string // Destination profile id RatingSubject string // Reference a rate subject defined in RatingProfiles diff --git a/utils/cond_loader.go b/utils/cond_loader.go deleted file mode 100644 index f14e49607..000000000 --- a/utils/cond_loader.go +++ /dev/null @@ -1,306 +0,0 @@ -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 ( - CondEQ = "*eq" - CondGT = "*gt" - CondGTE = "*gte" - CondLT = "*lt" - CondLTE = "*lte" - CondEXP = "*exp" - CondOR = "*or" - CondAND = "*and" - CondHAS = "*has" -) - -var operatorMap = map[string]func(field, value interface{}) (bool, error){ - CondEQ: func(field, value interface{}) (bool, error) { - return value == field, nil - }, - CondGT: func(field, value interface{}) (bool, error) { - var of, vf float64 - var ok bool - if of, ok = field.(float64); !ok { - return false, NewErrInvalidArgument(field) - } - if vf, ok = value.(float64); !ok { - return false, NewErrInvalidArgument(value) - } - return of > vf, nil - }, - CondGTE: func(field, value interface{}) (bool, error) { - var of, vf float64 - var ok bool - if of, ok = field.(float64); !ok { - return false, NewErrInvalidArgument(field) - } - if vf, ok = value.(float64); !ok { - return false, NewErrInvalidArgument(value) - } - return of >= vf, nil - }, - CondLT: func(field, value interface{}) (bool, error) { - var of, vf float64 - var ok bool - if of, ok = field.(float64); !ok { - return false, NewErrInvalidArgument(field) - } - if vf, ok = value.(float64); !ok { - return false, NewErrInvalidArgument(value) - } - return of < vf, nil - }, - CondLTE: func(field, value interface{}) (bool, error) { - var of, vf float64 - var ok bool - if of, ok = field.(float64); !ok { - return false, NewErrInvalidArgument(field) - } - if vf, ok = value.(float64); !ok { - return false, NewErrInvalidArgument(value) - } - return of <= vf, nil - }, - CondEXP: func(field, value interface{}) (bool, error) { - var expDate time.Time - var ok bool - if expDate, ok = field.(time.Time); !ok { - return false, NewErrInvalidArgument(field) - } - var expired bool - if expired, ok = value.(bool); !ok { - return false, NewErrInvalidArgument(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 - } - }, - CondHAS: func(field, value interface{}) (bool, error) { - var strMap StringMap - var ok bool - if strMap, ok = field.(StringMap); !ok { - return false, NewErrInvalidArgument(field) - } - var strSlice []interface{} - if strSlice, ok = value.([]interface{}); !ok { - return false, NewErrInvalidArgument(value) - } - for _, str := range strSlice { - if !strMap[str.(string)] { - return false, nil - } - } - return true, nil - }, -} - -func NewErrInvalidArgument(arg interface{}) error { - return fmt.Errorf("INVALID_ARGUMENT: %v", arg) -} - -type compositeElement interface { - element - addChild(element) error -} - -type element interface { - checkStruct(interface{}) (bool, error) -} - -type operatorSlice struct { - operator string - slice []element -} - -func (os *operatorSlice) addChild(ce element) error { - os.slice = append(os.slice, ce) - return nil -} -func (os *operatorSlice) checkStruct(o interface{}) (bool, error) { - switch os.operator { - case CondOR: - for _, cond := range os.slice { - check, err := cond.checkStruct(o) - if err != nil { - return false, err - } - if check { - return true, nil - } - } - case CondAND: - accumulator := true - for _, cond := range os.slice { - check, err := cond.checkStruct(o) - if err != nil { - return false, err - } - accumulator = accumulator && check - } - return accumulator, nil - } - return false, nil -} - -type keyStruct struct { - key string - elem element -} - -func (ks *keyStruct) addChild(ce element) error { - ks.elem = ce - return nil -} -func (ks *keyStruct) checkStruct(o interface{}) (bool, error) { - obj := reflect.ValueOf(o) - if obj.Kind() == reflect.Ptr { - obj = obj.Elem() - } - value := obj.FieldByName(ks.key) - if !value.IsValid() { - return false, NewErrInvalidArgument(ks.key) - } - return ks.elem.checkStruct(value.Interface()) -} - -type operatorValue struct { - operator string - value interface{} -} - -func (ov *operatorValue) checkStruct(o interface{}) (bool, error) { - if f, ok := operatorMap[ov.operator]; ok { - return f(o, ov.value) - } - return false, nil -} - -type keyValue struct { - key string - value interface{} -} - -func (kv *keyValue) checkStruct(o interface{}) (bool, error) { - obj := reflect.ValueOf(o) - if obj.Kind() == reflect.Ptr { - obj = obj.Elem() - } - value := obj.FieldByName(kv.key) - if !value.IsValid() { - return false, NewErrInvalidArgument(kv.key) - } - return value.Interface() == kv.value, nil -} - -type trueElement struct{} - -func (te *trueElement) checkStruct(o interface{}) (bool, error) { - return true, nil -} - -func isOperator(s string) bool { - return strings.HasPrefix(s, "*") -} - -func notEmpty(x interface{}) bool { - return !reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) -} - -type CondLoader struct { - rootElement element -} - -func (cp *CondLoader) load(a map[string]interface{}, parentElement compositeElement) (element, error) { - for key, value := range a { - var currentElement element - switch t := value.(type) { - case []interface{}: - if key == CondHAS { - currentElement = &operatorValue{operator: key, value: t} - } else { - currentElement = &operatorSlice{operator: key} - for _, e := range t { - cp.load(e.(map[string]interface{}), currentElement.(compositeElement)) - } - } - case map[string]interface{}: - currentElement = &keyStruct{key: key} - //log.Print("map: ", t) - cp.load(t, currentElement.(compositeElement)) - case interface{}: - if isOperator(key) { - currentElement = &operatorValue{operator: key, value: t} - } else { - currentElement = &keyValue{key: key, value: t} - } - //log.Print("generic interface: ", t) - default: - return nil, ErrParserError - } - if parentElement != nil { // normal recurrent action - parentElement.addChild(currentElement) - } else { - if len(a) > 1 { // we have more keys in the map - parentElement = &operatorSlice{operator: CondAND} - parentElement.addChild(currentElement) - } else { // it was only one key value - return currentElement, nil - } - } - } - return parentElement, nil -} - -func (cp *CondLoader) Parse(s string) (err error) { - a := make(map[string]interface{}) - 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{} - } - return -} - -func (cp *CondLoader) Check(o interface{}) (bool, error) { - if cp.rootElement == nil { - return false, ErrParserError - } - return cp.rootElement.checkStruct(o) -} diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go deleted file mode 100644 index 7352b3cc9..000000000 --- a/utils/cond_loader_test.go +++ /dev/null @@ -1,400 +0,0 @@ -package utils - -import ( - "strings" - "testing" - "time" -) - -func TestCondLoader(t *testing.T) { - cl := &CondLoader{} - err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) - if err != nil { - t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) - } - - err = cl.Parse(`{"*has":["NAT","RET","EUR"]}`) - if err != nil { - t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) - } - err = cl.Parse(`{"Field":7, "Other":true}`) - 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 - ExpDate time.Time - }{ - 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"}`) - 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(`{"Field":6}`) - 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}`) - 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(`{"Field":6, "Other":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(`{"Field":7, "Other":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(`{"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(`{"Other":true, "Field":{"*gt":7}}`) - 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)) - } - err = cl.Parse(`{"*and":[{"Field":{"*gte":50}},{"Test":{"*eq":"test"}}]}`) - 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(`{"WrongFieldName":{"*eq":1}}`) - 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) { - o := &struct { - Test string - Field float64 - Other bool - }{ - Test: "test", - Field: 6.0, - Other: true, - } - cl := &CondLoader{} - err := cl.Parse(`{"Test":"test"}`) - 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(`{"Field":6}`) - 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}`) - 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 TestCondOperatorValue(t *testing.T) { - root := &operatorValue{operator: CondGT, value: 3.4} - if check, err := root.checkStruct(3.5); !check || err != nil { - t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) - } - root = &operatorValue{operator: CondEQ, value: 3.4} - if check, err := root.checkStruct(3.5); check || err != nil { - t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) - } - root = &operatorValue{operator: CondEQ, value: 3.4} - if check, err := root.checkStruct(3.4); !check || err != nil { - t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) - } - root = &operatorValue{operator: CondEQ, value: "zinc"} - if check, err := root.checkStruct("zinc"); !check || err != nil { - t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) - } - root = &operatorValue{operator: CondHAS, value: []interface{}{"NAT", "RET", "EUR"}} - if check, err := root.checkStruct(StringMap{"WOR": true, "EUR": true, "NAT": true, "RET": true, "ROM": true}); !check || err != nil { - t.Errorf("Error checking struct: %v %v (%v)", !check, err, ToIJSON(root)) - } -} - -func TestCondKeyStruct(t *testing.T) { - o := struct { - Test string - Field float64 - Other bool - }{ - Test: "test", - Field: 6.0, - Other: true, - } - cl := &CondLoader{} - err := cl.Parse(`{"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(`{"Test":{"*gt": 5}}`) - if err != nil { - t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) - } - if check, err := cl.Check(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { - t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) - } - err = cl.Parse(`{"Field":{"*gte": 6}}`) - 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(`{"Field":{"*lt": 7}}`) - 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(`{"Field":{"*lte": 6}}`) - 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(`{"Field":{"*eq": 6}}`) - 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(`{"Test":{"*eq": "test"}}`) - 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 TestCondKeyStructPointer(t *testing.T) { - o := &struct { - Test string - Field float64 - Other bool - }{ - Test: "test", - Field: 6.0, - Other: true, - } - cl := &CondLoader{} - err := cl.Parse(`{"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(`{"Test":{"*gt": 5}}`) - if err != nil { - t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) - } - if check, err := cl.Check(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { - t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) - } - err = cl.Parse(`{"Field":{"*gte": 6}}`) - 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(`{"Field":{"*lt": 7}}`) - 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(`{"Field":{"*lte": 6}}`) - 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(`{"Field":{"*eq": 6}}`) - 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(`{"Test":{"*eq": "test"}}`) - 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 TestCondOperatorSlice(t *testing.T) { - o := &struct { - Test string - Field float64 - Other bool - }{ - Test: "test", - Field: 6.0, - Other: true, - } - cl := &CondLoader{} - err := cl.Parse(`{"*or":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":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(`{"*or":[{"Test":"test"},{"Field":{"*gt":7}},{"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(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":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(`{"*and":[{"Test":"test"},{"Field":{"*gt":7}},{"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)) - } -} - -func TestCondMixed(t *testing.T) { - o := &struct { - Test string - Field float64 - Categories StringMap - Other bool - }{ - Test: "test", - Field: 6.0, - Categories: StringMap{"call": true, "data": true, "voice": true}, - Other: true, - } - cl := &CondLoader{} - err := cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true},{"Categories":{"*has":["data", "call"]}}]}`) - 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 TestCondBalanceType(t *testing.T) { - type Balance struct { - Value float64 - } - - o := &struct { - BalanceType string - Balance - }{ - BalanceType: MONETARY, - Balance: Balance{Value: 10}, - } - cl := &CondLoader{} - err := cl.Parse(`{"BalanceType":"*monetary","Value":10}`) - 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)) - } -}