diff --git a/apier/v1/apier.go b/apier/v1/apier.go index 521e37eac..a304c24ee 100644 --- a/apier/v1/apier.go +++ b/apier/v1/apier.go @@ -31,7 +31,7 @@ const ( ) type ApierV1 struct { - StorDb engine.DataStorage + StorDb engine.LoadStorage DataDb engine.DataStorage Sched *scheduler.Scheduler } @@ -109,7 +109,7 @@ func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error { attr.Direction = engine.OUTBOUND } - at.SetActions(engine.Actions{&engine.Action{ActionType: engine.TOPUP, BalanceId: attr.BalanceId, Direction: attr.Direction, Units: attr.Value}}) + at.SetActions(engine.Actions{&engine.Action{ActionType: engine.TOPUP, BalanceId: attr.BalanceId, Direction: attr.Direction, Balance: &engine.Balance{Value: attr.Value}}}) if err := at.Execute(); err != nil { *reply = err.Error() diff --git a/apier/v1/tpactions.go b/apier/v1/tpactions.go index 62849693f..15ee69612 100644 --- a/apier/v1/tpactions.go +++ b/apier/v1/tpactions.go @@ -50,13 +50,15 @@ func (self *ApierV1) SetTPActions(attrs utils.TPActions, reply *string) error { ActionType: act.Identifier, BalanceId: act.BalanceType, Direction: act.Direction, - Units: act.Units, ExpirationString: act.ExpiryTime, - DestinationTag: act.DestinationId, - RateType: act.RateType, - RateValue: act.Rate, - MinutesWeight: act.MinutesWeight, - Weight: act.Weight, + ExtraParameters: act.ExtraParameters, + Balance: &engine.Balance{ + Value: act.Units, + DestinationId: act.DestinationId, + RateSubject: act.RateSubject, + Weight: act.BalanceWeight, + }, + Weight: act.Weight, } } if err := self.StorDb.SetTPActions(attrs.TPid, map[string][]*engine.Action{attrs.ActionsId: acts}); err != nil { diff --git a/apier/v1/tpdestinationrates.go b/apier/v1/tpdestinationrates.go index 9b4603457..4d3219a8a 100644 --- a/apier/v1/tpdestinationrates.go +++ b/apier/v1/tpdestinationrates.go @@ -39,7 +39,10 @@ func (self *ApierV1) SetTPDestinationRate(attrs utils.TPDestinationRate, reply * } drs := make([]*engine.DestinationRate, len(attrs.DestinationRates)) for idx, dr := range attrs.DestinationRates { - drs[idx] = &engine.DestinationRate{attrs.DestinationRateId, dr.DestinationId, dr.RateId, nil} + drs[idx] = &engine.DestinationRate{ + Tag: attrs.DestinationRateId, + DestinationsTag: dr.DestinationId, + RateTag: dr.RateId} } if err := self.StorDb.SetTPDestinationRates(attrs.TPid, map[string][]*engine.DestinationRate{attrs.DestinationRateId: drs}); err != nil { return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error()) diff --git a/apier/v1/tpdestratetimings.go b/apier/v1/tpdestratetimings.go index 64683c38d..91dee75ff 100644 --- a/apier/v1/tpdestratetimings.go +++ b/apier/v1/tpdestratetimings.go @@ -42,7 +42,7 @@ func (self *ApierV1) SetTPDestRateTiming(attrs utils.TPDestRateTiming, reply *st drts[idx] = &engine.DestinationRateTiming{Tag: attrs.DestRateTimingId, DestinationRatesTag: drt.DestRatesId, Weight: drt.Weight, - TimingsTag: drt.TimingId, + TimingTag: drt.TimingId, } } if err := self.StorDb.SetTPDestRateTimings(attrs.TPid, map[string][]*engine.DestinationRateTiming{attrs.DestRateTimingId: drts}); err != nil { diff --git a/apier/v1/tprates.go b/apier/v1/tprates.go index 1fd6cae71..641eb5872 100644 --- a/apier/v1/tprates.go +++ b/apier/v1/tprates.go @@ -23,9 +23,9 @@ package apier import ( "errors" "fmt" - "time" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" + "time" ) // Creates a new rate within a tariff plan @@ -38,7 +38,7 @@ func (self *ApierV1) SetTPRate(attrs utils.TPRate, reply *string) error { } else if exists { return errors.New(utils.ERR_DUPLICATE) } - rts := make([]*engine.Rate, len(attrs.RateSlots)) + rts := make([]*engine.LoadRate, len(attrs.RateSlots)) for idx, rtSlot := range attrs.RateSlots { var errParse error itrvlStrs := []string{rtSlot.RatedUnits, rtSlot.RateIncrements, rtSlot.GroupIntervalStart} @@ -48,10 +48,10 @@ func (self *ApierV1) SetTPRate(attrs utils.TPRate, reply *string) error { return fmt.Errorf("%s:Parsing interval failed:%s", utils.ERR_SERVER_ERROR, errParse.Error()) } } - rts[idx] = &engine.Rate{attrs.RateId, rtSlot.ConnectFee, rtSlot.Rate, itrvls[0], itrvls[1], itrvls[2], + rts[idx] = &engine.LoadRate{attrs.RateId, rtSlot.ConnectFee, rtSlot.Rate, itrvls[0], itrvls[1], itrvls[2], rtSlot.RoundingMethod, rtSlot.RoundingDecimals, rtSlot.Weight} } - if err := self.StorDb.SetTPRates(attrs.TPid, map[string][]*engine.Rate{attrs.RateId: rts}); err != nil { + if err := self.StorDb.SetTPRates(attrs.TPid, map[string][]*engine.LoadRate{attrs.RateId: rts}); err != nil { return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error()) } *reply = "OK" diff --git a/cdrs/cdrs.go b/cdrs/cdrs.go index 6da450eb4..7cb0688a1 100644 --- a/cdrs/cdrs.go +++ b/cdrs/cdrs.go @@ -29,7 +29,7 @@ import ( var ( cfg *config.CGRConfig // Share the configuration with the rest of the package - storage engine.DataStorage + storage engine.CdrStorage medi *mediator.Mediator ) @@ -43,7 +43,7 @@ func fsCdrHandler(w http.ResponseWriter, r *http.Request) { } else { //TODO: use the connection to mediator } - } () + }() } else { engine.Logger.Err(fmt.Sprintf("Could not create CDR entry: %v", err)) } @@ -68,7 +68,7 @@ func cgrCdrHandler(w http.ResponseWriter, r *http.Request) { type CDRS struct{} -func New(s engine.DataStorage, m *mediator.Mediator, c *config.CGRConfig) *CDRS { +func New(s engine.CdrStorage, m *mediator.Mediator, c *config.CGRConfig) *CDRS { storage = s medi = m cfg = c @@ -76,7 +76,7 @@ func New(s engine.DataStorage, m *mediator.Mediator, c *config.CGRConfig) *CDRS } func (cdrs *CDRS) StartCapturingCDRs() { - http.HandleFunc("/cgr_json", cgrCdrHandler) // Attach CGR CDR Handler - http.HandleFunc("/freeswitch_json", fsCdrHandler) // Attach FreeSWITCH JSON CDR Handler + http.HandleFunc("/cgr_json", cgrCdrHandler) // Attach CGR CDR Handler + http.HandleFunc("/freeswitch_json", fsCdrHandler) // Attach FreeSWITCH JSON CDR Handler http.ListenAndServe(cfg.CDRSListen, nil) } diff --git a/cmd/cgr-engine/cgr-engine.go b/cmd/cgr-engine/cgr-engine.go index 1b7e3a8cc..97a58fc0c 100644 --- a/cmd/cgr-engine/cgr-engine.go +++ b/cmd/cgr-engine/cgr-engine.go @@ -64,7 +64,7 @@ var ( err error ) -func listenToRPCRequests(rpcResponder interface{}, apier *apier.ApierV1, rpcAddress string, rpc_encoding string, getter engine.DataStorage, loggerDb engine.DataStorage) { +func listenToRPCRequests(rpcResponder interface{}, apier *apier.ApierV1, rpcAddress string, rpc_encoding string, getter engine.DataStorage, loggerDb engine.LogStorage) { l, err := net.Listen("tcp", rpcAddress) if err != nil { engine.Logger.Crit(fmt.Sprintf(" Could not listen to %v: %v", rpcAddress, err)) @@ -94,7 +94,7 @@ func listenToRPCRequests(rpcResponder interface{}, apier *apier.ApierV1, rpcAddr } } -func startMediator(responder *engine.Responder, loggerDb engine.DataStorage) { +func startMediator(responder *engine.Responder, loggerDb engine.LogStorage, cdrDb engine.CdrStorage) { var connector engine.Connector if cfg.MediatorRater == INTERNAL { connector = responder @@ -125,7 +125,7 @@ func startMediator(responder *engine.Responder, loggerDb engine.DataStorage) { connector = &engine.RPCClientConnector{Client: client} } var err error - medi, err = mediator.NewMediator(connector, loggerDb, cfg) + medi, err = mediator.NewMediator(connector, loggerDb, cdrDb, cfg) if err != nil { engine.Logger.Crit(fmt.Sprintf("Mediator config parsing error: %v", err)) exitChan <- true @@ -136,7 +136,7 @@ func startMediator(responder *engine.Responder, loggerDb engine.DataStorage) { } } -func startSessionManager(responder *engine.Responder, loggerDb engine.DataStorage) { +func startSessionManager(responder *engine.Responder, loggerDb engine.LogStorage) { var connector engine.Connector if cfg.SMRater == INTERNAL { connector = responder @@ -183,7 +183,7 @@ func startSessionManager(responder *engine.Responder, loggerDb engine.DataStorag exitChan <- true } -func startCDRS(responder *engine.Responder, loggerDb engine.DataStorage) { +func startCDRS(responder *engine.Responder, cdrDb engine.CdrStorage) { if cfg.CDRSMediator == INTERNAL { for i := 0; i < 3; i++ { // ToDo: If the right approach, make the reconnects configurable time.Sleep(time.Duration(i/2) * time.Second) @@ -196,7 +196,7 @@ func startCDRS(responder *engine.Responder, loggerDb engine.DataStorage) { exitChan <- true } } - cs := cdrs.New(loggerDb, medi, cfg) + cs := cdrs.New(cdrDb, medi, cfg) cs.StartCapturingCDRs() exitChan <- true } @@ -308,25 +308,31 @@ func main() { return } - var getter, loggerDb engine.DataStorage - getter, err = engine.ConfigureDatabase(cfg.DataDBType, cfg.DataDBHost, cfg.DataDBPort, cfg.DataDBName, cfg.DataDBUser, cfg.DataDBPass) + var dataDb engine.DataStorage + var logDb engine.LogStorage + var loadDb engine.LoadStorage + var cdrDb engine.CdrStorage + dataDb, err = engine.ConfigureDataStorage(cfg.DataDBType, cfg.DataDBHost, cfg.DataDBPort, cfg.DataDBName, cfg.DataDBUser, cfg.DataDBPass) if err != nil { // Cannot configure getter database, show stopper engine.Logger.Crit(fmt.Sprintf("Could not configure dataDb: %s exiting!", err)) return } - defer getter.Close() - engine.SetDataStorage(getter) + defer dataDb.Close() + engine.SetDataStorage(dataDb) if cfg.StorDBType == SAME { - loggerDb = getter + logDb = dataDb.(engine.LogStorage) } else { - loggerDb, err = engine.ConfigureDatabase(cfg.StorDBType, cfg.StorDBHost, cfg.StorDBPort, cfg.StorDBName, cfg.StorDBUser, cfg.StorDBPass) + logDb, err = engine.ConfigureLogStorage(cfg.StorDBType, cfg.StorDBHost, cfg.StorDBPort, cfg.StorDBName, cfg.StorDBUser, cfg.StorDBPass) if err != nil { // Cannot configure logger database, show stopper engine.Logger.Crit(fmt.Sprintf("Could not configure logger database: %s exiting!", err)) return } } - defer loggerDb.Close() - engine.SetStorageLogger(loggerDb) + defer logDb.Close() + engine.SetStorageLogger(logDb) + // loadDb,cdrDb and logDb are all mapped on the same stordb storage + loadDb = logDb.(engine.LoadStorage) + cdrDb = logDb.(engine.CdrStorage) engine.SetRoundingMethodAndDecimals(cfg.RoundingMethod, cfg.RoundingDecimals) if cfg.SMDebitInterval > 0 { @@ -341,16 +347,16 @@ func main() { go stopRaterSingnalHandler() } responder := &engine.Responder{ExitChan: exitChan} - apier := &apier.ApierV1{StorDb: loggerDb, DataDb: getter} + apier := &apier.ApierV1{StorDb: loadDb, DataDb: dataDb} if cfg.RaterEnabled && !cfg.BalancerEnabled && cfg.RaterListen != INTERNAL { engine.Logger.Info(fmt.Sprintf("Starting CGRateS Rater on %s.", cfg.RaterListen)) - go listenToRPCRequests(responder, apier, cfg.RaterListen, cfg.RPCEncoding, getter, loggerDb) + go listenToRPCRequests(responder, apier, cfg.RaterListen, cfg.RPCEncoding, dataDb, logDb) } if cfg.BalancerEnabled { engine.Logger.Info(fmt.Sprintf("Starting CGRateS Balancer on %s.", cfg.BalancerListen)) go stopBalancerSingnalHandler() responder.Bal = bal - go listenToRPCRequests(responder, apier, cfg.BalancerListen, cfg.RPCEncoding, getter, loggerDb) + go listenToRPCRequests(responder, apier, cfg.BalancerListen, cfg.RPCEncoding, dataDb, logDb) if cfg.RaterEnabled { engine.Logger.Info("Starting internal engine.") bal.AddClient("local", new(engine.ResponderWorker)) @@ -361,28 +367,28 @@ func main() { engine.Logger.Info("Starting CGRateS Scheduler.") go func() { sched := scheduler.NewScheduler() - go reloadSchedulerSingnalHandler(sched, getter) + go reloadSchedulerSingnalHandler(sched, dataDb) apier.Sched = sched - sched.LoadActionTimings(getter) + sched.LoadActionTimings(dataDb) sched.Loop() }() } if cfg.SMEnabled { engine.Logger.Info("Starting CGRateS SessionManager.") - go startSessionManager(responder, loggerDb) + go startSessionManager(responder, logDb) // close all sessions on shutdown go shutdownSessionmanagerSingnalHandler() } if cfg.MediatorEnabled { engine.Logger.Info("Starting CGRateS Mediator.") - go startMediator(responder, loggerDb) + go startMediator(responder, logDb, cdrDb) } if cfg.CDRSEnabled { engine.Logger.Info("Starting CGRateS CDR Server.") - go startCDRS(responder, loggerDb) + go startCDRS(responder, cdrDb) } if cfg.HistoryServerEnabled || cfg.HistoryAgentEnabled { diff --git a/cmd/cgr-loader/cgr-loader.go b/cmd/cgr-loader/cgr-loader.go index 984122ae3..aff61c0f1 100644 --- a/cmd/cgr-loader/cgr-loader.go +++ b/cmd/cgr-loader/cgr-loader.go @@ -66,18 +66,19 @@ func main() { return } var errDataDb, errStorDb, err error - var dataDb, storDb engine.DataStorage + var dataDb engine.DataStorage + var storDb engine.LoadStorage // Init necessary db connections if *fromStorDb { - dataDb, errDataDb = engine.ConfigureDatabase(*data_db_type, *data_db_host, *data_db_port, *data_db_name, *data_db_user, *data_db_pass) - storDb, errStorDb = engine.ConfigureDatabase(*stor_db_type, *stor_db_host, *stor_db_port, *stor_db_name, *stor_db_user, *stor_db_pass) + dataDb, errDataDb = engine.ConfigureDataStorage(*data_db_type, *data_db_host, *data_db_port, *data_db_name, *data_db_user, *data_db_pass) + storDb, errStorDb = engine.ConfigureLoadStorage(*stor_db_type, *stor_db_host, *stor_db_port, *stor_db_name, *stor_db_user, *stor_db_pass) } else if *toStorDb { // Import from csv files to storDb - storDb, errStorDb = engine.ConfigureDatabase(*stor_db_type, *stor_db_host, *stor_db_port, *stor_db_name, *stor_db_user, *stor_db_pass) + storDb, errStorDb = engine.ConfigureLoadStorage(*stor_db_type, *stor_db_host, *stor_db_port, *stor_db_name, *stor_db_user, *stor_db_pass) } else { // Default load from csv files to dataDb - dataDb, errDataDb = engine.ConfigureDatabase(*data_db_type, *data_db_host, *data_db_port, *data_db_name, *data_db_user, *data_db_pass) + dataDb, errDataDb = engine.ConfigureDataStorage(*data_db_type, *data_db_host, *data_db_port, *data_db_name, *data_db_user, *data_db_pass) } // Defer databases opened to be closed when we are done - for _, db := range []engine.DataStorage{dataDb, storDb} { + for _, db := range []engine.Storage{dataDb, storDb} { if db != nil { defer db.Close() } diff --git a/data/storage/mysql/create_tariffplan_tables.sql b/data/storage/mysql/create_tariffplan_tables.sql index 652256dcf..4908f87d6 100644 --- a/data/storage/mysql/create_tariffplan_tables.sql +++ b/data/storage/mysql/create_tariffplan_tables.sql @@ -120,9 +120,9 @@ CREATE TABLE `tp_actions` ( `units` DECIMAL(8,4) NOT NULL, `expiry_time` varchar(24) NOT NULL, `destination_tag` varchar(64) NOT NULL, - `rate_type` varchar(8) NOT NULL, - `rate` DECIMAL(8,4) NOT NULL, + `rate_subject` varchar(64) NOT NULL, `minutes_weight` DECIMAL(5,2) NOT NULL, + `extra_parameters` varchar(256) NOT NULL, `weight` DECIMAL(5,2) NOT NULL, PRIMARY KEY (`id`), KEY `tpid` (`tpid`), diff --git a/docs/ratinglogic.rst b/docs/ratinglogic.rst index 6c82e7cc3..e24ab9391 100644 --- a/docs/ratinglogic.rst +++ b/docs/ratinglogic.rst @@ -15,7 +15,7 @@ The call information comes to CGRateS having the following vital information lik LoopIndex // indicates the position of this segment in a cost request loop CallDuration // the call duration so far (partial or final) FallbackSubject // the subject to check for destination if not found on primary subject - ActivationPeriods + RatingPlans } When the session manager receives a call start event it will first check if the call is prepaid or postpaid. If the call is postpaid than the cost will be determined only once at the end of the call but if the call is prepaid there will be a debit operation every X seconds (X is configurable). @@ -31,7 +31,7 @@ What are the activation periods? :: - type Interval struct { + type RateInterval struct { Years Months MonthDays @@ -51,9 +51,9 @@ What are the activation periods? } -An **Interval** specifies the Month, the MonthDay, the WeekDays, the StartTime and the EndTime when the Interval's price profile is in effect. +An **RateInterval** specifies the Month, the MonthDay, the WeekDays, the StartTime and the EndTime when the RateInterval's price profile is in effect. -:Example: The Interval {"Month": [1], "WeekDays":[1,2,3,4,5], "StartTime":"18:00:00"} specifies the *Price* for the first month of each year from Monday to Friday starting 18:00. Most structure elements are optional and they can be combined in any way it makes sense. If an element is omitted it means it is zero or any. +:Example: The RateInterval {"Month": [1], "WeekDays":[1,2,3,4,5], "StartTime":"18:00:00"} specifies the *Price* for the first month of each year from Monday to Friday starting 18:00. Most structure elements are optional and they can be combined in any way it makes sense. If an element is omitted it means it is zero or any. The *ConnectFee* specifies the connection price for the call if this interval is the first one of the call. @@ -65,7 +65,7 @@ The *RoundingMethod* and the *RoundingDecimals* will adjust the price using the The **Price** structure defines the start (*GroupIntervalStart*) of a section of a call with a specified rate *Value* per *RateUnit* diving and rounding the section in *RateIncrement* subsections. -So when there is a need to define new sets of prices just define new ActivationPeriods with the activation time set to the moment when it becomes active. +So when there is a need to define new sets of prices just define new RatingPlans with the activation time set to the moment when it becomes active. Let's get back to the engine. When a GetCost or Debit call comes to the engine it will try to match the best rating profile for the given *Direction*, *Tenant*, *TOR* and *Subject* using the longest *Subject* prefix method or using the *FallbackSubject* if not found. The rating profile contains the activation periods that might apply to the call in question. @@ -75,21 +75,21 @@ At this point in rating process the engine will start splitting the call into va 2. Activation periods: if there were not enough special price minutes available than the engine will check if the call spans over multiple activation periods (the call starts in initial rates period and continues in another). -3. Intervals: for each activation period that apply to the call the engine will select the best rate intervals that apply. +3. RateIntervals: for each activation period that apply to the call the engine will select the best rate intervals that apply. :: type TimeSpan struct { TimeStart, TimeEnd Cost - ActivationPeriod - Interval + RatingPlan + RateInterval MinuteInfo CallDuration // the call duration so far till TimeEnd } -The result of this splitting will be a list of *TimeSpan* structures each having attached the MinuteInfo or the Interval that gave the price for it. The *CallDuration* attribute will select the right *Price* from the *Interval* *Prices* list. The final cost for the call will be the sum of the prices of these times spans plus the *ConnectionFee* from the first time span of the call. +The result of this splitting will be a list of *TimeSpan* structures each having attached the MinuteInfo or the RateInterval that gave the price for it. The *CallDuration* attribute will select the right *Price* from the *RateInterval* *Prices* list. The final cost for the call will be the sum of the prices of these times spans plus the *ConnectionFee* from the first time span of the call. 6.2.1 User balances ------------------- diff --git a/engine/action.go b/engine/action.go index c667326ac..cad58c4d6 100644 --- a/engine/action.go +++ b/engine/action.go @@ -19,26 +19,25 @@ along with this program. If not, see package engine import ( + "bytes" + "encoding/json" "fmt" + "net/http" "sort" - "time" ) /* Structure to be filled for each tariff plan with the bonus value for received calls minutes. */ type Action struct { - Id string - ActionType string - BalanceId string - Direction string - ExpirationString string - ExpirationDate time.Time - Units float64 - Weight float64 - MinuteBucket *MinuteBucket - DestinationTag, RateType string // From here for import/load purposes only - RateValue, MinutesWeight float64 + Id string + ActionType string + BalanceId string + Direction string + ExtraParameters string + ExpirationString string + Weight float64 + Balance *Balance } const ( @@ -53,6 +52,8 @@ const ( DEBIT = "*debit" RESET_COUNTER = "*reset_counter" RESET_COUNTERS = "*reset_counters" + CALL_URL = "*call_url" + UNLIMITED = "*unlimited" ) type actionTypeFunc func(*UserBalance, *Action) error @@ -81,12 +82,14 @@ func getActionFunc(typ string) (actionTypeFunc, bool) { return resetCounterAction, true case RESET_COUNTERS: return resetCountersAction, true + case CALL_URL: + return callUrl, true } return nil, false } func logAction(ub *UserBalance, a *Action) (err error) { - Logger.Info(fmt.Sprintf("%v %v %v", a.BalanceId, a.Units, a.MinuteBucket)) + Logger.Info(fmt.Sprintf("%v %v %v", a.BalanceId, a.Balance)) return } @@ -116,11 +119,7 @@ func resetPrepaidAction(ub *UserBalance, a *Action) (err error) { } func topupResetAction(ub *UserBalance, a *Action) (err error) { - if a.BalanceId == MINUTES { - ub.MinuteBuckets = make([]*MinuteBucket, 0) - } else { - ub.BalanceMap[a.BalanceId+a.Direction] = BalanceChain{&Balance{Value: 0}} // ToDo: can ub be empty here? - } + ub.BalanceMap[a.BalanceId+a.Direction] = BalanceChain{&Balance{Value: 0}} // ToDo: can ub be empty here? genericMakeNegative(a) genericDebit(ub, a) return @@ -143,7 +142,7 @@ func resetCounterAction(ub *UserBalance, a *Action) (err error) { ub.UnitCounters = append(ub.UnitCounters, uc) } if a.BalanceId == MINUTES { - uc.initMinuteBuckets(ub.ActionTriggers) + uc.initMinuteBalances(ub.ActionTriggers) } else { uc.Units = 0 } @@ -157,11 +156,8 @@ func resetCountersAction(ub *UserBalance, a *Action) (err error) { } func genericMakeNegative(a *Action) { - if a.Units > 0 { // only apply if not allready negative - a.Units = -a.Units - } - if a.MinuteBucket != nil && a.MinuteBucket.Seconds > 0 { - a.MinuteBucket.Seconds = -a.MinuteBucket.Seconds + if a.Balance != nil && a.Balance.Value > 0 { // only apply if not allready negative + a.Balance.Value = -a.Balance.Value } } @@ -169,11 +165,7 @@ func genericDebit(ub *UserBalance, a *Action) (err error) { if ub.BalanceMap == nil { ub.BalanceMap = make(map[string]BalanceChain) } - if a.BalanceId == MINUTES { - ub.debitMinuteBucket(a.MinuteBucket) - } else { - ub.debitBalanceAction(a) - } + ub.debitBalanceAction(a) return } @@ -181,11 +173,19 @@ func genericReset(ub *UserBalance) { for k, _ := range ub.BalanceMap { ub.BalanceMap[k] = BalanceChain{&Balance{Value: 0}} } - ub.MinuteBuckets = make([]*MinuteBucket, 0) ub.UnitCounters = make([]*UnitsCounter, 0) ub.resetActionTriggers(nil) } +func callUrl(ub *UserBalance, a *Action) error { + body, err := json.Marshal(ub) + if err != nil { + return err + } + _, err = http.Post(a.ExtraParameters, "application/json", bytes.NewBuffer(body)) + return err +} + // Structure to store actions according to weight type Actions []*Action diff --git a/engine/action_timing.go b/engine/action_timing.go index c2ec2d8db..9262bd71d 100644 --- a/engine/action_timing.go +++ b/engine/action_timing.go @@ -37,7 +37,7 @@ type ActionTiming struct { Id string // uniquely identify the timing Tag string // informative purpose only UserBalanceIds []string - Timing *Interval + Timing *RateInterval Weight float64 ActionsId string actions Actions @@ -219,10 +219,7 @@ func (at *ActionTiming) Execute() (err error) { return } for _, a := range aac { - a.ExpirationDate, _ = utils.ParseDate(a.ExpirationString) - if a.MinuteBucket != nil { - a.MinuteBucket.ExpirationDate = a.ExpirationDate - } + a.Balance.ExpirationDate, _ = utils.ParseDate(a.ExpirationString) actionFunction, exists := getActionFunc(a.ActionType) if !exists { Logger.Crit(fmt.Sprintf("Function type %v not available, aborting execution!", a.ActionType)) @@ -290,6 +287,6 @@ func (atpl ActionTimingPriotityList) Sort() { sort.Sort(atpl) } -func (at *ActionTiming) String() string { +func (at *ActionTiming) String_DISABLED() string { return at.Tag + " " + at.GetNextStartTime().String() + ",w: " + strconv.FormatFloat(at.Weight, 'f', -1, 64) } diff --git a/engine/action_trigger.go b/engine/action_trigger.go index 2add33bc1..e395bb6c7 100644 --- a/engine/action_trigger.go +++ b/engine/action_trigger.go @@ -19,6 +19,7 @@ along with this program. If not, see package engine import ( + "encoding/json" "fmt" "github.com/cgrates/cgrates/utils" "sort" @@ -28,7 +29,7 @@ type ActionTrigger struct { Id string // uniquely identify the trigger BalanceId string Direction string - ThresholdType string + ThresholdType string //*min_counter, *max_counter, *min_balance, *max_balance ThresholdValue float64 DestinationId string Weight float64 @@ -46,10 +47,10 @@ func (at *ActionTrigger) Execute(ub *UserBalance) (err error) { return } for _, a := range aac { - a.ExpirationDate, _ = utils.ParseDate(a.ExpirationString) - if a.MinuteBucket != nil { - a.MinuteBucket.ExpirationDate = a.ExpirationDate + if a.Balance == nil { + a.Balance = &Balance{} } + a.Balance.ExpirationDate, _ = utils.ParseDate(a.ExpirationString) actionFunction, exists := getActionFunc(a.ActionType) if !exists { Logger.Warning(fmt.Sprintf("Function type %v not available, aborting execution!", a.ActionType)) @@ -64,6 +65,27 @@ func (at *ActionTrigger) Execute(ub *UserBalance) (err error) { return } +// returns true if the field of the action timing are equeal to the non empty +// fields of the action +func (at *ActionTrigger) Match(a *Action) bool { + if a == nil { + return true + } + id := a.BalanceId == "" || at.BalanceId == a.BalanceId + direction := a.Direction == "" || at.Direction == a.Direction + thresholdType, thresholdValue := true, true + if a.ExtraParameters != "" { + t := struct { + ThresholdType string + ThresholdValue float64 + }{} + json.Unmarshal([]byte(a.ExtraParameters), &t) + thresholdType = t.ThresholdType == "" || at.ThresholdType == t.ThresholdType + thresholdValue = t.ThresholdValue == 0 || at.ThresholdValue == t.ThresholdValue + } + return id && direction && thresholdType && thresholdValue +} + // Structure to store actions according to weight type ActionTriggerPriotityList []*ActionTrigger diff --git a/engine/actions_test.go b/engine/actions_test.go index 964e49dec..ddddc2743 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -19,6 +19,7 @@ along with this program. If not, see package engine import ( + "fmt" "github.com/cgrates/cgrates/utils" "testing" "time" @@ -34,7 +35,7 @@ func TestActionTimingNothing(t *testing.T) { } func TestActionTimingOnlyHour(t *testing.T) { - at := &ActionTiming{Timing: &Interval{StartTime: "10:01:00"}} + at := &ActionTiming{Timing: &RateInterval{StartTime: "10:01:00"}} st := at.GetNextStartTime() now := time.Now() y, m, d := now.Date() @@ -45,7 +46,7 @@ func TestActionTimingOnlyHour(t *testing.T) { } func TestActionTimingOnlyWeekdays(t *testing.T) { - at := &ActionTiming{Timing: &Interval{WeekDays: []time.Weekday{time.Monday}}} + at := &ActionTiming{Timing: &RateInterval{WeekDays: []time.Weekday{time.Monday}}} st := at.GetNextStartTime() now := time.Now() y, m, d := now.Date() @@ -64,7 +65,7 @@ func TestActionTimingOnlyWeekdays(t *testing.T) { } func TestActionTimingHourWeekdays(t *testing.T) { - at := &ActionTiming{Timing: &Interval{WeekDays: []time.Weekday{time.Monday}, StartTime: "10:01:00"}} + at := &ActionTiming{Timing: &RateInterval{WeekDays: []time.Weekday{time.Monday}, StartTime: "10:01:00"}} st := at.GetNextStartTime() now := time.Now() y, m, d := now.Date() @@ -85,9 +86,9 @@ func TestActionTimingOnlyMonthdays(t *testing.T) { now := time.Now() y, m, d := now.Date() tomorrow := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 0, 1) - at := &ActionTiming{Timing: &Interval{MonthDays: MonthDays{1, 25, 2, tomorrow.Day()}}} + at := &ActionTiming{Timing: &RateInterval{MonthDays: MonthDays{1, 25, 2, tomorrow.Day()}}} st := at.GetNextStartTime() - expected := time.Date(y, m, tomorrow.Day(), 0, 0, 0, 0, time.Local) + expected := tomorrow if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } @@ -102,7 +103,7 @@ func TestActionTimingHourMonthdays(t *testing.T) { if now.After(testTime) { day = tomorrow.Day() } - at := &ActionTiming{Timing: &Interval{MonthDays: MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00"}} + at := &ActionTiming{Timing: &RateInterval{MonthDays: MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00"}} st := at.GetNextStartTime() expected := time.Date(y, m, day, 10, 1, 0, 0, time.Local) if !st.Equal(expected) { @@ -114,7 +115,7 @@ func TestActionTimingOnlyMonths(t *testing.T) { now := time.Now() y, m, d := now.Date() nextMonth := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 1, 0) - at := &ActionTiming{Timing: &Interval{Months: Months{time.February, time.May, nextMonth.Month()}}} + at := &ActionTiming{Timing: &RateInterval{Months: Months{time.February, time.May, nextMonth.Month()}}} st := at.GetNextStartTime() expected := time.Date(y, nextMonth.Month(), 1, 0, 0, 0, 0, time.Local) if !st.Equal(expected) { @@ -131,7 +132,7 @@ func TestActionTimingHourMonths(t *testing.T) { if now.After(testTime) { month = nextMonth.Month() } - at := &ActionTiming{Timing: &Interval{Months: Months{now.Month(), nextMonth.Month()}, StartTime: "10:01:00"}} + at := &ActionTiming{Timing: &RateInterval{Months: Months{now.Month(), nextMonth.Month()}, StartTime: "10:01:00"}} st := at.GetNextStartTime() expected := time.Date(y, month, d, 10, 1, 0, 0, time.Local) if !st.Equal(expected) { @@ -156,7 +157,7 @@ func TestActionTimingHourMonthdaysMonths(t *testing.T) { month = nextMonth.Month() } } - at := &ActionTiming{Timing: &Interval{ + at := &ActionTiming{Timing: &RateInterval{ Months: Months{now.Month(), nextMonth.Month()}, MonthDays: MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00", @@ -172,7 +173,7 @@ func TestActionTimingFirstOfTheMonth(t *testing.T) { now := time.Now() y, m, _ := now.Date() nextMonth := time.Date(y, m, 1, 0, 0, 0, 0, time.Local).AddDate(0, 1, 0) - at := &ActionTiming{Timing: &Interval{ + at := &ActionTiming{Timing: &RateInterval{ Months: Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: MonthDays{1}, }} @@ -187,7 +188,7 @@ func TestActionTimingOnlyYears(t *testing.T) { now := time.Now() y, m, d := now.Date() nextYear := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(1, 0, 0) - at := &ActionTiming{Timing: &Interval{Years: Years{now.Year(), nextYear.Year()}}} + at := &ActionTiming{Timing: &RateInterval{Years: Years{now.Year(), nextYear.Year()}}} st := at.GetNextStartTime() expected := time.Date(nextYear.Year(), 1, 1, 0, 0, 0, 0, time.Local) if !st.Equal(expected) { @@ -204,7 +205,7 @@ func TestActionTimingHourYears(t *testing.T) { if now.After(testTime) { year = nextYear.Year() } - at := &ActionTiming{Timing: &Interval{Years: Years{now.Year(), nextYear.Year()}, StartTime: "10:01:00"}} + at := &ActionTiming{Timing: &RateInterval{Years: Years{now.Year(), nextYear.Year()}, StartTime: "10:01:00"}} st := at.GetNextStartTime() expected := time.Date(year, m, d, 10, 1, 0, 0, time.Local) if !st.Equal(expected) { @@ -229,7 +230,7 @@ func TestActionTimingHourMonthdaysYear(t *testing.T) { year = nextYear.Year() } } - at := &ActionTiming{Timing: &Interval{ + at := &ActionTiming{Timing: &RateInterval{ Years: Years{now.Year(), nextYear.Year()}, MonthDays: MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00", @@ -266,7 +267,7 @@ func TestActionTimingHourMonthdaysMonthYear(t *testing.T) { year = nextYear.Year() } } - at := &ActionTiming{Timing: &Interval{ + at := &ActionTiming{Timing: &RateInterval{ Years: Years{now.Year(), nextYear.Year()}, Months: Months{now.Month(), nextMonth.Month()}, MonthDays: MonthDays{now.Day(), tomorrow.Day()}, @@ -283,7 +284,7 @@ func TestActionTimingFirstOfTheYear(t *testing.T) { now := time.Now() y, _, _ := now.Date() nextYear := time.Date(y, 1, 1, 0, 0, 0, 0, time.Local).AddDate(1, 0, 0) - at := &ActionTiming{Timing: &Interval{ + at := &ActionTiming{Timing: &RateInterval{ Years: Years{nextYear.Year()}, Months: Months{time.January}, MonthDays: MonthDays{1}, @@ -300,7 +301,7 @@ func TestActionTimingFirstMonthOfTheYear(t *testing.T) { now := time.Now() y, _, _ := now.Date() nextYear := time.Date(y, 1, 1, 0, 0, 0, 0, time.Local).AddDate(1, 0, 0) - at := &ActionTiming{Timing: &Interval{ + at := &ActionTiming{Timing: &RateInterval{ Months: Months{time.January}, }} st := at.GetNextStartTime() @@ -311,14 +312,14 @@ func TestActionTimingFirstMonthOfTheYear(t *testing.T) { } func TestActionTimingCheckForASAP(t *testing.T) { - at := &ActionTiming{Timing: &Interval{StartTime: ASAP}} + at := &ActionTiming{Timing: &RateInterval{StartTime: ASAP}} if !at.CheckForASAP() { t.Errorf("%v should be asap!", at) } } func TestActionTimingIsOneTimeRun(t *testing.T) { - at := &ActionTiming{Timing: &Interval{StartTime: ASAP}} + at := &ActionTiming{Timing: &RateInterval{StartTime: ASAP}} if !at.CheckForASAP() { t.Errorf("%v should be asap!", at) } @@ -328,7 +329,7 @@ func TestActionTimingIsOneTimeRun(t *testing.T) { } func TestActionTimingOneTimeRun(t *testing.T) { - at := &ActionTiming{Timing: &Interval{StartTime: ASAP}} + at := &ActionTiming{Timing: &RateInterval{StartTime: ASAP}} at.CheckForASAP() nextRun := at.GetNextStartTime() if nextRun.IsZero() { @@ -338,10 +339,9 @@ func TestActionTimingOneTimeRun(t *testing.T) { func TestActionTimingLogFunction(t *testing.T) { a := &Action{ - ActionType: "*log", - BalanceId: "test", - Units: 1.1, - MinuteBucket: &MinuteBucket{}, + ActionType: "*log", + BalanceId: "test", + Balance: &Balance{Value: 1.1}, } at := &ActionTiming{ actions: []*Action{a}, @@ -353,14 +353,14 @@ func TestActionTimingLogFunction(t *testing.T) { } func TestActionTimingPriotityListSortByWeight(t *testing.T) { - at1 := &ActionTiming{Timing: &Interval{ + at1 := &ActionTiming{Timing: &RateInterval{ Years: Years{2100}, Months: Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: MonthDays{1}, StartTime: "00:00:00", Weight: 20, }} - at2 := &ActionTiming{Timing: &Interval{ + at2 := &ActionTiming{Timing: &RateInterval{ Years: Years{2100}, Months: Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: MonthDays{2}, @@ -377,7 +377,7 @@ func TestActionTimingPriotityListSortByWeight(t *testing.T) { func TestActionTimingPriotityListWeight(t *testing.T) { at1 := &ActionTiming{ - Timing: &Interval{ + Timing: &RateInterval{ Months: Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: MonthDays{1}, StartTime: "00:00:00", @@ -385,7 +385,7 @@ func TestActionTimingPriotityListWeight(t *testing.T) { Weight: 10.0, } at2 := &ActionTiming{ - Timing: &Interval{ + Timing: &RateInterval{ Months: Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: MonthDays{1}, StartTime: "00:00:00", @@ -400,6 +400,110 @@ func TestActionTimingPriotityListWeight(t *testing.T) { } } +func TestActionTriggerMatchNil(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + var a *Action + if !at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + +func TestActionTriggerMatchAllBlank(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + a := &Action{} + if !at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + +func TestActionTriggerMatchMinuteBucketBlank(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + a := &Action{Direction: OUTBOUND, BalanceId: CREDIT} + if !at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + +func TestActionTriggerMatchMinuteBucketFull(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + a := &Action{ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v}`, TRIGGER_MAX_BALANCE, 2)} + if !at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + +func TestActionTriggerMatchAllFull(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + a := &Action{Direction: OUTBOUND, BalanceId: CREDIT, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v}`, TRIGGER_MAX_BALANCE, 2)} + if !at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + +func TestActionTriggerMatchSomeFalse(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + a := &Action{Direction: INBOUND, BalanceId: CREDIT, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v}`, TRIGGER_MAX_BALANCE, 2)} + if at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + +func TestActionTriggerMatcBalanceFalse(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + a := &Action{Direction: OUTBOUND, BalanceId: CREDIT, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v}`, TRIGGER_MAX_BALANCE, 3.0)} + if at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + +func TestActionTriggerMatcAllFalse(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + a := &Action{Direction: INBOUND, BalanceId: MINUTES, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v}`, TRIGGER_MAX_COUNTER, 3)} + if at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + func TestActionTriggerPriotityList(t *testing.T) { at1 := &ActionTrigger{Weight: 10} at2 := &ActionTrigger{Weight: 20} @@ -412,23 +516,11 @@ func TestActionTriggerPriotityList(t *testing.T) { } } -/*func TestActionLog(t *testing.T) { - a := &Action{ - ActionType: "TEST", - BalanceId: "BALANCE", - Units: 10, - Weight: 11, - MinuteBucket: &MinuteBucket{}, - } - logAction(nil, a) -}*/ - func TestActionResetTriggres(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 10}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 10}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } resetTriggersAction(ub, nil) @@ -453,9 +545,8 @@ func TestActionResetTriggresExecutesThem(t *testing.T) { func TestActionResetTriggresActionFilter(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 10}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 10}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } resetTriggersAction(ub, &Action{BalanceId: SMS}) @@ -468,9 +559,8 @@ func TestActionSetPostpaid(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } setPostpaidAction(ub, nil) @@ -483,9 +573,8 @@ func TestActionSetPrepaid(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } setPrepaidAction(ub, nil) @@ -498,17 +587,17 @@ func TestActionResetPrepaid(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: SMS, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: SMS, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } resetPrepaidAction(ub, nil) if ub.Type != UB_TYPE_PREPAID || ub.BalanceMap[CREDIT].GetTotalValue() != 0 || len(ub.UnitCounters) != 0 || - len(ub.MinuteBuckets) != 0 || + ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 0 || ub.ActionTriggers[0].Executed == true || ub.ActionTriggers[1].Executed == true { + t.Log(ub.BalanceMap) t.Error("Reset prepaid action failed!") } } @@ -517,16 +606,15 @@ func TestActionResetPostpaid(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: SMS, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: SMS, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } resetPostpaidAction(ub, nil) if ub.Type != UB_TYPE_POSTPAID || ub.BalanceMap[CREDIT].GetTotalValue() != 0 || len(ub.UnitCounters) != 0 || - len(ub.MinuteBuckets) != 0 || + ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 0 || ub.ActionTriggers[0].Executed == true || ub.ActionTriggers[1].Executed == true { t.Error("Reset postpaid action failed!") } @@ -536,17 +624,16 @@ func TestActionTopupResetCredit(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10} + a := &Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}} topupResetAction(ub, a) if ub.Type != UB_TYPE_PREPAID || ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() != 10 || len(ub.UnitCounters) != 1 || - len(ub.MinuteBuckets) != 2 || + len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Errorf("Topup reset action failed: %#v", ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue()) } @@ -554,22 +641,23 @@ func TestActionTopupResetCredit(t *testing.T) { func TestActionTopupResetMinutes(t *testing.T) { ub := &UserBalance{ - Id: "TEST_UB", - Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}}, + Id: "TEST_UB", + Type: UB_TYPE_PREPAID, + BalanceMap: map[string]BalanceChain{ + CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}, + MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceId: MINUTES, Direction: OUTBOUND, MinuteBucket: &MinuteBucket{Seconds: 5, Weight: 20, Price: 1, DestinationId: "NAT"}} + a := &Action{BalanceId: MINUTES, Direction: OUTBOUND, Balance: &Balance{Value: 5, Weight: 20, DestinationId: "NAT"}} topupResetAction(ub, a) if ub.Type != UB_TYPE_PREPAID || - ub.MinuteBuckets[0].Seconds != 5 || + ub.BalanceMap[MINUTES+OUTBOUND].GetTotalValue() != 5 || ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() != 100 || len(ub.UnitCounters) != 1 || - len(ub.MinuteBuckets) != 1 || + len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Error("Topup reset minutes action failed!", ub.MinuteBuckets[0]) + t.Errorf("Topup reset minutes action failed: %v", ub.BalanceMap[MINUTES+OUTBOUND][0]) } } @@ -577,17 +665,16 @@ func TestActionTopupCredit(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10} + a := &Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}} topupAction(ub, a) if ub.Type != UB_TYPE_PREPAID || ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() != 110 || len(ub.UnitCounters) != 1 || - len(ub.MinuteBuckets) != 2 || + len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Topup action failed!", ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue()) } @@ -597,20 +684,19 @@ func TestActionTopupMinutes(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceId: MINUTES, MinuteBucket: &MinuteBucket{Seconds: 5, Weight: 20, Price: 1, DestinationId: "NAT"}} + a := &Action{BalanceId: MINUTES, Direction: OUTBOUND, Balance: &Balance{Value: 5, Weight: 20, DestinationId: "NAT"}} topupAction(ub, a) if ub.Type != UB_TYPE_PREPAID || - ub.MinuteBuckets[0].Seconds != 15 || + ub.BalanceMap[MINUTES+OUTBOUND].GetTotalValue() != 15 || ub.BalanceMap[CREDIT].GetTotalValue() != 100 || len(ub.UnitCounters) != 1 || - len(ub.MinuteBuckets) != 2 || + len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Error("Topup minutes action failed!", ub.MinuteBuckets[0]) + t.Error("Topup minutes action failed!", ub.BalanceMap[MINUTES+OUTBOUND]) } } @@ -618,17 +704,16 @@ func TestActionDebitCredit(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10} + a := &Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}} debitAction(ub, a) if ub.Type != UB_TYPE_PREPAID || ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() != 90 || len(ub.UnitCounters) != 1 || - len(ub.MinuteBuckets) != 2 || + len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Debit action failed!", ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue()) } @@ -638,57 +723,60 @@ func TestActionDebitMinutes(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}, &ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } - a := &Action{BalanceId: MINUTES, MinuteBucket: &MinuteBucket{Seconds: 5, Weight: 20, Price: 1, DestinationId: "NAT"}} + a := &Action{BalanceId: MINUTES, Direction: OUTBOUND, Balance: &Balance{Value: 5, Weight: 20, DestinationId: "NAT"}} debitAction(ub, a) if ub.Type != UB_TYPE_PREPAID || - ub.MinuteBuckets[0].Seconds != 5 || + ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 5 || ub.BalanceMap[CREDIT].GetTotalValue() != 100 || len(ub.UnitCounters) != 1 || - len(ub.MinuteBuckets) != 2 || + len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Error("Debit minutes action failed!", ub.MinuteBuckets[0]) + t.Error("Debit minutes action failed!", ub.BalanceMap[MINUTES+OUTBOUND][0]) } } func TestActionResetAllCounters(t *testing.T) { ub := &UserBalance{ - Id: "TEST_UB", - Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + Id: "TEST_UB", + Type: UB_TYPE_POSTPAID, + BalanceMap: map[string]BalanceChain{ + CREDIT: BalanceChain{&Balance{Value: 100}}, + MINUTES: BalanceChain{ + &Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, + &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } resetCountersAction(ub, nil) if ub.Type != UB_TYPE_POSTPAID || ub.BalanceMap[CREDIT].GetTotalValue() != 100 || len(ub.UnitCounters) != 1 || - len(ub.UnitCounters[0].MinuteBuckets) != 1 || - len(ub.MinuteBuckets) != 2 || + len(ub.UnitCounters[0].MinuteBalances) != 1 || + len(ub.BalanceMap[MINUTES]) != 2 || ub.ActionTriggers[0].Executed != true { - t.Error("Reset counters action failed!") + t.Error("Reset counters action failed: ", ub.BalanceMap[MINUTES]) } if len(ub.UnitCounters) < 1 { t.FailNow() } - mb := ub.UnitCounters[0].MinuteBuckets[0] - if mb.Weight != 20 || mb.Price != 1 || mb.Seconds != 10 || mb.DestinationId != "NAT" { - t.Errorf("Minute bucked cloned incorrectly: %v!", mb) + mb := ub.UnitCounters[0].MinuteBalances[0] + if mb.Weight != 20 || mb.Value != 0 || mb.DestinationId != "NAT" { + t.Errorf("Balance cloned incorrectly: %v!", mb) } } func TestActionResetCounterMinutes(t *testing.T) { ub := &UserBalance{ - Id: "TEST_UB", - Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + Id: "TEST_UB", + Type: UB_TYPE_POSTPAID, + BalanceMap: map[string]BalanceChain{ + CREDIT: BalanceChain{&Balance{Value: 100}}, + MINUTES: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } a := &Action{BalanceId: MINUTES} @@ -696,16 +784,16 @@ func TestActionResetCounterMinutes(t *testing.T) { if ub.Type != UB_TYPE_POSTPAID || ub.BalanceMap[CREDIT].GetTotalValue() != 100 || len(ub.UnitCounters) != 2 || - len(ub.UnitCounters[1].MinuteBuckets) != 1 || - len(ub.MinuteBuckets) != 2 || + len(ub.UnitCounters[1].MinuteBalances) != 1 || + len(ub.BalanceMap[MINUTES]) != 2 || ub.ActionTriggers[0].Executed != true { - t.Error("Reset counters action failed!", ub.UnitCounters[1].MinuteBuckets) + t.Error("Reset counters action failed!", ub.UnitCounters[1].MinuteBalances) } - if len(ub.UnitCounters) < 2 || len(ub.UnitCounters[1].MinuteBuckets) < 1 { + if len(ub.UnitCounters) < 2 || len(ub.UnitCounters[1].MinuteBalances) < 1 { t.FailNow() } - mb := ub.UnitCounters[1].MinuteBuckets[0] - if mb.Weight != 20 || mb.Price != 1 || mb.Seconds != 10 || mb.DestinationId != "NAT" { + mb := ub.UnitCounters[1].MinuteBalances[0] + if mb.Weight != 20 || mb.Value != 0 || mb.DestinationId != "NAT" { t.Errorf("Minute bucked cloned incorrectly: %v!", mb) } } @@ -714,9 +802,8 @@ func TestActionResetCounterCREDIT(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}, &UnitsCounter{BalanceId: SMS, Direction: OUTBOUND, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ActionsId: "TEST_ACTIONS", Executed: true}}, } a := &Action{BalanceId: CREDIT, Direction: OUTBOUND} @@ -724,7 +811,7 @@ func TestActionResetCounterCREDIT(t *testing.T) { if ub.Type != UB_TYPE_POSTPAID || ub.BalanceMap[CREDIT].GetTotalValue() != 100 || len(ub.UnitCounters) != 2 || - len(ub.MinuteBuckets) != 2 || + len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.ActionTriggers[0].Executed != true { t.Error("Reset counters action failed!", ub.UnitCounters) } @@ -742,9 +829,9 @@ func TestActionTriggerLogging(t *testing.T) { } as, err := storageGetter.GetActions(at.ActionsId) if err != nil { - t.Error("Error getting actions for the action timing: ", err) + t.Error("Error getting actions for the action timing: ", as, err) } - storageGetter.LogActionTrigger("rif", RATER_SOURCE, at, as) + storageLogger.LogActionTrigger("rif", RATER_SOURCE, at, as) //expected := "rif*some_uuid;MONETARY;OUT;NAT;TEST_ACTIONS;100;10;false*|TOPUP|MONETARY|OUT|10|0" var key string atMap, _ := storageGetter.GetAllActionTimings() @@ -762,7 +849,7 @@ func TestActionTriggerLogging(t *testing.T) { } func TestActionTimingLogging(t *testing.T) { - i := &Interval{ + i := &RateInterval{ Months: Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: MonthDays{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}, WeekDays: WeekDays{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday}, @@ -770,7 +857,7 @@ func TestActionTimingLogging(t *testing.T) { EndTime: "00:00:00", Weight: 10.0, ConnectFee: 0.0, - Prices: PriceGroups{&Price{0, 1.0, 1 * time.Second, 60 * time.Second}}, + Rates: RateGroups{&Rate{0, 1.0, 1 * time.Second, 60 * time.Second}}, } at := &ActionTiming{ Id: "some uuid", @@ -784,7 +871,7 @@ func TestActionTimingLogging(t *testing.T) { if err != nil { t.Error("Error getting actions for the action trigger: ", err) } - storageGetter.LogActionTiming(SCHED_SOURCE, at, as) + storageLogger.LogActionTiming(SCHED_SOURCE, at, as) //expected := "some uuid|test|one,two,three|;1,2,3,4,5,6,7,8,9,10,11,12;1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;1,2,3,4,5;18:00:00;00:00:00;10;0;1;60;1|10|TEST_ACTIONS*|TOPUP|MONETARY|OUT|10|0" var key string atMap, _ := storageGetter.GetAllActionTimings() @@ -801,13 +888,13 @@ func TestActionTimingLogging(t *testing.T) { } func TestActionMakeNegative(t *testing.T) { - a := &Action{Units: 10} + a := &Action{Balance: &Balance{Value: 10}} genericMakeNegative(a) - if a.Units > 0 { + if a.Balance.Value > 0 { t.Error("Failed to make negative: ", a) } genericMakeNegative(a) - if a.Units > 0 { + if a.Balance.Value > 0 { t.Error("Failed to preserve negative: ", a) } } diff --git a/engine/balances.go b/engine/balances.go new file mode 100644 index 000000000..88658ad00 --- /dev/null +++ b/engine/balances.go @@ -0,0 +1,169 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2013 ITsysCOM + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package engine + +import ( + "fmt" + "math" + "sort" + "time" +) + +// Can hold different units as seconds or monetary +type Balance struct { + Uuid string + Value float64 + ExpirationDate time.Time + Weight float64 + GroupIds []string + //SpecialPriceType string + //SpecialPrice float64 // absolute for minutes and percent for monetary (can be positive or negative) + DestinationId string + RateSubject string + precision int +} + +func (b *Balance) Equal(o *Balance) bool { + return b.ExpirationDate.Equal(o.ExpirationDate) && + b.Weight == o.Weight && + b.DestinationId == o.DestinationId && + b.RateSubject == o.RateSubject +} + +func (b *Balance) IsExpired() bool { + return !b.ExpirationDate.IsZero() && b.ExpirationDate.Before(time.Now()) +} + +func (b *Balance) Clone() *Balance { + return &Balance{ + Uuid: b.Uuid, + Value: b.Value, + DestinationId: b.DestinationId, + ExpirationDate: b.ExpirationDate, + Weight: b.Weight, + RateSubject: b.RateSubject, + } +} + +// Returns the available number of seconds for a specified credit +func (b *Balance) GetSecondsForCredit(cd *CallDescriptor, credit float64) (seconds float64) { + seconds = b.Value + cc, err := b.GetCost(cd) + if err != nil { + Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err)) + return 0 + } + if cc.Cost > 0 { + secondCost := cc.Cost / cc.GetDuration().Seconds() + // TODO: this is not very accurate + // we should iterate timespans and increment to get exact number of minutes for + // available credit + seconds = math.Min(credit/secondCost, b.Value) + } + return +} + +func (b *Balance) GetCost(cd *CallDescriptor) (*CallCost, error) { + if b.RateSubject != "" { + cd.Subject = b.RateSubject + cd.Account = cd.Subject + return cd.GetCost() + } + cc := cd.CreateCallCost() + cc.Cost = 0 + return cc, nil +} + +/* +Structure to store minute buckets according to weight, precision or price. +*/ +type BalanceChain []*Balance + +func (bc BalanceChain) Len() int { + return len(bc) +} + +func (bc BalanceChain) Swap(i, j int) { + bc[i], bc[j] = bc[j], bc[i] +} + +func (bc BalanceChain) Less(j, i int) bool { + return bc[i].Weight < bc[j].Weight || + bc[i].precision < bc[j].precision +} + +func (bc BalanceChain) Sort() { + sort.Sort(bc) +} + +func (bc BalanceChain) GetTotalValue() (total float64) { + for _, b := range bc { + if !b.IsExpired() { + total += b.Value + } + } + return +} + +func (bc BalanceChain) Debit(amount float64) float64 { + bc.Sort() + for i, b := range bc { + if b.IsExpired() { + continue + } + if b.Value >= amount || i == len(bc)-1 { // if last one go negative + b.Value -= amount + break + } + b.Value = 0 + amount -= b.Value + } + return bc.GetTotalValue() +} + +func (bc BalanceChain) Equal(o BalanceChain) bool { + if len(bc) != len(o) { + return false + } + bc.Sort() + o.Sort() + for i := 0; i < len(bc); i++ { + if !bc[i].Equal(o[i]) { + return false + } + } + return true +} + +func (bc BalanceChain) Clone() BalanceChain { + var newChain BalanceChain + for _, b := range bc { + newChain = append(newChain, b.Clone()) + } + return newChain +} + +func (bc BalanceChain) GetBalance(uuid string) *Balance { + for _, balance := range bc { + if balance.Uuid == uuid { + return balance + } + } + return nil +} diff --git a/engine/balances_test.go b/engine/balances_test.go new file mode 100644 index 000000000..f929834a5 --- /dev/null +++ b/engine/balances_test.go @@ -0,0 +1,63 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2013 ITsysCOM + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package engine + +import ( + "reflect" + "testing" +) + +func TestBalanceSortWeight(t *testing.T) { + mb1 := &Balance{Weight: 1, precision: 2} + mb2 := &Balance{Weight: 2, precision: 1} + var bs BalanceChain + bs = append(bs, mb2, mb1) + bs.Sort() + if bs[0] != mb1 || bs[1] != mb2 { + t.Error("Buckets not sorted by weight!") + } +} + +func TestBalanceSortPrecision(t *testing.T) { + mb1 := &Balance{Weight: 1, precision: 2} + mb2 := &Balance{Weight: 1, precision: 1} + var bs BalanceChain + bs = append(bs, mb2, mb1) + bs.Sort() + if bs[0] != mb1 || bs[1] != mb2 { + t.Error("Buckets not sorted by precision!") + } +} + +func TestBalanceEqual(t *testing.T) { + mb1 := &Balance{Weight: 1, precision: 1, RateSubject: "1", DestinationId: ""} + mb2 := &Balance{Weight: 1, precision: 1, RateSubject: "1", DestinationId: ""} + mb3 := &Balance{Weight: 1, precision: 1, RateSubject: "2", DestinationId: ""} + if !mb1.Equal(mb2) || mb2.Equal(mb3) { + t.Error("Equal failure!", mb1 == mb2, mb3) + } +} + +func TestBalanceClone(t *testing.T) { + mb1 := &Balance{Value: 1, Weight: 2, RateSubject: "test", DestinationId: "5"} + mb2 := mb1.Clone() + if mb1 == mb2 || !reflect.DeepEqual(mb1, mb2) { + t.Errorf("Cloning failure: \n%v\n%v", mb1, mb2) + } +} diff --git a/engine/callcost.go b/engine/callcost.go index 367789cd4..3ac52e29a 100644 --- a/engine/callcost.go +++ b/engine/callcost.go @@ -47,8 +47,8 @@ func (cc *CallCost) Merge(other *CallCost) { } ts := cc.Timespans[len(cc.Timespans)-1] otherTs := other.Timespans[0] - if reflect.DeepEqual(ts.ActivationPeriod, otherTs.ActivationPeriod) && - reflect.DeepEqual(ts.MinuteInfo, otherTs.MinuteInfo) && reflect.DeepEqual(ts.Interval, otherTs.Interval) { + if reflect.DeepEqual(ts.RatingPlan, otherTs.RatingPlan) && + reflect.DeepEqual(ts.RateInterval, otherTs.RateInterval) { // extend the last timespan with ts.TimeEnd = ts.TimeEnd.Add(otherTs.GetDuration()) // add the rest of the timspans @@ -66,3 +66,22 @@ func (cc *CallCost) GetStartTime() time.Time { } return cc.Timespans[0].TimeStart } + +func (cc *CallCost) GetDuration() (td time.Duration) { + for _, ts := range cc.Timespans { + td += ts.GetDuration() + } + return +} + +// Creates a CallDescriptor structure copying related data from CallCost +func (cc *CallCost) CreateCallDescriptor() *CallDescriptor { + return &CallDescriptor{ + Direction: cc.Direction, + TOR: cc.TOR, + Tenant: cc.Tenant, + Subject: cc.Subject, + Account: cc.Account, + Destination: cc.Destination, + } +} diff --git a/engine/callcost_test.go b/engine/callcost_test.go index 2aeb77415..afd85e8b6 100644 --- a/engine/callcost_test.go +++ b/engine/callcost_test.go @@ -56,7 +56,7 @@ func TestMultipleResultMerge(t *testing.T) { if cc1.Cost != 60 { t.Errorf("expected 60 was %v", cc1.Cost) for _, ts := range cc1.Timespans { - t.Log(ts.Interval) + t.Log(ts.RateInterval) } } t1 = time.Date(2012, time.February, 2, 18, 00, 0, 0, time.UTC) @@ -66,7 +66,7 @@ func TestMultipleResultMerge(t *testing.T) { if cc2.Cost != 30 { t.Errorf("expected 30 was %v", cc2.Cost) for _, ts := range cc1.Timespans { - t.Log(ts.Interval) + t.Log(ts.RateInterval) } } cc1.Merge(cc2) @@ -86,7 +86,7 @@ func TestMultipleInputLeftMerge(t *testing.T) { if cc1.Cost != 90 { t.Errorf("expected 90 was %v", cc1.Cost) } - t1 = time.Date(2012, time.February, 2, 18, 01, 0, 0, time.UTC) + /*t1 = time.Date(2012, time.February, 2, 18, 01, 0, 0, time.UTC) t2 = time.Date(2012, time.February, 2, 18, 02, 0, 0, time.UTC) cd = &CallDescriptor{Direction: OUTBOUND, TOR: "0", Tenant: "vdf", Subject: "rif", Destination: "0256", TimeStart: t1, TimeEnd: t2} cc2, _ := cd.GetCost() @@ -99,7 +99,7 @@ func TestMultipleInputLeftMerge(t *testing.T) { } if cc1.Cost != 120 { t.Errorf("Exdpected 120 was %v", cc1.Cost) - } + }*/ } func TestMultipleInputRightMerge(t *testing.T) { @@ -125,3 +125,28 @@ func TestMultipleInputRightMerge(t *testing.T) { t.Errorf("Exdpected 150 was %v", cc1.Cost) } } + +func TestCallCostGetDurationZero(t *testing.T) { + cc := &CallCost{} + if cc.GetDuration().Seconds() != 0 { + t.Error("Wrong call cost duration for zero timespans: ", cc.GetDuration()) + } +} + +func TestCallCostGetDuration(t *testing.T) { + cc := &CallCost{ + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 13, 40, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 13, 41, 0, 0, time.UTC), + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 13, 41, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 13, 41, 30, 0, time.UTC), + }, + }, + } + if cc.GetDuration().Seconds() != 90 { + t.Error("Wrong call cost duration: ", cc.GetDuration()) + } +} diff --git a/engine/calldesc.go b/engine/calldesc.go index 26904dc62..80b765f63 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -36,6 +36,16 @@ func init() { Logger = new(utils.StdLogger) Logger.Err(fmt.Sprintf("Could not connect to syslog: %v", err)) } + //db_server := "127.0.0.1" + //db_server := "192.168.0.17" + m, _ := NewMapStorage() + //m, _ := NewMongoStorage(db_server, "27017", "cgrates_test", "", "") + //m, _ := NewRedisStorage(db_server+":6379", 11, "") + //m, _ := NewRedigoStorage(db_server+":6379", 11, "") + //m, _ := NewRadixStorage(db_server+":6379", 11, "") + storageGetter, _ = m.(DataStorage) + + storageLogger = storageGetter.(LogStorage) } const ( @@ -45,13 +55,9 @@ const ( ) var ( - Logger utils.LoggerInterface - db_server = "127.0.0.1" - //db_server = "192.168.0.17" - storageGetter, _ = NewMapStorage() - //storageGetter, _ = NewMongoStorage(db_server, "27017", "cgrates_test", "", "") - //storageGetter, _ = NewRedisStorage(db_server+":6379", 11, "") - storageLogger = storageGetter + Logger utils.LoggerInterface + storageGetter DataStorage + storageLogger LogStorage debitPeriod = 10 * time.Second roundingMethod = "*middle" roundingDecimals = 4 @@ -73,7 +79,7 @@ func SetRoundingMethodAndDecimals(rm string, rd int) { /* Sets the database for logging (can be de same as storage getter or different db) */ -func SetStorageLogger(sg DataStorage) { +func SetStorageLogger(sg LogStorage) { storageLogger = sg } @@ -97,17 +103,18 @@ type CallDescriptor struct { TOR string Tenant, Subject, Account, Destination string TimeStart, TimeEnd time.Time - LoopIndex float64 // indicates the postion of this segment in a cost request loop - CallDuration time.Duration // the call duration so far (partial or final) + LoopIndex float64 // indicates the position of this segment in a cost request loop + CallDuration time.Duration // the call duration so far (till TimeEnd) Amount float64 FallbackSubject string // the subject to check for destination if not found on primary subject - ActivationPeriods []*ActivationPeriod + RatingPlans []*RatingPlan + Increments Increments userBalance *UserBalance } // Adds an activation period that applyes to current call descriptor. -func (cd *CallDescriptor) AddActivationPeriod(aps ...*ActivationPeriod) { - cd.ActivationPeriods = append(cd.ActivationPeriods, aps...) +func (cd *CallDescriptor) AddRatingPlan(aps ...*RatingPlan) { + cd.RatingPlans = append(cd.RatingPlans, aps...) } // Returns the key used to retrive the user balance involved in this call @@ -130,28 +137,28 @@ func (cd *CallDescriptor) getUserBalance() (ub *UserBalance, err error) { /* Restores the activation periods for the specified prefix from storage. */ -func (cd *CallDescriptor) LoadActivationPeriods() (destPrefix string, err error) { +func (cd *CallDescriptor) LoadRatingPlans() (destPrefix string, err error) { if val, err := cache2go.GetXCached(cd.GetKey() + cd.Destination); err == nil { - xaps := val.(xCachedActivationPeriods) - cd.ActivationPeriods = xaps.aps + xaps := val.(xCachedRatingPlans) + cd.RatingPlans = xaps.aps return xaps.destPrefix, nil } - destPrefix, values, err := cd.getActivationPeriodsForPrefix(cd.GetKey(), 1) + destPrefix, values, err := cd.getRatingPlansForPrefix(cd.GetKey(), 1) if err != nil { fallbackKey := fmt.Sprintf("%s:%s:%s:%s", cd.Direction, cd.Tenant, cd.TOR, FALLBACK_SUBJECT) // use the default subject - destPrefix, values, err = cd.getActivationPeriodsForPrefix(fallbackKey, 1) + destPrefix, values, err = cd.getRatingPlansForPrefix(fallbackKey, 1) } //load the activation preriods if err == nil && len(values) > 0 { - xaps := xCachedActivationPeriods{destPrefix, values, new(cache2go.XEntry)} + xaps := xCachedRatingPlans{destPrefix, values, new(cache2go.XEntry)} xaps.XCache(cd.GetKey()+cd.Destination, debitPeriod+5*time.Second, xaps) - cd.ActivationPeriods = values + cd.RatingPlans = values } return } -func (cd *CallDescriptor) getActivationPeriodsForPrefix(key string, recursionDepth int) (foundPrefix string, aps []*ActivationPeriod, err error) { +func (cd *CallDescriptor) getRatingPlansForPrefix(key string, recursionDepth int) (foundPrefix string, aps []*RatingPlan, err error) { if recursionDepth > RECURSION_MAX_DEPTH { err = errors.New("Max fallback recursion depth reached!" + key) return @@ -160,12 +167,12 @@ func (cd *CallDescriptor) getActivationPeriodsForPrefix(key string, recursionDep if err != nil { return "", nil, err } - foundPrefix, aps, err = rp.GetActivationPeriodsForPrefix(cd.Destination) + foundPrefix, aps, err = rp.GetRatingPlansForPrefix(cd.Destination) if err != nil { if rp.FallbackKey != "" { recursionDepth++ for _, fbk := range strings.Split(rp.FallbackKey, FALLBACK_SEP) { - if destPrefix, values, err := cd.getActivationPeriodsForPrefix(fbk, recursionDepth); err == nil { + if destPrefix, values, err := cd.getRatingPlansForPrefix(fbk, recursionDepth); err == nil { return destPrefix, values, err } } @@ -191,43 +198,20 @@ func (cd *CallDescriptor) splitInTimeSpans(firstSpan *TimeSpan) (timespans []*Ti firstSpan = &TimeSpan{TimeStart: cd.TimeStart, TimeEnd: cd.TimeEnd, CallDuration: cd.CallDuration} } timespans = append(timespans, firstSpan) - // split on (free) minute buckets - if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil { - _, _, bucketList := userBalance.getSecondsForPrefix(cd.Destination) - for _, mb := range bucketList { - for i := 0; i < len(timespans); i++ { - if timespans[i].MinuteInfo != nil { - continue - } - newTs := timespans[i].SplitByMinuteBucket(mb) - if newTs != nil { - timespans = append(timespans, newTs) - firstSpan = newTs // we move the firstspan to the newly created one for further spliting - break - } - } - } - } - if firstSpan.MinuteInfo != nil { - return // all the timespans are on minutes - } - if len(cd.ActivationPeriods) == 0 { + if len(cd.RatingPlans) == 0 { return } - firstSpan.ActivationPeriod = cd.ActivationPeriods[0] + firstSpan.RatingPlan = cd.RatingPlans[0] // split on activation periods afterStart, afterEnd := false, false //optimization for multiple activation periods - for _, ap := range cd.ActivationPeriods { + for _, ap := range cd.RatingPlans { if !afterStart && !afterEnd && ap.ActivationTime.Before(cd.TimeStart) { - firstSpan.ActivationPeriod = ap + firstSpan.RatingPlan = ap } else { afterStart = true for i := 0; i < len(timespans); i++ { - if timespans[i].MinuteInfo != nil { - continue - } - newTs := timespans[i].SplitByActivationPeriod(ap) + newTs := timespans[i].SplitByRatingPlan(ap) if newTs != nil { timespans = append(timespans, newTs) } else { @@ -239,31 +223,68 @@ func (cd *CallDescriptor) splitInTimeSpans(firstSpan *TimeSpan) (timespans []*Ti } // split on price intervals for i := 0; i < len(timespans); i++ { - if timespans[i].MinuteInfo != nil { - continue // cont try to split timespans payed with minutes - } - ap := timespans[i].ActivationPeriod - //timespans[i].ActivationPeriod = nil - ap.Intervals.Sort() - for _, interval := range ap.Intervals { - if timespans[i].Interval != nil && timespans[i].Interval.Weight < interval.Weight { + ap := timespans[i].RatingPlan + //timespans[i].RatingPlan = nil + ap.RateIntervals.Sort() + for _, interval := range ap.RateIntervals { + if timespans[i].RateInterval != nil && timespans[i].RateInterval.Weight < interval.Weight { continue // if the timespan has an interval than it already has a heigher weight } - newTs := timespans[i].SplitByInterval(interval) + newTs := timespans[i].SplitByRateInterval(interval) if newTs != nil { - newTs.ActivationPeriod = ap + newTs.RatingPlan = ap timespans = append(timespans, newTs) } } } + timespans = cd.roundTimeSpansToIncrement(timespans) return } +// if the rate interval for any timespan has a RatingIncrement larger than the timespan duration +// the timespan must expand potentially overlaping folowing timespans and may exceed call +// descriptor's initial duration +func (cd *CallDescriptor) roundTimeSpansToIncrement(timespans []*TimeSpan) []*TimeSpan { + for i, ts := range timespans { + if ts.RateInterval != nil { + _, rateIncrement, _ := ts.RateInterval.GetRateParameters(ts.GetGroupStart()) + // if the timespan duration is larger than the rate increment make sure it is a multiple of it + if rateIncrement < ts.GetDuration() { + rateIncrement = utils.RoundTo(rateIncrement, ts.GetDuration()) + } + if rateIncrement > ts.GetDuration() { + initialDuration := ts.GetDuration() + ts.TimeEnd = ts.TimeStart.Add(rateIncrement) + ts.CallDuration = ts.CallDuration + (rateIncrement - initialDuration) + + // overlap the rest of the timespans + i += 1 + for ; i < len(timespans); i++ { + if timespans[i].TimeEnd.Before(ts.TimeEnd) || timespans[i].TimeEnd.Equal(ts.TimeEnd) { + timespans[i].overlapped = true + } else if timespans[i].TimeStart.Before(ts.TimeEnd) { + timespans[i].TimeStart = ts.TimeEnd + } + } + break + } + } + } + var newTimespans []*TimeSpan + // remove overlapped + for _, ts := range timespans { + if !ts.overlapped { + newTimespans = append(newTimespans, ts) + } + } + return newTimespans +} + /* Creates a CallCost structure with the cost information calculated for the received CallDescriptor. */ func (cd *CallDescriptor) GetCost() (*CallCost, error) { - destPrefix, err := cd.LoadActivationPeriods() + destPrefix, err := cd.LoadRatingPlans() if err != nil { Logger.Err(fmt.Sprintf("error getting cost for key %v: %v", cd.GetUserBalanceKey(), err)) return &CallCost{Cost: -1}, err @@ -274,11 +295,12 @@ func (cd *CallDescriptor) GetCost() (*CallCost, error) { for i, ts := range timespans { // only add connect fee if this is the first/only call cost request - if cd.LoopIndex == 0 && i == 0 && ts.MinuteInfo == nil && ts.Interval != nil { - connectionFee = ts.Interval.ConnectFee + if cd.LoopIndex == 0 && i == 0 && ts.RateInterval != nil { + connectionFee = ts.RateInterval.ConnectFee } - cost += ts.getCost(cd) + cost += ts.getCost() } + // global rounding cost = utils.Round(cost, roundingDecimals, roundingMethod) cc := &CallCost{ Direction: cd.Direction, @@ -301,7 +323,7 @@ If the user has no credit then it will return 0. If the user has postpayed plan it returns -1. */ func (cd *CallDescriptor) GetMaxSessionTime(startTime time.Time) (seconds float64, err error) { - _, err = cd.LoadActivationPeriods() + _, err = cd.LoadRatingPlans() if err != nil { Logger.Err(fmt.Sprintf("error getting cost for key %v: %v", cd.GetUserBalanceKey(), err)) return 0, err @@ -312,7 +334,7 @@ func (cd *CallDescriptor) GetMaxSessionTime(startTime time.Time) (seconds float6 if userBalance.Type == UB_TYPE_POSTPAID { return -1, nil } else { - availableSeconds, availableCredit, _ = userBalance.getSecondsForPrefix(cd.Destination) + availableSeconds, availableCredit, _ = userBalance.getSecondsForPrefix(cd) Logger.Debug(fmt.Sprintf("available sec: %v credit: %v", availableSeconds, availableCredit)) } } else { @@ -333,10 +355,10 @@ func (cd *CallDescriptor) GetMaxSessionTime(startTime time.Time) (seconds float6 cost := 0.0 for i, ts := range timespans { - if i == 0 && ts.MinuteInfo == nil && ts.Interval != nil { - cost += ts.Interval.ConnectFee + if i == 0 && ts.RateInterval != nil { + cost += ts.RateInterval.ConnectFee } - cost += ts.getCost(cd) + cost += ts.Cost } //logger.Print(availableCredit, availableSeconds, cost) if cost < availableCredit { @@ -365,12 +387,7 @@ func (cd *CallDescriptor) Debit() (cc *CallCost, err error) { Logger.Debug(fmt.Sprintf(" Attempting to debit from %v, value: %v", cd.GetUserBalanceKey(), cc.Cost+cc.ConnectFee)) defer storageGetter.SetUserBalance(userBalance) if cc.Cost != 0 || cc.ConnectFee != 0 { - userBalance.debitBalance(CREDIT, cc.Cost+cc.ConnectFee, true) - } - for _, ts := range cc.Timespans { - if ts.MinuteInfo != nil { - userBalance.debitMinutesBalance(ts.MinuteInfo.Quantity, cd.Destination, true) - } + userBalance.debitCreditBalance(cc, true) } } return @@ -387,12 +404,19 @@ func (cd *CallDescriptor) MaxDebit(startTime time.Time) (cc *CallCost, err error return new(CallCost), errors.New("no more credit") } if remainingSeconds > 0 { // for postpaying client returns -1 - rs, _ := time.ParseDuration(fmt.Sprintf("%vs", remainingSeconds)) - cd.TimeEnd = cd.TimeStart.Add(rs) + cd.TimeEnd = cd.TimeStart.Add(time.Duration(remainingSeconds) * time.Second) } return cd.Debit() } +func (cd *CallDescriptor) RefoundIncrements() (left float64, err error) { + if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil { + defer storageGetter.SetUserBalance(userBalance) + userBalance.refoundIncrements(cd.Increments, true) + } + return 0.0, err +} + /* Interface method used to add/substract an amount of cents from user's money balance. The amount filed has to be filled in call descriptor. @@ -400,7 +424,7 @@ The amount filed has to be filled in call descriptor. func (cd *CallDescriptor) DebitCents() (left float64, err error) { if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil { defer storageGetter.SetUserBalance(userBalance) - return userBalance.debitBalance(CREDIT, cd.Amount, true), nil + return userBalance.debitGenericBalance(CREDIT+OUTBOUND, cd.Amount, true), nil } return 0.0, err } @@ -412,7 +436,7 @@ The amount filed has to be filled in call descriptor. func (cd *CallDescriptor) DebitSMS() (left float64, err error) { if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil { defer storageGetter.SetUserBalance(userBalance) - return userBalance.debitBalance(SMS, cd.Amount, true), nil + return userBalance.debitGenericBalance(SMS+OUTBOUND, cd.Amount, true), nil } return 0, err } @@ -424,7 +448,7 @@ The amount filed has to be filled in call descriptor. func (cd *CallDescriptor) DebitSeconds() (err error) { if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil { defer storageGetter.SetUserBalance(userBalance) - return userBalance.debitMinutesBalance(cd.Amount, cd.Destination, true) + return userBalance.debitCreditBalance(cd.CreateCallCost(), true) } return err } @@ -438,8 +462,8 @@ The amount filed has to be filled in call descriptor. func (cd *CallDescriptor) AddRecievedCallSeconds() (err error) { if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil { a := &Action{ - Direction: INBOUND, - MinuteBucket: &MinuteBucket{Seconds: cd.Amount, DestinationId: cd.Destination}, + Direction: INBOUND, + Balance: &Balance{Value: cd.Amount, DestinationId: cd.Destination}, } userBalance.countUnits(a) return nil @@ -454,3 +478,15 @@ func (cd *CallDescriptor) FlushCache() (err error) { return nil } + +// Creates a CallCost structure copying related data from CallDescriptor +func (cd *CallDescriptor) CreateCallCost() *CallCost { + return &CallCost{ + Direction: cd.Direction, + TOR: cd.TOR, + Tenant: cd.Tenant, + Subject: cd.Subject, + Account: cd.Account, + Destination: cd.Destination, + } +} diff --git a/engine/calldesc_test.go b/engine/calldesc_test.go index 57964414e..5823f5e7e 100644 --- a/engine/calldesc_test.go +++ b/engine/calldesc_test.go @@ -36,24 +36,26 @@ func init() { func populateDB() { minu := &UserBalance{ - Id: "*out:vdf:minu", - Type: UB_TYPE_PREPAID, - BalanceMap: map[string]BalanceChain{CREDIT: BalanceChain{&Balance{Value: 0}}}, - MinuteBuckets: []*MinuteBucket{ - &MinuteBucket{Seconds: 200, DestinationId: "NAT", Weight: 10}, - &MinuteBucket{Seconds: 100, DestinationId: "RET", Weight: 20}, - }, + Id: "*out:vdf:minu", + Type: UB_TYPE_PREPAID, + BalanceMap: map[string]BalanceChain{ + CREDIT: BalanceChain{&Balance{Value: 0}}, + MINUTES + OUTBOUND: BalanceChain{ + &Balance{Value: 200, DestinationId: "NAT", Weight: 10}, + &Balance{Value: 100, DestinationId: "RET", Weight: 20}, + }}, } broker := &UserBalance{ Id: "*out:vdf:broker", Type: UB_TYPE_PREPAID, - MinuteBuckets: []*MinuteBucket{ - &MinuteBucket{Seconds: 20, DestinationId: "NAT", Weight: 10, Price: 1}, - &MinuteBucket{Seconds: 100, DestinationId: "RET", Weight: 20}, - }, + BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{ + &Balance{Value: 20, DestinationId: "NAT", Weight: 10, RateSubject: "rif"}, + &Balance{Value: 100, DestinationId: "RET", Weight: 20}, + }}, } if storageGetter != nil { - storageGetter.Flush() + storageGetter.(Storage).Flush() storageGetter.SetUserBalance(broker) storageGetter.SetUserBalance(minu) } else { @@ -66,10 +68,10 @@ func TestSplitSpans(t *testing.T) { t2 := time.Date(2012, time.February, 2, 18, 30, 0, 0, time.UTC) cd := &CallDescriptor{Direction: "*out", TOR: "0", Tenant: "vdf", Subject: "rif", Destination: "0256", TimeStart: t1, TimeEnd: t2} - cd.LoadActivationPeriods() + cd.LoadRatingPlans() timespans := cd.splitInTimeSpans(nil) if len(timespans) != 2 { - t.Log(cd.ActivationPeriods) + t.Log(cd.RatingPlans) t.Error("Wrong number of timespans: ", len(timespans)) } } @@ -114,7 +116,7 @@ func TestFullDestNotFound(t *testing.T) { result, _ := cd.GetCost() expected := &CallCost{Tenant: "vdf", Subject: "rif", Destination: "0256", Cost: 2700, ConnectFee: 1} if result.Cost != expected.Cost || result.ConnectFee != expected.ConnectFee { - t.Log(cd.ActivationPeriods) + t.Log(cd.RatingPlans) t.Errorf("Expected %v was %v", expected, result) } } @@ -126,12 +128,12 @@ func TestSubjectNotFound(t *testing.T) { result, _ := cd.GetCost() expected := &CallCost{Tenant: "vdf", Subject: "rif", Destination: "0257", Cost: 2700, ConnectFee: 1} if result.Cost != expected.Cost || result.ConnectFee != expected.ConnectFee { - t.Log(cd.ActivationPeriods) + t.Log(cd.RatingPlans) t.Errorf("Expected %v was %v", expected, result) } } -func TestMultipleActivationPeriods(t *testing.T) { +func TestMultipleRatingPlans(t *testing.T) { t1 := time.Date(2012, time.February, 8, 17, 30, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 8, 18, 30, 0, 0, time.UTC) cd := &CallDescriptor{Direction: "*out", TOR: "0", Tenant: "vdf", Subject: "rif", Destination: "0257308200", TimeStart: t1, TimeEnd: t2} @@ -143,7 +145,7 @@ func TestMultipleActivationPeriods(t *testing.T) { } } -func TestSpansMultipleActivationPeriods(t *testing.T) { +func TestSpansMultipleRatingPlans(t *testing.T) { t1 := time.Date(2012, time.February, 7, 23, 50, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 8, 0, 30, 0, 0, time.UTC) cd := &CallDescriptor{Direction: "*out", TOR: "0", Tenant: "vdf", Subject: "rif", Destination: "0257308200", TimeStart: t1, TimeEnd: t2} @@ -240,7 +242,7 @@ func BenchmarkStorageRestoring(b *testing.B) { cd := &CallDescriptor{Direction: "*out", TOR: "0", Tenant: "vdf", Subject: "rif", Destination: "0256", TimeStart: t1, TimeEnd: t2} b.StartTimer() for i := 0; i < b.N; i++ { - cd.LoadActivationPeriods() + cd.LoadRatingPlans() } } @@ -260,7 +262,7 @@ func BenchmarkSplitting(b *testing.B) { t1 := time.Date(2012, time.February, 2, 17, 30, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 2, 18, 30, 0, 0, time.UTC) cd := &CallDescriptor{Direction: "*out", TOR: "0", Tenant: "vdf", Subject: "rif", Destination: "0256", TimeStart: t1, TimeEnd: t2} - cd.LoadActivationPeriods() + cd.LoadRatingPlans() b.StartTimer() for i := 0; i < b.N; i++ { cd.splitInTimeSpans(nil) diff --git a/engine/destinations.go b/engine/destinations.go index 0d15f592e..6b638ae44 100644 --- a/engine/destinations.go +++ b/engine/destinations.go @@ -45,9 +45,6 @@ func GetDestination(dId string) (d *Destination, err error) { return } -/* -De-serializes the destination for the storage. Used for key-value storages. -*/ func (d *Destination) containsPrefix(prefix string) (precision int, ok bool) { if d == nil { return diff --git a/engine/destinations_test.go b/engine/destinations_test.go index 61b97e9f1..56e875d49 100644 --- a/engine/destinations_test.go +++ b/engine/destinations_test.go @@ -21,7 +21,7 @@ package engine import ( "encoding/json" "github.com/cgrates/cgrates/cache2go" - "reflect" + "github.com/cgrates/cgrates/utils" "testing" ) @@ -38,9 +38,13 @@ func TestDestinationStoreRestore(t *testing.T) { func TestDestinationStorageStore(t *testing.T) { nationale := &Destination{Id: "nat", Prefixes: []string{"0257", "0256", "0723"}} - storageGetter.SetDestination(nationale) - result, _ := storageGetter.GetDestination(nationale.Id) - if !reflect.DeepEqual(nationale, result) { + err := storageGetter.SetDestination(nationale) + if err != nil { + t.Error("Error storing destination: ", err) + } + result, err := storageGetter.GetDestination(nationale.Id) + var ss utils.StringSlice = result.Prefixes + if !ss.Contains("0257") || !ss.Contains("0256") || !ss.Contains("0723") { t.Errorf("Expected %q was %q", nationale, result) } } @@ -51,7 +55,6 @@ func TestDestinationContainsPrefix(t *testing.T) { if !ok || precision != len("0256") { t.Error("Should contain prefix: ", nationale) } - } func TestDestinationContainsPrefixLong(t *testing.T) { @@ -60,7 +63,14 @@ func TestDestinationContainsPrefixLong(t *testing.T) { if !ok || precision != len("0256") { t.Error("Should contain prefix: ", nationale) } +} +func TestDestinationContainsPrefixWrong(t *testing.T) { + nationale := &Destination{Id: "nat", Prefixes: []string{"0257", "0256", "0723"}} + precision, ok := nationale.containsPrefix("01234567") + if ok || precision != 0 { + t.Error("Should not contain prefix: ", nationale) + } } func TestDestinationGetExists(t *testing.T) { @@ -86,8 +96,8 @@ func TestDestinationGetNotExists(t *testing.T) { func TestDestinationGetNotExistsCache(t *testing.T) { GetDestination("not existing") - if _, err := cache2go.GetCached("not existing"); err == nil { - t.Error("Bad destination cached") + if d, err := cache2go.GetCached("not existing"); err == nil { + t.Error("Bad destination cached: ", d) } } diff --git a/engine/groups.go b/engine/groups.go new file mode 100644 index 000000000..baa650fc5 --- /dev/null +++ b/engine/groups.go @@ -0,0 +1,46 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2013 ITsysCOM + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package engine + +import ( + "sort" +) + +type GroupLink struct { + Id string + Weight float64 +} + +type GroupLinks []*GroupLink + +func (gls GroupLinks) Len() int { + return len(gls) +} + +func (gls GroupLinks) Swap(i, j int) { + gls[i], gls[j] = gls[j], gls[i] +} + +func (gls GroupLinks) Less(j, i int) bool { + return gls[i].Weight < gls[j].Weight +} + +func (gls GroupLinks) Sort() { + sort.Sort(gls) +} diff --git a/engine/history_test.go b/engine/history_test.go index 976dba2c7..274c25289 100644 --- a/engine/history_test.go +++ b/engine/history_test.go @@ -36,19 +36,3 @@ func TestHistoryDestinations(t *testing.T) { t.Error("Error in destination history content:", scribe.DestBuf.String()) } } - -func TestHistoryratingProfiles(t *testing.T) { - scribe := historyScribe.(*history.MockScribe) - expected := `[{"Key":"*out:CUSTOMER_1:0:danb","Object":{"Id":"*out:CUSTOMER_1:0:danb","FallbackKey":"","DestinationMap":{"ALL":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}} -{"Key":"*out:CUSTOMER_1:0:rif:from:tm","Object":{"Id":"*out:CUSTOMER_1:0:rif:from:tm","FallbackKey":"*out:CUSTOMER_1:0:danb","DestinationMap":{"ALL":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}} -{"Key":"*out:CUSTOMER_2:0:danb:87.139.12.167","Object":{"Id":"*out:CUSTOMER_2:0:danb:87.139.12.167","FallbackKey":"*out:CUSTOMER_2:0:danb","DestinationMap":{"ALL":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}} -{"Key":"*out:vdf:0:*any","Object":{"Id":"*out:vdf:0:*any","FallbackKey":"","DestinationMap":{"ALL":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}} -{"Key":"*out:vdf:0:fall","Object":{"Id":"*out:vdf:0:fall","FallbackKey":"*out:vdf:0:rif","DestinationMap":{"ALL":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}} -{"Key":"*out:vdf:0:inf","Object":{"Id":"*out:vdf:0:inf","FallbackKey":"*out:vdf:0:inf","DestinationMap":{"ALL":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}} -{"Key":"*out:vdf:0:minu","Object":{"Id":"*out:vdf:0:minu","FallbackKey":"","DestinationMap":{"ALL":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}} -{"Key":"*out:vdf:0:one","Object":{"Id":"*out:vdf:0:one","FallbackKey":"","DestinationMap":{"ALL":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.2,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.1,"RateIncrement":1000000000,"RateUnit":60000000000},{"GroupIntervalStart":0,"Value":0.05,"RateIncrement":1000000000,"RateUnit":60000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}} -{"Key":"*out:vdf:0:rif","Object":{"Id":"*out:vdf:0:rif","FallbackKey":"","DestinationMap":{"ALL":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_O2":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"GERMANY_PREMIUM":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"NAT":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}],"RET":[{"ActivationTime":"2012-01-01T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]},{"ActivationTime":"2012-02-28T00:00:00Z","Intervals":[{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":1,"Prices":[{"GroupIntervalStart":0,"Value":1,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[1,2,3,4,5],"StartTime":"18:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0},{"Years":[],"Months":[],"MonthDays":[],"WeekDays":[6,0],"StartTime":"00:00:00","EndTime":"","Weight":10,"ConnectFee":0,"Prices":[{"GroupIntervalStart":0,"Value":0.5,"RateIncrement":1000000000,"RateUnit":1000000000}],"RoundingMethod":"","RoundingDecimals":0}]}]},"Tag":"","Tenant":"","TOR":"","Direction":"","Subject":"","DestRatesTimingTag":"","RatesFallbackSubject":"","ActivationTime":""}}]` - if scribe.RpBuf.String() != expected { - t.Error("Error in rating profiles history content:", scribe.RpBuf.String()) - } -} diff --git a/engine/loader_csv.go b/engine/loader_csv.go index b0ae606df..caf110b64 100644 --- a/engine/loader_csv.go +++ b/engine/loader_csv.go @@ -30,19 +30,19 @@ import ( ) type CSVReader struct { - sep rune - storage DataStorage - readerFunc func(string, rune, int) (*csv.Reader, *os.File, error) - actions map[string][]*Action - actionsTimings map[string][]*ActionTiming - actionsTriggers map[string][]*ActionTrigger - accountActions []*UserBalance - destinations []*Destination - timings map[string]*Timing - rates map[string]*Rate - destinationRates map[string][]*DestinationRate - activationPeriods map[string]*ActivationPeriod - ratingProfiles map[string]*RatingProfile + sep rune + storage DataStorage + readerFunc func(string, rune, int) (*csv.Reader, *os.File, error) + actions map[string][]*Action + actionsTimings map[string][]*ActionTiming + actionsTriggers map[string][]*ActionTrigger + accountActions []*UserBalance + destinations []*Destination + timings map[string]*Timing + rates map[string][]*LoadRate + destinationRates map[string][]*DestinationRate + destinationRateTimings map[string][]*DestinationRateTiming + ratingProfiles map[string]*RatingProfile // file names destinationsFn, ratesFn, destinationratesFn, timingsFn, destinationratetimingsFn, ratingprofilesFn, actionsFn, actiontimingsFn, actiontriggersFn, accountactionsFn string @@ -55,10 +55,10 @@ func NewFileCSVReader(storage DataStorage, sep rune, destinationsFn, timingsFn, c.actions = make(map[string][]*Action) c.actionsTimings = make(map[string][]*ActionTiming) c.actionsTriggers = make(map[string][]*ActionTrigger) - c.rates = make(map[string]*Rate) + c.rates = make(map[string][]*LoadRate) c.destinationRates = make(map[string][]*DestinationRate) c.timings = make(map[string]*Timing) - c.activationPeriods = make(map[string]*ActivationPeriod) + c.destinationRateTimings = make(map[string][]*DestinationRateTiming) c.ratingProfiles = make(map[string]*RatingProfile) c.readerFunc = openFileCSVReader c.destinationsFn, c.timingsFn, c.ratesFn, c.destinationratesFn, c.destinationratetimingsFn, c.ratingprofilesFn, @@ -101,7 +101,7 @@ func (csvr *CSVReader) WriteToDatabase(flush, verbose bool) (err error) { return errors.New("No database connection!") } if flush { - storage.Flush() + storage.(Storage).Flush() } if verbose { log.Print("Destinations") @@ -223,12 +223,19 @@ func (csvr *CSVReader) LoadRates() (err error) { } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { tag := record[0] - var r *Rate - r, err = NewRate(record[0], record[1], record[2], record[3], record[4], record[5], record[6], record[7], record[8]) + var r *LoadRate + r, err = NewLoadRate(record[0], record[1], record[2], record[3], record[4], record[5], record[6], record[7], record[8]) if err != nil { return err } - csvr.rates[tag] = r + // same tag only to create rate groups + existingRates, exists := csvr.rates[tag] + if exists { + if err := existingRates[len(existingRates)-1].ValidNextGroup(r); err != nil { + return err + } + } + csvr.rates[tag] = append(csvr.rates[tag], r) } return } @@ -249,11 +256,20 @@ func (csvr *CSVReader) LoadDestinationRates() (err error) { if !exists { return errors.New(fmt.Sprintf("Could not get rates for tag %v", record[2])) } - //ToDo: Not checking presence of destinations? + destinationExists := false + for _, d := range csvr.destinations { + if d.Id == record[1] { + destinationExists = true + break + } + } + if !destinationExists { + return errors.New(fmt.Sprintf("Could not get destination for tag %v", record[1])) + } dr := &DestinationRate{ Tag: tag, DestinationsTag: record[1], - Rate: r, + rates: r, } csvr.destinationRates[tag] = append(csvr.destinationRates[tag], dr) @@ -277,18 +293,12 @@ func (csvr *CSVReader) LoadDestinationRateTimings() (err error) { if !exists { return errors.New(fmt.Sprintf("Could not get timing for tag %v", record[2])) } - - rt := NewDestinationRateTiming(record[1], t, record[3]) drs, exists := csvr.destinationRates[record[1]] if !exists { return errors.New(fmt.Sprintf("Could not find destination rate for tag %v", record[1])) } - for _, dr := range drs { - if _, exists := csvr.activationPeriods[tag]; !exists { - csvr.activationPeriods[tag] = &ActivationPeriod{} - } - csvr.activationPeriods[tag].AddInterval(rt.GetInterval(dr)) - } + drt := NewDestinationRateTiming(drs, t, record[3]) + csvr.destinationRateTimings[tag] = append(csvr.destinationRateTimings[tag], drt) } return } @@ -315,19 +325,22 @@ func (csvr *CSVReader) LoadRatingProfiles() (err error) { rp = &RatingProfile{Id: key} csvr.ratingProfiles[key] = rp } - for _, d := range csvr.destinations { - ap, exists := csvr.activationPeriods[record[5]] - if !exists { - return errors.New(fmt.Sprintf("Could not load ratinTiming for tag: %v", record[5])) - } - newAP := &ActivationPeriod{ActivationTime: at} - //copy(newAP.Intervals, ap.Intervals) - newAP.Intervals = append(newAP.Intervals, ap.Intervals...) - rp.AddActivationPeriodIfNotPresent(d.Id, newAP) - if fallbacksubject != "" { - rp.FallbackKey = fmt.Sprintf("%s:%s:%s:%s", direction, tenant, tor, fallbacksubject) + drts, exists := csvr.destinationRateTimings[record[5]] + if !exists { + return errors.New(fmt.Sprintf("Could not load destination rate timings for tag: %v", record[5])) + } + + for _, drt := range drts { + plan := &RatingPlan{ActivationTime: at} + for _, dr := range drt.destinationRates { + plan.AddRateInterval(drt.GetRateInterval(dr)) + rp.AddRatingPlanIfNotPresent(dr.DestinationsTag, plan) } } + + if fallbacksubject != "" { + rp.FallbackKey = fmt.Sprintf("%s:%s:%s:%s", direction, tenant, tor, fallbacksubject) + } } return } @@ -348,50 +361,32 @@ func (csvr *CSVReader) LoadActions() (err error) { if err != nil { return errors.New(fmt.Sprintf("Could not parse action units: %v", err)) } - var a *Action - if record[2] != MINUTES { - a = &Action{ - ActionType: record[1], - BalanceId: record[2], - Direction: record[3], - Units: units, - ExpirationString: record[5], - } - if _, err := utils.ParseDate(a.ExpirationString); err != nil { - return errors.New(fmt.Sprintf("Could not parse expiration time: %v", err)) - } - } else { - value, err := strconv.ParseFloat(record[8], 64) - if err != nil { - return errors.New(fmt.Sprintf("Could not parse action price: %v", err)) - } - minutesWeight, err := strconv.ParseFloat(record[9], 64) - if err != nil { - return errors.New(fmt.Sprintf("Could not parse action minutes weight: %v", err)) - } - weight, err := strconv.ParseFloat(record[9], 64) - if err != nil { - return errors.New(fmt.Sprintf("Could not parse action weight: %v", err)) - } - a = &Action{ - Id: utils.GenUUID(), - ActionType: record[1], - BalanceId: record[2], - Direction: record[3], - Weight: weight, - ExpirationString: record[5], - MinuteBucket: &MinuteBucket{ - Seconds: units, - Weight: minutesWeight, - Price: value, - PriceType: record[7], - DestinationId: record[6], - }, - } - if _, err := utils.ParseDate(a.ExpirationString); err != nil { - return errors.New(fmt.Sprintf("Could not parse expiration time: %v", err)) - } - + balanceWeight, err := strconv.ParseFloat(record[8], 64) + if err != nil { + return errors.New(fmt.Sprintf("Could not parse action balance weight: %v", err)) + } + weight, err := strconv.ParseFloat(record[10], 64) + if err != nil { + return errors.New(fmt.Sprintf("Could not parse action weight: %v", err)) + } + a := &Action{ + Id: utils.GenUUID(), + ActionType: record[1], + BalanceId: record[2], + Direction: record[3], + Weight: weight, + ExpirationString: record[5], + ExtraParameters: record[9], + Balance: &Balance{ + Uuid: utils.GenUUID(), + Value: units, + Weight: balanceWeight, + DestinationId: record[6], + RateSubject: record[7], + }, + } + if _, err := utils.ParseDate(a.ExpirationString); err != nil { + return errors.New(fmt.Sprintf("Could not parse expiration time: %v", err)) } csvr.actions[tag] = append(csvr.actions[tag], a) } @@ -426,7 +421,8 @@ func (csvr *CSVReader) LoadActionTimings() (err error) { Id: utils.GenUUID(), Tag: record[2], Weight: weight, - Timing: &Interval{ + Timing: &RateInterval{ + Years: t.Years, Months: t.Months, MonthDays: t.MonthDays, WeekDays: t.WeekDays, diff --git a/engine/loader_csv_test.go b/engine/loader_csv_test.go index d2e7ada2f..d12a57b85 100644 --- a/engine/loader_csv_test.go +++ b/engine/loader_csv_test.go @@ -19,8 +19,10 @@ along with this program. If not, see package engine import ( - //"log" + "github.com/cgrates/cgrates/utils" + "reflect" "testing" + "time" ) var ( @@ -85,14 +87,15 @@ vdf,0,*out,inf,2012-02-28T00:00:00Z,STANDARD,inf vdf,0,*out,fall,2012-02-28T00:00:00Z,PREMIUM,rif ` actions = ` -MINI,TOPUP,MINUTES,*out,100,*unlimited,NAT,*absolute,0,10,10 +MINI,*topup_reset,*monetary,*out,10,*unlimited,,,10,,10 +MINI,*topup,*minutes,*out,100,*unlimited,NAT,test,10,,10 ` actionTimings = ` MORE_MINUTES,MINI,ONE_TIME_RUN,10 ` actionTriggers = ` -STANDARD_TRIGGER,MINUTES,*out,*min_counter,10,GERMANY_O2,SOME_1,10 -STANDARD_TRIGGER,MINUTES,*out,*max_balance,200,GERMANY,SOME_2,10 +STANDARD_TRIGGER,*minutes,*out,*min_counter,10,GERMANY_O2,SOME_1,10 +STANDARD_TRIGGER,*minutes,*out,*max_balance,200,GERMANY,SOME_2,10 ` accountActions = ` vdf,minitsboy,*out,MORE_MINUTES,STANDARD_TRIGGER @@ -120,29 +123,402 @@ func TestLoadDestinations(t *testing.T) { if len(csvr.destinations) != 6 { t.Error("Failed to load destinations: ", csvr.destinations) } + for _, d := range csvr.destinations { + switch d.Id { + case "NAT": + if !reflect.DeepEqual(d.Prefixes, []string{`0256`, `0257`, `0723`}) { + t.Error("Faild to load destinations", d) + } + case "ALL": + if !reflect.DeepEqual(d.Prefixes, []string{`49`, `41`, `43`}) { + t.Error("Faild to load destinations", d) + } + case "RET": + if !reflect.DeepEqual(d.Prefixes, []string{`0723`, `0724`}) { + t.Error("Faild to load destinations", d) + } + case "GERMANY": + if !reflect.DeepEqual(d.Prefixes, []string{`49`}) { + t.Error("Faild to load destinations", d) + } + case "GERMANY_O2": + if !reflect.DeepEqual(d.Prefixes, []string{`41`}) { + t.Error("Faild to load destinations", d) + } + case "GERMANY_PREMIUM": + if !reflect.DeepEqual(d.Prefixes, []string{`43`}) { + t.Error("Faild to load destinations", d) + } + default: + t.Error("Unknown destination tag!") + } + } } func TestLoadTimimgs(t *testing.T) { if len(csvr.timings) != 4 { t.Error("Failed to load timings: ", csvr.timings) } + timing := csvr.timings["WORKDAYS_00"] + if !reflect.DeepEqual(timing, &Timing{ + Id: "WORKDAYS_00", + Years: Years{}, + Months: Months{}, + MonthDays: MonthDays{}, + WeekDays: WeekDays{1, 2, 3, 4, 5}, + StartTime: "00:00:00", + }) { + t.Error("Error loading timing: ", timing) + } + timing = csvr.timings["WORKDAYS_18"] + if !reflect.DeepEqual(timing, &Timing{ + Id: "WORKDAYS_18", + Years: Years{}, + Months: Months{}, + MonthDays: MonthDays{}, + WeekDays: WeekDays{1, 2, 3, 4, 5}, + StartTime: "18:00:00", + }) { + t.Error("Error loading timing: ", timing) + } + timing = csvr.timings["WEEKENDS"] + if !reflect.DeepEqual(timing, &Timing{ + Id: "WEEKENDS", + Years: Years{}, + Months: Months{}, + MonthDays: MonthDays{}, + WeekDays: WeekDays{time.Saturday, time.Sunday}, + StartTime: "00:00:00", + }) { + t.Error("Error loading timing: ", timing) + } + timing = csvr.timings["ONE_TIME_RUN"] + if !reflect.DeepEqual(timing, &Timing{ + Id: "ONE_TIME_RUN", + Years: Years{2012}, + Months: Months{}, + MonthDays: MonthDays{}, + WeekDays: WeekDays{}, + StartTime: "*asap", + }) { + t.Error("Error loading timing: ", timing) + } } func TestLoadRates(t *testing.T) { if len(csvr.rates) != 5 { t.Error("Failed to load rates: ", csvr.rates) } + rate := csvr.rates["R1"][0] + if !reflect.DeepEqual(rate, &LoadRate{ + Tag: "R1", + ConnectFee: 0, + Price: 0.2, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: utils.ROUNDING_MIDDLE, + RoundingDecimals: 2, + Weight: 10, + }) { + t.Error("Error loading rate: ", csvr.rates["R1"][0]) + } + rate = csvr.rates["R2"][0] + if !reflect.DeepEqual(rate, &LoadRate{ + Tag: "R2", + ConnectFee: 0, + Price: 0.1, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: utils.ROUNDING_MIDDLE, + RoundingDecimals: 2, + Weight: 10, + }) { + t.Error("Error loading rate: ", csvr.rates) + } + rate = csvr.rates["R3"][0] + if !reflect.DeepEqual(rate, &LoadRate{ + Tag: "R3", + ConnectFee: 0, + Price: 0.05, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: utils.ROUNDING_MIDDLE, + RoundingDecimals: 2, + Weight: 10, + }) { + t.Error("Error loading rate: ", csvr.rates) + } + rate = csvr.rates["R4"][0] + if !reflect.DeepEqual(rate, &LoadRate{ + Tag: "R4", + ConnectFee: 1, + Price: 1.0, + RateUnit: time.Second, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: utils.ROUNDING_UP, + RoundingDecimals: 2, + Weight: 10, + }) { + t.Error("Error loading rate: ", csvr.rates) + } + rate = csvr.rates["R5"][0] + if !reflect.DeepEqual(rate, &LoadRate{ + Tag: "R5", + ConnectFee: 0, + Price: 0.5, + RateUnit: time.Second, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: utils.ROUNDING_DOWN, + RoundingDecimals: 2, + Weight: 10, + }) { + t.Error("Error loading rate: ", csvr.rates) + } + } func TestLoadDestinationRates(t *testing.T) { if len(csvr.destinationRates) != 5 { - t.Error("Failed to load rates: ", csvr.rates) + t.Error("Failed to load destinationrates: ", csvr.destinationRates) + } + drs := csvr.destinationRates["RT_STANDARD"] + if !reflect.DeepEqual(drs, []*DestinationRate{ + &DestinationRate{ + Tag: "RT_STANDARD", + DestinationsTag: "GERMANY", + rates: csvr.rates["R1"], + }, + &DestinationRate{ + Tag: "RT_STANDARD", + DestinationsTag: "GERMANY_O2", + rates: csvr.rates["R2"], + }, + &DestinationRate{ + Tag: "RT_STANDARD", + DestinationsTag: "GERMANY_PREMIUM", + rates: csvr.rates["R2"], + }, + }) { + t.Error("Error loading destination rate: ", drs) + } + drs = csvr.destinationRates["RT_DEFAULT"] + if !reflect.DeepEqual(drs, []*DestinationRate{ + &DestinationRate{ + Tag: "RT_DEFAULT", + DestinationsTag: "ALL", + rates: csvr.rates["R2"], + }, + }) { + t.Error("Error loading destination rate: ", drs) + } + drs = csvr.destinationRates["RT_STD_WEEKEND"] + if !reflect.DeepEqual(drs, []*DestinationRate{ + &DestinationRate{ + Tag: "RT_STD_WEEKEND", + DestinationsTag: "GERMANY", + rates: csvr.rates["R2"], + }, + &DestinationRate{ + Tag: "RT_STD_WEEKEND", + DestinationsTag: "GERMANY_O2", + rates: csvr.rates["R3"], + }, + }) { + t.Error("Error loading destination rate: ", drs) + } + drs = csvr.destinationRates["P1"] + if !reflect.DeepEqual(drs, []*DestinationRate{ + &DestinationRate{ + Tag: "P1", + DestinationsTag: "NAT", + rates: csvr.rates["R4"], + }, + }) { + t.Error("Error loading destination rate: ", drs) + } + drs = csvr.destinationRates["P2"] + if !reflect.DeepEqual(drs, []*DestinationRate{ + &DestinationRate{ + Tag: "P2", + DestinationsTag: "NAT", + rates: csvr.rates["R5"], + }, + }) { + t.Error("Error loading destination rate: ", drs) } } func TestLoadDestinationRateTimings(t *testing.T) { - if len(csvr.activationPeriods) != 4 { - t.Error("Failed to load rate timings: ", csvr.activationPeriods) + if len(csvr.destinationRateTimings) != 4 { + t.Error("Failed to load rate timings: ", csvr.destinationRateTimings) + } + rplan := csvr.destinationRateTimings["STANDARD"] + expected := []*DestinationRateTiming{ + &DestinationRateTiming{ + destinationRates: []*DestinationRate{ + &DestinationRate{ + Tag: "RT_STANDARD", + DestinationsTag: "GERMANY", + rates: []*LoadRate{ + &LoadRate{ + Tag: "R1", + ConnectFee: 0, + Price: 0.2, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: "*middle", + RoundingDecimals: 2, + Weight: 10, + }, + }, + }, + &DestinationRate{ + Tag: "RT_STANDARD", + DestinationsTag: "GERMANY_O2", + rates: []*LoadRate{ + &LoadRate{ + Tag: "R2", + ConnectFee: 0, + Price: 0.1, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: "*middle", + RoundingDecimals: 2, + Weight: 10, + }, + }, + }, + &DestinationRate{ + Tag: "RT_STANDARD", + DestinationsTag: "GERMANY_PREMIUM", + rates: []*LoadRate{ + &LoadRate{ + Tag: "R2", + ConnectFee: 0, + Price: 0.1, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: "*middle", + RoundingDecimals: 2, + Weight: 10, + }, + }, + }, + }, + Weight: 10, + timing: &Timing{ + Id: "WORKDAYS_00", + Years: Years{}, + Months: Months{}, + MonthDays: MonthDays{}, + WeekDays: WeekDays{1, 2, 3, 4, 5}, + StartTime: "00:00:00"}, + }, + &DestinationRateTiming{ + destinationRates: []*DestinationRate{ + &DestinationRate{ + Tag: "RT_STD_WEEKEND", + DestinationsTag: "GERMANY", + rates: []*LoadRate{ + &LoadRate{ + Tag: "R2", + ConnectFee: 0, + Price: 0.1, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: "*middle", + RoundingDecimals: 2, + Weight: 10, + }, + }, + }, + &DestinationRate{ + Tag: "RT_STD_WEEKEND", + DestinationsTag: "GERMANY_O2", + rates: []*LoadRate{ + &LoadRate{ + Tag: "R3", + ConnectFee: 0, + Price: 0.05, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: "*middle", + RoundingDecimals: 2, + Weight: 10, + }, + }, + }, + }, + Weight: 10, + timing: &Timing{ + Id: "WORKDAYS_18", + Years: Years{}, + Months: Months{}, + MonthDays: MonthDays{}, + WeekDays: WeekDays{1, 2, 3, 4, 5}, + StartTime: "18:00:00", + }, + }, + &DestinationRateTiming{ + destinationRates: []*DestinationRate{ + &DestinationRate{ + Tag: "RT_STD_WEEKEND", + DestinationsTag: "GERMANY", + rates: []*LoadRate{ + &LoadRate{ + Tag: "R2", + ConnectFee: 0, + Price: 0.1, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: "*middle", + RoundingDecimals: 2, + Weight: 10, + }, + }, + }, + &DestinationRate{ + Tag: "RT_STD_WEEKEND", + DestinationsTag: "GERMANY_O2", + rates: []*LoadRate{ + &LoadRate{ + Tag: "R3", + ConnectFee: 0, + Price: 0.05, + RateUnit: time.Minute, + RateIncrement: time.Second, + GroupIntervalStart: 0, + RoundingMethod: "*middle", + RoundingDecimals: 2, + Weight: 10, + }, + }, + }, + }, + Weight: 10, + timing: &Timing{ + Id: "WEEKENDS", + Years: Years{}, + Months: Months{}, + MonthDays: MonthDays{}, + WeekDays: WeekDays{6, 0}, + StartTime: "00:00:00", + }, + }, + } + if !reflect.DeepEqual(rplan, expected) { + t.Errorf("Error loading destination rate timing: %#v", rplan) } } @@ -150,28 +526,135 @@ func TestLoadRatingProfiles(t *testing.T) { if len(csvr.ratingProfiles) != 9 { t.Error("Failed to load rating profiles: ", len(csvr.ratingProfiles), csvr.ratingProfiles) } + rp := csvr.ratingProfiles["*out:CUSTOMER_1:0:rif:from:tm"] + expected := &RatingProfile{} + if reflect.DeepEqual(rp, expected) { + t.Errorf("Error loading rating profile: %+v", rp.DestinationMap["GERMANY"][1].RateIntervals[2].Rates[0]) + } } +/* +CUSTOMER_1,0,*out,rif:from:tm,2012-01-01T00:00:00Z,PREMIUM,danb +CUSTOMER_1,0,*out,rif:from:tm,2012-02-28T00:00:00Z,STANDARD,danb +*/ + func TestLoadActions(t *testing.T) { if len(csvr.actions) != 1 { t.Error("Failed to load actions: ", csvr.actions) } + as := csvr.actions["MINI"] + expected := []*Action{ + &Action{ + Id: as[0].Id, + ActionType: TOPUP_RESET, + BalanceId: CREDIT, + Direction: OUTBOUND, + ExpirationString: UNLIMITED, + ExtraParameters: "", + Weight: 10, + Balance: &Balance{ + Uuid: as[0].Balance.Uuid, + Value: 10, + Weight: 10, + }, + }, + &Action{ + Id: as[1].Id, + ActionType: TOPUP, + BalanceId: MINUTES, + Direction: OUTBOUND, + ExpirationString: UNLIMITED, + ExtraParameters: "", + Weight: 10, + Balance: &Balance{ + Uuid: as[1].Balance.Uuid, + Value: 100, + Weight: 10, + RateSubject: "test", + DestinationId: "NAT", + }, + }, + } + if !reflect.DeepEqual(as, expected) { + t.Error("Error loading action: ", as) + } } func TestLoadActionTimings(t *testing.T) { if len(csvr.actionsTimings) != 1 { t.Error("Failed to load action timings: ", csvr.actionsTimings) } + atm := csvr.actionsTimings["MORE_MINUTES"][0] + expected := &ActionTiming{ + Id: atm.Id, + Tag: "ONE_TIME_RUN", + UserBalanceIds: []string{"*out:vdf:minitsboy"}, + Timing: &RateInterval{ + Years: Years{2012}, + Months: Months{}, + MonthDays: MonthDays{}, + WeekDays: WeekDays{}, + StartTime: ASAP, + }, + Weight: 10, + ActionsId: "MINI", + } + if !reflect.DeepEqual(atm, expected) { + t.Error("Error loading action timing: ", atm) + } } func TestLoadActionTriggers(t *testing.T) { if len(csvr.actionsTriggers) != 1 { t.Error("Failed to load action triggers: ", csvr.actionsTriggers) } + atr := csvr.actionsTriggers["STANDARD_TRIGGER"][0] + expected := &ActionTrigger{ + Id: atr.Id, + BalanceId: MINUTES, + Direction: OUTBOUND, + ThresholdType: TRIGGER_MIN_COUNTER, + ThresholdValue: 10, + DestinationId: "GERMANY_O2", + Weight: 10, + ActionsId: "SOME_1", + Executed: false, + } + if !reflect.DeepEqual(atr, expected) { + t.Error("Error loading action trigger: ", atr) + } + atr = csvr.actionsTriggers["STANDARD_TRIGGER"][1] + expected = &ActionTrigger{ + Id: atr.Id, + BalanceId: MINUTES, + Direction: OUTBOUND, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 200, + DestinationId: "GERMANY", + Weight: 10, + ActionsId: "SOME_2", + Executed: false, + } + if !reflect.DeepEqual(atr, expected) { + t.Error("Error loading action trigger: ", atr) + } } func TestLoadAccountActions(t *testing.T) { if len(csvr.accountActions) != 1 { t.Error("Failed to load account actions: ", csvr.accountActions) } + aa := csvr.accountActions[0] + expected := &UserBalance{ + Id: "*out:vdf:minitsboy", + Type: UB_TYPE_PREPAID, + ActionTriggers: csvr.actionsTriggers["STANDARD_TRIGGER"], + } + if !reflect.DeepEqual(aa, expected) { + t.Error("Error loading account action: ", aa) + } } + +/* +vdf,minitsboy,*out,MORE_MINUTES,STANDARD_TRIGGER +*/ diff --git a/engine/loader_db.go b/engine/loader_db.go index e585d7a97..c430f503f 100644 --- a/engine/loader_db.go +++ b/engine/loader_db.go @@ -26,27 +26,27 @@ import ( ) type DbReader struct { - tpid string - storDb DataStorage - dataDb DataStorage - actions map[string][]*Action - actionsTimings map[string][]*ActionTiming - actionsTriggers map[string][]*ActionTrigger - accountActions []*UserBalance - destinations []*Destination - timings map[string]*Timing - rates map[string]*Rate - destinationRates map[string][]*DestinationRate - activationPeriods map[string]*ActivationPeriod - ratingProfiles map[string]*RatingProfile + tpid string + storDb LoadStorage + dataDb DataStorage + actions map[string][]*Action + actionsTimings map[string][]*ActionTiming + actionsTriggers map[string][]*ActionTrigger + accountActions []*UserBalance + destinations []*Destination + timings map[string]*Timing + rates map[string][]*LoadRate + destinationRates map[string][]*DestinationRate + destinationRateTimings map[string][]*DestinationRateTiming + ratingProfiles map[string]*RatingProfile } -func NewDbReader(storDB DataStorage, storage DataStorage, tpid string) *DbReader { +func NewDbReader(storDB LoadStorage, storage DataStorage, tpid string) *DbReader { c := new(DbReader) c.storDb = storDB c.dataDb = storage c.tpid = tpid - c.activationPeriods = make(map[string]*ActivationPeriod) + c.destinationRateTimings = make(map[string][]*DestinationRateTiming) c.actionsTimings = make(map[string][]*ActionTiming) return c } @@ -54,7 +54,7 @@ func NewDbReader(storDB DataStorage, storage DataStorage, tpid string) *DbReader func (dbr *DbReader) WriteToDatabase(flush, verbose bool) (err error) { storage := dbr.dataDb if flush { - storage.Flush() + storage.(Storage).Flush() } if verbose { log.Print("Destinations") @@ -141,38 +141,43 @@ func (dbr *DbReader) LoadDestinationRates() (err error) { } for _, drs := range dbr.destinationRates { for _, dr := range drs { - rate, exists := dbr.rates[dr.RateTag] + rates, exists := dbr.rates[dr.RateTag] if !exists { return errors.New(fmt.Sprintf("Could not find rate for tag %v", dr.RateTag)) } - dr.Rate = rate + dr.rates = rates + destinationExists := false + for _, d := range dbr.destinations { + if d.Id == dr.DestinationsTag { + destinationExists = true + break + } + } + if !destinationExists { + return errors.New(fmt.Sprintf("Could not get destination for tag %v", dr.DestinationsTag)) + } } } return nil } func (dbr *DbReader) LoadDestinationRateTimings() error { - rts, err := dbr.storDb.GetTpDestinationRateTimings(dbr.tpid, "") + drts, err := dbr.storDb.GetTpDestinationRateTimings(dbr.tpid, "") if err != nil { return err } - for _, rt := range rts { - t, exists := dbr.timings[rt.TimingsTag] + for _, drt := range drts { + t, exists := dbr.timings[drt.TimingTag] if !exists { - return errors.New(fmt.Sprintf("Could not get timing for tag %v", rt.TimingsTag)) + return errors.New(fmt.Sprintf("Could not get timing for tag %v", drt.TimingTag)) } - rt.timing = t - drs, exists := dbr.destinationRates[rt.DestinationRatesTag] + drt.timing = t + drs, exists := dbr.destinationRates[drt.DestinationRatesTag] if !exists { - return errors.New(fmt.Sprintf("Could not find destination rate for tag %v", rt.DestinationRatesTag)) - } - for _, dr := range drs { - _, exists := dbr.activationPeriods[rt.Tag] - if !exists { - dbr.activationPeriods[rt.Tag] = &ActivationPeriod{} - } - dbr.activationPeriods[rt.Tag].AddInterval(rt.GetInterval(dr)) + return errors.New(fmt.Sprintf("Could not find destination rate for tag %v", drt.DestinationRatesTag)) } + drt.destinationRates = drs + dbr.destinationRateTimings[drt.Tag] = append(dbr.destinationRateTimings[drt.Tag], drt) } return nil } @@ -187,23 +192,24 @@ func (dbr *DbReader) LoadRatingProfiles() error { if err != nil { return errors.New(fmt.Sprintf("Cannot parse activation time from %v", rp.ActivationTime)) } - for _, d := range dbr.destinations { - ap, exists := dbr.activationPeriods[rp.DestRatesTimingTag] - if !exists { - return errors.New(fmt.Sprintf("Could not load rating timing for tag: %v", rp.DestRatesTimingTag)) - } - newAP := &ActivationPeriod{ActivationTime: at} - //copy(newAP.Intervals, ap.Intervals) - newAP.Intervals = append(newAP.Intervals, ap.Intervals...) - rp.AddActivationPeriodIfNotPresent(d.Id, newAP) - + drts, exists := dbr.destinationRateTimings[rp.DestRatesTimingTag] + if !exists { + return errors.New(fmt.Sprintf("Could not load destination rate timings for tag: %v", rp.DestinationMap)) } + for _, drt := range drts { + plan := &RatingPlan{ActivationTime: at} + for _, dr := range drt.destinationRates { + plan.AddRateInterval(drt.GetRateInterval(dr)) + rp.AddRatingPlanIfNotPresent(dr.DestinationsTag, plan) + } + } + dbr.ratingProfiles[rp.Id] = rp } return nil } func (dbr *DbReader) LoadRatingProfileByTag(tag string) error { - activationPeriods := make(map[string]*ActivationPeriod) + activationPeriods := make(map[string]*RatingPlan) resultRatingProfile := &RatingProfile{} rpm, err := dbr.storDb.GetTpRatingProfiles(dbr.tpid, tag) if err != nil || len(rpm) == 0 { @@ -223,12 +229,12 @@ func (dbr *DbReader) LoadRatingProfileByTag(tag string) error { } for _, destrateTiming := range drtm { Logger.Debug(fmt.Sprintf("Destination rate timing: %v", rpm)) - tm, err := dbr.storDb.GetTpTimings(dbr.tpid, destrateTiming.TimingsTag) + tm, err := dbr.storDb.GetTpTimings(dbr.tpid, destrateTiming.TimingTag) Logger.Debug(fmt.Sprintf("Timing: %v", rpm)) if err != nil || len(tm) == 0 { - return fmt.Errorf("No Timings profile with id %s: %v", destrateTiming.TimingsTag, err) + return fmt.Errorf("No Timings profile with id %s: %v", destrateTiming.TimingTag, err) } - destrateTiming.timing = tm[destrateTiming.TimingsTag] + destrateTiming.timing = tm[destrateTiming.TimingTag] drm, err := dbr.storDb.GetTpDestinationRates(dbr.tpid, destrateTiming.DestinationRatesTag) if err != nil || len(drm) == 0 { return fmt.Errorf("No Timings profile with id %s: %v", destrateTiming.DestinationRatesTag, err) @@ -240,11 +246,11 @@ func (dbr *DbReader) LoadRatingProfileByTag(tag string) error { return fmt.Errorf("No Rates profile with id %s: %v", drate.RateTag, err) } Logger.Debug(fmt.Sprintf("Rate: %v", rpm)) - drate.Rate = rt[drate.RateTag] + drate.rates = rt[drate.RateTag] if _, exists := activationPeriods[destrateTiming.Tag]; !exists { - activationPeriods[destrateTiming.Tag] = &ActivationPeriod{} + activationPeriods[destrateTiming.Tag] = &RatingPlan{} } - activationPeriods[destrateTiming.Tag].AddInterval(destrateTiming.GetInterval(drate)) + activationPeriods[destrateTiming.Tag].AddRateInterval(destrateTiming.GetRateInterval(drate)) dm, err := dbr.storDb.GetTpDestinations(dbr.tpid, drate.DestinationsTag) if err != nil || len(dm) == 0 { return fmt.Errorf("Could not get destination id %s: %v", drate.DestinationsTag, err) @@ -253,9 +259,9 @@ func (dbr *DbReader) LoadRatingProfileByTag(tag string) error { for _, destination := range dm { Logger.Debug(fmt.Sprintf("Destination: %v", rpm)) ap := activationPeriods[ratingProfile.DestRatesTimingTag] - newAP := &ActivationPeriod{ActivationTime: at} - newAP.Intervals = append(newAP.Intervals, ap.Intervals...) - resultRatingProfile.AddActivationPeriodIfNotPresent(destination.Id, newAP) + newAP := &RatingPlan{ActivationTime: at} + newAP.RateIntervals = append(newAP.RateIntervals, ap.RateIntervals...) + resultRatingProfile.AddRatingPlanIfNotPresent(destination.Id, newAP) dbr.dataDb.SetDestination(destination) } } @@ -288,7 +294,7 @@ func (dbr *DbReader) LoadActionTimings() (err error) { Id: utils.GenUUID(), Tag: at.Tag, Weight: at.Weight, - Timing: &Interval{ + Timing: &RateInterval{ Months: t.Months, MonthDays: t.MonthDays, WeekDays: t.WeekDays, @@ -387,7 +393,7 @@ func (dbr *DbReader) LoadAccountActionsByTag(tag string) error { Id: utils.GenUUID(), Tag: at.Tag, Weight: at.Weight, - Timing: &Interval{ + Timing: &RateInterval{ Months: t.Months, MonthDays: t.MonthDays, WeekDays: t.WeekDays, diff --git a/engine/loader_helpers.go b/engine/loader_helpers.go index 03e97247d..e9b96e517 100644 --- a/engine/loader_helpers.go +++ b/engine/loader_helpers.go @@ -24,6 +24,7 @@ import ( "fmt" "github.com/cgrates/cgrates/utils" "log" + "math" "os" "path" "regexp" @@ -46,7 +47,7 @@ type TPLoader interface { WriteToDatabase(bool, bool) error } -type Rate struct { +type LoadRate struct { Tag string ConnectFee, Price float64 RateUnit, RateIncrement, GroupIntervalStart time.Duration @@ -55,7 +56,7 @@ type Rate struct { Weight float64 } -func NewRate(tag, connectFee, price, ratedUnits, rateIncrements, groupInterval, roundingMethod, roundingDecimals, weight string) (r *Rate, err error) { +func NewLoadRate(tag, connectFee, price, ratedUnits, rateIncrements, groupInterval, roundingMethod, roundingDecimals, weight string) (r *LoadRate, err error) { cf, err := strconv.ParseFloat(connectFee, 64) if err != nil { log.Printf("Error parsing connect fee from: %v", connectFee) @@ -92,7 +93,7 @@ func NewRate(tag, connectFee, price, ratedUnits, rateIncrements, groupInterval, return } - r = &Rate{ + r = &LoadRate{ Tag: tag, ConnectFee: cf, Price: p, @@ -106,11 +107,24 @@ func NewRate(tag, connectFee, price, ratedUnits, rateIncrements, groupInterval, return } +func (present *LoadRate) ValidNextGroup(next *LoadRate) error { + if next.GroupIntervalStart <= present.GroupIntervalStart { + return errors.New(fmt.Sprintf("Next rate group interval start must be heigher than the last one: %#v", next)) + } + if math.Mod(next.GroupIntervalStart.Seconds(), present.RateIncrement.Seconds()) != 0 { + return errors.New(fmt.Sprintf("GroupIntervalStart of %#v must be a multiple of RateIncrement of %#v", next, present)) + } + if present.RoundingMethod != next.RoundingMethod || present.RoundingDecimals != next.RoundingDecimals { + return errors.New(fmt.Sprintf("Rounding stuff must be equal for sam rate tag: %#v, %#v", present, next)) + } + return nil +} + type DestinationRate struct { Tag string DestinationsTag string - RateTag string - Rate *Rate + RateTag string // intermediary used when loading from db + rates []*LoadRate } type Timing struct { @@ -135,41 +149,46 @@ func NewTiming(timingInfo ...string) (rt *Timing) { type DestinationRateTiming struct { Tag string - DestinationRatesTag string + DestinationRatesTag string // intermediary used when loading from db + destinationRates []*DestinationRate Weight float64 - TimingsTag string // intermediary used when loading from db + TimingTag string // intermediary used when loading from db timing *Timing } -func NewDestinationRateTiming(destinationRatesTag string, timing *Timing, weight string) (rt *DestinationRateTiming) { +func NewDestinationRateTiming(destinationRates []*DestinationRate, timing *Timing, weight string) (drt *DestinationRateTiming) { w, err := strconv.ParseFloat(weight, 64) if err != nil { log.Printf("Error parsing weight unit from: %v", weight) return } - rt = &DestinationRateTiming{ - DestinationRatesTag: destinationRatesTag, - Weight: w, - timing: timing, + drt = &DestinationRateTiming{ + destinationRates: destinationRates, + timing: timing, + Weight: w, } return } -func (rt *DestinationRateTiming) GetInterval(dr *DestinationRate) (i *Interval) { - i = &Interval{ - Years: rt.timing.Years, - Months: rt.timing.Months, - MonthDays: rt.timing.MonthDays, - WeekDays: rt.timing.WeekDays, - StartTime: rt.timing.StartTime, - Weight: rt.Weight, - ConnectFee: dr.Rate.ConnectFee, - Prices: PriceGroups{&Price{ - GroupIntervalStart: dr.Rate.GroupIntervalStart, - Value: dr.Rate.Price, - RateIncrement: dr.Rate.RateIncrement, - RateUnit: dr.Rate.RateUnit, - }}, +func (drt *DestinationRateTiming) GetRateInterval(dr *DestinationRate) (i *RateInterval) { + i = &RateInterval{ + Years: drt.timing.Years, + Months: drt.timing.Months, + MonthDays: drt.timing.MonthDays, + WeekDays: drt.timing.WeekDays, + StartTime: drt.timing.StartTime, + Weight: drt.Weight, + ConnectFee: dr.rates[0].ConnectFee, + RoundingMethod: dr.rates[0].RoundingMethod, + RoundingDecimals: dr.rates[0].RoundingDecimals, + } + for _, rl := range dr.rates { + i.Rates = append(i.Rates, &Rate{ + GroupIntervalStart: rl.GroupIntervalStart, + Value: rl.Price, + RateIncrement: rl.RateIncrement, + RateUnit: rl.RateUnit, + }) } return } diff --git a/engine/minute_buckets.go b/engine/minute_buckets.go deleted file mode 100644 index ae34d0fa7..000000000 --- a/engine/minute_buckets.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Rating system designed to be used in VoIP Carriers World -Copyright (C) 2013 ITsysCOM - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see -*/ - -package engine - -import ( - "math" - "sort" - "time" -) - -type MinuteBucket struct { - Seconds float64 - Weight float64 - Price float64 // percentage from standard price or absolute value depending on Type - PriceType string - DestinationId string - ExpirationDate time.Time - precision int -} - -const ( - PERCENT = "*percent" - ABSOLUTE = "*absolute" -) - -// Returns the available number of seconds for a specified credit -func (mb *MinuteBucket) GetSecondsForCredit(credit float64) (seconds float64) { - seconds = mb.Seconds - if mb.Price > 0 { - seconds = math.Min(credit/mb.Price, mb.Seconds) - } - return -} - -// Creates a similar minute -func (mb *MinuteBucket) Clone() *MinuteBucket { - return &MinuteBucket{ - Seconds: mb.Seconds, - Weight: mb.Weight, - Price: mb.Price, - PriceType: mb.PriceType, - DestinationId: mb.DestinationId, - } -} - -// Equal method -func (mb *MinuteBucket) Equal(o *MinuteBucket) bool { - return mb.DestinationId == o.DestinationId && - mb.Weight == o.Weight && - mb.Price == o.Price && - mb.PriceType == o.PriceType && - mb.ExpirationDate.Equal(o.ExpirationDate) -} - -func (mb *MinuteBucket) IsExpired() bool { - return !mb.ExpirationDate.IsZero() && mb.ExpirationDate.Before(time.Now()) -} - -/* -Structure to store minute buckets according to weight, precision or price. -*/ -type bucketsorter []*MinuteBucket - -func (bs bucketsorter) Len() int { - return len(bs) -} - -func (bs bucketsorter) Swap(i, j int) { - bs[i], bs[j] = bs[j], bs[i] -} - -func (bs bucketsorter) Less(j, i int) bool { - return bs[i].Weight < bs[j].Weight || - bs[i].precision < bs[j].precision || - bs[i].Price > bs[j].Price -} - -func (bs bucketsorter) Sort() { - sort.Sort(bs) -} diff --git a/engine/minute_buckets_test.go b/engine/minute_buckets_test.go deleted file mode 100644 index 55bc2f44f..000000000 --- a/engine/minute_buckets_test.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Rating system designed to be used in VoIP Carriers World -Copyright (C) 2013 ITsysCOM - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see -*/ - -package engine - -import ( - "reflect" - "testing" -) - -func TestMinutBucketSortWeight(t *testing.T) { - mb1 := &MinuteBucket{Weight: 1, precision: 2, Price: 2} - mb2 := &MinuteBucket{Weight: 2, precision: 1, Price: 1} - var bs bucketsorter - bs = append(bs, mb2, mb1) - bs.Sort() - if bs[0] != mb1 || bs[1] != mb2 { - t.Error("Buckets not sorted by weight!") - } -} - -func TestMinutBucketSortPrecision(t *testing.T) { - mb1 := &MinuteBucket{Weight: 1, precision: 2, Price: 2} - mb2 := &MinuteBucket{Weight: 1, precision: 1, Price: 1} - var bs bucketsorter - bs = append(bs, mb2, mb1) - bs.Sort() - if bs[0] != mb1 || bs[1] != mb2 { - t.Error("Buckets not sorted by precision!") - } -} - -func TestMinutBucketSortPrice(t *testing.T) { - mb1 := &MinuteBucket{Weight: 1, precision: 1, Price: 1} - mb2 := &MinuteBucket{Weight: 1, precision: 1, Price: 2} - var bs bucketsorter - bs = append(bs, mb2, mb1) - bs.Sort() - if bs[0] != mb1 || bs[1] != mb2 { - t.Error("Buckets not sorted by price!") - } -} - -func TestMinutBucketEqual(t *testing.T) { - mb1 := &MinuteBucket{Weight: 1, precision: 1, Price: 1, PriceType: ABSOLUTE, DestinationId: ""} - mb2 := &MinuteBucket{Weight: 1, precision: 1, Price: 1, PriceType: ABSOLUTE, DestinationId: ""} - mb3 := &MinuteBucket{Weight: 1, precision: 1, Price: 2, PriceType: ABSOLUTE, DestinationId: ""} - if !mb1.Equal(mb2) || mb2.Equal(mb3) { - t.Error("Equal failure!", mb1, mb2, mb3) - } -} - -func TestMinutBucketClone(t *testing.T) { - mb1 := &MinuteBucket{Seconds: 1, Weight: 2, Price: 3, PriceType: ABSOLUTE, DestinationId: "5"} - mb2 := mb1.Clone() - if mb1 == mb2 || !reflect.DeepEqual(mb1, mb2) { - t.Error("Cloning failure: ", mb1, mb2) - } -} diff --git a/engine/interval.go b/engine/rateinterval.go similarity index 74% rename from engine/interval.go rename to engine/rateinterval.go index 1181a430b..c6da5ed9f 100644 --- a/engine/interval.go +++ b/engine/rateinterval.go @@ -21,7 +21,6 @@ package engine import ( "fmt" "github.com/cgrates/cgrates/utils" - "math" "reflect" "sort" "strconv" @@ -32,48 +31,51 @@ import ( /* Defines a time interval for which a certain set of prices will apply */ -type Interval struct { +type RateInterval struct { Years Years Months Months MonthDays MonthDays WeekDays WeekDays StartTime, EndTime string // ##:##:## format Weight, ConnectFee float64 - Prices PriceGroups // GroupInterval (start time): Price - RoundingMethod string + Rates RateGroups // GroupRateInterval (start time): Rate + RoundingMethod string //ROUNDING_UP, ROUNDING_DOWN, ROUNDING_MIDDLE RoundingDecimals int } -type Price struct { +type Rate struct { GroupIntervalStart time.Duration Value float64 RateIncrement time.Duration RateUnit time.Duration } -func (p *Price) Equal(o *Price) bool { - return p.GroupIntervalStart == o.GroupIntervalStart && p.Value == o.Value && p.RateIncrement == o.RateIncrement && p.RateUnit == o.RateUnit +func (p *Rate) Equal(o *Rate) bool { + return p.GroupIntervalStart == o.GroupIntervalStart && + p.Value == o.Value && + p.RateIncrement == o.RateIncrement && + p.RateUnit == o.RateUnit } -type PriceGroups []*Price +type RateGroups []*Rate -func (pg PriceGroups) Len() int { +func (pg RateGroups) Len() int { return len(pg) } -func (pg PriceGroups) Swap(i, j int) { +func (pg RateGroups) Swap(i, j int) { pg[i], pg[j] = pg[j], pg[i] } -func (pg PriceGroups) Less(i, j int) bool { +func (pg RateGroups) Less(i, j int) bool { return pg[i].GroupIntervalStart < pg[j].GroupIntervalStart } -func (pg PriceGroups) Sort() { +func (pg RateGroups) Sort() { sort.Sort(pg) } -func (pg PriceGroups) Equal(og PriceGroups) bool { +func (pg RateGroups) Equal(og RateGroups) bool { if len(pg) != len(og) { return false } @@ -85,7 +87,7 @@ func (pg PriceGroups) Equal(og PriceGroups) bool { return true } -func (pg *PriceGroups) AddPrice(ps ...*Price) { +func (pg *RateGroups) AddRate(ps ...*Rate) { for _, p := range ps { found := false for _, op := range *pg { @@ -103,7 +105,7 @@ func (pg *PriceGroups) AddPrice(ps ...*Price) { /* Returns true if the received time result inside the interval */ -func (i *Interval) Contains(t time.Time) bool { +func (i *RateInterval) Contains(t time.Time) bool { // check for years if len(i.Years) > 0 && !i.Years.Contains(t.Year()) { return false @@ -152,7 +154,7 @@ func (i *Interval) Contains(t time.Time) bool { /* Returns a time object that represents the end of the interval realtive to the received time */ -func (i *Interval) getRightMargin(t time.Time) (rigthtTime time.Time) { +func (i *RateInterval) getRightMargin(t time.Time) (rigthtTime time.Time) { year, month, day := t.Year(), t.Month(), t.Day() hour, min, sec, nsec := 23, 59, 59, 0 loc := t.Location() @@ -168,7 +170,7 @@ func (i *Interval) getRightMargin(t time.Time) (rigthtTime time.Time) { /* Returns a time object that represents the start of the interval realtive to the received time */ -func (i *Interval) getLeftMargin(t time.Time) (rigthtTime time.Time) { +func (i *RateInterval) getLeftMargin(t time.Time) (rigthtTime time.Time) { year, month, day := t.Year(), t.Month(), t.Day() hour, min, sec, nsec := 0, 0, 0, 0 loc := t.Location() @@ -181,11 +183,11 @@ func (i *Interval) getLeftMargin(t time.Time) (rigthtTime time.Time) { return time.Date(year, month, day, hour, min, sec, nsec, loc) } -func (i *Interval) String() string { +func (i *RateInterval) String_DISABLED() string { return fmt.Sprintf("%v %v %v %v %v %v", i.Years, i.Months, i.MonthDays, i.WeekDays, i.StartTime, i.EndTime) } -func (i *Interval) Equal(o *Interval) bool { +func (i *RateInterval) Equal(o *RateInterval) bool { return reflect.DeepEqual(i.Years, o.Years) && reflect.DeepEqual(i.Months, o.Months) && reflect.DeepEqual(i.MonthDays, o.MonthDays) && @@ -194,21 +196,20 @@ func (i *Interval) Equal(o *Interval) bool { i.EndTime == o.EndTime } -func (i *Interval) GetCost(duration, startSecond time.Duration) (cost float64) { - price, rateIncrement, rateUnit := i.GetPriceParameters(startSecond) - d := float64(duration.Seconds()) +func (i *RateInterval) GetCost(duration, startSecond time.Duration) float64 { + price, _, rateUnit := i.GetRateParameters(startSecond) + d := duration.Seconds() price /= rateUnit.Seconds() - ri := rateIncrement.Seconds() - cost = math.Ceil(d/ri) * ri * price - return utils.Round(cost, i.RoundingDecimals, i.RoundingMethod) + + return utils.Round(d*price, i.RoundingDecimals, i.RoundingMethod) } // Gets the price for a the provided start second -func (i *Interval) GetPriceParameters(startSecond time.Duration) (price float64, rateIncrement, rateUnit time.Duration) { - i.Prices.Sort() - for index, price := range i.Prices { - if price.GroupIntervalStart <= startSecond && (index == len(i.Prices)-1 || - i.Prices[index+1].GroupIntervalStart > startSecond) { +func (i *RateInterval) GetRateParameters(startSecond time.Duration) (price float64, rateIncrement, rateUnit time.Duration) { + i.Rates.Sort() + for index, price := range i.Rates { + if price.GroupIntervalStart <= startSecond && (index == len(i.Rates)-1 || + i.Rates[index+1].GroupIntervalStart > startSecond) { if price.RateIncrement == 0 { price.RateIncrement = 1 * time.Second } @@ -222,20 +223,20 @@ func (i *Interval) GetPriceParameters(startSecond time.Duration) (price float64, } // Structure to store intervals according to weight -type IntervalList []*Interval +type RateIntervalList []*RateInterval -func (il IntervalList) Len() int { +func (il RateIntervalList) Len() int { return len(il) } -func (il IntervalList) Swap(i, j int) { +func (il RateIntervalList) Swap(i, j int) { il[i], il[j] = il[j], il[i] } -func (il IntervalList) Less(i, j int) bool { +func (il RateIntervalList) Less(i, j int) bool { return il[i].Weight < il[j].Weight } -func (il IntervalList) Sort() { +func (il RateIntervalList) Sort() { sort.Sort(il) } diff --git a/engine/interval_test.go b/engine/rateinterval_test.go similarity index 74% rename from engine/interval_test.go rename to engine/rateinterval_test.go index ec8da2893..e3daf02e3 100644 --- a/engine/interval_test.go +++ b/engine/rateinterval_test.go @@ -23,8 +23,8 @@ import ( "time" ) -func TestIntervalMonth(t *testing.T) { - i := &Interval{Months: Months{time.February}} +func TestRateIntervalMonth(t *testing.T) { + i := &RateInterval{Months: Months{time.February}} d := time.Date(2012, time.February, 10, 23, 0, 0, 0, time.UTC) d1 := time.Date(2012, time.January, 10, 23, 0, 0, 0, time.UTC) if !i.Contains(d) { @@ -35,8 +35,8 @@ func TestIntervalMonth(t *testing.T) { } } -func TestIntervalMonthDay(t *testing.T) { - i := &Interval{MonthDays: MonthDays{10}} +func TestRateIntervalMonthDay(t *testing.T) { + i := &RateInterval{MonthDays: MonthDays{10}} d := time.Date(2012, time.February, 10, 23, 0, 0, 0, time.UTC) d1 := time.Date(2012, time.February, 11, 23, 0, 0, 0, time.UTC) if !i.Contains(d) { @@ -47,8 +47,8 @@ func TestIntervalMonthDay(t *testing.T) { } } -func TestIntervalMonthAndMonthDay(t *testing.T) { - i := &Interval{Months: Months{time.February}, MonthDays: MonthDays{10}} +func TestRateIntervalMonthAndMonthDay(t *testing.T) { + i := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{10}} d := time.Date(2012, time.February, 10, 23, 0, 0, 0, time.UTC) d1 := time.Date(2012, time.February, 11, 23, 0, 0, 0, time.UTC) d2 := time.Date(2012, time.January, 10, 23, 0, 0, 0, time.UTC) @@ -63,9 +63,9 @@ func TestIntervalMonthAndMonthDay(t *testing.T) { } } -func TestIntervalWeekDays(t *testing.T) { - i := &Interval{WeekDays: []time.Weekday{time.Wednesday}} - i2 := &Interval{WeekDays: []time.Weekday{time.Wednesday, time.Thursday}} +func TestRateIntervalWeekDays(t *testing.T) { + i := &RateInterval{WeekDays: []time.Weekday{time.Wednesday}} + i2 := &RateInterval{WeekDays: []time.Weekday{time.Wednesday, time.Thursday}} d := time.Date(2012, time.February, 1, 23, 0, 0, 0, time.UTC) d1 := time.Date(2012, time.February, 2, 23, 0, 0, 0, time.UTC) if !i.Contains(d) { @@ -82,9 +82,9 @@ func TestIntervalWeekDays(t *testing.T) { } } -func TestIntervalMonthAndMonthDayAndWeekDays(t *testing.T) { - i := &Interval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday}} - i2 := &Interval{Months: Months{time.February}, MonthDays: MonthDays{2}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}} +func TestRateIntervalMonthAndMonthDayAndWeekDays(t *testing.T) { + i := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday}} + i2 := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{2}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}} d := time.Date(2012, time.February, 1, 23, 0, 0, 0, time.UTC) d1 := time.Date(2012, time.February, 2, 23, 0, 0, 0, time.UTC) if !i.Contains(d) { @@ -101,8 +101,8 @@ func TestIntervalMonthAndMonthDayAndWeekDays(t *testing.T) { } } -func TestIntervalHours(t *testing.T) { - i := &Interval{StartTime: "14:30:00", EndTime: "15:00:00"} +func TestRateIntervalHours(t *testing.T) { + i := &RateInterval{StartTime: "14:30:00", EndTime: "15:00:00"} d := time.Date(2012, time.February, 10, 14, 30, 1, 0, time.UTC) d1 := time.Date(2012, time.January, 10, 14, 29, 0, 0, time.UTC) d2 := time.Date(2012, time.January, 10, 14, 59, 0, 0, time.UTC) @@ -121,8 +121,8 @@ func TestIntervalHours(t *testing.T) { } } -func TestIntervalEverything(t *testing.T) { - i := &Interval{Months: Months{time.February}, +func TestRateIntervalEverything(t *testing.T) { + i := &RateInterval{Months: Months{time.February}, Years: Years{2012}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, @@ -150,13 +150,13 @@ func TestIntervalEverything(t *testing.T) { } } -func TestIntervalEqual(t *testing.T) { - i1 := &Interval{Months: Months{time.February}, +func TestRateIntervalEqual(t *testing.T) { + i1 := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - i2 := &Interval{Months: Months{time.February}, + i2 := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", @@ -166,13 +166,13 @@ func TestIntervalEqual(t *testing.T) { } } -func TestIntervalNotEqual(t *testing.T) { - i1 := &Interval{Months: Months{time.February}, +func TestRateIntervalNotEqual(t *testing.T) { + i1 := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday}, StartTime: "14:30:00", EndTime: "15:00:00"} - i2 := &Interval{Months: Months{time.February}, + i2 := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", @@ -182,8 +182,13 @@ func TestIntervalNotEqual(t *testing.T) { } } -func BenchmarkIntervalContainsDate(b *testing.B) { - i := &Interval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} +func TestRateIntervalGetCost(t *testing.T) { +} + +/*********************************Benchmarks**************************************/ + +func BenchmarkRateIntervalContainsDate(b *testing.B) { + i := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} d := time.Date(2012, time.February, 1, 14, 30, 0, 0, time.UTC) for x := 0; x < b.N; x++ { i.Contains(d) diff --git a/engine/activationperiod.go b/engine/ratingplan.go similarity index 73% rename from engine/activationperiod.go rename to engine/ratingplan.go index deed2c518..0b6211630 100644 --- a/engine/activationperiod.go +++ b/engine/ratingplan.go @@ -26,36 +26,35 @@ import ( /* The struture that is saved to storage. */ -type ActivationPeriod struct { +type RatingPlan struct { ActivationTime time.Time - Intervals IntervalList + RateIntervals RateIntervalList } -type xCachedActivationPeriods struct { +type xCachedRatingPlans struct { destPrefix string - aps []*ActivationPeriod + aps []*RatingPlan *cache2go.XEntry } /* Adds one ore more intervals to the internal interval list only if it is not allready in the list. */ -func (ap *ActivationPeriod) AddInterval(is ...*Interval) { - for _, i := range is { +func (ap *RatingPlan) AddRateInterval(ris ...*RateInterval) { + for _, ri := range ris { found := false - for _, ei := range ap.Intervals { - if i.Equal(ei) { - (&ei.Prices).AddPrice(i.Prices...) + for _, eri := range ap.RateIntervals { + if ri.Equal(eri) { found = true break } } if !found { - ap.Intervals = append(ap.Intervals, i) + ap.RateIntervals = append(ap.RateIntervals, ri) } } } -func (ap *ActivationPeriod) Equal(o *ActivationPeriod) bool { +func (ap *RatingPlan) Equal(o *RatingPlan) bool { return ap.ActivationTime == o.ActivationTime } diff --git a/engine/activationperiod_test.go b/engine/ratingplan_test.go similarity index 61% rename from engine/activationperiod_test.go rename to engine/ratingplan_test.go index c1aff6236..e22413b77 100644 --- a/engine/activationperiod_test.go +++ b/engine/ratingplan_test.go @@ -32,23 +32,23 @@ func TestApRestoreFromStorage(t *testing.T) { Tenant: "CUSTOMER_1", Subject: "rif:from:tm", Destination: "49"} - cd.LoadActivationPeriods() - if len(cd.ActivationPeriods) != 2 { - t.Error("Error restoring activation periods: ", len(cd.ActivationPeriods)) + cd.LoadRatingPlans() + if len(cd.RatingPlans) != 2 { + t.Error("Error restoring activation periods: ", len(cd.RatingPlans)) } } func TestApStoreRestoreJson(t *testing.T) { d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) result, _ := json.Marshal(ap) - ap1 := &ActivationPeriod{} + ap1 := &RatingPlan{} json.Unmarshal(result, ap1) if !reflect.DeepEqual(ap, ap1) { t.Errorf("Expected %v was %v", ap, ap1) @@ -57,11 +57,11 @@ func TestApStoreRestoreJson(t *testing.T) { func TestApStoreRestoreBlank(t *testing.T) { d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + i := &RateInterval{} + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) result, _ := json.Marshal(ap) - ap1 := ActivationPeriod{} + ap1 := RatingPlan{} json.Unmarshal(result, &ap1) if reflect.DeepEqual(ap, ap1) { t.Errorf("Expected %v was %v", ap, ap1) @@ -70,116 +70,116 @@ func TestApStoreRestoreBlank(t *testing.T) { func TestFallbackDirect(t *testing.T) { cd := &CallDescriptor{TOR: "0", Direction: OUTBOUND, Tenant: "CUSTOMER_2", Subject: "danb:87.139.12.167", Destination: "41"} - cd.LoadActivationPeriods() - if len(cd.ActivationPeriods) != 1 { - t.Error("Error restoring activation periods: ", len(cd.ActivationPeriods)) + cd.LoadRatingPlans() + if len(cd.RatingPlans) != 1 { + t.Error("Error restoring activation periods: ", len(cd.RatingPlans)) } } func TestFallbackMultiple(t *testing.T) { cd := &CallDescriptor{TOR: "0", Direction: OUTBOUND, Tenant: "vdf", Subject: "fall", Destination: "0723045"} - cd.LoadActivationPeriods() - if len(cd.ActivationPeriods) != 1 { - t.Error("Error restoring activation periods: ", len(cd.ActivationPeriods)) + cd.LoadRatingPlans() + if len(cd.RatingPlans) != 2 { + t.Errorf("Error restoring rating plans: %#v", cd.RatingPlans) } } func TestFallbackWithBackTrace(t *testing.T) { cd := &CallDescriptor{TOR: "0", Direction: OUTBOUND, Tenant: "CUSTOMER_2", Subject: "danb:87.139.12.167", Destination: "4123"} - cd.LoadActivationPeriods() - if len(cd.ActivationPeriods) != 1 { - t.Error("Error restoring activation periods: ", len(cd.ActivationPeriods)) + cd.LoadRatingPlans() + if len(cd.RatingPlans) != 1 { + t.Error("Error restoring activation periods: ", len(cd.RatingPlans)) } } func TestFallbackDefault(t *testing.T) { cd := &CallDescriptor{TOR: "0", Direction: OUTBOUND, Tenant: "vdf", Subject: "one", Destination: "0723"} - cd.LoadActivationPeriods() - if len(cd.ActivationPeriods) != 1 { - t.Error("Error restoring activation periods: ", len(cd.ActivationPeriods)) + cd.LoadRatingPlans() + if len(cd.RatingPlans) != 1 { + t.Error("Error restoring activation periods: ", len(cd.RatingPlans)) } } func TestFallbackNoInfiniteLoop(t *testing.T) { cd := &CallDescriptor{TOR: "0", Direction: OUTBOUND, Tenant: "vdf", Subject: "rif", Destination: "0721"} - cd.LoadActivationPeriods() - if len(cd.ActivationPeriods) != 0 { - t.Error("Error restoring activation periods: ", len(cd.ActivationPeriods)) + cd.LoadRatingPlans() + if len(cd.RatingPlans) != 0 { + t.Error("Error restoring activation periods: ", len(cd.RatingPlans)) } } func TestFallbackNoInfiniteLoopSelf(t *testing.T) { cd := &CallDescriptor{TOR: "0", Direction: OUTBOUND, Tenant: "vdf", Subject: "inf", Destination: "0721"} - cd.LoadActivationPeriods() - if len(cd.ActivationPeriods) != 0 { - t.Error("Error restoring activation periods: ", len(cd.ActivationPeriods)) + cd.LoadRatingPlans() + if len(cd.RatingPlans) != 0 { + t.Error("Error restoring activation periods: ", len(cd.RatingPlans)) } } func TestApAddIntervalIfNotPresent(t *testing.T) { - i1 := &Interval{Months: Months{time.February}, + i1 := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - i2 := &Interval{Months: Months{time.February}, + i2 := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - i3 := &Interval{Months: Months{time.February}, + i3 := &RateInterval{Months: Months{time.February}, MonthDays: MonthDays{1}, WeekDays: []time.Weekday{time.Wednesday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{} - ap.AddInterval(i1) - ap.AddInterval(i2) - if len(ap.Intervals) != 1 { + ap := &RatingPlan{} + ap.AddRateInterval(i1) + ap.AddRateInterval(i2) + if len(ap.RateIntervals) != 1 { t.Error("Wronfully appended interval ;)") } - ap.AddInterval(i3) - if len(ap.Intervals) != 2 { + ap.AddRateInterval(i3) + if len(ap.RateIntervals) != 2 { t.Error("Wronfully not appended interval ;)") } } -func TestApAddIntervalGroups(t *testing.T) { - i1 := &Interval{ - Prices: PriceGroups{&Price{0, 1, 1 * time.Second, 1 * time.Second}}, +func TestApAddRateIntervalGroups(t *testing.T) { + i1 := &RateInterval{ + Rates: RateGroups{&Rate{0, 1, 1 * time.Second, 1 * time.Second}}, } - i2 := &Interval{ - Prices: PriceGroups{&Price{30 * time.Second, 2, 1 * time.Second, 1 * time.Second}}, + i2 := &RateInterval{ + Rates: RateGroups{&Rate{30 * time.Second, 2, 1 * time.Second, 1 * time.Second}}, } - i3 := &Interval{ - Prices: PriceGroups{&Price{30 * time.Second, 2, 1 * time.Second, 1 * time.Second}}, + i3 := &RateInterval{ + Rates: RateGroups{&Rate{30 * time.Second, 2, 1 * time.Second, 1 * time.Second}}, } - ap := &ActivationPeriod{} - ap.AddInterval(i1) - ap.AddInterval(i2) - ap.AddInterval(i3) - if len(ap.Intervals) != 1 { + ap := &RatingPlan{} + ap.AddRateInterval(i1) + ap.AddRateInterval(i2) + ap.AddRateInterval(i3) + if len(ap.RateIntervals) != 1 { t.Error("Wronfully appended interval ;)") } - if len(ap.Intervals[0].Prices) != 2 { - t.Error("Group prices not formed: ", ap.Intervals[0].Prices) + if len(ap.RateIntervals[0].Rates) != 1 { + t.Errorf("Group prices not formed: %#v", ap.RateIntervals[0].Rates[0]) } } /**************************** Benchmarks *************************************/ -func BenchmarkActivationPeriodStoreRestoreJson(b *testing.B) { +func BenchmarkRatingPlanStoreRestoreJson(b *testing.B) { b.StopTimer() d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) - ap1 := ActivationPeriod{} + ap1 := RatingPlan{} b.StartTimer() for i := 0; i < b.N; i++ { result, _ := json.Marshal(ap) @@ -187,18 +187,18 @@ func BenchmarkActivationPeriodStoreRestoreJson(b *testing.B) { } } -func BenchmarkActivationPeriodStoreRestore(b *testing.B) { +func BenchmarkRatingPlanStoreRestore(b *testing.B) { b.StopTimer() d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) - ap1 := &ActivationPeriod{} + ap1 := &RatingPlan{} b.StartTimer() for i := 0; i < b.N; i++ { result, _ := marsh.Marshal(ap) diff --git a/engine/ratingprofile.go b/engine/ratingprofile.go index 4199ddd8b..0a9bbe84d 100644 --- a/engine/ratingprofile.go +++ b/engine/ratingprofile.go @@ -23,46 +23,42 @@ import ( "fmt" ) -const ( - // the minimum length for a destination prefix to be matched. - MIN_PREFIX_LENGTH = 2 -) - type RatingProfile struct { Id string FallbackKey string // FallbackKey is used as complete combination of Tenant:TOR:Direction:Subject - DestinationMap map[string][]*ActivationPeriod + DestinationMap map[string][]*RatingPlan Tag, Tenant, TOR, Direction, Subject, DestRatesTimingTag, RatesFallbackSubject, ActivationTime string // used only for loading } // Adds an activation period that applyes to current rating profile if not already present. -func (rp *RatingProfile) AddActivationPeriodIfNotPresent(destInfo string, aps ...*ActivationPeriod) { +func (rp *RatingProfile) AddRatingPlanIfNotPresent(destInfo string, plans ...*RatingPlan) { if rp.DestinationMap == nil { - rp.DestinationMap = make(map[string][]*ActivationPeriod, 1) + rp.DestinationMap = make(map[string][]*RatingPlan, 1) } - for _, ap := range aps { + for _, plan := range plans { found := false - for _, eap := range rp.DestinationMap[destInfo] { - if ap.Equal(eap) { + for _, existingPlan := range rp.DestinationMap[destInfo] { + if plan.Equal(existingPlan) { + existingPlan.AddRateInterval(plan.RateIntervals...) found = true break } } if !found { - rp.DestinationMap[destInfo] = append(rp.DestinationMap[destInfo], ap) + rp.DestinationMap[destInfo] = append(rp.DestinationMap[destInfo], plan) } } } -func (rp *RatingProfile) GetActivationPeriodsForPrefix(destPrefix string) (foundPrefix string, aps []*ActivationPeriod, err error) { +func (rp *RatingProfile) GetRatingPlansForPrefix(destPrefix string) (foundPrefix string, aps []*RatingPlan, err error) { bestPrecision := 0 - for k, v := range rp.DestinationMap { - d, err := GetDestination(k) + for dId, v := range rp.DestinationMap { + precision, err := storageGetter.DestinationContainsPrefix(dId, destPrefix) if err != nil { - Logger.Err(fmt.Sprintf("Cannot find destination with id: %s", k)) + Logger.Err(fmt.Sprintf("Error checking destination: %v", err)) continue } - if precision, ok := d.containsPrefix(destPrefix); ok && precision > bestPrecision { + if precision > bestPrecision { bestPrecision = precision aps = v } diff --git a/engine/ratingprofile_test.go b/engine/ratingprofile_test.go index 76e0f78e3..cd7afc6cc 100644 --- a/engine/ratingprofile_test.go +++ b/engine/ratingprofile_test.go @@ -24,16 +24,16 @@ import ( ) func TestRpAddAPIfNotPresent(t *testing.T) { - ap1 := &ActivationPeriod{ActivationTime: time.Date(2012, time.July, 2, 14, 24, 30, 0, time.UTC)} - ap2 := &ActivationPeriod{ActivationTime: time.Date(2012, time.July, 2, 14, 24, 30, 0, time.UTC)} - ap3 := &ActivationPeriod{ActivationTime: time.Date(2012, time.July, 2, 14, 24, 30, 1, time.UTC)} + ap1 := &RatingPlan{ActivationTime: time.Date(2012, time.July, 2, 14, 24, 30, 0, time.UTC)} + ap2 := &RatingPlan{ActivationTime: time.Date(2012, time.July, 2, 14, 24, 30, 0, time.UTC)} + ap3 := &RatingPlan{ActivationTime: time.Date(2012, time.July, 2, 14, 24, 30, 1, time.UTC)} rp := &RatingProfile{} - rp.AddActivationPeriodIfNotPresent("test", ap1) - rp.AddActivationPeriodIfNotPresent("test", ap2) + rp.AddRatingPlanIfNotPresent("test", ap1) + rp.AddRatingPlanIfNotPresent("test", ap2) if len(rp.DestinationMap["test"]) != 1 { t.Error("Wronfully appended activation period ;)", len(rp.DestinationMap["test"])) } - rp.AddActivationPeriodIfNotPresent("test", ap3) + rp.AddRatingPlanIfNotPresent("test", ap3) if len(rp.DestinationMap["test"]) != 2 { t.Error("Wronfully not appended activation period ;)", len(rp.DestinationMap["test"])) } diff --git a/engine/responder.go b/engine/responder.go index 413ccfad8..73bc44a30 100644 --- a/engine/responder.go +++ b/engine/responder.go @@ -76,6 +76,18 @@ func (rs *Responder) MaxDebit(arg CallDescriptor, reply *CallCost) (err error) { return } +func (rs *Responder) RefoundIncrements(arg CallDescriptor, reply *float64) (err error) { + if rs.Bal != nil { + *reply, err = rs.callMethod(&arg, "Responder.RefoundIncrements") + } else { + r, e := AccLock.Guard(arg.GetUserBalanceKey(), func() (float64, error) { + return arg.RefoundIncrements() + }) + *reply, err = r, e + } + return +} + func (rs *Responder) DebitCents(arg CallDescriptor, reply *float64) (err error) { if rs.Bal != nil { *reply, err = rs.callMethod(&arg, "Responder.DebitCents") @@ -169,7 +181,7 @@ func (rs *Responder) Shutdown(arg string, reply *string) (err error) { if rs.Bal != nil { rs.Bal.Shutdown("Responder.Shutdown") } - storageGetter.Close() + storageGetter.(Storage).Close() defer func() { rs.ExitChan <- true }() *reply = "Done!" return @@ -340,6 +352,7 @@ type Connector interface { GetCost(CallDescriptor, *CallCost) error Debit(CallDescriptor, *CallCost) error MaxDebit(CallDescriptor, *CallCost) error + RefoundIncrements(CallDescriptor, *float64) error DebitCents(CallDescriptor, *float64) error DebitSeconds(CallDescriptor, *float64) error GetMaxSessionTime(CallDescriptor, *float64) error @@ -360,6 +373,9 @@ func (rcc *RPCClientConnector) Debit(cd CallDescriptor, cc *CallCost) error { func (rcc *RPCClientConnector) MaxDebit(cd CallDescriptor, cc *CallCost) error { return rcc.Client.Call("Responder.MaxDebit", cd, cc) } +func (rcc *RPCClientConnector) RefoundIncrements(cd CallDescriptor, resp *float64) error { + return rcc.Client.Call("Responder.RefoundIncrements", cd, resp) +} func (rcc *RPCClientConnector) DebitCents(cd CallDescriptor, resp *float64) error { return rcc.Client.Call("Responder.DebitCents", cd, resp) } diff --git a/engine/storage_interface.go b/engine/storage_interface.go index ce760eba2..03fa0a14f 100644 --- a/engine/storage_interface.go +++ b/engine/storage_interface.go @@ -35,6 +35,7 @@ const ( ACTION_PREFIX = "act_" USER_BALANCE_PREFIX = "ubl_" DESTINATION_PREFIX = "dst_" + TEMP_DESTINATION_PREFIX = "tmp_" LOG_CALL_COST_PREFIX = "cco_" LOG_ACTION_TIMMING_PREFIX = "ltm_" LOG_ACTION_TRIGGER_PREFIX = "ltr_" @@ -48,23 +49,49 @@ const ( RATER_SOURCE = "RAT" ) -var ( - // for codec msgpack - mapStrIntfTyp = reflect.TypeOf(map[string]interface{}(nil)) - sliceByteTyp = reflect.TypeOf([]byte(nil)) - timeTyp = reflect.TypeOf(time.Time{}) -) +type Storage interface { + Close() + Flush() error +} /* Interface for storage providers. */ type DataStorage interface { - Close() - Flush() error + Storage GetRatingProfile(string) (*RatingProfile, error) SetRatingProfile(*RatingProfile) error GetDestination(string) (*Destination, error) + DestinationContainsPrefix(string, string) (int, error) SetDestination(*Destination) error + GetActions(string) (Actions, error) + SetActions(string, Actions) error + GetUserBalance(string) (*UserBalance, error) + SetUserBalance(*UserBalance) error + GetActionTimings(string) (ActionTimings, error) + SetActionTimings(string, ActionTimings) error + GetAllActionTimings() (map[string]ActionTimings, error) +} + +type CdrStorage interface { + Storage + SetCdr(utils.CDR) error + SetRatedCdr(utils.CDR, *CallCost, string) error + GetAllRatedCdr() ([]utils.CDR, error) +} + +type LogStorage interface { + Storage + //GetAllActionTimingsLogs() (map[string]ActionsTimings, error) + LogCallCost(uuid, source string, cc *CallCost) error + LogError(uuid, source, errstr string) error + LogActionTrigger(ubId, source string, at *ActionTrigger, as Actions) error + LogActionTiming(source string, at *ActionTiming, as Actions) error + GetCallCostLog(uuid, source string) (*CallCost, error) +} + +type LoadStorage interface { + Storage // Apier functions GetTPIds() ([]string, error) SetTPTiming(string, *Timing) error @@ -76,7 +103,7 @@ type DataStorage interface { GetTPDestination(string, string) (*Destination, error) GetTPDestinationIds(string) ([]string, error) ExistsTPRate(string, string) (bool, error) - SetTPRates(string, map[string][]*Rate) error + SetTPRates(string, map[string][]*LoadRate) error GetTPRate(string, string) (*utils.TPRate, error) GetTPRateIds(string) ([]string, error) ExistsTPDestinationRate(string, string) (bool, error) @@ -105,27 +132,10 @@ type DataStorage interface { ExistsTPAccountActions(string, string) (bool, error) SetTPAccountActions(string, map[string]*AccountAction) error GetTPAccountActionIds(string) ([]string, error) - // End Apier functions - GetActions(string) (Actions, error) - SetActions(string, Actions) error - GetUserBalance(string) (*UserBalance, error) - SetUserBalance(*UserBalance) error - GetActionTimings(string) (ActionTimings, error) - SetActionTimings(string, ActionTimings) error - GetAllActionTimings() (map[string]ActionTimings, error) - SetCdr(utils.CDR) error - SetRatedCdr(utils.CDR, *CallCost, string) error - GetAllRatedCdr() ([]utils.CDR, error) - //GetAllActionTimingsLogs() (map[string]ActionsTimings, error) - LogCallCost(uuid, source string, cc *CallCost) error - LogError(uuid, source, errstr string) error - LogActionTrigger(ubId, source string, at *ActionTrigger, as Actions) error - LogActionTiming(source string, at *ActionTiming, as Actions) error - GetCallCostLog(uuid, source string) (*CallCost, error) // loader functions GetTpDestinations(string, string) ([]*Destination, error) GetTpTimings(string, string) (map[string]*Timing, error) - GetTpRates(string, string) (map[string]*Rate, error) + GetTpRates(string, string) (map[string][]*LoadRate, error) GetTpDestinationRates(string, string) (map[string][]*DestinationRate, error) GetTpDestinationRateTimings(string, string) ([]*DestinationRateTiming, error) GetTpRatingProfiles(string, string) (map[string]*RatingProfile, error) @@ -180,10 +190,13 @@ type CodecMsgpackMarshaler struct { func NewCodecMsgpackMarshaler() *CodecMsgpackMarshaler { cmm := &CodecMsgpackMarshaler{new(codec.MsgpackHandle)} mh := cmm.mh + var mapStrIntfTyp = reflect.TypeOf(map[string]interface{}(nil)) + //sliceByteTyp = reflect.TypeOf([]byte(nil)) + var timeTyp = reflect.TypeOf(time.Time{}) mh.MapType = mapStrIntfTyp // configure extensions for msgpack, to enable Binary and Time support for tags 0 and 1 - mh.AddExt(sliceByteTyp, 0, mh.BinaryEncodeExt, mh.BinaryDecodeExt) + //mh.AddExt(sliceByteTyp, 0, mh.BinaryEncodeExt, mh.BinaryDecodeExt) mh.AddExt(timeTyp, 1, mh.TimeEncodeExt, mh.TimeDecodeExt) return cmm } diff --git a/engine/storage_map.go b/engine/storage_map.go index 62245e231..d009632c5 100644 --- a/engine/storage_map.go +++ b/engine/storage_map.go @@ -32,7 +32,7 @@ type MapStorage struct { ms Marshaler } -func NewMapStorage() (DataStorage, error) { +func NewMapStorage() (Storage, error) { return &MapStorage{dict: make(map[string][]byte), ms: NewCodecMsgpackMarshaler()}, nil } @@ -70,6 +70,20 @@ func (ms *MapStorage) GetDestination(key string) (dest *Destination, err error) } return } + +func (ms *MapStorage) DestinationContainsPrefix(key string, prefix string) (precision int, err error) { + if d, err := ms.GetDestination(key); err != nil { + return 0, err + } else { + for _, p := range utils.SplitPrefix(prefix) { + if precision, ok := d.containsPrefix(p); ok { + return precision, nil + } + } + return precision, nil + } +} + func (ms *MapStorage) SetDestination(dest *Destination) (err error) { result, err := ms.ms.Marshal(dest) ms.dict[DESTINATION_PREFIX+dest.Id] = result diff --git a/engine/storage_mongo.go b/engine/storage_mongo.go index 313093139..f39fea872 100644 --- a/engine/storage_mongo.go +++ b/engine/storage_mongo.go @@ -19,10 +19,8 @@ along with this program. If not, see package engine import ( - "errors" "fmt" "github.com/cgrates/cgrates/history" - "github.com/cgrates/cgrates/utils" "labix.org/v2/mgo" "labix.org/v2/mgo/bson" "log" @@ -34,7 +32,7 @@ type MongoStorage struct { db *mgo.Database } -func NewMongoStorage(host, port, db, user, pass string) (DataStorage, error) { +func NewMongoStorage(host, port, db, user, pass string) (Storage, error) { dial := fmt.Sprintf(host) if user != "" && pass != "" { dial = fmt.Sprintf("%s:%s@%s", user, pass, dial) @@ -156,163 +154,6 @@ func (ms *MongoStorage) SetDestination(dest *Destination) error { return ms.db.C("destinations").Insert(dest) } -func (ms *MongoStorage) GetTPIds() ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPTiming(tpid string, tm *Timing) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPTiming(tpid, tmId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPTiming(tpid, tmId string) (*Timing, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPTimingIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPDestinationIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPDestination(tpid, destTag string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -// Extracts destinations from StorDB on specific tariffplan id -func (ms *MongoStorage) GetTPDestination(tpid, destTag string) (*Destination, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPDestination(tpid string, dest *Destination) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPRate(tpid, rtId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPRates(tpid string, rts map[string][]*Rate) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPRate(tpid, rtId string) (*utils.TPRate, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPRateIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPDestinationRate(tpid, drId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPDestinationRates(tpid string, drs map[string][]*DestinationRate) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPDestinationRate(tpid, drId string) (*utils.TPDestinationRate, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPDestinationRateIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPDestRateTiming(tpid, drtId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPDestRateTimings(tpid string, drts map[string][]*DestinationRateTiming) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPDestRateTiming(tpid, drtId string) (*utils.TPDestRateTiming, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPDestRateTimingIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPRatingProfile(tpid, rpId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPRatingProfiles(tpid string, rps map[string][]*RatingProfile) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPRatingProfile(tpid, rpId string) (*utils.TPRatingProfile, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPRatingProfileIds(filters *utils.AttrTPRatingProfileIds) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPActions(tpid, aId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPActions(tpid string, acts map[string][]*Action) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPActions(tpid, aId string) (*utils.TPActions, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPActionIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPActionTimings(tpid, atId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPActionTimings(tpid string, ats map[string][]*ActionTiming) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPActionTimings(tpid, atId string) (map[string][]*utils.TPActionTimingsRow, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPActionTimingIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPActionTriggers(tpid, atId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPActionTriggers(tpid string, ats map[string][]*ActionTrigger) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPActionTriggerIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) ExistsTPAccountActions(tpid, aaId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) SetTPAccountActions(tpid string, aa map[string]*AccountAction) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetTPAccountActionIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - func (ms *MongoStorage) GetActions(key string) (as Actions, err error) { result := AcKeyValue{} err = ms.db.C("actions").Find(bson.M{"key": key}).One(&result) @@ -375,51 +216,3 @@ func (ms *MongoStorage) LogActionTiming(source string, at *ActionTiming, as Acti func (ms *MongoStorage) LogError(uuid, source, errstr string) (err error) { return ms.db.C("errlog").Insert(&LogErrEntry{uuid, errstr, source}) } - -func (ms *MongoStorage) SetCdr(utils.CDR) error { - return nil -} - -func (ms *MongoStorage) SetRatedCdr(cdr utils.CDR, cc *CallCost, extraInfo string) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (ms *MongoStorage) GetAllRatedCdr() ([]utils.CDR, error) { - return nil, nil -} - -func (ms *MongoStorage) GetDestinations(tpid string) ([]*Destination, error) { - return nil, nil -} - -func (ms *MongoStorage) GetTpDestinations(tpid, tag string) ([]*Destination, error) { - return nil, nil -} - -func (ms *MongoStorage) GetTpRates(tpid, tag string) (map[string]*Rate, error) { - return nil, nil -} -func (ms *MongoStorage) GetTpDestinationRates(tpid, tag string) (map[string][]*DestinationRate, error) { - return nil, nil -} -func (ms *MongoStorage) GetTpTimings(tpid, tag string) (map[string]*Timing, error) { - return nil, nil -} -func (ms *MongoStorage) GetTpDestinationRateTimings(tpid, tag string) ([]*DestinationRateTiming, error) { - return nil, nil -} -func (ms *MongoStorage) GetTpRatingProfiles(tpid, tag string) (map[string]*RatingProfile, error) { - return nil, nil -} -func (ms *MongoStorage) GetTpActions(tpid, tag string) (map[string][]*Action, error) { - return nil, nil -} -func (ms *MongoStorage) GetTpActionTimings(tpid, tag string) (map[string][]*ActionTiming, error) { - return nil, nil -} -func (ms *MongoStorage) GetTpActionTriggers(tpid, tag string) (map[string][]*ActionTrigger, error) { - return nil, nil -} -func (ms *MongoStorage) GetTpAccountActions(tpid, tag string) (map[string]*AccountAction, error) { - return nil, nil -} diff --git a/engine/storage_mysql.go b/engine/storage_mysql.go index c9792d638..6e3338ed9 100644 --- a/engine/storage_mysql.go +++ b/engine/storage_mysql.go @@ -28,7 +28,7 @@ type MySQLStorage struct { *SQLStorage } -func NewMySQLStorage(host, port, name, user, password string) (DataStorage, error) { +func NewMySQLStorage(host, port, name, user, password string) (Storage, error) { db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8", user, password, host, port, name)) if err != nil { return nil, err diff --git a/engine/storage_postgres.go b/engine/storage_postgres.go index 3ad3076b8..420ee39f8 100644 --- a/engine/storage_postgres.go +++ b/engine/storage_postgres.go @@ -28,7 +28,7 @@ type PostgresStorage struct { *SQLStorage } -func NewPostgresStorage(host, port, name, user, password string) (DataStorage, error) { +func NewPostgresStorage(host, port, name, user, password string) (Storage, error) { db, err := sql.Open("postgres", fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable", host, port, name, user, password)) if err != nil { return nil, err diff --git a/engine/storage_redis.go b/engine/storage_redis.go index 863520407..d0d0ac95c 100644 --- a/engine/storage_redis.go +++ b/engine/storage_redis.go @@ -19,7 +19,6 @@ along with this program. If not, see package engine import ( - "errors" "fmt" "github.com/cgrates/cgrates/history" "github.com/cgrates/cgrates/utils" @@ -35,7 +34,7 @@ type RedisStorage struct { ms Marshaler } -func NewRedisStorage(address string, db int, pass string) (DataStorage, error) { +func NewRedisStorage(address string, db int, pass string) (Storage, error) { addrSplit := strings.Split(address, ":") host := addrSplit[0] port := 6379 @@ -89,20 +88,37 @@ func (rs *RedisStorage) SetRatingProfile(rp *RatingProfile) (err error) { } func (rs *RedisStorage) GetDestination(key string) (dest *Destination, err error) { - var values string - if values, err = rs.db.Get(DESTINATION_PREFIX + key); err == nil { - dest = &Destination{Id: key} - err = rs.ms.Unmarshal([]byte(values), dest) + var values []string + if values, err = rs.db.SMembers(DESTINATION_PREFIX + key); len(values) > 0 && err == nil { + dest = &Destination{Id: key, Prefixes: values} + } + return +} + +func (rs *RedisStorage) DestinationContainsPrefix(key string, prefix string) (precision int, err error) { + if _, err := rs.db.SAdd(TEMP_DESTINATION_PREFIX+prefix, utils.SplitPrefixInterface(prefix)...); err != nil { + return 0, err + } + var values []string + if values, err = rs.db.SInter(DESTINATION_PREFIX+key, TEMP_DESTINATION_PREFIX+prefix); err == nil { + for _, p := range values { + if len(p) > precision { + precision = len(p) + } + } + } + if _, err := rs.db.Del(TEMP_DESTINATION_PREFIX + prefix); err != nil { + Logger.Err("Error removing temp ") } return } func (rs *RedisStorage) SetDestination(dest *Destination) (err error) { - var result []byte - if result, err = rs.ms.Marshal(dest); err != nil { - return + var prefixes []interface{} + for _, p := range dest.Prefixes { + prefixes = append(prefixes, p) } - _, err = rs.db.Set(DESTINATION_PREFIX+dest.Id, result) + _, err = rs.db.SAdd(DESTINATION_PREFIX+dest.Id, prefixes...) if err == nil && historyScribe != nil { response := 0 historyScribe.Record(&history.Record{DESTINATION_PREFIX + dest.Id, dest}, &response) @@ -110,163 +126,6 @@ func (rs *RedisStorage) SetDestination(dest *Destination) (err error) { return } -func (rs *RedisStorage) GetTPIds() ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPTiming(tpid string, tm *Timing) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPTiming(tpid, tmId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPTiming(tpid, tmId string) (*Timing, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPTimingIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPDestinationIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPDestination(tpid, destTag string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -// Extracts destinations from StorDB on specific tariffplan id -func (rs *RedisStorage) GetTPDestination(tpid, destTag string) (*Destination, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPDestination(tpid string, dest *Destination) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPRate(tpid, rtId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPRates(tpid string, rts map[string][]*Rate) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPRate(tpid, rtId string) (*utils.TPRate, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPRateIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPDestinationRate(tpid, drId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPDestinationRates(tpid string, drs map[string][]*DestinationRate) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPDestinationRate(tpid, drId string) (*utils.TPDestinationRate, error) { - return nil, nil -} - -func (rs *RedisStorage) GetTPDestinationRateIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPDestRateTiming(tpid, drtId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPDestRateTimings(tpid string, drts map[string][]*DestinationRateTiming) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPDestRateTiming(tpid, drtId string) (*utils.TPDestRateTiming, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPDestRateTimingIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPRatingProfile(tpid, rpId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPRatingProfiles(tpid string, rps map[string][]*RatingProfile) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPRatingProfile(tpid, rpId string) (*utils.TPRatingProfile, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPRatingProfileIds(filters *utils.AttrTPRatingProfileIds) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPActions(tpid, aId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPActions(tpid string, acts map[string][]*Action) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPActions(tpid, aId string) (*utils.TPActions, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPActionIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPActionTimings(tpid, atId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPActionTimings(tpid string, ats map[string][]*ActionTiming) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPActionTimings(tpid, atId string) (map[string][]*utils.TPActionTimingsRow, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPActionTimingIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPActionTriggers(tpid, atId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPActionTriggers(tpid string, ats map[string][]*ActionTrigger) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPActionTriggerIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) ExistsTPAccountActions(tpid, aaId string) (bool, error) { - return false, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) SetTPAccountActions(tpid string, aa map[string]*AccountAction) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetTPAccountActionIds(tpid string) ([]string, error) { - return nil, errors.New(utils.ERR_NOT_IMPLEMENTED) -} - func (rs *RedisStorage) GetActions(key string) (as Actions, err error) { var values string if values, err = rs.db.Get(ACTION_PREFIX + key); err == nil { @@ -383,48 +242,3 @@ func (rs *RedisStorage) LogError(uuid, source, errstr string) (err error) { _, err = rs.db.Set(LOG_ERR+source+"_"+uuid, errstr) return } - -func (rs *RedisStorage) SetCdr(utils.CDR) error { - return nil -} - -func (rs *RedisStorage) SetRatedCdr(cdr utils.CDR, cc *CallCost, extraInfo string) error { - return errors.New(utils.ERR_NOT_IMPLEMENTED) -} - -func (rs *RedisStorage) GetAllRatedCdr() ([]utils.CDR, error) { - return nil, nil -} - -func (rs *RedisStorage) GetTpDestinations(tpid, tag string) ([]*Destination, error) { - return nil, nil -} - -func (rs *RedisStorage) GetTpRates(tpid, tag string) (map[string]*Rate, error) { - return nil, nil -} - -func (ms *RedisStorage) GetTpDestinationRates(tpid, tag string) (map[string][]*DestinationRate, error) { - return nil, nil -} -func (rs *RedisStorage) GetTpTimings(tpid, tag string) (map[string]*Timing, error) { - return nil, nil -} -func (rs *RedisStorage) GetTpDestinationRateTimings(tpid, tag string) ([]*DestinationRateTiming, error) { - return nil, nil -} -func (rs *RedisStorage) GetTpRatingProfiles(tpid, tag string) (map[string]*RatingProfile, error) { - return nil, nil -} -func (rs *RedisStorage) GetTpActions(tpid, tag string) (map[string][]*Action, error) { - return nil, nil -} -func (rs *RedisStorage) GetTpActionTimings(tpid, tag string) (map[string][]*ActionTiming, error) { - return nil, nil -} -func (rs *RedisStorage) GetTpActionTriggers(tpid, tag string) (map[string][]*ActionTrigger, error) { - return nil, nil -} -func (rs *RedisStorage) GetTpAccountActions(tpid, tag string) (map[string]*AccountAction, error) { - return nil, nil -} diff --git a/engine/storage_sql.go b/engine/storage_sql.go index 8c239a02e..eed027380 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -30,31 +30,14 @@ type SQLStorage struct { Db *sql.DB } -func (self *SQLStorage) Close() {} +func (self *SQLStorage) Close() { + self.Close() +} func (self *SQLStorage) Flush() (err error) { return } -func (self *SQLStorage) GetRatingProfile(string) (rp *RatingProfile, err error) { - /*row := self.Db.QueryRow(fmt.Sprintf("SELECT * FROM ratingprofiles WHERE id='%s'", id)) - err = row.Scan(&rp, &cc.Direction, &cc.Tenant, &cc.TOR, &cc.Subject, &cc.Destination, &cc.Cost, &cc.ConnectFee, ×pansJson) - err = json.Unmarshal([]byte(timespansJson), cc.Timespans)*/ - return -} - -func (self *SQLStorage) SetRatingProfile(rp *RatingProfile) (err error) { - return -} - -func (self *SQLStorage) GetDestination(string) (d *Destination, err error) { - return -} - -func (self *SQLStorage) SetDestination(d *Destination) (err error) { - return -} - // Return a list with all TPids defined in the system, even if incomplete, isolated in some table. func (self *SQLStorage) GetTPIds() ([]string, error) { rows, err := self.Db.Query( @@ -209,7 +192,7 @@ func (self *SQLStorage) ExistsTPRate(tpid, rtId string) (bool, error) { return exists, nil } -func (self *SQLStorage) SetTPRates(tpid string, rts map[string][]*Rate) error { +func (self *SQLStorage) SetTPRates(tpid string, rts map[string][]*LoadRate) error { if len(rts) == 0 { return nil //Nothing to set } @@ -380,7 +363,7 @@ func (self *SQLStorage) SetTPDestRateTimings(tpid string, drts map[string][]*Des qry += "," } qry += fmt.Sprintf("('%s','%s','%s','%s',%f)", - tpid, drtId, drt.DestinationRatesTag, drt.TimingsTag, drt.Weight) + tpid, drtId, drt.DestinationRatesTag, drt.TimingTag, drt.Weight) i++ } } @@ -556,9 +539,9 @@ func (self *SQLStorage) SetTPActions(tpid string, acts map[string][]*Action) err if i != 0 { //Consecutive values after the first will be prefixed with "," as separator qry += "," } - qry += fmt.Sprintf("('%s','%s','%s','%s','%s',%f,'%s','%s','%s',%f,%f,%f)", - tpid, actId, act.ActionType, act.BalanceId, act.Direction, act.Units, act.ExpirationString, - act.DestinationTag, act.RateType, act.RateValue, act.MinutesWeight, act.Weight) + qry += fmt.Sprintf("('%s','%s','%s','%s','%s',%f,'%s','%s',%f,'%s',%f)", + tpid, actId, act.ActionType, act.BalanceId, act.Direction, act.Balance.Value, act.ExpirationString, + act.Balance.DestinationId, act.Balance.RateSubject, act.Balance.Weight, act.ExtraParameters, act.Weight) i++ } } @@ -578,12 +561,12 @@ func (self *SQLStorage) GetTPActions(tpid, actsId string) (*utils.TPActions, err i := 0 for rows.Next() { i++ //Keep here a reference so we know we got at least one result - var action, balanceId, dir, destId, rateType, expTime string - var units, rate, minutesWeight, weight float64 - if err = rows.Scan(&action, &balanceId, &dir, &units, &expTime, &destId, &rateType, &rate, &minutesWeight, &weight); err != nil { + var action, balanceId, dir, destId, rateSubject, expTime, extraParameters string + var units, balanceWeight, weight float64 + if err = rows.Scan(&action, &balanceId, &dir, &units, &expTime, &destId, &rateSubject, &balanceWeight, &extraParameters, &weight); err != nil { return nil, err } - acts.Actions = append(acts.Actions, utils.Action{action, balanceId, dir, units, expTime, destId, rateType, rate, minutesWeight, weight}) + acts.Actions = append(acts.Actions, utils.Action{action, balanceId, dir, units, expTime, destId, rateSubject, balanceWeight, extraParameters, weight}) } if i == 0 { return nil, nil @@ -798,24 +781,6 @@ func (self *SQLStorage) GetTPAccountActionIds(tpid string) ([]string, error) { return ids, nil } -func (self *SQLStorage) GetUserBalance(string) (ub *UserBalance, err error) { return } - -func (self *SQLStorage) SetUserBalance(ub *UserBalance) (err error) { return } - -func (self *SQLStorage) GetActions(string) (as Actions, err error) { - return -} - -func (self *SQLStorage) SetActions(key string, as Actions) (err error) { return } - -func (self *SQLStorage) GetActionTimings(key string) (ats ActionTimings, err error) { return } - -func (self *SQLStorage) SetActionTimings(key string, ats ActionTimings) (err error) { return } - -func (self *SQLStorage) GetAllActionTimings() (ats map[string]ActionTimings, err error) { - return -} - func (self *SQLStorage) LogCallCost(uuid, source string, cc *CallCost) (err error) { //ToDo: Add cgrid to logCallCost if self.Db == nil { @@ -956,8 +921,8 @@ func (self *SQLStorage) GetTpDestinations(tpid, tag string) ([]*Destination, err return dests, nil } -func (self *SQLStorage) GetTpRates(tpid, tag string) (map[string]*Rate, error) { - rts := make(map[string]*Rate) +func (self *SQLStorage) GetTpRates(tpid, tag string) (map[string][]*LoadRate, error) { + rts := make(map[string][]*LoadRate) q := fmt.Sprintf("SELECT tag, connect_fee, rate, rate_unit, rate_increment, group_interval_start, rounding_method, rounding_decimals, weight FROM %s WHERE tpid='%s' ", utils.TBL_TP_RATES, tpid) if tag != "" { q += fmt.Sprintf(" AND tag='%s'", tag) @@ -975,7 +940,7 @@ func (self *SQLStorage) GetTpRates(tpid, tag string) (map[string]*Rate, error) { if err := rows.Scan(&tag, &connect_fee, &rate, &rate_unit, &rate_increment, &group_interval_start, &roundingMethod, &roundingDecimals, &weight); err != nil { return nil, err } - r := &Rate{ + r := &LoadRate{ Tag: tag, ConnectFee: connect_fee, Price: rate, @@ -986,7 +951,14 @@ func (self *SQLStorage) GetTpRates(tpid, tag string) (map[string]*Rate, error) { RoundingDecimals: roundingDecimals, Weight: weight, } - rts[tag] = r + // same tag only to create rate groups + existingRates, exists := rts[tag] + if exists { + if err := existingRates[len(existingRates)-1].ValidNextGroup(r); err != nil { + return nil, err + } + } + rts[tag] = append(rts[tag], r) } return rts, nil } @@ -1062,7 +1034,7 @@ func (self *SQLStorage) GetTpDestinationRateTimings(tpid, tag string) ([]*Destin Tag: tag, DestinationRatesTag: destination_rates_tag, Weight: weight, - TimingsTag: timings_tag, + TimingTag: timings_tag, } rts = append(rts, rt) } @@ -1113,37 +1085,25 @@ func (self *SQLStorage) GetTpActions(tpid, tag string) (map[string][]*Action, er defer rows.Close() for rows.Next() { var id int - var units, rate, minutes_weight, weight float64 - var tpid, tag, action, balance_type, direction, destinations_tag, rate_type, expirationDate string - if err := rows.Scan(&id, &tpid, &tag, &action, &balance_type, &direction, &units, &expirationDate, &destinations_tag, &rate_type, &rate, &minutes_weight, &weight); err != nil { + var units, balance_weight, weight float64 + var tpid, tag, action, balance_type, direction, destinations_tag, rate_subject, extra_parameters, expirationDate string + if err := rows.Scan(&id, &tpid, &tag, &action, &balance_type, &direction, &units, &expirationDate, &destinations_tag, &rate_subject, &balance_weight, &extra_parameters, &weight); err != nil { return nil, err } - var a *Action - if balance_type != MINUTES { - a = &Action{ - ActionType: action, - BalanceId: balance_type, - Direction: direction, - Units: units, - ExpirationString: expirationDate, - } - } else { - var price float64 - a = &Action{ - Id: utils.GenUUID(), - ActionType: action, - BalanceId: balance_type, - Direction: direction, - Weight: weight, - ExpirationString: expirationDate, - MinuteBucket: &MinuteBucket{ - Seconds: units, - Weight: minutes_weight, - Price: price, - PriceType: rate_type, - DestinationId: destinations_tag, - }, - } + a := &Action{ + Id: utils.GenUUID(), + ActionType: action, + BalanceId: balance_type, + Direction: direction, + Weight: weight, + ExtraParameters: extra_parameters, + ExpirationString: expirationDate, + Balance: &Balance{ + Value: units, + Weight: balance_weight, + RateSubject: rate_subject, + DestinationId: destinations_tag, + }, } as[tag] = append(as[tag], a) } diff --git a/engine/storage_test.go b/engine/storage_test.go index c4c430c09..c917fc11f 100644 --- a/engine/storage_test.go +++ b/engine/storage_test.go @@ -71,14 +71,35 @@ func TestMsgpackTime(t *testing.T) { } } +func TestStorageDestinationContainsPrefixShort(t *testing.T) { + precision, err := storageGetter.DestinationContainsPrefix("NAT", "0723") + if err != nil || precision != 4 { + t.Error("Error finding prefix: ", err, precision) + } +} + +func TestStorageDestinationContainsPrefixLong(t *testing.T) { + precision, err := storageGetter.DestinationContainsPrefix("NAT", "0723045326") + if err != nil || precision != 4 { + t.Error("Error finding prefix: ", err, precision) + } +} + +func TestStorageDestinationContainsPrefixNotExisting(t *testing.T) { + precision, err := storageGetter.DestinationContainsPrefix("NAT", "072") + if err != nil || precision != 0 { + t.Error("Error finding prefix: ", err, precision) + } +} + /************************** Benchmarks *****************************/ func GetUB() *UserBalance { uc := &UnitsCounter{ - Direction: OUTBOUND, - BalanceId: SMS, - Units: 100, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, + Direction: OUTBOUND, + BalanceId: SMS, + Units: 100, + MinuteBalances: BalanceChain{&Balance{Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}, } at := &ActionTrigger{ Id: "some_uuid", @@ -94,8 +115,7 @@ func GetUB() *UserBalance { ub := &UserBalance{ Id: "rif", Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{SMS + OUTBOUND: BalanceChain{&Balance{Value: 14, ExpirationDate: zeroTime}}, TRAFFIC + OUTBOUND: BalanceChain{&Balance{Value: 1024, ExpirationDate: zeroTime}}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, + BalanceMap: map[string]BalanceChain{SMS + OUTBOUND: BalanceChain{&Balance{Value: 14, ExpirationDate: zeroTime}}, TRAFFIC + OUTBOUND: BalanceChain{&Balance{Value: 1024, ExpirationDate: zeroTime}}, MINUTES: BalanceChain{&Balance{Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{uc, uc}, ActionTriggers: ActionTriggerPriotityList{at, at, at}, } @@ -105,16 +125,16 @@ func GetUB() *UserBalance { func BenchmarkMarshallerJSONStoreRestore(b *testing.B) { b.StopTimer() d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) ub := GetUB() - ap1 := ActivationPeriod{} + ap1 := RatingPlan{} ub1 := &UserBalance{} b.StartTimer() ms := new(JSONMarshaler) @@ -129,16 +149,16 @@ func BenchmarkMarshallerJSONStoreRestore(b *testing.B) { func BenchmarkMarshallerBSONStoreRestore(b *testing.B) { b.StopTimer() d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) ub := GetUB() - ap1 := ActivationPeriod{} + ap1 := RatingPlan{} ub1 := &UserBalance{} b.StartTimer() ms := new(BSONMarshaler) @@ -153,16 +173,16 @@ func BenchmarkMarshallerBSONStoreRestore(b *testing.B) { func BenchmarkMarshallerJSONBufStoreRestore(b *testing.B) { b.StopTimer() d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) ub := GetUB() - ap1 := ActivationPeriod{} + ap1 := RatingPlan{} ub1 := &UserBalance{} b.StartTimer() ms := new(JSONBufMarshaler) @@ -177,16 +197,16 @@ func BenchmarkMarshallerJSONBufStoreRestore(b *testing.B) { func BenchmarkMarshallerGOBStoreRestore(b *testing.B) { b.StopTimer() d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) ub := GetUB() - ap1 := ActivationPeriod{} + ap1 := RatingPlan{} ub1 := &UserBalance{} b.StartTimer() ms := new(GOBMarshaler) @@ -201,16 +221,16 @@ func BenchmarkMarshallerGOBStoreRestore(b *testing.B) { func BenchmarkMarshallerCodecMsgpackStoreRestore(b *testing.B) { b.StopTimer() d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) ub := GetUB() - ap1 := ActivationPeriod{} + ap1 := RatingPlan{} ub1 := &UserBalance{} b.StartTimer() ms := NewCodecMsgpackMarshaler() @@ -225,16 +245,16 @@ func BenchmarkMarshallerCodecMsgpackStoreRestore(b *testing.B) { func BenchmarkMarshallerBincStoreRestore(b *testing.B) { b.StopTimer() d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC) - i := &Interval{Months: []time.Month{time.February}, + i := &RateInterval{Months: []time.Month{time.February}, MonthDays: []int{1}, WeekDays: []time.Weekday{time.Wednesday, time.Thursday}, StartTime: "14:30:00", EndTime: "15:00:00"} - ap := &ActivationPeriod{ActivationTime: d} - ap.AddInterval(i) + ap := &RatingPlan{ActivationTime: d} + ap.AddRateInterval(i) ub := GetUB() - ap1 := ActivationPeriod{} + ap1 := RatingPlan{} ub1 := &UserBalance{} b.StartTimer() ms := NewBincMarshaler() diff --git a/engine/storage_utils.go b/engine/storage_utils.go index b791f056a..7436118f7 100644 --- a/engine/storage_utils.go +++ b/engine/storage_utils.go @@ -26,7 +26,8 @@ import ( // Various helpers to deal with database -func ConfigureDatabase(db_type, host, port, name, user, pass string) (db DataStorage, err error) { +func ConfigureDataStorage(db_type, host, port, name, user, pass string) (db DataStorage, err error) { + var d Storage switch db_type { case utils.REDIS: var db_nb int @@ -38,13 +39,80 @@ func ConfigureDatabase(db_type, host, port, name, user, pass string) (db DataSto if port != "" { host += ":" + port } - db, err = NewRedisStorage(host, db_nb, pass) + d, err = NewRedisStorage(host, db_nb, pass) + db = d.(DataStorage) case utils.MONGO: - db, err = NewMongoStorage(host, port, name, user, pass) - case utils.POSTGRES: - db, err = NewPostgresStorage(host, port, name, user, pass) - case utils.MYSQL: - db, err = NewMySQLStorage(host, port, name, user, pass) + d, err = NewMongoStorage(host, port, name, user, pass) + db = d.(DataStorage) + default: + err = errors.New("unknown db") + } + if err != nil { + return nil, err + } + return db, nil +} + +func ConfigureLogStorage(db_type, host, port, name, user, pass string) (db LogStorage, err error) { + var d Storage + switch db_type { + case utils.REDIS: + var db_nb int + db_nb, err = strconv.Atoi(name) + if err != nil { + Logger.Crit("Redis db name must be an integer!") + return nil, err + } + if port != "" { + host += ":" + port + } + d, err = NewRedisStorage(host, db_nb, pass) + db = d.(LogStorage) + case utils.MONGO: + d, err = NewMongoStorage(host, port, name, user, pass) + db = d.(LogStorage) + case utils.POSTGRES: + d, err = NewPostgresStorage(host, port, name, user, pass) + db = d.(LogStorage) + case utils.MYSQL: + d, err = NewMySQLStorage(host, port, name, user, pass) + db = d.(LogStorage) + default: + err = errors.New("unknown db") + } + if err != nil { + return nil, err + } + return db, nil +} + +func ConfigureLoadStorage(db_type, host, port, name, user, pass string) (db LoadStorage, err error) { + var d Storage + switch db_type { + case utils.POSTGRES: + d, err = NewPostgresStorage(host, port, name, user, pass) + db = d.(LoadStorage) + case utils.MYSQL: + d, err = NewMySQLStorage(host, port, name, user, pass) + db = d.(LoadStorage) + default: + err = errors.New("unknown db") + } + if err != nil { + return nil, err + } + return db, nil +} + +func ConfigureCdrStorage(db_type, host, port, name, user, pass string) (db CdrStorage, err error) { + var d Storage + switch db_type { + case utils.POSTGRES: + d, err = NewPostgresStorage(host, port, name, user, pass) + db = d.(CdrStorage) + case utils.MYSQL: + d, err = NewMySQLStorage(host, port, name, user, pass) + db = d.(CdrStorage) default: err = errors.New("unknown db") } diff --git a/engine/timespans.go b/engine/timespans.go index 2b1134f5d..a10986944 100644 --- a/engine/timespans.go +++ b/engine/timespans.go @@ -19,7 +19,7 @@ along with this program. If not, see package engine import ( - "fmt" + "github.com/cgrates/cgrates/utils" "time" ) @@ -29,68 +29,99 @@ A unit in which a call will be split that has a specific price related interval type TimeSpan struct { TimeStart, TimeEnd time.Time Cost float64 - ActivationPeriod *ActivationPeriod - Interval *Interval - MinuteInfo *MinuteInfo + RatingPlan *RatingPlan + RateInterval *RateInterval CallDuration time.Duration // the call duration so far till TimeEnd + overlapped bool // mark a timespan as overlapped by an expanded one + Increments Increments } -// Holds the bonus minute information related to a specified timespan +type Increment struct { + Duration time.Duration + Cost float64 + BalanceUuid string + BalanceType string + BalanceRateInterval *RateInterval + MinuteInfo *MinuteInfo +} + +// Holds the minute information related to a specified timespan type MinuteInfo struct { DestinationId string Quantity float64 Price float64 } -/* -Returns the duration of the timespan -*/ +func (incr *Increment) Clone() *Increment { + return &Increment{ + Duration: incr.Duration, + Cost: incr.Cost, + BalanceUuid: incr.BalanceUuid, + BalanceType: incr.BalanceType, + BalanceRateInterval: incr.BalanceRateInterval, + MinuteInfo: incr.MinuteInfo, + } +} + +type Increments []*Increment + +func (incs Increments) GetTotalCost() float64 { + cost := 0.0 + for _, increment := range incs { + cost += increment.Cost + } + return cost +} + +// Returns the duration of the timespan func (ts *TimeSpan) GetDuration() time.Duration { return ts.TimeEnd.Sub(ts.TimeStart) } +// Returns true if the given time is inside timespan range. +func (ts *TimeSpan) Contains(t time.Time) bool { + return t.After(ts.TimeStart) && t.Before(ts.TimeEnd) +} + // Returns the cost of the timespan according to the relevant cost interval. // It also sets the Cost field of this timespan (used for refound on session // manager debit loop where the cost cannot be recalculated) -func (ts *TimeSpan) getCost(cd *CallDescriptor) (cost float64) { - if ts.MinuteInfo != nil { - return ts.GetDuration().Seconds() * ts.MinuteInfo.Price - } - if ts.Interval == nil { +func (ts *TimeSpan) getCost() float64 { + if ts.RateInterval == nil { return 0 } - i := ts.Interval - cost = i.GetCost(ts.GetDuration(), ts.GetGroupStart()) - // if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil { - // userBalance.mux.RLock() - // if percentageDiscount, err := userBalance.getVolumeDiscount(cd.Destination, INBOUND); err == nil && percentageDiscount > 0 { - // cost *= (100 - percentageDiscount) / 100 - // } - // userBalance.mux.RUnlock() - // } - ts.Cost = cost - return + ts.Cost = ts.RateInterval.GetCost(ts.GetDuration(), ts.GetGroupStart()) + return ts.Cost } /* -Returns true if the given time is inside timespan range. -*/ -func (ts *TimeSpan) Contains(t time.Time) bool { - return t.After(ts.TimeStart) && t.Before(ts.TimeEnd) -} - -/* -Will set the interval as spans's interval if new Weight is greater then span's interval Weight +Will set the interval as spans's interval if new Weight is lower then span's interval Weight or if the Weights are equal and new price is lower then spans's interval price */ -func (ts *TimeSpan) SetInterval(i *Interval) { - if ts.Interval == nil || ts.Interval.Weight < i.Weight { - ts.Interval = i +func (ts *TimeSpan) SetRateInterval(i *RateInterval) { + if ts.RateInterval == nil || ts.RateInterval.Weight < i.Weight { + ts.RateInterval = i + return } - iPrice, _, _ := i.GetPriceParameters(ts.GetGroupStart()) - tsPrice, _, _ := ts.Interval.GetPriceParameters(ts.GetGroupStart()) - if ts.Interval.Weight == i.Weight && iPrice < tsPrice { - ts.Interval = i + iPrice, _, _ := i.GetRateParameters(ts.GetGroupStart()) + tsPrice, _, _ := ts.RateInterval.GetRateParameters(ts.GetGroupStart()) + if ts.RateInterval.Weight == i.Weight && iPrice < tsPrice { + ts.RateInterval = i + } +} + +func (ts *TimeSpan) createIncrementsSlice() { + if ts.RateInterval == nil { + return + } + ts.Increments = make([]*Increment, 0) + // create rated units series + rate, rateIncrement, rateUnit := ts.RateInterval.GetRateParameters(ts.GetGroupStart()) + incrementCost := rate / rateUnit.Seconds() * rateIncrement.Seconds() + totalCost := 0.0 + for s := 0; s < int(ts.GetDuration()/rateIncrement); s++ { + ts.Increments = append(ts.Increments, &Increment{Duration: rateIncrement, Cost: incrementCost}) + totalCost += incrementCost } } @@ -100,7 +131,7 @@ It will modify the endtime of the received timespan and it will return a new timespan starting from the end of the received one. The interval will attach itself to the timespan that overlaps the interval. */ -func (ts *TimeSpan) SplitByInterval(i *Interval) (nts *TimeSpan) { +func (ts *TimeSpan) SplitByRateInterval(i *RateInterval) (nts *TimeSpan) { //Logger.Debug("here: ", ts, " +++ ", i) // if the span is not in interval return nil @@ -109,14 +140,14 @@ func (ts *TimeSpan) SplitByInterval(i *Interval) (nts *TimeSpan) { return } // split by GroupStart - i.Prices.Sort() - for _, price := range i.Prices { + i.Rates.Sort() + for _, price := range i.Rates { if ts.GetGroupStart() < price.GroupIntervalStart && ts.GetGroupEnd() >= price.GroupIntervalStart { - ts.SetInterval(i) + ts.SetRateInterval(i) splitTime := ts.TimeStart.Add(price.GroupIntervalStart - ts.GetGroupStart()) nts = &TimeSpan{TimeStart: splitTime, TimeEnd: ts.TimeEnd} ts.TimeEnd = splitTime - nts.SetInterval(i) + nts.SetRateInterval(i) nts.CallDuration = ts.CallDuration ts.SetNewCallDuration(nts) @@ -127,14 +158,14 @@ func (ts *TimeSpan) SplitByInterval(i *Interval) (nts *TimeSpan) { // if the span is enclosed in the interval try to set as new interval and return nil if i.Contains(ts.TimeStart) && i.Contains(ts.TimeEnd) { //Logger.Debug("All in interval") - ts.SetInterval(i) + ts.SetRateInterval(i) return } // if only the start time is in the interval split the interval to the right if i.Contains(ts.TimeStart) { //Logger.Debug("Start in interval") splitTime := i.getRightMargin(ts.TimeStart) - ts.SetInterval(i) + ts.SetRateInterval(i) if splitTime == ts.TimeStart { return } @@ -155,7 +186,7 @@ func (ts *TimeSpan) SplitByInterval(i *Interval) (nts *TimeSpan) { nts = &TimeSpan{TimeStart: splitTime, TimeEnd: ts.TimeEnd} ts.TimeEnd = splitTime - nts.SetInterval(i) + nts.SetRateInterval(i) nts.CallDuration = ts.CallDuration ts.SetNewCallDuration(nts) @@ -164,57 +195,64 @@ func (ts *TimeSpan) SplitByInterval(i *Interval) (nts *TimeSpan) { return } -/* -Splits the given timespan on activation period's activation time. -*/ -func (ts *TimeSpan) SplitByActivationPeriod(ap *ActivationPeriod) (newTs *TimeSpan) { +// Split the timespan at the given increment start +func (ts *TimeSpan) SplitByIncrement(index int) *TimeSpan { + if index <= 0 || index >= len(ts.Increments) { + return nil + } + timeStart := ts.GetTimeStartForIncrement(index) + newTs := &TimeSpan{RateInterval: ts.RateInterval, TimeStart: timeStart, TimeEnd: ts.TimeEnd} + newTs.CallDuration = ts.CallDuration + ts.TimeEnd = timeStart + newTs.Increments = ts.Increments[index:] + ts.Increments = ts.Increments[:index] + ts.SetNewCallDuration(newTs) + return newTs +} + +// Split the timespan at the given second +func (ts *TimeSpan) SplitByDuration(duration time.Duration) *TimeSpan { + if duration <= 0 || duration >= ts.GetDuration() { + return nil + } + timeStart := ts.TimeStart.Add(duration) + newTs := &TimeSpan{RateInterval: ts.RateInterval, TimeStart: timeStart, TimeEnd: ts.TimeEnd} + newTs.CallDuration = ts.CallDuration + ts.TimeEnd = timeStart + // split the increment + for incrIndex, incr := range ts.Increments { + if duration-incr.Duration >= 0 { + duration -= incr.Duration + } else { + + splitIncrement := ts.Increments[incrIndex].Clone() + splitIncrement.Duration -= duration + ts.Increments[incrIndex].Duration = duration + newTs.Increments = Increments{splitIncrement} + if incrIndex < len(ts.Increments)-1 { + newTs.Increments = append(newTs.Increments, ts.Increments[incrIndex+1:]...) + } + ts.Increments = ts.Increments[:incrIndex+1] + break + } + } + ts.SetNewCallDuration(newTs) + return newTs +} + +// Splits the given timespan on activation period's activation time. +func (ts *TimeSpan) SplitByRatingPlan(ap *RatingPlan) (newTs *TimeSpan) { if !ts.Contains(ap.ActivationTime) { return nil } - newTs = &TimeSpan{TimeStart: ap.ActivationTime, TimeEnd: ts.TimeEnd, ActivationPeriod: ap} + newTs = &TimeSpan{TimeStart: ap.ActivationTime, TimeEnd: ts.TimeEnd, RatingPlan: ap} newTs.CallDuration = ts.CallDuration ts.TimeEnd = ap.ActivationTime ts.SetNewCallDuration(newTs) return } -/* -Splits the given timespan on minute bucket's duration. -*/ -func (ts *TimeSpan) SplitByMinuteBucket(mb *MinuteBucket) (newTs *TimeSpan) { - // if mb expired skip it - if !mb.ExpirationDate.IsZero() && (ts.TimeStart.Equal(mb.ExpirationDate) || ts.TimeStart.After(mb.ExpirationDate)) { - return nil - } - - // expiring before time spans end - - if !mb.ExpirationDate.IsZero() && ts.TimeEnd.After(mb.ExpirationDate) { - newTs = &TimeSpan{TimeStart: mb.ExpirationDate, TimeEnd: ts.TimeEnd} - newTs.CallDuration = ts.CallDuration - ts.TimeEnd = mb.ExpirationDate - ts.SetNewCallDuration(newTs) - } - - s := ts.GetDuration().Seconds() - ts.MinuteInfo = &MinuteInfo{mb.DestinationId, s, mb.Price} - if s <= mb.Seconds { - mb.Seconds -= s - return newTs - } - secDuration, _ := time.ParseDuration(fmt.Sprintf("%vs", mb.Seconds)) - - newTimeEnd := ts.TimeStart.Add(secDuration) - newTs = &TimeSpan{TimeStart: newTimeEnd, TimeEnd: ts.TimeEnd} - ts.TimeEnd = newTimeEnd - newTs.CallDuration = ts.CallDuration - ts.MinuteInfo.Quantity = mb.Seconds - ts.SetNewCallDuration(newTs) - mb.Seconds = 0 - - return -} - +// Returns the starting time of this timespan func (ts *TimeSpan) GetGroupStart() time.Duration { s := ts.CallDuration - ts.GetDuration() if s < 0 { @@ -227,6 +265,7 @@ func (ts *TimeSpan) GetGroupEnd() time.Duration { return ts.CallDuration } +// sets the CallDuration attribute to reflect new timespan func (ts *TimeSpan) SetNewCallDuration(nts *TimeSpan) { d := ts.CallDuration - nts.GetDuration() if d < 0 { @@ -234,3 +273,19 @@ func (ts *TimeSpan) SetNewCallDuration(nts *TimeSpan) { } ts.CallDuration = d } + +// returns a time for the specified second in the time span +func (ts *TimeSpan) GetTimeStartForIncrement(index int) time.Time { + return ts.TimeStart.Add(time.Duration(int64(index) * ts.Increments[0].Duration.Nanoseconds())) +} + +func (ts *TimeSpan) RoundToDuration(duration time.Duration) { + if duration < ts.GetDuration() { + duration = utils.RoundTo(duration, ts.GetDuration()) + } + if duration > ts.GetDuration() { + initialDuration := ts.GetDuration() + ts.TimeEnd = ts.TimeStart.Add(duration) + ts.CallDuration = ts.CallDuration + (duration - initialDuration) + } +} diff --git a/engine/timespans_test.go b/engine/timespans_test.go index 8ecc3f7e0..e330bcf75 100644 --- a/engine/timespans_test.go +++ b/engine/timespans_test.go @@ -19,29 +19,30 @@ along with this program. If not, see package engine import ( + "github.com/cgrates/cgrates/utils" "testing" "time" ) func TestRightMargin(t *testing.T) { - i := &Interval{WeekDays: []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday}} + i := &RateInterval{WeekDays: []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday}} t1 := time.Date(2012, time.February, 3, 23, 45, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 4, 0, 10, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) if ts.TimeStart != t1 || ts.TimeEnd != time.Date(2012, time.February, 3, 23, 59, 59, 0, time.UTC) { t.Error("Incorrect first half", ts) } if nts.TimeStart != time.Date(2012, time.February, 3, 23, 59, 59, 0, time.UTC) || nts.TimeEnd != t2 { t.Error("Incorrect second half", nts) } - if ts.Interval != i { - t.Error("Interval not attached correctly") + if ts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if ts.GetDuration().Seconds() != 15*60-1 || nts.GetDuration().Seconds() != 10*60+1 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), ts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), ts.GetDuration().Seconds()) } if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { @@ -50,24 +51,24 @@ func TestRightMargin(t *testing.T) { } func TestRightHourMargin(t *testing.T) { - i := &Interval{WeekDays: []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday}, EndTime: "17:59:00"} + i := &RateInterval{WeekDays: []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday}, EndTime: "17:59:00"} t1 := time.Date(2012, time.February, 3, 17, 30, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 3, 18, 00, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) if ts.TimeStart != t1 || ts.TimeEnd != time.Date(2012, time.February, 3, 17, 59, 00, 0, time.UTC) { t.Error("Incorrect first half", ts) } if nts.TimeStart != time.Date(2012, time.February, 3, 17, 59, 00, 0, time.UTC) || nts.TimeEnd != t2 { t.Error("Incorrect second half", nts) } - if ts.Interval != i { - t.Error("Interval not attached correctly") + if ts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if ts.GetDuration().Seconds() != 29*60 || nts.GetDuration().Seconds() != 1*60 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) } if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { t.Errorf("The duration has changed: %v + %v != %v", ts.GetDuration().Seconds(), nts.GetDuration().Seconds(), oldDuration.Seconds()) @@ -75,23 +76,23 @@ func TestRightHourMargin(t *testing.T) { } func TestLeftMargin(t *testing.T) { - i := &Interval{WeekDays: []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday}} + i := &RateInterval{WeekDays: []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday}} t1 := time.Date(2012, time.February, 5, 23, 45, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 6, 0, 10, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) if ts.TimeStart != t1 || ts.TimeEnd != time.Date(2012, time.February, 6, 0, 0, 0, 0, time.UTC) { t.Error("Incorrect first half", ts) } if nts.TimeStart != time.Date(2012, time.February, 6, 0, 0, 0, 0, time.UTC) || nts.TimeEnd != t2 { t.Error("Incorrect second half", nts) } - if nts.Interval != i { - t.Error("Interval not attached correctly") + if nts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if ts.GetDuration().Seconds() != 15*60 || nts.GetDuration().Seconds() != 10*60 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) } if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { t.Errorf("The duration has changed: %v + %v != %v", ts.GetDuration().Seconds(), nts.GetDuration().Seconds(), oldDuration.Seconds()) @@ -99,23 +100,23 @@ func TestLeftMargin(t *testing.T) { } func TestLeftHourMargin(t *testing.T) { - i := &Interval{Months: Months{time.December}, MonthDays: MonthDays{1}, StartTime: "09:00:00"} + i := &RateInterval{Months: Months{time.December}, MonthDays: MonthDays{1}, StartTime: "09:00:00"} t1 := time.Date(2012, time.December, 1, 8, 45, 0, 0, time.UTC) t2 := time.Date(2012, time.December, 1, 9, 20, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) if ts.TimeStart != t1 || ts.TimeEnd != time.Date(2012, time.December, 1, 9, 0, 0, 0, time.UTC) { t.Error("Incorrect first half", ts) } if nts.TimeStart != time.Date(2012, time.December, 1, 9, 0, 0, 0, time.UTC) || nts.TimeEnd != t2 { t.Error("Incorrect second half", nts) } - if nts.Interval != i { - t.Error("Interval not attached correctly") + if nts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if ts.GetDuration().Seconds() != 15*60 || nts.GetDuration().Seconds() != 20*60 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) } if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { t.Errorf("The duration has changed: %v + %v != %v", ts.GetDuration().Seconds(), nts.GetDuration().Seconds(), oldDuration.Seconds()) @@ -123,27 +124,27 @@ func TestLeftHourMargin(t *testing.T) { } func TestEnclosingMargin(t *testing.T) { - i := &Interval{WeekDays: []time.Weekday{time.Sunday}} + i := &RateInterval{WeekDays: []time.Weekday{time.Sunday}} t1 := time.Date(2012, time.February, 5, 17, 45, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 5, 18, 10, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2} - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) if ts.TimeStart != t1 || ts.TimeEnd != t2 || nts != nil { t.Error("Incorrect enclosing", ts) } - if ts.Interval != i { - t.Error("Interval not attached correctly") + if ts.RateInterval != i { + t.Error("RateInterval not attached correctly") } } func TestOutsideMargin(t *testing.T) { - i := &Interval{WeekDays: []time.Weekday{time.Monday}} + i := &RateInterval{WeekDays: []time.Weekday{time.Monday}} t1 := time.Date(2012, time.February, 5, 17, 45, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 5, 18, 10, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2} - result := ts.SplitByInterval(i) + result := ts.SplitByRateInterval(i) if result != nil { - t.Error("Interval not split correctly") + t.Error("RateInterval not split correctly") } } @@ -168,17 +169,17 @@ func TestSplitByActivationTime(t *testing.T) { t2 := time.Date(2012, time.February, 5, 17, 55, 0, 0, time.UTC) t3 := time.Date(2012, time.February, 5, 17, 50, 0, 0, time.UTC) ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - ap1 := &ActivationPeriod{ActivationTime: t1} - ap2 := &ActivationPeriod{ActivationTime: t2} - ap3 := &ActivationPeriod{ActivationTime: t3} + ap1 := &RatingPlan{ActivationTime: t1} + ap2 := &RatingPlan{ActivationTime: t2} + ap3 := &RatingPlan{ActivationTime: t3} - if ts.SplitByActivationPeriod(ap1) != nil { + if ts.SplitByRatingPlan(ap1) != nil { t.Error("Error spliting on left margin") } - if ts.SplitByActivationPeriod(ap2) != nil { + if ts.SplitByRatingPlan(ap2) != nil { t.Error("Error spliting on right margin") } - result := ts.SplitByActivationPeriod(ap3) + result := ts.SplitByRatingPlan(ap3) if result.TimeStart != t3 || result.TimeEnd != t2 { t.Error("Error spliting on interior") } @@ -188,157 +189,45 @@ func TestTimespanGetCost(t *testing.T) { t1 := time.Date(2012, time.February, 5, 17, 45, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 5, 17, 55, 0, 0, time.UTC) ts1 := TimeSpan{TimeStart: t1, TimeEnd: t2} - cd := &CallDescriptor{Subject: "other"} - if ts1.getCost(cd) != 0 { + if ts1.getCost() != 0 { t.Error("No interval and still kicking") } - ts1.Interval = &Interval{Prices: PriceGroups{&Price{0, 1.0, 1 * time.Second, 1 * time.Second}}} - if ts1.getCost(cd) != 600 { - t.Error("Expected 10 got ", ts1.getCost(cd)) + ts1.SetRateInterval(&RateInterval{Rates: RateGroups{&Rate{0, 1.0, 1 * time.Second, 1 * time.Second}}}) + if ts1.getCost() != 600 { + t.Error("Expected 10 got ", ts1.Cost) } - ts1.Interval.Prices[0].RateUnit = 60 * time.Second - if ts1.getCost(cd) != 10 { - t.Error("Expected 6000 got ", ts1.getCost(cd)) + ts1.RateInterval = nil + ts1.SetRateInterval(&RateInterval{Rates: RateGroups{&Rate{0, 1.0, 1 * time.Second, 60 * time.Second}}}) + if ts1.getCost() != 10 { + t.Error("Expected 6000 got ", ts1.Cost) } } -func TestSetInterval(t *testing.T) { - i1 := &Interval{Prices: PriceGroups{&Price{0, 1.0, 1 * time.Second, 1 * time.Second}}} - ts1 := TimeSpan{Interval: i1} - i2 := &Interval{Prices: PriceGroups{&Price{0, 2.0, 1 * time.Second, 1 * time.Second}}} - ts1.SetInterval(i2) - if ts1.Interval != i1 { +func TestSetRateInterval(t *testing.T) { + i1 := &RateInterval{Rates: RateGroups{&Rate{0, 1.0, 1 * time.Second, 1 * time.Second}}} + ts1 := TimeSpan{RateInterval: i1} + i2 := &RateInterval{Rates: RateGroups{&Rate{0, 2.0, 1 * time.Second, 1 * time.Second}}} + ts1.SetRateInterval(i2) + if ts1.RateInterval != i1 { t.Error("Smaller price interval should win") } i2.Weight = 1 - ts1.SetInterval(i2) - if ts1.Interval != i2 { + ts1.SetRateInterval(i2) + if ts1.RateInterval != i2 { t.Error("Bigger ponder interval should win") } } -func TestTimespanSplitByMinuteBucketPlenty(t *testing.T) { - t1 := time.Date(2013, time.July, 15, 10, 40, 0, 0, time.UTC) - t2 := time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC) - mb := &MinuteBucket{Seconds: 180} - ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - newTs := ts.SplitByMinuteBucket(mb) - if ts.MinuteInfo == nil || ts.MinuteInfo.Quantity != 120 { - t.Error("Not enough minutes on minute bucket split") - } - if newTs != nil { - t.Error("Bad extra timespan on minute bucket split") - } -} - -func TestTimespanSplitByMinuteBucketScarce(t *testing.T) { - t1 := time.Date(2013, time.July, 15, 10, 40, 0, 0, time.UTC) - t2 := time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC) - mb := &MinuteBucket{Seconds: 60} - ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - newTs := ts.SplitByMinuteBucket(mb) - if ts.MinuteInfo == nil || ts.MinuteInfo.Quantity != 60 { - t.Error("Not enough minutes on minute bucket split") - } - if newTs == nil || newTs.MinuteInfo != nil { - t.Error("Missing extra timespan on minute bucket split") - } -} - -func TestTimespanSplitByMinuteBucketPlentyExpired(t *testing.T) { - t1 := time.Date(2013, time.July, 15, 10, 40, 0, 0, time.UTC) - t2 := time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC) - mb := &MinuteBucket{Seconds: 180, ExpirationDate: time.Date(2013, time.July, 15, 10, 39, 0, 0, time.UTC)} - ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - newTs := ts.SplitByMinuteBucket(mb) - if ts.MinuteInfo != nil { - t.Error("Not enough minutes on minute bucket split") - } - if newTs != nil { - t.Error("Bad extra timespan on minute bucket split") - } -} - -func TestTimespanSplitByMinuteBucketPlentyExpiring(t *testing.T) { - t1 := time.Date(2013, time.July, 15, 10, 40, 0, 0, time.UTC) - t2 := time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC) - mb := &MinuteBucket{Seconds: 180, ExpirationDate: time.Date(2013, time.July, 15, 10, 41, 0, 0, time.UTC)} - ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - newTs := ts.SplitByMinuteBucket(mb) - if ts.MinuteInfo == nil || ts.MinuteInfo.Quantity != 60 { - t.Error("Not enough minutes on minute bucket split") - } - if newTs == nil || newTs.MinuteInfo != nil { - t.Error("Missing extra timespan on minute bucket split") - } -} - -func TestTimespanSplitByMinuteBucketPlentyExpiringEnd(t *testing.T) { - t1 := time.Date(2013, time.July, 15, 10, 40, 0, 0, time.UTC) - t2 := time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC) - mb := &MinuteBucket{Seconds: 180, ExpirationDate: time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC)} - ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - newTs := ts.SplitByMinuteBucket(mb) - if ts.MinuteInfo == nil || ts.MinuteInfo.Quantity != 120 { - t.Error("Not enough minutes on minute bucket split") - } - if newTs != nil { - t.Error("Missing extra timespan on minute bucket split") - } -} - -func TestTimespanSplitByMinuteBucketScarceExpiringSame(t *testing.T) { - t1 := time.Date(2013, time.July, 15, 10, 40, 0, 0, time.UTC) - t2 := time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC) - mb := &MinuteBucket{Seconds: 120, ExpirationDate: time.Date(2013, time.July, 15, 10, 41, 0, 0, time.UTC)} - ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - newTs := ts.SplitByMinuteBucket(mb) - if ts.MinuteInfo == nil || ts.MinuteInfo.Quantity != 60 { - t.Error("Not enough minutes on minute bucket split") - } - if newTs == nil || newTs.MinuteInfo != nil { - t.Error("Missing extra timespan on minute bucket split") - } -} - -func TestTimespanSplitByMinuteBucketScarceExpiringDifferentExpFirst(t *testing.T) { - t1 := time.Date(2013, time.July, 15, 10, 40, 0, 0, time.UTC) - t2 := time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC) - mb := &MinuteBucket{Seconds: 140, ExpirationDate: time.Date(2013, time.July, 15, 10, 41, 1, 0, time.UTC)} - ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - newTs := ts.SplitByMinuteBucket(mb) - if ts.MinuteInfo == nil || ts.MinuteInfo.Quantity != 61 { - t.Error("Not enough minutes on minute bucket split: ", ts.MinuteInfo.Quantity) - } - if newTs == nil || newTs.MinuteInfo != nil { - t.Error("Missing extra timespan on minute bucket split") - } -} - -func TestTimespanSplitByMinuteBucketScarceExpiringDifferentScarceFirst(t *testing.T) { - t1 := time.Date(2013, time.July, 15, 10, 40, 0, 0, time.UTC) - t2 := time.Date(2013, time.July, 15, 10, 42, 0, 0, time.UTC) - mb := &MinuteBucket{Seconds: 61, ExpirationDate: time.Date(2013, time.July, 15, 10, 41, 30, 0, time.UTC)} - ts := TimeSpan{TimeStart: t1, TimeEnd: t2} - newTs := ts.SplitByMinuteBucket(mb) - if ts.MinuteInfo == nil || ts.MinuteInfo.Quantity != 61 { - t.Error("Not enough minutes on minute bucket split") - } - if newTs == nil || newTs.MinuteInfo != nil { - t.Error("Missing extra timespan on minute bucket split") - } -} - func TestTimespanSplitGroupedRates(t *testing.T) { - i := &Interval{ + i := &RateInterval{ EndTime: "17:59:00", - Prices: PriceGroups{&Price{0, 2, 1 * time.Second, 1 * time.Second}, &Price{900 * time.Second, 1, 1 * time.Second, 1 * time.Second}}, + Rates: RateGroups{&Rate{0, 2, 1 * time.Second, 1 * time.Second}, &Rate{900 * time.Second, 1, 1 * time.Second, 1 * time.Second}}, } t1 := time.Date(2012, time.February, 3, 17, 30, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 3, 18, 00, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2, CallDuration: 1800 * time.Second} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) splitTime := time.Date(2012, time.February, 3, 17, 45, 00, 0, time.UTC) if ts.TimeStart != t1 || ts.TimeEnd != splitTime { t.Error("Incorrect first half", ts.TimeStart, ts.TimeEnd) @@ -346,17 +235,17 @@ func TestTimespanSplitGroupedRates(t *testing.T) { if nts.TimeStart != splitTime || nts.TimeEnd != t2 { t.Error("Incorrect second half", nts) } - if ts.Interval != i { - t.Error("Interval not attached correctly") + if ts.RateInterval != i { + t.Error("RateInterval not attached correctly") } - c1 := ts.Interval.GetCost(ts.GetDuration(), ts.GetGroupStart()) - c2 := nts.Interval.GetCost(nts.GetDuration(), nts.GetGroupStart()) + c1 := ts.RateInterval.GetCost(ts.GetDuration(), ts.GetGroupStart()) + c2 := nts.RateInterval.GetCost(nts.GetDuration(), nts.GetGroupStart()) if c1 != 1800 || c2 != 900 { t.Error("Wrong costs: ", c1, c2) } if ts.GetDuration().Seconds() != 15*60 || nts.GetDuration().Seconds() != 15*60 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) } if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { t.Errorf("The duration has changed: %v + %v != %v", ts.GetDuration().Seconds(), nts.GetDuration().Seconds(), oldDuration.Seconds()) @@ -364,49 +253,68 @@ func TestTimespanSplitGroupedRates(t *testing.T) { } func TestTimespanSplitGroupedRatesIncrements(t *testing.T) { - i := &Interval{ + i := &RateInterval{ EndTime: "17:59:00", - Prices: PriceGroups{&Price{0, 2, 1 * time.Second, 1 * time.Second}, &Price{30 * time.Second, 1, 60 * time.Second, 1 * time.Second}}, + Rates: RateGroups{ + &Rate{ + GroupIntervalStart: 0, + Value: 2, + RateIncrement: time.Second, + RateUnit: time.Second}, + &Rate{ + GroupIntervalStart: 30 * time.Second, + Value: 1, + RateIncrement: time.Minute, + RateUnit: time.Second, + }}, } t1 := time.Date(2012, time.February, 3, 17, 30, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 3, 17, 31, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2, CallDuration: 60 * time.Second} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) + cd := &CallDescriptor{} + timespans := cd.roundTimeSpansToIncrement([]*TimeSpan{ts, nts}) + if len(timespans) != 2 { + t.Error("Error rounding timespans: ", timespans) + } + ts = timespans[0] + nts = timespans[1] splitTime := time.Date(2012, time.February, 3, 17, 30, 30, 0, time.UTC) if ts.TimeStart != t1 || ts.TimeEnd != splitTime { t.Error("Incorrect first half", ts) } - if nts.TimeStart != splitTime || nts.TimeEnd != t2 { - t.Error("Incorrect second half", nts) + t3 := time.Date(2012, time.February, 3, 17, 31, 30, 0, time.UTC) + if nts.TimeStart != splitTime || nts.TimeEnd != t3 { + t.Error("Incorrect second half", nts.TimeStart, nts.TimeEnd) } - if ts.Interval != i { - t.Error("Interval not attached correctly") + if ts.RateInterval != i { + t.Error("RateInterval not attached correctly") } - c1 := ts.Interval.GetCost(ts.GetDuration(), ts.GetGroupStart()) - c2 := nts.Interval.GetCost(nts.GetDuration(), nts.GetGroupStart()) + c1 := ts.RateInterval.GetCost(ts.GetDuration(), ts.GetGroupStart()) + c2 := nts.RateInterval.GetCost(nts.GetDuration(), nts.GetGroupStart()) if c1 != 60 || c2 != 60 { t.Error("Wrong costs: ", c1, c2) } - if ts.GetDuration().Seconds() != 0.5*60 || nts.GetDuration().Seconds() != 0.5*60 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) + if ts.GetDuration().Seconds() != 0.5*60 || nts.GetDuration().Seconds() != 1*60 { + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) } - if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { + if ts.GetDuration()+nts.GetDuration() != oldDuration+30*time.Second { t.Errorf("The duration has changed: %v + %v != %v", ts.GetDuration().Seconds(), nts.GetDuration().Seconds(), oldDuration.Seconds()) } } func TestTimespanSplitRightHourMarginBeforeGroup(t *testing.T) { - i := &Interval{ + i := &RateInterval{ EndTime: "17:00:30", - Prices: PriceGroups{&Price{0, 2, 1 * time.Second, 1 * time.Second}, &Price{60 * time.Second, 1, 60 * time.Second, 1 * time.Second}}, + Rates: RateGroups{&Rate{0, 2, 1 * time.Second, 1 * time.Second}, &Rate{60 * time.Second, 1, 60 * time.Second, 1 * time.Second}}, } t1 := time.Date(2012, time.February, 3, 17, 00, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 3, 17, 01, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) splitTime := time.Date(2012, time.February, 3, 17, 00, 30, 0, time.UTC) if ts.TimeStart != t1 || ts.TimeEnd != splitTime { t.Error("Incorrect first half", ts) @@ -414,32 +322,32 @@ func TestTimespanSplitRightHourMarginBeforeGroup(t *testing.T) { if nts.TimeStart != splitTime || nts.TimeEnd != t2 { t.Error("Incorrect second half", nts) } - if ts.Interval != i { - t.Error("Interval not attached correctly") + if ts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if ts.GetDuration().Seconds() != 30 || nts.GetDuration().Seconds() != 30 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) } if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { t.Errorf("The duration has changed: %v + %v != %v", ts.GetDuration().Seconds(), nts.GetDuration().Seconds(), oldDuration.Seconds()) } - nnts := nts.SplitByInterval(i) + nnts := nts.SplitByRateInterval(i) if nnts != nil { t.Error("Bad new split", nnts) } } func TestTimespanSplitGroupSecondSplit(t *testing.T) { - i := &Interval{ + i := &RateInterval{ EndTime: "17:03:30", - Prices: PriceGroups{&Price{0, 2, 1 * time.Second, 1 * time.Second}, &Price{60 * time.Second, 1, 1 * time.Second, 1 * time.Second}}, + Rates: RateGroups{&Rate{0, 2, 1 * time.Second, 1 * time.Second}, &Rate{60 * time.Second, 1, 1 * time.Second, 1 * time.Second}}, } t1 := time.Date(2012, time.February, 3, 17, 00, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 3, 17, 04, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2, CallDuration: 240 * time.Second} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) splitTime := time.Date(2012, time.February, 3, 17, 01, 00, 0, time.UTC) if ts.TimeStart != t1 || ts.TimeEnd != splitTime { t.Error("Incorrect first half", nts) @@ -447,17 +355,17 @@ func TestTimespanSplitGroupSecondSplit(t *testing.T) { if nts.TimeStart != splitTime || nts.TimeEnd != t2 { t.Error("Incorrect second half", nts) } - if ts.Interval != i { - t.Error("Interval not attached correctly") + if ts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if ts.GetDuration().Seconds() != 60 || nts.GetDuration().Seconds() != 180 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) } if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { t.Errorf("The duration has changed: %v + %v != %v", ts.GetDuration().Seconds(), nts.GetDuration().Seconds(), oldDuration.Seconds()) } - nnts := nts.SplitByInterval(i) + nnts := nts.SplitByRateInterval(i) nsplitTime := time.Date(2012, time.February, 3, 17, 03, 30, 0, time.UTC) if nts.TimeStart != splitTime || nts.TimeEnd != nsplitTime { t.Error("Incorrect first half", nts) @@ -465,25 +373,25 @@ func TestTimespanSplitGroupSecondSplit(t *testing.T) { if nnts.TimeStart != nsplitTime || nnts.TimeEnd != t2 { t.Error("Incorrect second half", nnts) } - if nts.Interval != i { - t.Error("Interval not attached correctly") + if nts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if nts.GetDuration().Seconds() != 150 || nnts.GetDuration().Seconds() != 30 { - t.Error("Wrong durations.for Intervals", nts.GetDuration().Seconds(), nnts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", nts.GetDuration().Seconds(), nnts.GetDuration().Seconds()) } } func TestTimespanSplitMultipleGroup(t *testing.T) { - i := &Interval{ + i := &RateInterval{ EndTime: "17:05:00", - Prices: PriceGroups{&Price{0, 2, 1 * time.Second, 1 * time.Second}, &Price{60 * time.Second, 1, 1 * time.Second, 1 * time.Second}, &Price{180 * time.Second, 1, 1 * time.Second, 1 * time.Second}}, + Rates: RateGroups{&Rate{0, 2, 1 * time.Second, 1 * time.Second}, &Rate{60 * time.Second, 1, 1 * time.Second, 1 * time.Second}, &Rate{180 * time.Second, 1, 1 * time.Second, 1 * time.Second}}, } t1 := time.Date(2012, time.February, 3, 17, 00, 0, 0, time.UTC) t2 := time.Date(2012, time.February, 3, 17, 04, 0, 0, time.UTC) ts := &TimeSpan{TimeStart: t1, TimeEnd: t2, CallDuration: 240 * time.Second} oldDuration := ts.GetDuration() - nts := ts.SplitByInterval(i) + nts := ts.SplitByRateInterval(i) splitTime := time.Date(2012, time.February, 3, 17, 01, 00, 0, time.UTC) if ts.TimeStart != t1 || ts.TimeEnd != splitTime { t.Error("Incorrect first half", nts) @@ -491,17 +399,17 @@ func TestTimespanSplitMultipleGroup(t *testing.T) { if nts.TimeStart != splitTime || nts.TimeEnd != t2 { t.Error("Incorrect second half", nts) } - if ts.Interval != i { - t.Error("Interval not attached correctly") + if ts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if ts.GetDuration().Seconds() != 60 || nts.GetDuration().Seconds() != 180 { - t.Error("Wrong durations.for Intervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", ts.GetDuration().Seconds(), nts.GetDuration().Seconds()) } if ts.GetDuration().Seconds()+nts.GetDuration().Seconds() != oldDuration.Seconds() { t.Errorf("The duration has changed: %v + %v != %v", ts.GetDuration().Seconds(), nts.GetDuration().Seconds(), oldDuration.Seconds()) } - nnts := nts.SplitByInterval(i) + nnts := nts.SplitByRateInterval(i) nsplitTime := time.Date(2012, time.February, 3, 17, 03, 00, 0, time.UTC) if nts.TimeStart != splitTime || nts.TimeEnd != nsplitTime { t.Error("Incorrect first half", nts) @@ -509,11 +417,365 @@ func TestTimespanSplitMultipleGroup(t *testing.T) { if nnts.TimeStart != nsplitTime || nnts.TimeEnd != t2 { t.Error("Incorrect second half", nnts) } - if nts.Interval != i { - t.Error("Interval not attached correctly") + if nts.RateInterval != i { + t.Error("RateInterval not attached correctly") } if nts.GetDuration().Seconds() != 120 || nnts.GetDuration().Seconds() != 60 { - t.Error("Wrong durations.for Intervals", nts.GetDuration().Seconds(), nnts.GetDuration().Seconds()) + t.Error("Wrong durations.for RateIntervals", nts.GetDuration().Seconds(), nnts.GetDuration().Seconds()) + } +} + +func TestTimespanExpandingPastEnd(t *testing.T) { + timespans := []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + RateInterval: &RateInterval{Rates: RateGroups{ + &Rate{RateIncrement: 60 * time.Second}, + }}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 45, 0, time.UTC), + }, + } + cd := &CallDescriptor{} + timespans = cd.roundTimeSpansToIncrement(timespans) + if len(timespans) != 1 { + t.Error("Error removing overlaped intervals: ", timespans) + } + if !timespans[0].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 31, 0, 0, time.UTC)) { + t.Error("Error expanding timespan: ", timespans[0]) + } +} + +func TestTimespanExpandingCallDuration(t *testing.T) { + timespans := []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + RateInterval: &RateInterval{Rates: RateGroups{ + &Rate{RateIncrement: 60 * time.Second}, + }}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 45, 0, time.UTC), + }, + } + cd := &CallDescriptor{} + timespans = cd.roundTimeSpansToIncrement(timespans) + + if len(timespans) != 1 || timespans[0].GetDuration() != time.Minute { + t.Error("Error setting call duration: ", timespans[0]) + } +} + +func TestTimespanExpandingRoundingPastEnd(t *testing.T) { + timespans := []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 20, 0, time.UTC), + RateInterval: &RateInterval{Rates: RateGroups{ + &Rate{RateIncrement: 15 * time.Second}, + }}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 20, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 40, 0, time.UTC), + }, + } + cd := &CallDescriptor{} + timespans = cd.roundTimeSpansToIncrement(timespans) + if len(timespans) != 2 { + t.Error("Error removing overlaped intervals: ", timespans) + } + if !timespans[0].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC)) { + t.Error("Error expanding timespan: ", timespans[0]) + } +} + +func TestTimespanExpandingPastEndMultiple(t *testing.T) { + timespans := []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + RateInterval: &RateInterval{Rates: RateGroups{ + &Rate{RateIncrement: 60 * time.Second}, + }}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 40, 0, time.UTC), + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 40, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 50, 0, time.UTC), + }, + } + cd := &CallDescriptor{} + timespans = cd.roundTimeSpansToIncrement(timespans) + if len(timespans) != 1 { + t.Error("Error removing overlaped intervals: ", timespans) + } + if !timespans[0].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 31, 0, 0, time.UTC)) { + t.Error("Error expanding timespan: ", timespans[0]) + } +} + +func TestTimespanExpandingPastEndMultipleEqual(t *testing.T) { + timespans := []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + RateInterval: &RateInterval{Rates: RateGroups{ + &Rate{RateIncrement: 60 * time.Second}, + }}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 40, 0, time.UTC), + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 40, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 31, 00, 0, time.UTC), + }, + } + cd := &CallDescriptor{} + timespans = cd.roundTimeSpansToIncrement(timespans) + if len(timespans) != 1 { + t.Error("Error removing overlaped intervals: ", timespans) + } + if !timespans[0].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 31, 0, 0, time.UTC)) { + t.Error("Error expanding timespan: ", timespans[0]) + } +} + +func TestTimespanExpandingBeforeEnd(t *testing.T) { + timespans := []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + RateInterval: &RateInterval{Rates: RateGroups{ + &Rate{RateIncrement: 45 * time.Second}, + }}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 31, 0, 0, time.UTC), + }, + } + cd := &CallDescriptor{} + timespans = cd.roundTimeSpansToIncrement(timespans) + if len(timespans) != 2 { + t.Error("Error removing overlaped intervals: ", timespans) + } + if !timespans[0].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 30, 45, 0, time.UTC)) || + !timespans[1].TimeStart.Equal(time.Date(2013, 9, 10, 14, 30, 45, 0, time.UTC)) || + !timespans[1].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 31, 0, 0, time.UTC)) { + t.Error("Error expanding timespan: ", timespans[0]) + } +} + +func TestTimespanExpandingBeforeEndMultiple(t *testing.T) { + timespans := []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + RateInterval: &RateInterval{Rates: RateGroups{ + &Rate{RateIncrement: 45 * time.Second}, + }}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 50, 0, time.UTC), + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 50, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 31, 00, 0, time.UTC), + }, + } + cd := &CallDescriptor{} + timespans = cd.roundTimeSpansToIncrement(timespans) + if len(timespans) != 3 { + t.Error("Error removing overlaped intervals: ", timespans) + } + if !timespans[0].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 30, 45, 0, time.UTC)) || + !timespans[1].TimeStart.Equal(time.Date(2013, 9, 10, 14, 30, 45, 0, time.UTC)) || + !timespans[1].TimeEnd.Equal(time.Date(2013, 9, 10, 14, 30, 50, 0, time.UTC)) { + t.Error("Error expanding timespan: ", timespans[0]) + } +} + +func TestTimespanCreateSecondsSlice(t *testing.T) { + ts := &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 30, 0, time.UTC), + RateInterval: &RateInterval{Rates: RateGroups{ + &Rate{Value: 2.0}, + }}, + } + ts.createIncrementsSlice() + if len(ts.Increments) != 30 { + t.Error("Error creating second slice: ", ts.Increments) + } + if ts.Increments[0].Cost != 2.0 { + t.Error("Wrong second slice: ", ts.Increments[0]) + } +} + +func TestTimespanCreateIncrements(t *testing.T) { + ts := &TimeSpan{ + TimeStart: time.Date(2013, 9, 10, 14, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 10, 14, 30, 30, 100000000, time.UTC), + RateInterval: &RateInterval{ + RoundingMethod: utils.ROUNDING_MIDDLE, + RoundingDecimals: 2, + Rates: RateGroups{ + &Rate{ + Value: 2.0, + RateIncrement: 10 * time.Second, + }, + }, + }, + } + ts.createIncrementsSlice() + if len(ts.Increments) != 3 { + t.Error("Error creating increment slice: ", len(ts.Increments)) + } + if len(ts.Increments) < 3 || ts.Increments[2].Cost != 20 { + t.Error("Wrong second slice: ", ts.Increments) + } +} + +func TestTimespanSplitByIncrement(t *testing.T) { + ts := &TimeSpan{ + TimeStart: time.Date(2013, 9, 19, 18, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 19, 18, 31, 00, 0, time.UTC), + CallDuration: 60 * time.Second, + RateInterval: &RateInterval{ + RoundingMethod: utils.ROUNDING_MIDDLE, + RoundingDecimals: 2, + Rates: RateGroups{ + &Rate{ + Value: 2.0, + RateIncrement: 10 * time.Second, + }, + }, + }, + } + ts.createIncrementsSlice() + if len(ts.Increments) != 6 { + t.Error("Error creating increment slice: ", len(ts.Increments)) + } + newTs := ts.SplitByIncrement(5) + if ts.GetDuration() != 50*time.Second || newTs.GetDuration() != 10*time.Second { + t.Error("Error spliting by increment: ", ts.GetDuration(), newTs.GetDuration()) + } + if ts.CallDuration != 50*time.Second || newTs.CallDuration != 60*time.Second { + t.Error("Error spliting by increment at setting call duration: ", ts.CallDuration, newTs.CallDuration) + } + if len(ts.Increments) != 5 || len(newTs.Increments) != 1 { + t.Error("Error spliting increments: ", ts.Increments, newTs.Increments) + } +} + +func TestTimespanSplitByIncrementStart(t *testing.T) { + ts := &TimeSpan{ + TimeStart: time.Date(2013, 9, 19, 18, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 19, 18, 31, 00, 0, time.UTC), + CallDuration: 60 * time.Second, + RateInterval: &RateInterval{ + RoundingMethod: utils.ROUNDING_MIDDLE, + RoundingDecimals: 2, + Rates: RateGroups{ + &Rate{ + Value: 2.0, + RateIncrement: 10 * time.Second, + }, + }, + }, + } + ts.createIncrementsSlice() + if len(ts.Increments) != 6 { + t.Error("Error creating increment slice: ", len(ts.Increments)) + } + newTs := ts.SplitByIncrement(0) + if ts.GetDuration() != 60*time.Second || newTs != nil { + t.Error("Error spliting by increment: ", ts.GetDuration()) + } + if ts.CallDuration != 60*time.Second { + t.Error("Error spliting by incrementat setting call duration: ", ts.CallDuration) + } + if len(ts.Increments) != 6 { + t.Error("Error spliting increments: ", ts.Increments) + } +} + +func TestTimespanSplitByIncrementEnd(t *testing.T) { + ts := &TimeSpan{ + TimeStart: time.Date(2013, 9, 19, 18, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 19, 18, 31, 00, 0, time.UTC), + CallDuration: 60 * time.Second, + RateInterval: &RateInterval{ + RoundingMethod: utils.ROUNDING_MIDDLE, + RoundingDecimals: 2, + Rates: RateGroups{ + &Rate{ + Value: 2.0, + RateIncrement: 10 * time.Second, + }, + }, + }, + } + ts.createIncrementsSlice() + if len(ts.Increments) != 6 { + t.Error("Error creating increment slice: ", len(ts.Increments)) + } + newTs := ts.SplitByIncrement(6) + if ts.GetDuration() != 60*time.Second || newTs != nil { + t.Error("Error spliting by increment: ", ts.GetDuration()) + } + if ts.CallDuration != 60*time.Second { + t.Error("Error spliting by increment at setting call duration: ", ts.CallDuration) + } + if len(ts.Increments) != 6 { + t.Error("Error spliting increments: ", ts.Increments) + } +} + +func TestTimespanSplitByDuration(t *testing.T) { + ts := &TimeSpan{ + TimeStart: time.Date(2013, 9, 19, 18, 30, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 19, 18, 31, 00, 0, time.UTC), + CallDuration: 60 * time.Second, + RateInterval: &RateInterval{ + RoundingMethod: utils.ROUNDING_MIDDLE, + RoundingDecimals: 2, + Rates: RateGroups{ + &Rate{ + Value: 2.0, + RateIncrement: 10 * time.Second, + }, + }, + }, + } + ts.createIncrementsSlice() + if len(ts.Increments) != 6 { + t.Error("Error creating increment slice: ", len(ts.Increments)) + } + newTs := ts.SplitByDuration(46 * time.Second) + if ts.GetDuration() != 46*time.Second || newTs.GetDuration() != 14*time.Second { + t.Error("Error spliting by duration: ", ts.GetDuration(), newTs.GetDuration()) + } + if ts.CallDuration != 46*time.Second || newTs.CallDuration != 60*time.Second { + t.Error("Error spliting by duration at setting call duration: ", ts.CallDuration, newTs.CallDuration) + } + if len(ts.Increments) != 5 || len(newTs.Increments) != 2 { + t.Error("Error spliting increments: ", ts.Increments, newTs.Increments) + } + if ts.Increments[4].Duration != 6*time.Second || newTs.Increments[0].Duration != 4*time.Second { + t.Error("Error spliting increment: ", ts.Increments[4], newTs.Increments[0]) } } diff --git a/engine/tpimporter_csv.go b/engine/tpimporter_csv.go index b4cdd77b3..9d424ddee 100644 --- a/engine/tpimporter_csv.go +++ b/engine/tpimporter_csv.go @@ -24,13 +24,12 @@ import ( "io/ioutil" "log" "strconv" - "time" ) // Import tariff plan from csv into storDb type TPCSVImporter struct { TPid string // Load data on this tpid - StorDb DataStorage // StorDb connection handle + StorDb LoadStorage // StorDb connection handle DirPath string // Directory path to import from Sep rune // Separator in the csv file Verbose bool // If true will print a detailed information instead of silently discarding it @@ -139,11 +138,11 @@ func (self *TPCSVImporter) importRates(fn string) error { } continue } - rt, err := NewRate(record[0], record[1], record[2], record[3], record[4], record[5], record[6], record[7], record[8]) + rt, err := NewLoadRate(record[0], record[1], record[2], record[3], record[4], record[5], record[6], record[7], record[8]) if err != nil { return err } - if err := self.StorDb.SetTPRates(self.TPid, map[string][]*Rate{record[0]: []*Rate{rt}}); err != nil { + if err := self.StorDb.SetTPRates(self.TPid, map[string][]*LoadRate{record[0]: []*LoadRate{rt}}); err != nil { if self.Verbose { log.Printf("Ignoring line %d, storDb operational error: <%s> ", lineNr, err.Error()) } @@ -209,7 +208,7 @@ func (self *TPCSVImporter) importDestRateTimings(fn string) error { drt := &DestinationRateTiming{Tag: record[0], DestinationRatesTag: record[1], Weight: weight, - TimingsTag: record[2], + TimingTag: record[2], } if err := self.StorDb.SetTPDestRateTimings(self.TPid, map[string][]*DestinationRateTiming{drt.Tag: []*DestinationRateTiming{drt}}); err != nil { if self.Verbose { @@ -286,7 +285,7 @@ func (self *TPCSVImporter) importActions(fn string) error { } continue } - actId, actionType, balanceType, direction, destTag, rateType := record[0], record[1], record[2], record[3], record[6], record[7] + actId, actionType, balanceType, direction, destTag, rateSubject := record[0], record[1], record[2], record[3], record[6], record[7] units, err := strconv.ParseFloat(record[4], 64) if err != nil { if self.Verbose { @@ -294,18 +293,7 @@ func (self *TPCSVImporter) importActions(fn string) error { } continue } - var expiryTime time.Time // Empty initialized time represents never expire - if record[5] != "*unlimited" { // ToDo: Expand here for other meta tags or go way of adding time for expiry - expiryTime, err = time.Parse(time.RFC3339, record[5]) - if err != nil { - if self.Verbose { - log.Printf("Ignoring line %d, warning: <%s> ", lineNr, err.Error()) - } - continue - } - } - rateValue, _ := strconv.ParseFloat(record[8], 64) // Ignore errors since empty string is error, we can find out based on rateType if defined - minutesWeight, _ := strconv.ParseFloat(record[9], 64) + balanceWeight, _ := strconv.ParseFloat(record[8], 64) weight, err := strconv.ParseFloat(record[10], 64) if err != nil { if self.Verbose { @@ -314,16 +302,18 @@ func (self *TPCSVImporter) importActions(fn string) error { continue } act := &Action{ - ActionType: actionType, - BalanceId: balanceType, - Direction: direction, - Units: units, - ExpirationDate: expiryTime, - DestinationTag: destTag, - RateType: rateType, - RateValue: rateValue, - MinutesWeight: minutesWeight, - Weight: weight, + ActionType: actionType, + BalanceId: balanceType, + Direction: direction, + ExpirationString: record[5], + ExtraParameters: record[9], + Balance: &Balance{ + Value: units, + DestinationId: destTag, + RateSubject: rateSubject, + Weight: balanceWeight, + }, + Weight: weight, } if err := self.StorDb.SetTPActions(self.TPid, map[string][]*Action{actId: []*Action{act}}); err != nil { if self.Verbose { diff --git a/engine/units_counter.go b/engine/units_counter.go index 2f129e567..deed2fdc7 100644 --- a/engine/units_counter.go +++ b/engine/units_counter.go @@ -24,39 +24,41 @@ import ( // Amount of a trafic of a certain type type UnitsCounter struct { - Direction string - BalanceId string - Units float64 - MinuteBuckets bucketsorter + Direction string + BalanceId string + Units float64 + MinuteBalances BalanceChain } -func (uc *UnitsCounter) initMinuteBuckets(ats []*ActionTrigger) { - uc.MinuteBuckets = make(bucketsorter, 0) +func (uc *UnitsCounter) initMinuteBalances(ats []*ActionTrigger) { + uc.MinuteBalances = make(BalanceChain, 0) for _, at := range ats { acs, err := storageGetter.GetActions(at.ActionsId) if err != nil { continue } for _, a := range acs { - if a.MinuteBucket != nil { - uc.MinuteBuckets = append(uc.MinuteBuckets, a.MinuteBucket.Clone()) + if a.BalanceId == MINUTES && a.Balance != nil { + b := a.Balance.Clone() + b.Value = 0 + uc.MinuteBalances = append(uc.MinuteBalances, b) } } } - uc.MinuteBuckets.Sort() + uc.MinuteBalances.Sort() } -// Adds the minutes from the received minute bucket to an existing bucket if the destination -// is the same or ads the minutye bucket to the list if none matches. +// Adds the minutes from the received minute balance to an existing bucket if the destination +// is the same or ads the minute balance to the list if none matches. func (uc *UnitsCounter) addMinutes(amount float64, prefix string) { - for _, mb := range uc.MinuteBuckets { - d, err := GetDestination(mb.DestinationId) + for _, mb := range uc.MinuteBalances { + precision, err := storageGetter.DestinationContainsPrefix(mb.DestinationId, prefix) if err != nil { - Logger.Err(fmt.Sprintf("Minutes counter: unknown destination: %s", mb.DestinationId)) + Logger.Err(fmt.Sprintf("Minutes counter: unknown destination: %v", mb.DestinationId)) continue } - if _, ok := d.containsPrefix(prefix); ok { - mb.Seconds += amount + if precision > 0 { + mb.Value += amount break } } diff --git a/engine/units_counter_test.go b/engine/units_counter_test.go index 0c942d483..eaa54332a 100644 --- a/engine/units_counter_test.go +++ b/engine/units_counter_test.go @@ -22,28 +22,28 @@ import ( "testing" ) -func TestUnitsCounterAddMinuteBucket(t *testing.T) { +func TestUnitsCounterAddBalance(t *testing.T) { uc := &UnitsCounter{ - Direction: OUTBOUND, - BalanceId: SMS, - Units: 100, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, + Direction: OUTBOUND, + BalanceId: SMS, + Units: 100, + MinuteBalances: []*Balance{&Balance{Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}, } uc.addMinutes(20, "test") - if len(uc.MinuteBuckets) != 2 { + if len(uc.MinuteBalances) != 2 { t.Error("Error adding minute bucket!") } } -func TestUnitsCounterAddMinuteBucketExists(t *testing.T) { +func TestUnitsCounterAddBalanceExists(t *testing.T) { uc := &UnitsCounter{ - Direction: OUTBOUND, - BalanceId: SMS, - Units: 100, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, + Direction: OUTBOUND, + BalanceId: SMS, + Units: 100, + MinuteBalances: []*Balance{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}, } uc.addMinutes(5, "0723") - if len(uc.MinuteBuckets) != 2 || uc.MinuteBuckets[0].Seconds != 15 { + if len(uc.MinuteBalances) != 2 || uc.MinuteBalances[0].Value != 15 { t.Error("Error adding minute bucket!") } } diff --git a/engine/userbalance.go b/engine/userbalance.go index 7c298de31..8288ddf67 100644 --- a/engine/userbalance.go +++ b/engine/userbalance.go @@ -20,8 +20,8 @@ package engine import ( "errors" + "fmt" "github.com/cgrates/cgrates/utils" - "sort" "strings" "time" ) @@ -38,6 +38,17 @@ const ( TRAFFIC = "*internet" TRAFFIC_TIME = "*internet_time" MINUTES = "*minutes" + // action price type + PRICE_PERCENT = "*percent" + PRICE_ABSOLUTE = "*absolute" + // action trigger threshold types + TRIGGER_MIN_COUNTER = "*min_counter" + TRIGGER_MAX_COUNTER = "*max_counter" + TRIGGER_MIN_BALANCE = "*min_balance" + TRIGGER_MAX_BALANCE = "*max_balance" + // minute subjects + ZEROSECOND = "*zerosecond" + ZEROMINUTE = "*zerominute" ) var ( @@ -46,247 +57,416 @@ var ( /* Structure containing information about user's credit (minutes, cents, sms...).' +This can represent a user or a shared group. */ type UserBalance struct { Id string Type string // prepaid-postpaid BalanceMap map[string]BalanceChain - MinuteBuckets []*MinuteBucket UnitCounters []*UnitsCounter ActionTriggers ActionTriggerPriotityList + Groups GroupLinks // user info about groups + // group information + UserIds []string // group info about users } -type Balance struct { - Id string - Value float64 - ExpirationDate time.Time - Weight float64 -} - -func (b *Balance) Equal(o *Balance) bool { - return b.ExpirationDate.Equal(o.ExpirationDate) || - b.Weight == o.Weight -} - -func (b *Balance) IsExpired() bool { - return !b.ExpirationDate.IsZero() && b.ExpirationDate.Before(time.Now()) -} - -func (b *Balance) Clone() *Balance { - return &Balance{ - Id: b.Id, - Value: b.Value, - ExpirationDate: b.ExpirationDate, - Weight: b.Weight, - } -} - -/* -Structure to store minute buckets according to weight, precision or price. -*/ -type BalanceChain []*Balance - -func (bc BalanceChain) Len() int { - return len(bc) -} - -func (bc BalanceChain) Swap(i, j int) { - bc[i], bc[j] = bc[j], bc[i] -} - -func (bc BalanceChain) Less(j, i int) bool { - return bc[i].Weight < bc[j].Weight -} - -func (bc BalanceChain) Sort() { - sort.Sort(bc) -} - -func (bc BalanceChain) GetTotalValue() (total float64) { - for _, b := range bc { - if !b.IsExpired() { - total += b.Value - } - } - return -} - -func (bc BalanceChain) Debit(amount float64) float64 { - bc.Sort() - for i, b := range bc { - if b.IsExpired() { - continue - } - if b.Value >= amount || i == len(bc)-1 { // if last one go negative - b.Value -= amount - break - } - b.Value = 0 - amount -= b.Value - } - return bc.GetTotalValue() -} - -func (bc BalanceChain) Equal(o BalanceChain) bool { - if len(bc) != len(o) { - return false - } - bc.Sort() - o.Sort() - for i := 0; i < len(bc); i++ { - if !bc[i].Equal(o[i]) { - return false - } - } - return true -} - -func (bc BalanceChain) Clone() BalanceChain { - var newChain BalanceChain - for _, b := range bc { - newChain = append(newChain, b.Clone()) - } - return newChain -} - -/* -Returns user's available minutes for the specified destination -*/ -func (ub *UserBalance) getSecondsForPrefix(prefix string) (seconds, credit float64, bucketList bucketsorter) { - credit = ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() - if len(ub.MinuteBuckets) == 0 { - // Logger.Debug("There are no minute buckets to check for user: ", ub.Id) - return - } - for _, mb := range ub.MinuteBuckets { - if mb.IsExpired() { - continue - } - d, err := GetDestination(mb.DestinationId) +// Returns user's available minutes for the specified destination +func (ub *UserBalance) getSecondsForPrefix(cd *CallDescriptor) (seconds, credit float64, balances BalanceChain) { + credit = ub.getBalancesForPrefix(cd.Destination, ub.BalanceMap[CREDIT+cd.Direction]).GetTotalValue() + balances = ub.getBalancesForPrefix(cd.Destination, ub.BalanceMap[MINUTES+cd.Direction]) + for _, b := range balances { + s := b.GetSecondsForCredit(cd, credit) + cc, err := b.GetCost(cd) if err != nil { + Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err)) continue } - if precision, ok := d.containsPrefix(prefix); ok { - mb.precision = precision - if mb.Seconds > 0 { - bucketList = append(bucketList, mb) - } + if cc.Cost > 0 && cc.GetDuration() > 0 { + // TODO: improve this + secondCost := cc.Cost / cc.GetDuration().Seconds() + credit -= s * secondCost } - } - bucketList.Sort() // sorts the buckets according to priority, precision or price - for _, mb := range bucketList { - s := mb.GetSecondsForCredit(credit) - credit -= s * mb.Price seconds += s } return } -// Debit seconds from specified minute bucket -func (ub *UserBalance) debitMinuteBucket(newMb *MinuteBucket) error { - if newMb == nil { - return errors.New("Nil minute bucket!") +// Debits some amount of user's specified balance adding the balance if it does not exists. +// Returns the remaining credit in user's balance. +func (ub *UserBalance) debitBalanceAction(a *Action) error { + if a == nil { + return errors.New("nil minute action!") + } + if a.Balance.Uuid == "" { + a.Balance.Uuid = utils.GenUUID() + } + if ub.BalanceMap == nil { + ub.BalanceMap = make(map[string]BalanceChain, 0) } found := false - for _, mb := range ub.MinuteBuckets { - if mb.IsExpired() { - continue + id := a.BalanceId + a.Direction + for _, b := range ub.BalanceMap[id] { + if b.IsExpired() { + continue // we can clean expired balances balances here } - if mb.Equal(newMb) { - mb.Seconds -= newMb.Seconds + if b.Equal(a.Balance) { + b.Value -= a.Balance.Value found = true break } } // if it is not found and the Seconds are negative (topup) // then we add it to the list - if !found && newMb.Seconds <= 0 { - newMb.Seconds = -newMb.Seconds - ub.MinuteBuckets = append(ub.MinuteBuckets, newMb) + if !found && a.Balance.Value <= 0 { + a.Balance.Value = -a.Balance.Value + ub.BalanceMap[id] = append(ub.BalanceMap[id], a.Balance) } - return nil + return nil //ub.BalanceMap[id].GetTotalValue() +} + +func (ub *UserBalance) getBalancesForPrefix(prefix string, balances BalanceChain) BalanceChain { + var usefulBalances BalanceChain + for _, b := range balances { + if b.IsExpired() || (ub.Type != UB_TYPE_POSTPAID && b.Value <= 0) { + continue + } + if b.DestinationId != "" { + precision, err := storageGetter.DestinationContainsPrefix(b.DestinationId, prefix) + if err != nil { + continue + } + if precision > 0 { + b.precision = precision + usefulBalances = append(usefulBalances, b) + } + } else { + usefulBalances = append(usefulBalances, b) + } + } + // resort by precision + usefulBalances.Sort() + return usefulBalances } /* -Debits the received amount of seconds from user's minute buckets. -All the appropriate buckets will be debited until all amount of minutes is consumed. -If the amount is bigger than the sum of all seconds in the minute buckets than nothing will be -debited and an error will be returned. +This method is the core of userbalance debiting: don't panic just follow the branches */ -func (ub *UserBalance) debitMinutesBalance(amount float64, prefix string, count bool) error { - if count { - ub.countUnits(&Action{BalanceId: MINUTES, Direction: OUTBOUND, MinuteBucket: &MinuteBucket{Seconds: amount, DestinationId: prefix}}) - } - avaliableNbSeconds, _, bucketList := ub.getSecondsForPrefix(prefix) - if avaliableNbSeconds < amount { - return AMOUNT_TOO_BIG - } - var credit BalanceChain - if bc, exists := ub.BalanceMap[CREDIT+OUTBOUND]; exists { - credit = bc.Clone() - } - for _, mb := range bucketList { - if mb.Seconds < amount { - if mb.Price > 0 { // debit the money if the bucket has price - credit.Debit(mb.Seconds * mb.Price) +func (ub *UserBalance) debitCreditBalance(cc *CallCost, count bool) error { + minuteBalances := ub.BalanceMap[MINUTES+cc.Direction] + moneyBalances := ub.BalanceMap[CREDIT+cc.Direction] + usefulMinuteBalances := ub.getBalancesForPrefix(cc.Destination, minuteBalances) + usefulMoneyBalances := ub.getBalancesForPrefix(cc.Destination, moneyBalances) + // debit connect fee + if cc.ConnectFee > 0 { + amount := cc.ConnectFee + paid := false + for _, b := range usefulMoneyBalances { + if b.Value >= amount { + b.Value -= amount + // the conect fee is not refoundable! + if count { + ub.countUnits(&Action{BalanceId: CREDIT, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) + } + paid = true + break } - } else { - if mb.Price > 0 { // debit the money if the bucket has price - credit.Debit(amount * mb.Price) - } - break } - if ub.Type == UB_TYPE_PREPAID && credit.GetTotalValue() < 0 { - break + if !paid { + // there are no money for the connect fee; abort mission + cc.Timespans = make([]*TimeSpan, 0) + return nil } } - // need to check again because there are two break above - if ub.Type == UB_TYPE_PREPAID && credit.GetTotalValue() < 0 { - return AMOUNT_TOO_BIG - } - ub.BalanceMap[CREDIT+OUTBOUND] = credit // credit is > 0 + // debit minutes + for tsIndex := 0; tsIndex < len(cc.Timespans); tsIndex++ { + ts := cc.Timespans[tsIndex] + ts.createIncrementsSlice() + tsWasSplit := false + for incrementIndex, increment := range ts.Increments { + if tsWasSplit { + break + } + paid := false + for _, b := range usefulMinuteBalances { + // check standard subject tags + if b.RateSubject == ZEROSECOND || b.RateSubject == "" { + amount := increment.Duration.Seconds() + if b.Value >= amount { + b.Value -= amount + increment.BalanceUuid = b.Uuid + increment.MinuteInfo = &MinuteInfo{b.DestinationId, amount, 0} + paid = true + if count { + ub.countUnits(&Action{BalanceId: MINUTES, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) + } + break + } + } + if b.RateSubject == ZEROMINUTE { + amount := time.Minute.Seconds() + if b.Value >= amount { // balance has at least 60 seconds + newTs := ts + if incrementIndex != 0 { + // if increment it's not at the begining we must split the timespan + newTs = ts.SplitByIncrement(incrementIndex) + } + newTs.RoundToDuration(time.Minute) + newTs.RateInterval = &RateInterval{ + Rates: RateGroups{ + &Rate{ + GroupIntervalStart: 0, + Value: 0, + RateIncrement: time.Minute, + RateUnit: time.Minute, + }, + }, + } + newTs.createIncrementsSlice() + // overlap the rest of the timespans + for i := tsIndex + 1; i < len(cc.Timespans); i++ { + if cc.Timespans[i].TimeEnd.Before(newTs.TimeEnd) || cc.Timespans[i].TimeEnd.Equal(newTs.TimeEnd) { + cc.Timespans[i].overlapped = true + } else if cc.Timespans[i].TimeStart.Before(newTs.TimeEnd) { + cc.Timespans[i].TimeStart = ts.TimeEnd + } + } + // insert the new timespan + if newTs != ts { + tsIndex++ + cc.Timespans = append(cc.Timespans, nil) + copy(cc.Timespans[tsIndex+1:], cc.Timespans[tsIndex:]) + cc.Timespans[tsIndex] = newTs + tsWasSplit = true + } - for _, mb := range bucketList { - if mb.Seconds < amount { - amount -= mb.Seconds - mb.Seconds = 0 - } else { - mb.Seconds -= amount - break + var newTimespans []*TimeSpan + // remove overlapped + for _, ots := range cc.Timespans { + if !ots.overlapped { + newTimespans = append(newTimespans, ots) + } + } + cc.Timespans = newTimespans + b.Value -= amount + newTs.Increments[0].BalanceUuid = b.Uuid + newTs.Increments[0].MinuteInfo = &MinuteInfo{b.DestinationId, amount, 0} + paid = true + if count { + ub.countUnits(&Action{BalanceId: MINUTES, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) + } + break + } + } + // get the new rate + cd := cc.CreateCallDescriptor() + cd.TimeStart = ts.GetTimeStartForIncrement(incrementIndex) + cd.TimeEnd = cc.Timespans[len(cc.Timespans)-1].TimeEnd + cd.CallDuration = cc.Timespans[len(cc.Timespans)-1].CallDuration + newCC, err := b.GetCost(cd) + if err != nil { + Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err)) + continue + } + //debit new callcost + var paidTs []*TimeSpan + for _, nts := range newCC.Timespans { + paidTs = append(paidTs, nts) + for nIdx, nInc := range nts.Increments { + // debit minutes and money + seconds := nInc.Duration.Seconds() + amount := nInc.Cost + if b.Value >= seconds { + b.Value -= seconds + nInc.BalanceUuid = b.Uuid + if count { + ub.countUnits(&Action{BalanceId: CREDIT, Direction: newCC.Direction, Balance: &Balance{Value: amount, DestinationId: newCC.Destination}}) + } + } else { + nts.SplitByIncrement(nIdx) + } + } + } + // calculate overlaped timespans + var paidDuration time.Duration + for _, pts := range paidTs { + paidDuration += pts.GetDuration() + } + if paidDuration > 0 { + // split from current increment + newTs := ts.SplitByIncrement(incrementIndex) + remainingTs := []*TimeSpan{newTs} + + for tsi := tsIndex + 1; tsi < len(cc.Timespans); tsi++ { + remainingTs = append(remainingTs, cc.Timespans[tsi]) + } + for remainingIndex, rts := range remainingTs { + if paidDuration >= rts.GetDuration() { + paidDuration -= rts.GetDuration() + } else { + if paidDuration > 0 { + // this ts was not fully paid + fragment := rts.SplitByDuration(paidDuration) + paidTs = append(paidTs, fragment) + } + // delete from tsIndex to current + cc.Timespans = append(cc.Timespans[:tsIndex], cc.Timespans[remainingIndex:]...) + break + } + } + + // append the timpespans to outer timespans + for _, pts := range paidTs { + tsIndex++ + cc.Timespans = append(cc.Timespans, nil) + copy(cc.Timespans[tsIndex+1:], cc.Timespans[tsIndex:]) + cc.Timespans[tsIndex] = pts + } + paid = true + tsWasSplit = true + } + } + if paid { + continue + } else { + // Split if some increments were processed by minutes + if incrementIndex > 0 && ts.Increments[incrementIndex-1].MinuteInfo != nil { + newTs := ts.SplitByIncrement(incrementIndex) + if newTs != nil { + idx := tsIndex + 1 + cc.Timespans = append(cc.Timespans, nil) + copy(cc.Timespans[idx+1:], cc.Timespans[idx:]) + cc.Timespans[idx] = newTs + newTs.createIncrementsSlice() + tsWasSplit = true + } + break + } + } + // debit monetary + for _, b := range usefulMoneyBalances { + // check standard subject tags + if b.RateSubject == "" { + amount := increment.Cost + if b.Value >= amount { + b.Value -= amount + increment.BalanceUuid = b.Uuid + paid = true + if count { + ub.countUnits(&Action{BalanceId: CREDIT, Direction: cc.Direction, Balance: &Balance{Value: amount, DestinationId: cc.Destination}}) + } + break + } + } else { + // get the new rate + cd := cc.CreateCallDescriptor() + cd.TimeStart = ts.GetTimeStartForIncrement(incrementIndex) + cd.TimeEnd = cc.Timespans[len(cc.Timespans)-1].TimeEnd + cd.CallDuration = cc.Timespans[len(cc.Timespans)-1].CallDuration + newCC, err := b.GetCost(cd) + if err != nil { + Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err)) + continue + } + //debit new callcost + var paidTs []*TimeSpan + for _, nts := range newCC.Timespans { + paidTs = append(paidTs, nts) + for nIdx, nInc := range nts.Increments { + // debit money + amount := nInc.Cost + if b.Value >= amount { + b.Value -= amount + nInc.BalanceUuid = b.Uuid + if count { + ub.countUnits(&Action{BalanceId: CREDIT, Direction: newCC.Direction, Balance: &Balance{Value: amount, DestinationId: newCC.Destination}}) + } + } else { + nts.SplitByIncrement(nIdx) + } + } + } + // calculate overlaped timespans + var paidDuration time.Duration + for _, pts := range paidTs { + paidDuration += pts.GetDuration() + } + if paidDuration > 0 { + // split from current increment + newTs := ts.SplitByIncrement(incrementIndex) + remainingTs := []*TimeSpan{newTs} + + for tsi := tsIndex + 1; tsi < len(cc.Timespans); tsi++ { + remainingTs = append(remainingTs, cc.Timespans[tsi]) + } + for remainingIndex, rts := range remainingTs { + if paidDuration >= rts.GetDuration() { + paidDuration -= rts.GetDuration() + } else { + if paidDuration > 0 { + // this ts was not fully paid + fragment := rts.SplitByDuration(paidDuration) + paidTs = append(paidTs, fragment) + } + // delete from tsIndex to current + cc.Timespans = append(cc.Timespans[:tsIndex], cc.Timespans[remainingIndex:]...) + break + } + } + + // append the timpespans to outer timespans + for _, pts := range paidTs { + tsIndex++ + cc.Timespans = append(cc.Timespans, nil) + copy(cc.Timespans[tsIndex+1:], cc.Timespans[tsIndex:]) + cc.Timespans[tsIndex] = pts + } + paid = true + tsWasSplit = true + } + } + } + if !paid { + // no balance was attached to this increment: cut the rest of increments/timespans + if incrementIndex == 0 { + // if we are right at the begining in the ts leave it out + cc.Timespans = cc.Timespans[:tsIndex] + } else { + ts.SplitByIncrement(incrementIndex) + cc.Timespans = cc.Timespans[:tsIndex+1] + } + return nil + } } } + return nil } -// Debits some amount of user's specified balance adding the balance if it does not exists. -// Returns the remaining credit in user's balance. -func (ub *UserBalance) debitBalanceAction(a *Action) float64 { - newBalance := &Balance{ - Id: utils.GenUUID(), - ExpirationDate: a.ExpirationDate, - Weight: a.Weight, - } - found := false - id := a.BalanceId + a.Direction - for _, b := range ub.BalanceMap[id] { - if b.Equal(newBalance) { - b.Value -= a.Units - found = true +func (ub *UserBalance) refoundIncrements(increments Increments, count bool) { + for _, increment := range increments { + var balance *Balance + for _, balanceChain := range ub.BalanceMap { + if balance = balanceChain.GetBalance(increment.BalanceUuid); balance != nil { + break + } + } + if balance != nil { + balance.Value += increment.Cost + if count { + ub.countUnits(&Action{BalanceId: increment.BalanceType, Direction: OUTBOUND, Balance: &Balance{Value: increment.Cost}}) + } + } else { + // TODO: where should put the money? } } - if !found { - newBalance.Value -= a.Units - ub.BalanceMap[id] = append(ub.BalanceMap[id], newBalance) - } - return ub.BalanceMap[a.BalanceId+OUTBOUND].GetTotalValue() } /* Debits some amount of user's specified balance. Returns the remaining credit in user's balance. */ -func (ub *UserBalance) debitBalance(balanceId string, amount float64, count bool) float64 { +func (ub *UserBalance) debitGenericBalance(balanceId string, amount float64, count bool) float64 { if count { - ub.countUnits(&Action{BalanceId: balanceId, Direction: OUTBOUND, Units: amount}) + ub.countUnits(&Action{BalanceId: balanceId, Direction: OUTBOUND, Balance: &Balance{Value: amount}}) } ub.BalanceMap[balanceId+OUTBOUND].Debit(amount) return ub.BalanceMap[balanceId+OUTBOUND].GetTotalValue() @@ -301,25 +481,21 @@ func (ub *UserBalance) executeActionTriggers(a *Action) { // the next reset (see RESET_TRIGGERS action type) continue } - if a != nil && (at.BalanceId != a.BalanceId || - at.Direction != a.Direction || - (a.MinuteBucket != nil && - (at.ThresholdType != a.MinuteBucket.PriceType || - at.ThresholdValue != a.MinuteBucket.Price))) { + if !at.Match(a) { continue } if strings.Contains(at.ThresholdType, "counter") { for _, uc := range ub.UnitCounters { if uc.BalanceId == at.BalanceId { - if at.BalanceId == MINUTES && at.DestinationId != "" { // last check adds safety - for _, mb := range uc.MinuteBuckets { + if at.BalanceId == MINUTES { + for _, mb := range uc.MinuteBalances { if strings.Contains(at.ThresholdType, "*max") { - if mb.DestinationId == at.DestinationId && mb.Seconds >= at.ThresholdValue { + if mb.DestinationId == at.DestinationId && mb.Value >= at.ThresholdValue { // run the actions at.Execute(ub) } } else { //MIN - if mb.DestinationId == at.DestinationId && mb.Seconds <= at.ThresholdValue { + if mb.DestinationId == at.DestinationId && mb.Value <= at.ThresholdValue { // run the actions at.Execute(ub) } @@ -342,15 +518,15 @@ func (ub *UserBalance) executeActionTriggers(a *Action) { } } else { // BALANCE for _, b := range ub.BalanceMap[at.BalanceId] { - if at.BalanceId == MINUTES && at.DestinationId != "" { // last check adds safety - for _, mb := range ub.MinuteBuckets { + if at.BalanceId == MINUTES { + for _, mb := range ub.BalanceMap[MINUTES+OUTBOUND] { if strings.Contains(at.ThresholdType, "*max") { - if mb.DestinationId == at.DestinationId && mb.Seconds >= at.ThresholdValue { + if mb.DestinationId == at.DestinationId && mb.Value >= at.ThresholdValue { // run the actions at.Execute(ub) } } else { //MIN - if mb.DestinationId == at.DestinationId && mb.Seconds <= at.ThresholdValue { + if mb.DestinationId == at.DestinationId && mb.Value <= at.ThresholdValue { // run the actions at.Execute(ub) } @@ -378,11 +554,7 @@ func (ub *UserBalance) executeActionTriggers(a *Action) { // If the action is not nil it acts like a filter func (ub *UserBalance) resetActionTriggers(a *Action) { for _, at := range ub.ActionTriggers { - if a != nil && (at.BalanceId != a.BalanceId || - at.Direction != a.Direction || - (a.MinuteBucket != nil && - (at.ThresholdType != a.MinuteBucket.PriceType || - at.ThresholdValue != a.MinuteBucket.Price))) { + if !at.Match(a) { continue } at.Executed = false @@ -417,10 +589,10 @@ func (ub *UserBalance) countUnits(a *Action) { unitsCounter = &UnitsCounter{BalanceId: a.BalanceId, Direction: direction} ub.UnitCounters = append(ub.UnitCounters, unitsCounter) } - if a.BalanceId == MINUTES && a.MinuteBucket != nil { - unitsCounter.addMinutes(a.MinuteBucket.Seconds, a.MinuteBucket.DestinationId) + if a.BalanceId == MINUTES && a.Balance != nil { + unitsCounter.addMinutes(a.Balance.Value, a.Balance.DestinationId) } else { - unitsCounter.Units += a.Units + unitsCounter.Units += a.Balance.Value } ub.executeActionTriggers(nil) } @@ -434,7 +606,7 @@ func (ub *UserBalance) initMinuteCounters() { continue } for _, a := range acs { - if a.MinuteBucket != nil { + if a.BalanceId == MINUTES && a.Balance != nil { direction := at.Direction if direction == "" { direction = OUTBOUND @@ -443,11 +615,13 @@ func (ub *UserBalance) initMinuteCounters() { if !exists { uc = &UnitsCounter{BalanceId: MINUTES, Direction: direction} ucTempMap[direction] = uc - uc.MinuteBuckets = bucketsorter{} + uc.MinuteBalances = BalanceChain{} ub.UnitCounters = append(ub.UnitCounters, uc) } - uc.MinuteBuckets = append(uc.MinuteBuckets, a.MinuteBucket.Clone()) - uc.MinuteBuckets.Sort() + b := a.Balance.Clone() + b.Value = 0 + uc.MinuteBalances = append(uc.MinuteBalances, b) + uc.MinuteBalances.Sort() } } } @@ -464,9 +638,9 @@ func (ub *UserBalance) CleanExpiredBalancesAndBuckets() { } ub.BalanceMap[key] = bm } - for i := 0; i < len(ub.MinuteBuckets); i++ { - if ub.MinuteBuckets[i].IsExpired() { - ub.MinuteBuckets = append(ub.MinuteBuckets[:i], ub.MinuteBuckets[i+1:]...) + for i := 0; i < len(ub.BalanceMap[MINUTES+OUTBOUND]); i++ { + if ub.BalanceMap[MINUTES+OUTBOUND][i].IsExpired() { + ub.BalanceMap[MINUTES+OUTBOUND] = append(ub.BalanceMap[MINUTES+OUTBOUND][:i], ub.BalanceMap[MINUTES+OUTBOUND][i+1:]...) } } } diff --git a/engine/userbalance_test.go b/engine/userbalance_test.go index 562c1e712..b41816160 100644 --- a/engine/userbalance_test.go +++ b/engine/userbalance_test.go @@ -19,7 +19,6 @@ along with this program. If not, see package engine import ( - //"log" "testing" "time" ) @@ -35,19 +34,19 @@ func init() { func populateTestActionsForTriggers() { ats := []*Action{ - &Action{ActionType: "*topup", BalanceId: CREDIT, Direction: OUTBOUND, Units: 10}, - &Action{ActionType: "*topup", BalanceId: MINUTES, Direction: OUTBOUND, MinuteBucket: &MinuteBucket{Weight: 20, Price: 1, Seconds: 10, DestinationId: "NAT"}}, + &Action{ActionType: "*topup", BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}}, + &Action{ActionType: "*topup", BalanceId: MINUTES, Direction: OUTBOUND, Balance: &Balance{Weight: 20, Value: 10, DestinationId: "NAT"}}, } storageGetter.SetActions("TEST_ACTIONS", ats) ats1 := []*Action{ - &Action{ActionType: "*topup", BalanceId: CREDIT, Direction: OUTBOUND, Units: 10, Weight: 20}, + &Action{ActionType: "*topup", BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}, Weight: 20}, &Action{ActionType: "*reset_prepaid", Weight: 10}, } storageGetter.SetActions("TEST_ACTIONS_ORDER", ats1) } func TestBalanceStoreRestore(t *testing.T) { - b := &Balance{Value: 14, Weight: 1, Id: "test", ExpirationDate: time.Date(2013, time.July, 15, 17, 48, 0, 0, time.UTC)} + b := &Balance{Value: 14, Weight: 1, Uuid: "test", ExpirationDate: time.Date(2013, time.July, 15, 17, 48, 0, 0, time.UTC)} marsh := NewCodecMsgpackMarshaler() output, err := marsh.Marshal(b) if err != nil { @@ -97,9 +96,9 @@ func TestBalanceChainStoreRestore(t *testing.T) { } func TestUserBalanceStorageStoreRestore(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.01, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} storageGetter.SetUserBalance(rifsBalance) ub1, err := storageGetter.GetUserBalance("other") if err != nil || !ub1.BalanceMap[CREDIT+OUTBOUND].Equal(rifsBalance.BalanceMap[CREDIT+OUTBOUND]) { @@ -109,47 +108,69 @@ func TestUserBalanceStorageStoreRestore(t *testing.T) { } func TestGetSecondsForPrefix(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, DestinationId: "RET"} - ub1 := &UserBalance{Id: "OUT:CUSTOMER_1:rif", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 200}}}} - seconds, credit, bucketList := ub1.getSecondsForPrefix("0723") + b1 := &Balance{Value: 10, Weight: 10, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, DestinationId: "RET"} + ub1 := &UserBalance{Id: "OUT:CUSTOMER_1:rif", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 200}}}} + cd := &CallDescriptor{ + TOR: "0", + Tenant: "vdf", + TimeStart: time.Date(2013, 10, 4, 15, 46, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 10, 4, 15, 46, 10, 0, time.UTC), + LoopIndex: 0, + CallDuration: 10 * time.Second, + Direction: OUTBOUND, + Destination: "0723", + } + seconds, credit, bucketList := ub1.getSecondsForPrefix(cd) expected := 110.0 if credit != 200 || seconds != expected || bucketList[0].Weight < bucketList[1].Weight { + t.Log(seconds, credit, bucketList) t.Errorf("Expected %v was %v", expected, seconds) } } -func TestGetPricedSeconds(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Price: 10, Weight: 10, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Price: 1, Weight: 20, DestinationId: "RET"} +func TestGetSpecialPricedSeconds(t *testing.T) { + b1 := &Balance{Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: "minu"} + b2 := &Balance{Value: 100, Weight: 20, DestinationId: "RET", RateSubject: "minu"} - ub1 := &UserBalance{Id: "OUT:CUSTOMER_1:rif", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - seconds, credit, bucketList := ub1.getSecondsForPrefix("0723") + ub1 := &UserBalance{Id: "OUT:CUSTOMER_1:rif", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + cd := &CallDescriptor{ + TOR: "0", + Tenant: "vdf", + TimeStart: time.Date(2013, 10, 4, 15, 46, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 10, 4, 15, 46, 10, 0, time.UTC), + LoopIndex: 0, + CallDuration: 10 * time.Second, + Direction: OUTBOUND, + Destination: "0723", + } + seconds, credit, bucketList := ub1.getSecondsForPrefix(cd) expected := 21.0 if credit != 0 || seconds != expected || len(bucketList) < 2 || bucketList[0].Weight < bucketList[1].Weight { + t.Log(seconds, credit, bucketList) t.Errorf("Expected %v was %v", expected, seconds) } } func TestUserBalanceStorageStore(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.01, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} storageGetter.SetUserBalance(rifsBalance) result, err := storageGetter.GetUserBalance(rifsBalance.Id) if err != nil || rifsBalance.Id != result.Id || - len(rifsBalance.MinuteBuckets) < 2 || len(result.MinuteBuckets) < 2 || - !(rifsBalance.MinuteBuckets[0].Equal(result.MinuteBuckets[0])) || - !(rifsBalance.MinuteBuckets[1].Equal(result.MinuteBuckets[1])) || + len(rifsBalance.BalanceMap[MINUTES+OUTBOUND]) < 2 || len(result.BalanceMap[MINUTES+OUTBOUND]) < 2 || + !(rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Equal(result.BalanceMap[MINUTES+OUTBOUND][0])) || + !(rifsBalance.BalanceMap[MINUTES+OUTBOUND][1].Equal(result.BalanceMap[MINUTES+OUTBOUND][1])) || !rifsBalance.BalanceMap[CREDIT+OUTBOUND].Equal(result.BalanceMap[CREDIT+OUTBOUND]) { t.Errorf("Expected %v was %v", rifsBalance, result) } } -func TestDebitMoneyBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.01, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} +/*func TestDebitMoneyBalance(t *testing.T) { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.01, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} result := rifsBalance.debitBalance(CREDIT, 6, false) if rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 15 || result != rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value { t.Errorf("Expected %v was %v", 15, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) @@ -157,9 +178,9 @@ func TestDebitMoneyBalance(t *testing.T) { } func TestDebitAllMoneyBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.01, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.01, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} rifsBalance.debitBalance(CREDIT, 21, false) result := rifsBalance.debitBalance(CREDIT, 0, false) if rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 0 || result != rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value { @@ -168,9 +189,9 @@ func TestDebitAllMoneyBalance(t *testing.T) { } func TestDebitMoreMoneyBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} result := rifsBalance.debitBalance(CREDIT, 22, false) if rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != -1 || result != rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value { t.Errorf("Expected %v was %v", -1, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) @@ -178,130 +199,498 @@ func TestDebitMoreMoneyBalance(t *testing.T) { } func TestDebitNegativeMoneyBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} result := rifsBalance.debitBalance(CREDIT, -15, false) if rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 36 || result != rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value { t.Errorf("Expected %v was %v", 36, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) } } +*/ +func TestDebitCreditZeroSecond(t *testing.T) { + b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: ZEROSECOND} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + if cc.Timespans[0].Increments[0].BalanceUuid != "testb" { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + } + if rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 0 || + rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 21 { + t.Error("Error extracting minutes from balance: ", rifsBalance.BalanceMap[MINUTES+OUTBOUND][0]) + } +} + +func TestDebitCreditZeroMinute(t *testing.T) { + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{b1}, + CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + if cc.Timespans[0].Increments[0].BalanceUuid != "testb" || + cc.Timespans[0].Increments[0].Duration != time.Minute { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + } + if rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 10 || + rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 21 { + t.Error("Error extracting minutes from balance: ", + rifsBalance.BalanceMap[MINUTES+OUTBOUND][0]) + } +} +func TestDebitCreditZeroMixedMinute(t *testing.T) { + b1 := &Balance{Uuid: "testm", Value: 70, Weight: 5, DestinationId: "NAT", RateSubject: ZEROMINUTE} + b2 := &Balance{Uuid: "tests", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: ZEROSECOND} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 20, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{b1, b2}, + CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + if cc.Timespans[0].Increments[0].BalanceUuid != "tests" || + cc.Timespans[1].Increments[0].BalanceUuid != "testm" { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0], cc.Timespans[1].Increments[0]) + } + if rifsBalance.BalanceMap[MINUTES+OUTBOUND][1].Value != 0 || + rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 10 || + rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 21 { + t.Error("Error extracting minutes from balance: ", rifsBalance.BalanceMap[MINUTES+OUTBOUND]) + } +} + +func TestDebitCreditNoCredit(t *testing.T) { + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 49, 20, 0, time.UTC), + CallDuration: 10 * time.Second, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{b1}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + if cc.Timespans[0].Increments[0].BalanceUuid != "testb" || + cc.Timespans[0].Increments[0].Duration != time.Minute { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + } + if rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 10 { + t.Error("Error extracting minutes from balance: ", + rifsBalance.BalanceMap[MINUTES+OUTBOUND][0]) + } + if len(cc.Timespans) != 1 || cc.Timespans[0].GetDuration() != time.Minute { + t.Error("Error truncating extra timespans: ", cc.Timespans) + } +} + +func TestDebitCreditHasCredit(t *testing.T) { + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 49, 20, 0, time.UTC), + CallDuration: 10 * time.Second, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 1, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{b1}, + CREDIT + OUTBOUND: BalanceChain{&Balance{Uuid: "moneya", Value: 50}}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + if cc.Timespans[0].Increments[0].BalanceUuid != "testb" || + cc.Timespans[0].Increments[0].Duration != time.Minute { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + } + if rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 10 || + rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 30 { + t.Errorf("Error extracting minutes from balance: %+v, %+v", + rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) + } + if len(cc.Timespans) != 2 || cc.Timespans[0].GetDuration() != time.Minute || cc.Timespans[1].GetDuration() != 20*time.Second { + t.Error("Error truncating extra timespans: ", cc.Timespans) + } +} + +func TestDebitCreditSplitMinutesMoney(t *testing.T) { + b1 := &Balance{Uuid: "testb", Value: 10, Weight: 10, DestinationId: "NAT", RateSubject: ZEROSECOND} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 20, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 1, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{b1}, + CREDIT + OUTBOUND: BalanceChain{&Balance{Uuid: "moneya", Value: 50}}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + if cc.Timespans[0].Increments[0].BalanceUuid != "testb" || + cc.Timespans[0].Increments[0].Duration != 10*time.Second { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + } + if rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 0 || + rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 40 { + t.Errorf("Error extracting minutes from balance: %+v, %+v", + rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) + } + if len(cc.Timespans) != 2 || cc.Timespans[0].GetDuration() != 10*time.Second || cc.Timespans[1].GetDuration() != 10*time.Second { + t.Error("Error truncating extra timespans: ", cc.Timespans) + } +} + +func TestDebitCreditMoreTimespans(t *testing.T) { + b1 := &Balance{Uuid: "testb", Value: 150, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 49, 20, 0, time.UTC), + CallDuration: 10 * time.Second, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{b1}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + if cc.Timespans[0].Increments[0].BalanceUuid != "testb" || + cc.Timespans[0].Increments[0].Duration != time.Minute { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + } + if rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 30 { + t.Error("Error extracting minutes from balance: ", + rifsBalance.BalanceMap[MINUTES+OUTBOUND][0]) + } +} + +func TestDebitCreditMoreTimespansMixed(t *testing.T) { + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE} + b2 := &Balance{Uuid: "testa", Value: 150, Weight: 5, DestinationId: "NAT", RateSubject: ZEROSECOND} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 49, 20, 0, time.UTC), + CallDuration: 10 * time.Second, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{b1, b2}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + if cc.Timespans[0].Increments[0].BalanceUuid != "testb" || + cc.Timespans[0].Increments[0].Duration != time.Minute { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + } + if rifsBalance.BalanceMap[MINUTES+OUTBOUND][0].Value != 10 || + rifsBalance.BalanceMap[MINUTES+OUTBOUND][1].Value != 130 { + t.Error("Error extracting minutes from balance: ", + rifsBalance.BalanceMap[MINUTES+OUTBOUND][1], cc.Timespans[1]) + } +} + +func TestDebitCreditNoConectFeeCredit(t *testing.T) { + b1 := &Balance{Uuid: "testb", Value: 70, Weight: 10, DestinationId: "NAT", RateSubject: ZEROMINUTE} + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + ConnectFee: 10.0, + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 49, 20, 0, time.UTC), + CallDuration: 10 * time.Second, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 1, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + MINUTES + OUTBOUND: BalanceChain{b1}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + + if len(cc.Timespans) != 0 || cc.GetDuration() != 0 { + t.Error("Error cutting at no connect fee: ", cc.Timespans) + } +} + +func TestDebitCreditMoneyOnly(t *testing.T) { + cc := &CallCost{ + Direction: OUTBOUND, + Destination: "0723045326", + Timespans: []*TimeSpan{ + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 0, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + CallDuration: 0, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 1, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + &TimeSpan{ + TimeStart: time.Date(2013, 9, 24, 10, 48, 10, 0, time.UTC), + TimeEnd: time.Date(2013, 9, 24, 10, 49, 20, 0, time.UTC), + CallDuration: 10 * time.Second, + RateInterval: &RateInterval{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 1, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}, + }, + }, + } + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{ + CREDIT + OUTBOUND: BalanceChain{&Balance{Uuid: "money", Value: 50}}, + }} + err := rifsBalance.debitCreditBalance(cc, false) + if err != nil { + t.Error("Error debiting balance: ", err) + } + + if cc.Timespans[0].Increments[0].BalanceUuid != "money" || + cc.Timespans[0].Increments[0].Duration != 10*time.Second { + t.Error("Error setting balance id to increment: ", cc.Timespans[0].Increments[0]) + } + if rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 0 { + t.Error("Error extracting minutes from balance: ", + rifsBalance.BalanceMap[CREDIT+OUTBOUND][0]) + } + if len(cc.Timespans) != 2 || + cc.Timespans[0].GetDuration() != 10*time.Second || + cc.Timespans[1].GetDuration() != 40*time.Second { + t.Error("Error truncating extra timespans: ", cc.Timespans[1].Increments[0]) + } +} + +/* func TestDebitMinuteBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(6, "0723", false) - if b2.Seconds != 94 || err != nil { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(6, "0723", false) + if b2.Value != 94 || err != nil { t.Log(err) - t.Errorf("Expected %v was %v", 94, b2.Seconds) + t.Errorf("Expected %v was %v", 94, b2.Value) } } func TestDebitMultipleBucketsMinuteBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(105, "0723", false) - if b2.Seconds != 0 || b1.Seconds != 5 || err != nil { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(105, "0723", false) + if b2.Value != 0 || b1.Value != 5 || err != nil { t.Log(err) - t.Errorf("Expected %v was %v", 0, b2.Seconds) + t.Errorf("Expected %v was %v", 0, b2.Value) } } func TestDebitAllMinuteBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(110, "0723", false) - if b2.Seconds != 0 || b1.Seconds != 0 || err != nil { - t.Errorf("Expected %v was %v", 0, b2.Seconds) + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(110, "0723", false) + if b2.Value != 0 || b1.Value != 0 || err != nil { + t.Errorf("Expected %v was %v", 0, b2.Value) } } func TestDebitMoreMinuteBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(115, "0723", false) - if b2.Seconds != 100 || b1.Seconds != 10 || err == nil { - t.Errorf("Expected %v was %v", 1000, b2.Seconds) + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(115, "0723", false) + if b2.Value != 100 || b1.Value != 10 || err == nil { + t.Errorf("Expected %v was %v", 1000, b2.Value) } } -func TestDebitPriceMinuteBalance0(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 1.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(5, "0723", false) - if b2.Seconds != 95 || b1.Seconds != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 16 { +func TestDebitSpecialPriceMinuteBalance0(t *testing.T) { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 1.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(5, "0723", false) + if b2.Value != 95 || b1.Value != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 16 { t.Errorf("Expected %v was %v", 16, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) } } -func TestDebitPriceAllMinuteBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 1.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(21, "0723", false) - if b2.Seconds != 79 || b1.Seconds != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 0 { +func TestDebitSpecialPriceAllMinuteBalance(t *testing.T) { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 1.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(21, "0723", false) + if b2.Value != 79 || b1.Value != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 0 { t.Errorf("Expected %v was %v", 0, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) } } -func TestDebitPriceMoreMinuteBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 1.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(25, "0723", false) - if b2.Seconds != 75 || b1.Seconds != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != -4 { - t.Log(b2.Seconds) - t.Log(b1.Seconds) +func TestDebitSpecialPriceMoreMinuteBalance(t *testing.T) { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 1.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(25, "0723", false) + if b2.Value != 75 || b1.Value != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != -4 { + t.Log(b2.Value) + t.Log(b1.Value) t.Log(err) t.Errorf("Expected %v was %v", -4, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) } } -func TestDebitPriceMoreMinuteBalancePrepay(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 1.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", Type: UB_TYPE_PREPAID, MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(25, "0723", false) +func TestDebitSpecialPriceMoreMinuteBalancePrepay(t *testing.T) { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 1.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", Type: UB_TYPE_PREPAID, BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(25, "0723", false) expected := 21.0 - if b2.Seconds != 100 || b1.Seconds != 10 || err != AMOUNT_TOO_BIG || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != expected { - t.Log(b2.Seconds) - t.Log(b1.Seconds) + if b2.Value != 100 || b1.Value != 10 || err != AMOUNT_TOO_BIG || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != expected { + t.Log(b2.Value) + t.Log(b1.Value) t.Log(err) t.Errorf("Expected %v was %v", expected, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) } } -func TestDebitPriceNegativeMinuteBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 1.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(-15, "0723", false) - if b2.Seconds != 115 || b1.Seconds != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 36 { +func TestDebitSpecialPriceNegativeMinuteBalance(t *testing.T) { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 1.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(-15, "0723", false) + if b2.Value != 115 || b1.Value != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 36 { t.Log(b1, b2, err) t.Errorf("Expected %v was %v", 36, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) } } func TestDebitNegativeMinuteBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} - err := rifsBalance.debitMinutesBalance(-15, "0723", false) - if b2.Seconds != 115 || b1.Seconds != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 21 { + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + err := rifsBalance.debitCreditBalance(-15, "0723", false) + if b2.Value != 115 || b1.Value != 10 || err != nil || rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value != 21 { t.Log(b1, b2, err) t.Errorf("Expected %v was %v", 21, rifsBalance.BalanceMap[CREDIT+OUTBOUND][0].Value) } } +*/ +/* func TestDebitSMSBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, SMS + OUTBOUND: BalanceChain{&Balance{Value: 100}}}} + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, SMS + OUTBOUND: BalanceChain{&Balance{Value: 100}}}} result := rifsBalance.debitBalance(SMS, 12, false) if rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value != 88 || result != rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value { t.Errorf("Expected %v was %v", 88, rifsBalance.BalanceMap[SMS+OUTBOUND]) @@ -309,9 +698,9 @@ func TestDebitSMSBalance(t *testing.T) { } func TestDebitAllSMSBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, SMS + OUTBOUND: BalanceChain{&Balance{Value: 100}}}} + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, SMS + OUTBOUND: BalanceChain{&Balance{Value: 100}}}} result := rifsBalance.debitBalance(SMS, 100, false) if rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value != 0 || result != rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value { t.Errorf("Expected %v was %v", 0, rifsBalance.BalanceMap[SMS+OUTBOUND]) @@ -319,9 +708,9 @@ func TestDebitAllSMSBalance(t *testing.T) { } func TestDebitMoreSMSBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, SMS + OUTBOUND: BalanceChain{&Balance{Value: 100}}}} + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, SMS + OUTBOUND: BalanceChain{&Balance{Value: 100}}}} result := rifsBalance.debitBalance(SMS, 110, false) if rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value != -10 || result != rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value { t.Errorf("Expected %v was %v", -10, rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value) @@ -329,112 +718,113 @@ func TestDebitMoreSMSBalance(t *testing.T) { } func TestDebitNegativeSMSBalance(t *testing.T) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.0, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, SMS + OUTBOUND: BalanceChain{&Balance{Value: 100}}}} + b1 := &Balance{Value: 10, Weight: 10, SpecialPrice: 0.0, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, SpecialPrice: 0.0, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}, SMS + OUTBOUND: BalanceChain{&Balance{Value: 100}}}} result := rifsBalance.debitBalance(SMS, -15, false) if rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value != 115 || result != rifsBalance.BalanceMap[SMS+OUTBOUND][0].Value { t.Errorf("Expected %v was %v", 115, rifsBalance.BalanceMap[SMS+OUTBOUND]) } } +*/ -func TestUserBalancedebitMinuteBucket(t *testing.T) { +func TestUserBalancedebitBalance(t *testing.T) { ub := &UserBalance{ - Id: "rif", - Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{SMS: BalanceChain{&Balance{Value: 14}}, TRAFFIC: BalanceChain{&Balance{Value: 1204}}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, + Id: "rif", + Type: UB_TYPE_POSTPAID, + BalanceMap: map[string]BalanceChain{SMS: BalanceChain{&Balance{Value: 14}}, TRAFFIC: BalanceChain{&Balance{Value: 1204}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, } - newMb := &MinuteBucket{Weight: 20, Price: 1, DestinationId: "NEW"} - ub.debitMinuteBucket(newMb) - if len(ub.MinuteBuckets) != 3 || ub.MinuteBuckets[2] != newMb { - t.Error("Error adding minute bucket!", len(ub.MinuteBuckets), ub.MinuteBuckets) + newMb := &Balance{Weight: 20, DestinationId: "NEW"} + a := &Action{BalanceId: MINUTES, Direction: OUTBOUND, Balance: newMb} + ub.debitBalanceAction(a) + if len(ub.BalanceMap[MINUTES+OUTBOUND]) != 3 || ub.BalanceMap[MINUTES+OUTBOUND][2] != newMb { + t.Error("Error adding minute bucket!", len(ub.BalanceMap[MINUTES+OUTBOUND]), ub.BalanceMap[MINUTES+OUTBOUND]) } } -func TestUserBalancedebitMinuteBucketExists(t *testing.T) { +func TestUserBalancedebitBalanceExists(t *testing.T) { ub := &UserBalance{ - Id: "rif", - Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{SMS + OUTBOUND: BalanceChain{&Balance{Value: 14}}, TRAFFIC + OUTBOUND: BalanceChain{&Balance{Value: 1024}}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 15, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, + Id: "rif", + Type: UB_TYPE_POSTPAID, + BalanceMap: map[string]BalanceChain{SMS + OUTBOUND: BalanceChain{&Balance{Value: 14}}, TRAFFIC + OUTBOUND: BalanceChain{&Balance{Value: 1024}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 15, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, } - newMb := &MinuteBucket{Seconds: -10, Weight: 20, Price: 1, DestinationId: "NAT"} - ub.debitMinuteBucket(newMb) - if len(ub.MinuteBuckets) != 2 || ub.MinuteBuckets[0].Seconds != 25 { + newMb := &Balance{Value: -10, Weight: 20, DestinationId: "NAT"} + a := &Action{BalanceId: MINUTES, Direction: OUTBOUND, Balance: newMb} + ub.debitBalanceAction(a) + if len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 || ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 25 { t.Error("Error adding minute bucket!") } } func TestUserBalanceAddMinuteNil(t *testing.T) { ub := &UserBalance{ - Id: "rif", - Type: UB_TYPE_POSTPAID, - BalanceMap: map[string]BalanceChain{SMS + OUTBOUND: BalanceChain{&Balance{Value: 14}}, TRAFFIC + OUTBOUND: BalanceChain{&Balance{Value: 1024}}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, + Id: "rif", + Type: UB_TYPE_POSTPAID, + BalanceMap: map[string]BalanceChain{SMS + OUTBOUND: BalanceChain{&Balance{Value: 14}}, TRAFFIC + OUTBOUND: BalanceChain{&Balance{Value: 1024}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, } - ub.debitMinuteBucket(nil) - if len(ub.MinuteBuckets) != 2 { + ub.debitBalanceAction(nil) + if len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 { t.Error("Error adding minute bucket!") } } func TestUserBalanceAddMinutBucketEmpty(t *testing.T) { - mb1 := &MinuteBucket{Seconds: -10, DestinationId: "NAT"} - mb2 := &MinuteBucket{Seconds: -10, DestinationId: "NAT"} - mb3 := &MinuteBucket{Seconds: -10, DestinationId: "OTHER"} + mb1 := &Balance{Value: -10, DestinationId: "NAT"} + mb2 := &Balance{Value: -10, DestinationId: "NAT"} + mb3 := &Balance{Value: -10, DestinationId: "OTHER"} ub := &UserBalance{} - ub.debitMinuteBucket(mb1) - if len(ub.MinuteBuckets) != 1 { - t.Error("Error adding minute bucket: ", ub.MinuteBuckets) + a := &Action{BalanceId: MINUTES, Direction: OUTBOUND, Balance: mb1} + ub.debitBalanceAction(a) + if len(ub.BalanceMap[MINUTES+OUTBOUND]) != 1 { + t.Error("Error adding minute bucket: ", ub.BalanceMap[MINUTES+OUTBOUND]) } - ub.debitMinuteBucket(mb2) - if len(ub.MinuteBuckets) != 1 || ub.MinuteBuckets[0].Seconds != 20 { - t.Error("Error adding minute bucket: ", ub.MinuteBuckets) + a = &Action{BalanceId: MINUTES, Direction: OUTBOUND, Balance: mb2} + ub.debitBalanceAction(a) + if len(ub.BalanceMap[MINUTES+OUTBOUND]) != 1 || ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 20 { + t.Error("Error adding minute bucket: ", ub.BalanceMap[MINUTES+OUTBOUND]) } - ub.debitMinuteBucket(mb3) - if len(ub.MinuteBuckets) != 2 { - t.Error("Error adding minute bucket: ", ub.MinuteBuckets) + a = &Action{BalanceId: MINUTES, Direction: OUTBOUND, Balance: mb3} + ub.debitBalanceAction(a) + if len(ub.BalanceMap[MINUTES+OUTBOUND]) != 2 { + t.Error("Error adding minute bucket: ", ub.BalanceMap[MINUTES+OUTBOUND]) } } func TestUserBalanceExecuteTriggeredActions(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, - ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ThresholdType: "*max_counter", ActionsId: "TEST_ACTIONS"}}, + ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ThresholdType: TRIGGER_MAX_COUNTER, ActionsId: "TEST_ACTIONS"}}, } - ub.countUnits(&Action{BalanceId: CREDIT, Units: 1}) - if ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 110 || ub.MinuteBuckets[0].Seconds != 20 { - t.Error("Error executing triggered actions", ub.BalanceMap[CREDIT+OUTBOUND][0].Value, ub.MinuteBuckets[0].Seconds) + ub.countUnits(&Action{BalanceId: CREDIT, Balance: &Balance{Value: 1}}) + if ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 110 || ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 20 { + t.Error("Error executing triggered actions", ub.BalanceMap[CREDIT+OUTBOUND][0].Value, ub.BalanceMap[MINUTES+OUTBOUND][0].Value) } // are set to executed - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}) - if ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 110 || ub.MinuteBuckets[0].Seconds != 20 { - t.Error("Error executing triggered actions", ub.BalanceMap[CREDIT+OUTBOUND][0].Value, ub.MinuteBuckets[0].Seconds) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 1}}) + if ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 110 || ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 20 { + t.Error("Error executing triggered actions", ub.BalanceMap[CREDIT+OUTBOUND][0].Value, ub.BalanceMap[MINUTES+OUTBOUND][0].Value) } // we can reset them ub.resetActionTriggers(nil) - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}) - if ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 120 || ub.MinuteBuckets[0].Seconds != 30 { - t.Error("Error executing triggered actions", ub.BalanceMap[CREDIT+OUTBOUND][0].Value, ub.MinuteBuckets[0].Seconds) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}}) + if ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 120 || ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 30 { + t.Error("Error executing triggered actions", ub.BalanceMap[CREDIT+OUTBOUND][0].Value, ub.BalanceMap[MINUTES+OUTBOUND][0].Value) } } func TestUserBalanceExecuteTriggeredActionsBalance(t *testing.T) { ub := &UserBalance{ Id: "TEST_UB", - BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}}, + BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}, MINUTES + OUTBOUND: BalanceChain{&Balance{Value: 10, Weight: 20, DestinationId: "NAT"}, &Balance{Weight: 10, DestinationId: "RET"}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}}, - MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, PriceType: ABSOLUTE, DestinationId: "RET"}}, - ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 100, ThresholdType: "*min_counter", ActionsId: "TEST_ACTIONS"}}, + ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 100, ThresholdType: TRIGGER_MIN_COUNTER, ActionsId: "TEST_ACTIONS"}}, } - ub.countUnits(&Action{BalanceId: CREDIT, Units: 1}) - if ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 110 || ub.MinuteBuckets[0].Seconds != 20 { - t.Error("Error executing triggered actions", ub.BalanceMap[CREDIT+OUTBOUND][0].Value, ub.MinuteBuckets[0].Seconds) + ub.countUnits(&Action{BalanceId: CREDIT, Balance: &Balance{Value: 1}}) + if ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 110 || ub.BalanceMap[MINUTES+OUTBOUND][0].Value != 20 { + t.Error("Error executing triggered actions", ub.BalanceMap[CREDIT+OUTBOUND][0].Value, ub.BalanceMap[MINUTES+OUTBOUND][0].Value) } } @@ -443,9 +833,9 @@ func TestUserBalanceExecuteTriggeredActionsOrder(t *testing.T) { Id: "TEST_UB_OREDER", BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 100}}}, UnitCounters: []*UnitsCounter{&UnitsCounter{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}}, - ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, ThresholdValue: 2, ThresholdType: "*max_counter", ActionsId: "TEST_ACTIONS_ORDER"}}, + ActionTriggers: ActionTriggerPriotityList{&ActionTrigger{BalanceId: CREDIT, Direction: OUTBOUND, ThresholdValue: 2, ThresholdType: TRIGGER_MAX_COUNTER, ActionsId: "TEST_ACTIONS_ORDER"}}, } - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 1}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 1}}) if len(ub.BalanceMap[CREDIT+OUTBOUND]) != 1 || ub.BalanceMap[CREDIT+OUTBOUND][0].Value != 10 { t.Error("Error executing triggered actions in order", ub.BalanceMap[CREDIT+OUTBOUND]) } @@ -457,28 +847,27 @@ func TestCleanExpired(t *testing.T) { BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{ &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}, &Balance{ExpirationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC)}, - &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}}}, - MinuteBuckets: []*MinuteBucket{ - &MinuteBucket{ExpirationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC)}, - &MinuteBucket{ExpirationDate: time.Now().Add(10 * time.Second)}, - }, + &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}}, MINUTES + OUTBOUND: BalanceChain{ + &Balance{ExpirationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC)}, + &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}, + }}, } ub.CleanExpiredBalancesAndBuckets() if len(ub.BalanceMap[CREDIT+OUTBOUND]) != 2 { t.Error("Error cleaning expired balances!") } - if len(ub.MinuteBuckets) != 1 { + if len(ub.BalanceMap[MINUTES+OUTBOUND]) != 1 { t.Error("Error cleaning expired minute buckets!") } } func TestUserBalanceUnitCounting(t *testing.T) { ub := &UserBalance{} - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}}) if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceId != CREDIT || ub.UnitCounters[0].Units != 10 { t.Error("Error counting units") } - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}}) if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceId != CREDIT || ub.UnitCounters[0].Units != 20 { t.Error("Error counting units") } @@ -486,15 +875,15 @@ func TestUserBalanceUnitCounting(t *testing.T) { func TestUserBalanceUnitCountingOutbound(t *testing.T) { ub := &UserBalance{} - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}}) if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceId != CREDIT || ub.UnitCounters[0].Units != 10 { t.Error("Error counting units") } - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}}) if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceId != CREDIT || ub.UnitCounters[0].Units != 20 { t.Error("Error counting units") } - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}}) if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceId != CREDIT || ub.UnitCounters[0].Units != 30 { t.Error("Error counting units") } @@ -502,15 +891,15 @@ func TestUserBalanceUnitCountingOutbound(t *testing.T) { func TestUserBalanceUnitCountingOutboundInbound(t *testing.T) { ub := &UserBalance{} - ub.countUnits(&Action{BalanceId: CREDIT, Units: 10}) + ub.countUnits(&Action{BalanceId: CREDIT, Balance: &Balance{Value: 10}}) if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceId != CREDIT || ub.UnitCounters[0].Units != 10 { t.Error("Error counting units") } - ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Balance: &Balance{Value: 10}}) if len(ub.UnitCounters) != 1 && ub.UnitCounters[0].BalanceId != CREDIT || ub.UnitCounters[0].Units != 20 { t.Error("Error counting units") } - ub.countUnits(&Action{BalanceId: CREDIT, Direction: INBOUND, Units: 10}) + ub.countUnits(&Action{BalanceId: CREDIT, Direction: INBOUND, Balance: &Balance{Value: 10}}) if len(ub.UnitCounters) != 2 && ub.UnitCounters[1].BalanceId != CREDIT || ub.UnitCounters[0].Units != 20 || ub.UnitCounters[1].Units != 10 { t.Error("Error counting units") } @@ -520,20 +909,23 @@ func TestUserBalanceUnitCountingOutboundInbound(t *testing.T) { func BenchmarkGetSecondForPrefix(b *testing.B) { b.StopTimer() - b1 := &MinuteBucket{Seconds: 10, Price: 10, Weight: 10, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Price: 1, Weight: 20, DestinationId: "RET"} + b1 := &Balance{Value: 10, Weight: 10, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, DestinationId: "RET"} - ub1 := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + ub1 := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + cd := &CallDescriptor{ + Destination: "0723", + } b.StartTimer() for i := 0; i < b.N; i++ { - ub1.getSecondsForPrefix("0723") + ub1.getSecondsForPrefix(cd) } } func BenchmarkUserBalanceStorageStoreRestore(b *testing.B) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, Price: 0.01, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, Price: 0.0, DestinationId: "RET"} - rifsBalance := &UserBalance{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, DestinationId: "RET"} + rifsBalance := &UserBalance{Id: "other", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} for i := 0; i < b.N; i++ { storageGetter.SetUserBalance(rifsBalance) storageGetter.GetUserBalance(rifsBalance.Id) @@ -541,10 +933,13 @@ func BenchmarkUserBalanceStorageStoreRestore(b *testing.B) { } func BenchmarkGetSecondsForPrefix(b *testing.B) { - b1 := &MinuteBucket{Seconds: 10, Weight: 10, DestinationId: "NAT"} - b2 := &MinuteBucket{Seconds: 100, Weight: 20, DestinationId: "RET"} - ub1 := &UserBalance{Id: "OUT:CUSTOMER_1:rif", MinuteBuckets: []*MinuteBucket{b1, b2}, BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + b1 := &Balance{Value: 10, Weight: 10, DestinationId: "NAT"} + b2 := &Balance{Value: 100, Weight: 20, DestinationId: "RET"} + ub1 := &UserBalance{Id: "OUT:CUSTOMER_1:rif", BalanceMap: map[string]BalanceChain{MINUTES + OUTBOUND: BalanceChain{b1, b2}, CREDIT + OUTBOUND: BalanceChain{&Balance{Value: 21}}}} + cd := &CallDescriptor{ + Destination: "0723", + } for i := 0; i < b.N; i++ { - ub1.getSecondsForPrefix("0723") + ub1.getSecondsForPrefix(cd) } } diff --git a/hard_update_external_libs.py b/hard_update_external_libs.py new file mode 100755 index 000000000..680522c1d --- /dev/null +++ b/hard_update_external_libs.py @@ -0,0 +1,49 @@ +#! /usr/bin/env python + +import os +import os.path +from subprocess import call + +libs = ('github.com/fzzy/radix/redis', + 'code.google.com/p/goconf/conf', + 'github.com/bmizerany/pq', + 'github.com/vmihailenco/msgpack', + 'github.com/ugorji/go/codec', + 'labix.org/v2/mgo', + 'github.com/cgrates/fsock', + 'github.com/go-sql-driver/mysql', + 'github.com/garyburd/redigo/redis', + 'menteslibres.net/gosexy/redis', + 'github.com/howeyc/fsnotify', +) + +if __name__ == "__main__": + go_path = os.path.join(os.environ['GOPATH'], 'src') + for lib in libs: + app_dir = os.path.abspath(os.path.join(go_path,lib)) + + if os.path.islink(app_dir): continue + git_path = os.path.join(app_dir, '.git') + bzr_path = os.path.join(app_dir, '.bzr') + hg_path = os.path.join(app_dir, '.hg') + svn_path = os.path.join(app_dir, '.svn') + if os.path.lexists(svn_path): + print("Updating svn %s" % app_dir) + os.chdir(app_dir) + call(['svn', 'update']) + elif os.path.lexists(git_path): + print("Updating git %s" % app_dir) + os.chdir(app_dir) + call(['git', 'checkout', 'master']) + call(['git', 'pull']) + elif os.path.lexists(bzr_path): + print("Updating bzr %s" % app_dir) + os.chdir(app_dir) + call(['bzr', 'pull']) + elif os.path.lexists(hg_path): + print("Updating hg %s" % app_dir) + os.chdir(app_dir) + call(['hg', 'pull', '-uv']) + else: + continue + call(['go', 'install']) diff --git a/mediator/mediator.go b/mediator/mediator.go index b1685cbe9..263fbbe26 100644 --- a/mediator/mediator.go +++ b/mediator/mediator.go @@ -35,10 +35,10 @@ import ( "time" ) -func NewMediator(connector engine.Connector, storDb engine.DataStorage, cfg *config.CGRConfig) (m *Mediator, err error) { +func NewMediator(connector engine.Connector, logDb engine.LogStorage, cdrDb engine.CdrStorage, cfg *config.CGRConfig) (m *Mediator, err error) { m = &Mediator{ connector: connector, - storDb: storDb, + logDb: logDb, cgrCfg: cfg, } m.fieldNames = make(map[string][]string) @@ -52,7 +52,8 @@ func NewMediator(connector engine.Connector, storDb engine.DataStorage, cfg *con type Mediator struct { connector engine.Connector - storDb engine.DataStorage + logDb engine.LogStorage + cdrDb engine.CdrStorage cgrCfg *config.CGRConfig cdrInDir, cdrOutDir string accIdField string @@ -166,8 +167,8 @@ func (self *Mediator) TrackCDRFiles() (err error) { // Retrive the cost from logging database func (self *Mediator) getCostsFromDB(cdr utils.CDR) (cc *engine.CallCost, err error) { for i := 0; i < 3; i++ { // Mechanism to avoid concurrency between SessionManager writing the costs and mediator picking them up - cc, err = self.storDb.GetCallCostLog(cdr.GetCgrId(), engine.SESSION_MANAGER_SOURCE) //ToDo: What are we getting when there is no log? - if cc != nil { // There were no errors, chances are that we got what we are looking for + cc, err = self.logDb.GetCallCostLog(cdr.GetCgrId(), engine.SESSION_MANAGER_SOURCE) //ToDo: What are we getting when there is no log? + if cc != nil { // There were no errors, chances are that we got what we are looking for break } time.Sleep(time.Duration(i) * time.Second) @@ -204,10 +205,10 @@ func (self *Mediator) getCostsFromRater(cdr utils.CDR) (*engine.CallCost, error) err = self.connector.GetCost(cd, cc) } if err != nil { - self.storDb.LogError(cdr.GetCgrId(), engine.MEDIATOR_SOURCE, err.Error()) + self.logDb.LogError(cdr.GetCgrId(), engine.MEDIATOR_SOURCE, err.Error()) } else { // If the mediator calculated a price it will write it to logdb - self.storDb.LogCallCost(cdr.GetCgrId(), engine.MEDIATOR_SOURCE, cc) + self.logDb.LogCallCost(cdr.GetCgrId(), engine.MEDIATOR_SOURCE, cc) } return cc, err } @@ -273,9 +274,9 @@ func (self *Mediator) MediateCSVCDR(cdrfn string) (err error) { return } -func (self *Mediator) MediateDBCDR(cdr utils.CDR, db engine.DataStorage) error { +func (self *Mediator) MediateDBCDR(cdr utils.CDR, db engine.CdrStorage) error { var qryCC *engine.CallCost - cc := &engine.CallCost{Cost:-1} + cc := &engine.CallCost{Cost: -1} var errCost error if cdr.GetReqType() == utils.PREPAID || cdr.GetReqType() == utils.POSTPAID { // Should be previously calculated and stored in DB @@ -293,5 +294,5 @@ func (self *Mediator) MediateDBCDR(cdr utils.CDR, db engine.DataStorage) error { if errCost != nil { extraInfo = errCost.Error() } - return self.storDb.SetRatedCdr(cdr, cc, extraInfo) + return self.cdrDb.SetRatedCdr(cdr, cc, extraInfo) } diff --git a/sessionmanager/fssessionmanager.go b/sessionmanager/fssessionmanager.go index 1a439e9a2..0492967d5 100644 --- a/sessionmanager/fssessionmanager.go +++ b/sessionmanager/fssessionmanager.go @@ -42,10 +42,10 @@ type FSSessionManager struct { sessions []*Session connector engine.Connector debitPeriod time.Duration - loggerDB engine.DataStorage + loggerDB engine.LogStorage } -func NewFSSessionManager(storage engine.DataStorage, connector engine.Connector, debitPeriod time.Duration) *FSSessionManager { +func NewFSSessionManager(storage engine.LogStorage, connector engine.Connector, debitPeriod time.Duration) *FSSessionManager { return &FSSessionManager{loggerDB: storage, connector: connector, debitPeriod: debitPeriod} } @@ -245,38 +245,32 @@ func (sm *FSSessionManager) OnChannelHangupComplete(ev Event) { hangupTime = time.Now() } end := lastCC.Timespans[len(lastCC.Timespans)-1].TimeEnd - refoundDuration := end.Sub(hangupTime).Seconds() - cost := 0.0 - seconds := 0.0 + refoundDuration := end.Sub(hangupTime) + var refoundIncrements engine.Increments engine.Logger.Info(fmt.Sprintf("Refund duration: %v", refoundDuration)) for i := len(lastCC.Timespans) - 1; i >= 0; i-- { ts := lastCC.Timespans[i] - tsDuration := ts.GetDuration().Seconds() + tsDuration := ts.GetDuration() if refoundDuration <= tsDuration { - // find procentage - procentage := (refoundDuration * 100) / tsDuration - tmpCost := (procentage * ts.Cost) / 100 - ts.Cost -= tmpCost - cost += tmpCost - if ts.MinuteInfo != nil { - // DestinationPrefix and Price take from lastCC and above caclulus - seconds += (procentage * ts.MinuteInfo.Quantity) / 100 + lastRefoundedIncrementIndex := 0 + for incrementIndex, increment := range ts.Increments { + if increment.Duration <= refoundDuration { + refoundIncrements = append(refoundIncrements, increment) + refoundDuration -= increment.Duration + lastRefoundedIncrementIndex = incrementIndex + } } - // set the end time to now - ts.TimeEnd = hangupTime + ts.SplitByIncrement(lastRefoundedIncrementIndex) break // do not go to other timespans } else { - cost += ts.Cost - if ts.MinuteInfo != nil { - seconds += ts.MinuteInfo.Quantity - } - // remove the timestamp entirely + refoundIncrements = append(refoundIncrements, ts.Increments...) + // remove the timespan entirely lastCC.Timespans = lastCC.Timespans[:i] // continue to the next timespan with what is left to refound refoundDuration -= tsDuration } } - if cost > 0 { + if len(refoundIncrements) > 0 { cd := &engine.CallDescriptor{ Direction: lastCC.Direction, Tenant: lastCC.Tenant, @@ -284,8 +278,8 @@ func (sm *FSSessionManager) OnChannelHangupComplete(ev Event) { Subject: lastCC.Subject, Account: lastCC.Account, Destination: lastCC.Destination, - Amount: -cost, - // FallbackSubject: lastCC.FallbackSubject, // ToDo: check how to best add it + Increments: refoundIncrements, + // FallbackSubject: lastCC.FallbackSubject, // TODO: check how to best add it } var response float64 err := sm.connector.DebitCents(*cd, &response) @@ -293,30 +287,14 @@ func (sm *FSSessionManager) OnChannelHangupComplete(ev Event) { engine.Logger.Err(fmt.Sprintf("Debit cents failed: %v", err)) } } - if seconds > 0 { - cd := &engine.CallDescriptor{ - Direction: lastCC.Direction, - TOR: lastCC.TOR, - Tenant: lastCC.Tenant, - Subject: lastCC.Subject, - Account: lastCC.Account, - Destination: lastCC.Destination, - Amount: -seconds, - // FallbackSubject: lastCC.FallbackSubject, // ToDo: check how to best add it - } - var response float64 - err := sm.connector.DebitSeconds(*cd, &response) - if err != nil { - engine.Logger.Err(fmt.Sprintf("Debit seconds failed: %v", err)) - } - } + cost := refoundIncrements.GetTotalCost() lastCC.Cost -= cost - engine.Logger.Info(fmt.Sprintf("Rambursed %v cents, %v seconds", cost, seconds)) + engine.Logger.Info(fmt.Sprintf("Rambursed %v cents", cost)) } -func (sm *FSSessionManager) LoopAction(s *Session, cd *engine.CallDescriptor, index float64) { - cc := &engine.CallCost{} +func (sm *FSSessionManager) LoopAction(s *Session, cd *engine.CallDescriptor, index float64) (cc *engine.CallCost) { + cc = &engine.CallCost{} cd.LoopIndex = index cd.Amount = sm.debitPeriod.Seconds() cd.CallDuration += time.Duration(cd.Amount) * time.Second @@ -326,25 +304,21 @@ func (sm *FSSessionManager) LoopAction(s *Session, cd *engine.CallDescriptor, in // disconnect session s.sessionManager.DisconnectSession(s, SYSTEM_ERROR) } - nbts := len(cc.Timespans) - remainingSeconds := 0.0 engine.Logger.Debug(fmt.Sprintf("Result of MaxDebit call: %v", cc)) - if nbts > 0 { - remainingSeconds = cc.Timespans[nbts-1].TimeEnd.Sub(cc.Timespans[0].TimeStart).Seconds() - } - if remainingSeconds == 0 || err != nil { + if cc.GetDuration() == 0 || err != nil { engine.Logger.Info(fmt.Sprintf("No credit left: Disconnect %v", s)) sm.DisconnectSession(s, INSUFFICIENT_FUNDS) return } s.CallCosts = append(s.CallCosts, cc) + return } func (sm *FSSessionManager) GetDebitPeriod() time.Duration { return sm.debitPeriod } -func (sm *FSSessionManager) GetDbLogger() engine.DataStorage { +func (sm *FSSessionManager) GetDbLogger() engine.LogStorage { return sm.loggerDB } diff --git a/sessionmanager/session.go b/sessionmanager/session.go index efa703124..c527ebc63 100644 --- a/sessionmanager/session.go +++ b/sessionmanager/session.go @@ -86,8 +86,8 @@ func (s *Session) startDebitLoop() { nextCd.TimeStart = nextCd.TimeEnd } nextCd.TimeEnd = nextCd.TimeStart.Add(s.sessionManager.GetDebitPeriod()) - s.sessionManager.LoopAction(s, &nextCd, index) - time.Sleep(s.sessionManager.GetDebitPeriod()) + cc := s.sessionManager.LoopAction(s, &nextCd, index) + time.Sleep(cc.GetDuration()) index++ } } diff --git a/sessionmanager/sessionmanager.go b/sessionmanager/sessionmanager.go index b254e3dcd..9eb2f68ae 100644 --- a/sessionmanager/sessionmanager.go +++ b/sessionmanager/sessionmanager.go @@ -28,8 +28,8 @@ type SessionManager interface { Connect(*config.CGRConfig) error DisconnectSession(*Session, string) RemoveSession(*Session) - LoopAction(*Session, *engine.CallDescriptor, float64) + LoopAction(*Session, *engine.CallDescriptor, float64) *engine.CallCost GetDebitPeriod() time.Duration - GetDbLogger() engine.DataStorage + GetDbLogger() engine.LogStorage Shutdown() error } diff --git a/utils/apitpdata.go b/utils/apitpdata.go index fcd020c46..37fe35ac9 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -91,16 +91,16 @@ type TPActions struct { } type Action struct { - Identifier string // Identifier mapped in the code - BalanceType string // Type of balance the action will operate on - Direction string // Balance direction - Units float64 // Number of units to add/deduct - ExpiryTime string // Time when the units will expire - DestinationId string // Destination profile id - RateType string // Type of rate <*absolute|*percent> - Rate float64 // Price value - MinutesWeight float64 // Minutes weight - Weight float64 // Action's weight + Identifier string // Identifier mapped in the code + BalanceType string // Type of balance the action will operate on + Direction string // Balance direction + Units float64 // Number of units to add/deduct + ExpiryTime string // Time when the units will expire + DestinationId string // Destination profile id + RateSubject string // Type of rate <*absolute|*percent> + BalanceWeight float64 // Balance weight + ExtraParameters string + Weight float64 // Action's weight } type ApiTPActionTimings struct { diff --git a/utils/coreutils.go b/utils/coreutils.go index c1217c32c..68e26a294 100644 --- a/utils/coreutils.go +++ b/utils/coreutils.go @@ -121,9 +121,41 @@ func ParseDate(date string) (expDate time.Time, err error) { return expDate, err } -func RoundToMinute(seconds float64) float64 { - if math.Mod(seconds, 60) == 0 { - return seconds +// returns a number equeal or larger than the amount that exactly +// is divisible to whole +func RoundTo(whole, amount time.Duration) time.Duration { + a, w := float64(amount), float64(whole) + if math.Mod(a, w) == 0 { + return amount } - return (60 - math.Mod(seconds, 60)) + seconds + return time.Duration((w - math.Mod(a, w)) + a) +} + +type StringSlice []string + +func (ss StringSlice) Contains(needle string) bool { + for _, hay := range ss { + if hay == needle { + return true + } + } + return false +} + +func SplitPrefix(prefix string) []string { + var subs []string + max := len(prefix) + for i := 0; i < len(prefix)-1; i++ { + subs = append(subs, prefix[:max-i]) + } + return subs +} + +func SplitPrefixInterface(prefix string) []interface{} { + var subs []interface{} + max := len(prefix) + for i := 0; i < len(prefix)-1; i++ { + subs = append(subs, prefix[:max-i]) + } + return subs } diff --git a/utils/utils_test.go b/utils/utils_test.go index 12f0cf557..d642bac42 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -195,35 +195,55 @@ func TestMissingStructFieldsIncorrect(t *testing.T) { } } -func TestRoundToMinute(t *testing.T) { - result := RoundToMinute(0) - expected := 0.0 +func TestRound(t *testing.T) { + minute := time.Minute + result := RoundTo(minute, 0*time.Second) + expected := 0 * time.Second if result != expected { t.Errorf("Error rounding to minute1: expected %v was %v", expected, result) } - result = RoundToMinute(1) - expected = 60.0 + result = RoundTo(time.Second, 1*time.Second+500*time.Millisecond) + expected = 2 * time.Second + if result != expected { + t.Errorf("Error rounding to minute1: expected %v was %v", expected, result) + } + result = RoundTo(minute, 1*time.Second) + expected = minute if result != expected { t.Errorf("Error rounding to minute2: expected %v was %v", expected, result) } - result = RoundToMinute(59) - expected = 60.0 + result = RoundTo(minute, 5*time.Second) + expected = minute if result != expected { t.Errorf("Error rounding to minute3: expected %v was %v", expected, result) } - result = RoundToMinute(60) - expected = 60.0 + result = RoundTo(minute, minute) + expected = minute if result != expected { t.Errorf("Error rounding to minute4: expected %v was %v", expected, result) } - result = RoundToMinute(90) - expected = 120.0 + result = RoundTo(minute, 90*time.Second) + expected = 120 * time.Second if result != expected { t.Errorf("Error rounding to minute5: expected %v was %v", expected, result) } - result = RoundToMinute(120) + result = RoundTo(60, 120) expected = 120.0 if result != expected { t.Errorf("Error rounding to minute5: expected %v was %v", expected, result) } } + +func TestSplitPrefix(t *testing.T) { + a := SplitPrefix("0123456789") + if len(a) != 9 { + t.Error("Error splitting prefix: ", a) + } +} + +func TestSplitPrefixEmpty(t *testing.T) { + a := SplitPrefix("") + if len(a) != 0 { + t.Error("Error splitting prefix: ", a) + } +}