diff --git a/apier/apier.go b/apier/apier.go index 4b4e34278..2c6cea4cc 100644 --- a/apier/apier.go +++ b/apier/apier.go @@ -147,14 +147,10 @@ func (self *Apier) SetRatingProfile(attrs AttrSetRatingProfile, reply *string) e } dbReader := rater.NewDbReader(self.StorDb, self.DataDb, attrs.TPid) - newRP, err := dbReader.LoadRatingProfileByTag(attrs.RateProfileId) - if err != nil { - return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error()) - } - err = self.DataDb.SetRatingProfile(newRP) - if err != nil { + if err := dbReader.LoadRatingProfileByTag(attrs.RateProfileId); err != nil { return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error()) } + *reply = "OK" return nil } @@ -240,3 +236,22 @@ func (self *Apier) AddAccount(attr *AttrAccount, reply *float64) error { } return nil } + +type AttrSetAccountAction struct { + TPid string + RateProfileId string +} + +// Process dependencies and load a specific rating profile from storDb into dataDb. +func (self *Apier) SetAccountAction(attrs AttrSetAccountAction, reply *string) error { + if missing := utils.MissingStructFields(&attrs, []string{"TPid", "RateProfileId"}); len(missing) != 0 { + return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing) + } + dbReader := rater.NewDbReader(self.StorDb, self.DataDb, attrs.TPid) + + if err := dbReader.LoadAccountActionByTag(attrs.RateProfileId); err != nil { + return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error()) + } + *reply = "OK" + return nil +} diff --git a/rater/action.go b/rater/action.go index 334da4f53..5c602a0a8 100644 --- a/rater/action.go +++ b/rater/action.go @@ -21,19 +21,21 @@ package rater import ( "fmt" "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 - Units float64 - Weight float64 - MinuteBucket *MinuteBucket + Id string + ActionType string + BalanceId string + Direction string + ExpirationDate time.Time + Units float64 + Weight float64 + MinuteBucket *MinuteBucket } const ( @@ -164,17 +166,10 @@ func genericDebit(ub *UserBalance, a *Action) (err error) { if ub.BalanceMap == nil { ub.BalanceMap = make(map[string]BalanceChain) } - switch a.BalanceId { - case CREDIT: - ub.debitBalance(CREDIT, a.Units, false) - case SMS: - ub.debitBalance(SMS, a.Units, false) - case MINUTES: + if a.BalanceId == MINUTES { ub.debitMinuteBucket(a.MinuteBucket) - case TRAFFIC: - ub.debitBalance(TRAFFIC, a.Units, false) - case TRAFFIC_TIME: - ub.debitBalance(TRAFFIC_TIME, a.Units, false) + } else { + ub.debitBalanceAction(a) } return } diff --git a/rater/actions_test.go b/rater/actions_test.go index 28771bd31..227f68c34 100644 --- a/rater/actions_test.go +++ b/rater/actions_test.go @@ -508,7 +508,7 @@ func TestActionTopupResetCredit(t *testing.T) { len(ub.UnitCounters) != 1 || len(ub.MinuteBuckets) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Errorf("Topup reset action failed: %#v", ub) + t.Errorf("Topup reset action failed: %#v", ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue()) } } @@ -542,14 +542,14 @@ func TestActionTopupCredit(t *testing.T) { MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, Percent: 0, 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, Units: 10} + a := &Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10} topupAction(ub, a) if ub.Type != UB_TYPE_PREPAID || ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() != 110 || len(ub.UnitCounters) != 1 || len(ub.MinuteBuckets) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Error("Topup action failed!", ub) + t.Error("Topup action failed!", ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue()) } } @@ -583,14 +583,14 @@ func TestActionDebitCredit(t *testing.T) { MinuteBuckets: []*MinuteBucket{&MinuteBucket{Seconds: 10, Weight: 20, Price: 1, DestinationId: "NAT"}, &MinuteBucket{Weight: 10, Price: 10, Percent: 0, 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, Units: 10} + a := &Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10} debitAction(ub, a) if ub.Type != UB_TYPE_PREPAID || ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue() != 90 || len(ub.UnitCounters) != 1 || len(ub.MinuteBuckets) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { - t.Error("Debit action failed!", ub) + t.Error("Debit action failed!", ub.BalanceMap[CREDIT+OUTBOUND].GetTotalValue()) } } diff --git a/rater/loader_csv.go b/rater/loader_csv.go index df1f249fb..713084112 100644 --- a/rater/loader_csv.go +++ b/rater/loader_csv.go @@ -379,27 +379,33 @@ func (csvr *CSVReader) LoadActions() (err error) { if err != nil { return errors.New(fmt.Sprintf("Could not parse action units: %v", err)) } + unix, err := strconv.ParseInt(record[5], 10, 64) + if err != nil { + return errors.New(fmt.Sprintf("Could not parse expiration date: %v", err)) + } + expDate := time.Unix(unix, 0) var a *Action if record[2] != MINUTES { a = &Action{ - ActionType: record[1], - BalanceId: record[2], - Direction: record[3], - Units: units, + ActionType: record[1], + BalanceId: record[2], + Direction: record[3], + Units: units, + ExpirationDate: expDate, } } else { price, percent := 0.0, 0.0 - value, err := strconv.ParseFloat(record[7], 64) + value, err := strconv.ParseFloat(record[8], 64) if err != nil { return errors.New(fmt.Sprintf("Could not parse action price: %v", err)) } - if record[6] == PERCENT { + if record[7] == PERCENT { percent = value } - if record[6] == ABSOLUTE { + if record[7] == ABSOLUTE { price = value } - minutesWeight, err := strconv.ParseFloat(record[8], 64) + minutesWeight, err := strconv.ParseFloat(record[9], 64) if err != nil { return errors.New(fmt.Sprintf("Could not parse action minutes weight: %v", err)) } @@ -408,17 +414,19 @@ func (csvr *CSVReader) LoadActions() (err error) { 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, + Id: utils.GenUUID(), + ActionType: record[1], + BalanceId: record[2], + Direction: record[3], + Weight: weight, + ExpirationDate: expDate, MinuteBucket: &MinuteBucket{ - Seconds: units, - Weight: minutesWeight, - Price: price, - Percent: percent, - DestinationId: record[5], + Seconds: units, + Weight: minutesWeight, + Price: price, + Percent: percent, + DestinationId: record[6], + ExpirationDate: expDate, }, } } diff --git a/rater/loader_csv_test.go b/rater/loader_csv_test.go index 4c5c770b2..1b771cc27 100644 --- a/rater/loader_csv_test.go +++ b/rater/loader_csv_test.go @@ -84,7 +84,7 @@ vdf,0,OUT,inf,inf,STANDARD,2012-02-28T00:00:00Z vdf,0,OUT,fall,one|rif,PREMIUM,2012-02-28T00:00:00Z ` actions = ` -MINI,TOPUP,MINUTES,OUT,100,NAT,ABSOLUTE,0,10,10 +MINI,TOPUP,MINUTES,OUT,100,1374239002,NAT,ABSOLUTE,0,10,10 ` actionTimings = ` MORE_MINUTES,MINI,ONE_TIME_RUN,10 diff --git a/rater/loader_db.go b/rater/loader_db.go index 1dd1a2b7e..95bc21fb2 100644 --- a/rater/loader_db.go +++ b/rater/loader_db.go @@ -28,8 +28,8 @@ import ( type DbReader struct { tpid string - storDB DataStorage - storage DataStorage + storDb DataStorage + dataDb DataStorage actions map[string][]*Action actionsTimings map[string][]*ActionTiming actionsTriggers map[string][]*ActionTrigger @@ -44,8 +44,8 @@ type DbReader struct { func NewDbReader(storDB DataStorage, storage DataStorage, tpid string) *DbReader { c := new(DbReader) - c.storDB = storDB - c.storage = storage + c.storDb = storDB + c.dataDb = storage c.tpid = tpid c.activationPeriods = make(map[string]*ActivationPeriod) c.actionsTimings = make(map[string][]*ActionTiming) @@ -53,7 +53,7 @@ func NewDbReader(storDB DataStorage, storage DataStorage, tpid string) *DbReader } func (dbr *DbReader) WriteToDatabase(flush, verbose bool) (err error) { - storage := dbr.storage + storage := dbr.dataDb if flush { storage.Flush() } @@ -121,22 +121,22 @@ func (dbr *DbReader) WriteToDatabase(flush, verbose bool) (err error) { } func (dbr *DbReader) LoadDestinations() (err error) { - dbr.destinations, err = dbr.storDB.GetTpDestinations(dbr.tpid, "") + dbr.destinations, err = dbr.storDb.GetTpDestinations(dbr.tpid, "") return } func (dbr *DbReader) LoadTimings() (err error) { - dbr.timings, err = dbr.storDB.GetTpTimings(dbr.tpid, "") + dbr.timings, err = dbr.storDb.GetTpTimings(dbr.tpid, "") return err } func (dbr *DbReader) LoadRates() (err error) { - dbr.rates, err = dbr.storDB.GetTpRates(dbr.tpid, "") + dbr.rates, err = dbr.storDb.GetTpRates(dbr.tpid, "") return err } func (dbr *DbReader) LoadDestinationRates() (err error) { - dbr.destinationRates, err = dbr.storDB.GetTpDestinationRates(dbr.tpid, "") + dbr.destinationRates, err = dbr.storDb.GetTpDestinationRates(dbr.tpid, "") if err != nil { return err } @@ -153,7 +153,7 @@ func (dbr *DbReader) LoadDestinationRates() (err error) { } func (dbr *DbReader) LoadDestinationRateTimings() error { - rts, err := dbr.storDB.GetTpDestinationRateTimings(dbr.tpid, "") + rts, err := dbr.storDb.GetTpDestinationRateTimings(dbr.tpid, "") if err != nil { return err } @@ -179,7 +179,7 @@ func (dbr *DbReader) LoadDestinationRateTimings() error { } func (dbr *DbReader) LoadRatingProfiles() error { - rpfs, err := dbr.storDB.GetTpRatingProfiles(dbr.tpid, "") + rpfs, err := dbr.storDb.GetTpRatingProfiles(dbr.tpid, "") if err != nil { return err } @@ -200,75 +200,74 @@ func (dbr *DbReader) LoadRatingProfiles() error { return nil } -func (dbr *DbReader) LoadRatingProfileByTag(tag string) (*RatingProfile, error) { +func (dbr *DbReader) LoadRatingProfileByTag(tag string) error { activationPeriods := make(map[string]*ActivationPeriod) resultRatingProfile := &RatingProfile{Id: tag} - rpm, err := dbr.storDB.GetTpRatingProfiles(dbr.tpid, tag) + rpm, err := dbr.storDb.GetTpRatingProfiles(dbr.tpid, tag) if err != nil { - return nil, err + return err } else if len(rpm) == 0 { - return nil, fmt.Errorf("No RateProfile with id: %s", tag) + return fmt.Errorf("No RateProfile with id: %s", tag) } for _, ratingProfile := range rpm { resultRatingProfile.FallbackKey = ratingProfile.FallbackKey // it will be the last fallback key at := time.Unix(ratingProfile.activationTime, 0) - drtm, err := dbr.storDB.GetTpDestinationRateTimings(dbr.tpid, ratingProfile.destRatesTimingTag) + drtm, err := dbr.storDb.GetTpDestinationRateTimings(dbr.tpid, ratingProfile.destRatesTimingTag) if err != nil { - return nil, err + return err } else if len(drtm) == 0 { - return nil, fmt.Errorf("No DestRateTimings profile with id: %s", ratingProfile.destRatesTimingTag) + return fmt.Errorf("No DestRateTimings profile with id: %s", ratingProfile.destRatesTimingTag) } for _, destrateTiming := range drtm { - tm, err := dbr.storDB.GetTpTimings(dbr.tpid, destrateTiming.TimingsTag) + tm, err := dbr.storDb.GetTpTimings(dbr.tpid, destrateTiming.TimingsTag) if err != nil { - return nil, err + return err } else if len(tm) == 0 { - return nil, fmt.Errorf("No Timings profile with id: %s", destrateTiming.TimingsTag) + return fmt.Errorf("No Timings profile with id: %s", destrateTiming.TimingsTag) } destrateTiming.timing = tm[destrateTiming.TimingsTag] - drm, err := dbr.storDB.GetTpDestinationRates(dbr.tpid, destrateTiming.DestinationRatesTag) + drm, err := dbr.storDb.GetTpDestinationRates(dbr.tpid, destrateTiming.DestinationRatesTag) if err != nil { - return nil, err + return err } else if len(drm) == 0 { - return nil, fmt.Errorf("No Timings profile with id: %s", destrateTiming.DestinationRatesTag) + return fmt.Errorf("No Timings profile with id: %s", destrateTiming.DestinationRatesTag) } for _, drate := range drm[destrateTiming.DestinationRatesTag] { - rt, err := dbr.storDB.GetTpRates(dbr.tpid, drate.RateTag) + rt, err := dbr.storDb.GetTpRates(dbr.tpid, drate.RateTag) if err != nil { - return nil, err + return err } else if len(rt) == 0 { - return nil, fmt.Errorf("No Rates profile with id: %s", drate.RateTag) + return fmt.Errorf("No Rates profile with id: %s", drate.RateTag) } drate.Rate = rt[drate.RateTag] if _, exists := activationPeriods[destrateTiming.Tag]; !exists { activationPeriods[destrateTiming.Tag] = &ActivationPeriod{} } activationPeriods[destrateTiming.Tag].AddIntervalIfNotPresent(destrateTiming.GetInterval(drate)) - dm, err := dbr.storDB.GetTpDestinations(dbr.tpid, drate.DestinationsTag) + dm, err := dbr.storDb.GetTpDestinations(dbr.tpid, drate.DestinationsTag) if err != nil { - return nil, err + return err } for _, destination := range dm { ap := activationPeriods[ratingProfile.destRatesTimingTag] newAP := &ActivationPeriod{ActivationTime: at} newAP.Intervals = append(newAP.Intervals, ap.Intervals...) resultRatingProfile.AddActivationPeriodIfNotPresent(destination.Id, newAP) - dbr.storage.SetDestination(destination) + dbr.dataDb.SetDestination(destination) } } } } - - return resultRatingProfile, nil + return dbr.dataDb.SetRatingProfile(resultRatingProfile) } func (dbr *DbReader) LoadActions() (err error) { - dbr.actions, err = dbr.storDB.GetTpActions(dbr.tpid, "") + dbr.actions, err = dbr.storDb.GetTpActions(dbr.tpid, "") return err } func (dbr *DbReader) LoadActionTimings() (err error) { - atsMap, err := dbr.storDB.GetTpActionTimings(dbr.tpid, "") + atsMap, err := dbr.storDb.GetTpActionTimings(dbr.tpid, "") if err != nil { return err } @@ -301,12 +300,12 @@ func (dbr *DbReader) LoadActionTimings() (err error) { } func (dbr *DbReader) LoadActionTriggers() (err error) { - dbr.actionsTriggers, err = dbr.storDB.GetTpActionTriggers(dbr.tpid, "") + dbr.actionsTriggers, err = dbr.storDb.GetTpActionTriggers(dbr.tpid, "") return err } func (dbr *DbReader) LoadAccountActions() (err error) { - acs, err := dbr.storDB.GetTpAccountActions(dbr.tpid, "") + acs, err := dbr.storDb.GetTpAccountActions(dbr.tpid, "") if err != nil { return err } @@ -335,3 +334,117 @@ func (dbr *DbReader) LoadAccountActions() (err error) { } return nil } + +func (dbr *DbReader) LoadAccountActionsByTag(tag string) error { + accountActions, err := dbr.storDb.GetTpAccountActions(dbr.tpid, tag) + if err != nil || len(accountActions) != 1 { + return err + } + accountAction := accountActions[0] + actionTimingsMap, err := dbr.storDb.GetTpActionTimings(dbr.tpid, accountAction.ActionTimingsTag) + if err != nil { + return err + } + actionTriggersMap, err := dbr.storDb.GetTpActionTriggers(dbr.tpid, accountAction.ActionTriggersTag) + if err != nil { + return err + } + + // collecting action ids + var actionsIds []string + for _, atms := range actionTimingsMap { + for _, atm := range atms { + actionsIds = append(actionsIds, atm.ActionsId) + } + } + for _, atrs := range actionTriggersMap { + for _, atr := range atrs { + actionsIds = append(actionsIds, atr.ActionsId) + } + } + var acts map[string][]*Action + for _, actId := range actionsIds { + actions, err := dbr.storDb.GetTpActions(dbr.tpid, actId) + if err != nil { + return err + } + for id, act := range actions { + acts[id] = act + } + } + + // write action timings + for k, atms := range actionTimingsMap { + err = dbr.storDb.SetActionTimings(k, atms) + if err != nil { + return err + } + } + // write actions + for k, as := range acts { + err = dbr.storDb.SetActions(k, as) + if err != nil { + return err + } + } + + activationPeriods := make(map[string]*ActivationPeriod) + resultRatingProfile := &RatingProfile{Id: tag} + rpm, err := dbr.storDb.GetTpRatingProfiles(dbr.tpid, tag) + if err != nil { + return err + } else if len(rpm) == 0 { + return fmt.Errorf("No RateProfile with id: %s", tag) + } + for _, ratingProfile := range rpm { + resultRatingProfile.FallbackKey = ratingProfile.FallbackKey // it will be the last fallback key + at := time.Unix(ratingProfile.activationTime, 0) + drtm, err := dbr.storDb.GetTpDestinationRateTimings(dbr.tpid, ratingProfile.destRatesTimingTag) + if err != nil { + return err + } else if len(drtm) == 0 { + return fmt.Errorf("No DestRateTimings profile with id: %s", ratingProfile.destRatesTimingTag) + } + for _, destrateTiming := range drtm { + tm, err := dbr.storDb.GetTpTimings(dbr.tpid, destrateTiming.TimingsTag) + if err != nil { + return err + } else if len(tm) == 0 { + return fmt.Errorf("No Timings profile with id: %s", destrateTiming.TimingsTag) + } + destrateTiming.timing = tm[destrateTiming.TimingsTag] + drm, err := dbr.storDb.GetTpDestinationRates(dbr.tpid, destrateTiming.DestinationRatesTag) + if err != nil { + return err + } else if len(drm) == 0 { + return fmt.Errorf("No Timings profile with id: %s", destrateTiming.DestinationRatesTag) + } + for _, drate := range drm[destrateTiming.DestinationRatesTag] { + rt, err := dbr.storDb.GetTpRates(dbr.tpid, drate.RateTag) + if err != nil { + return err + } else if len(rt) == 0 { + return fmt.Errorf("No Rates profile with id: %s", drate.RateTag) + } + drate.Rate = rt[drate.RateTag] + if _, exists := activationPeriods[destrateTiming.Tag]; !exists { + activationPeriods[destrateTiming.Tag] = &ActivationPeriod{} + } + activationPeriods[destrateTiming.Tag].AddIntervalIfNotPresent(destrateTiming.GetInterval(drate)) + dm, err := dbr.storDb.GetTpDestinations(dbr.tpid, drate.DestinationsTag) + if err != nil { + return err + } + for _, destination := range dm { + ap := activationPeriods[ratingProfile.destRatesTimingTag] + newAP := &ActivationPeriod{ActivationTime: at} + newAP.Intervals = append(newAP.Intervals, ap.Intervals...) + resultRatingProfile.AddActivationPeriodIfNotPresent(destination.Id, newAP) + dbr.dataDb.SetDestination(destination) + } + } + } + } + + return dbr.dataDb.SetRatingProfile(resultRatingProfile) +} diff --git a/rater/minute_buckets.go b/rater/minute_buckets.go index 7cbcd0750..8ad1d3293 100644 --- a/rater/minute_buckets.go +++ b/rater/minute_buckets.go @@ -62,6 +62,10 @@ func (mb *MinuteBucket) Equal(o *MinuteBucket) bool { mb.Percent == o.Percent } +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. */ diff --git a/rater/simple_serializer.go b/rater/simple_serializer.go index 638a558ce..a9d46da05 100644 --- a/rater/simple_serializer.go +++ b/rater/simple_serializer.go @@ -112,6 +112,7 @@ func (a *Action) Store() (result string, err error) { result += a.ActionType + "|" result += a.BalanceId + "|" result += a.Direction + "|" + result += a.ExpirationDate.Format(time.RFC3339) + "|" result += strconv.FormatFloat(a.Units, 'f', -1, 64) + "|" result += strconv.FormatFloat(a.Weight, 'f', -1, 64) if a.MinuteBucket != nil { @@ -125,20 +126,24 @@ func (a *Action) Store() (result string, err error) { return } -func (a *Action) Restore(input string) error { +func (a *Action) Restore(input string) (err error) { elements := strings.Split(input, "|") - if len(elements) < 6 { + if len(elements) < 7 { return notEnoughElements } a.Id = elements[0] a.ActionType = elements[1] a.BalanceId = elements[2] a.Direction = elements[3] - a.Units, _ = strconv.ParseFloat(elements[4], 64) - a.Weight, _ = strconv.ParseFloat(elements[5], 64) - if len(elements) == 7 { + a.ExpirationDate, err = time.Parse(time.RFC3339, elements[4]) + if err != nil { + return err + } + a.Units, _ = strconv.ParseFloat(elements[5], 64) + a.Weight, _ = strconv.ParseFloat(elements[6], 64) + if len(elements) == 8 { a.MinuteBucket = &MinuteBucket{} - if err := a.MinuteBucket.Restore(elements[6]); err != nil { + if err := a.MinuteBucket.Restore(elements[7]); err != nil { return err } } diff --git a/rater/storage_sql.go b/rater/storage_sql.go index dafefc9e0..738371d4e 100644 --- a/rater/storage_sql.go +++ b/rater/storage_sql.go @@ -23,6 +23,8 @@ import ( "encoding/json" "fmt" "github.com/cgrates/cgrates/utils" + "strconv" + "time" ) type SQLStorage struct { @@ -550,10 +552,10 @@ 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 string + var action, balanceId, dir, destId, rateType string var expTime int64 var units, rate, minutesWeight, weight float64 - if err = rows.Scan(&action, &balanceId, &dir, &units, &expTime, &destId, &rateType, &rate, &minutesWeight, &weight); err!= nil { + if err = rows.Scan(&action, &balanceId, &dir, &units, &expTime, &destId, &rateType, &rate, &minutesWeight, &weight); err != nil { return nil, err } acts.Actions = append(acts.Actions, utils.Action{action, balanceId, dir, units, expTime, destId, rateType, rate, minutesWeight, weight}) @@ -632,7 +634,7 @@ func (self *SQLStorage) GetTPActionTimings(tpid, atId string) (map[string][]*uti i++ //Keep here a reference so we know we got at least one result var tag, actionsId, timingId string var weight float64 - if err = rows.Scan(&tag, &actionsId, &timingId, &weight); err!= nil { + if err = rows.Scan(&tag, &actionsId, &timingId, &weight); err != nil { return nil, err } ats[tag] = append(ats[tag], &utils.TPActionTimingsRow{actionsId, timingId, weight}) @@ -969,17 +971,23 @@ func (self *SQLStorage) GetTpActions(tpid, tag string) (map[string][]*Action, er for rows.Next() { var id int var units, rate, minutes_weight, weight float64 - var tpid, tag, action, balance_tag, direction, destinations_tag, rate_type string - if err := rows.Scan(&id, &tpid, &tag, &action, &balance_tag, &direction, &units, &destinations_tag, &rate_type, &rate, &minutes_weight, &weight); err != nil { + var tpid, tag, action, balance_tag, direction, destinations_tag, rate_type, expirationDate string + if err := rows.Scan(&id, &tpid, &tag, &action, &balance_tag, &direction, &units, &expirationDate, &destinations_tag, &rate_type, &rate, &minutes_weight, &weight); err != nil { return nil, err } + unix, err := strconv.ParseInt(expirationDate, 10, 64) + if err != nil { + return nil, err + } + expDate := time.Unix(unix, 0) var a *Action if balance_tag != MINUTES { a = &Action{ - ActionType: action, - BalanceId: balance_tag, - Direction: direction, - Units: units, + ActionType: action, + BalanceId: balance_tag, + Direction: direction, + Units: units, + ExpirationDate: expDate, } } else { var percent, price float64 @@ -990,17 +998,19 @@ func (self *SQLStorage) GetTpActions(tpid, tag string) (map[string][]*Action, er price = rate } a = &Action{ - Id: utils.GenUUID(), - ActionType: action, - BalanceId: balance_tag, - Direction: direction, - Weight: weight, + Id: utils.GenUUID(), + ActionType: action, + BalanceId: balance_tag, + Direction: direction, + Weight: weight, + ExpirationDate: expDate, MinuteBucket: &MinuteBucket{ - Seconds: units, - Weight: minutes_weight, - Price: price, - Percent: percent, - DestinationId: destinations_tag, + Seconds: units, + Weight: minutes_weight, + Price: price, + Percent: percent, + DestinationId: destinations_tag, + ExpirationDate: expDate, }, } } diff --git a/rater/userbalance.go b/rater/userbalance.go index 6f1346cad..1701a7419 100644 --- a/rater/userbalance.go +++ b/rater/userbalance.go @@ -20,6 +20,7 @@ package rater import ( "errors" + "github.com/cgrates/cgrates/utils" "sort" "strings" "time" @@ -62,8 +63,7 @@ type Balance struct { } func (b *Balance) Equal(o *Balance) bool { - return b.Value == o.Value || - b.ExpirationDate.Equal(o.ExpirationDate) || + return b.ExpirationDate.Equal(o.ExpirationDate) || b.Weight == o.Weight } @@ -150,6 +150,9 @@ func (ub *UserBalance) getSecondsForPrefix(prefix string) (seconds, credit float return } for _, mb := range ub.MinuteBuckets { + if mb.IsExpired() { + continue + } d, err := GetDestination(mb.DestinationId) if err != nil { continue @@ -177,6 +180,9 @@ func (ub *UserBalance) debitMinuteBucket(newMb *MinuteBucket) error { } found := false for _, mb := range ub.MinuteBuckets { + if mb.IsExpired() { + continue + } if mb.Equal(newMb) { mb.Seconds -= newMb.Seconds found = true @@ -242,6 +248,29 @@ func (ub *UserBalance) debitMinutesBalance(amount float64, prefix string, count 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 + } + } + 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. */ @@ -397,3 +426,21 @@ func (ub *UserBalance) initMinuteCounters() { } } } + +func (ub *UserBalance) CleanExpiredBalancesAndBuckets() { + for key, _ := range ub.BalanceMap { + bm := ub.BalanceMap[key] + for i := 0; i < len(bm); i++ { + if bm[i].IsExpired() { + // delete it + bm = append(bm[:i], bm[i+1:]...) + } + } + 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:]...) + } + } +} diff --git a/rater/userbalance_test.go b/rater/userbalance_test.go index fad779cfd..a291d5bb8 100644 --- a/rater/userbalance_test.go +++ b/rater/userbalance_test.go @@ -469,6 +469,27 @@ func TestUserBalanceExecuteTriggeredActionsOrder(t *testing.T) { } } +func TestCleanExpired(t *testing.T) { + ub := &UserBalance{ + Id: "TEST_UB_OREDER", + BalanceMap: map[string]BalanceChain{CREDIT + OUTBOUND: BalanceChain{ + &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}, + &Balance{ExpirationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC)}, + &Balance{ExpirationDate: time.Now().Add(10 * time.Second)}}}, + MinuteBuckets: []*MinuteBucket{ + &MinuteBucket{ExpirationDate: time.Date(2013, 7, 18, 14, 33, 0, 0, time.UTC)}, + &MinuteBucket{ExpirationDate: time.Now().Add(10 * time.Second)}, + }, + } + ub.CleanExpiredBalancesAndBuckets() + if len(ub.BalanceMap[CREDIT+OUTBOUND]) != 2 { + t.Error("Error cleaning expired balances!") + } + if len(ub.MinuteBuckets) != 1 { + t.Error("Error cleaning expired minute buckets!") + } +} + func TestUserBalanceUnitCounting(t *testing.T) { ub := &UserBalance{} ub.countUnits(&Action{BalanceId: CREDIT, Direction: OUTBOUND, Units: 10})