mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
merged work on new subject debiting
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
10
cdrs/cdrs.go
10
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)
|
||||
}
|
||||
|
||||
@@ -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("<Rater> 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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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
|
||||
-------------------
|
||||
|
||||
@@ -19,26 +19,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
169
engine/balances.go
Normal file
169
engine/balances.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
63
engine/balances_test.go
Normal file
63
engine/balances_test.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("<Rater> 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
46
engine/groups.go
Normal file
46
engine/groups.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
@@ -19,8 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"]))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,10 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,29 +19,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
49
hard_update_external_libs.py
Executable file
49
hard_update_external_libs.py
Executable file
@@ -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'])
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user