diff --git a/engine/action_trigger.go b/engine/action_trigger.go index 5fae1ca24..3a8aaa281 100644 --- a/engine/action_trigger.go +++ b/engine/action_trigger.go @@ -64,6 +64,22 @@ 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.MinuteBucket != nil { + thresholdType = a.MinuteBucket.PriceType == "" || at.ThresholdType == a.MinuteBucket.PriceType + thresholdValue = a.MinuteBucket.Price == 0 || at.ThresholdValue == a.MinuteBucket.Price + } + return id && direction && thresholdType && thresholdValue +} + // Structure to store actions according to weight type ActionTriggerPriotityList []*ActionTrigger diff --git a/engine/actions_test.go b/engine/actions_test.go index 23a5301ff..3c9d2480c 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -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{MinuteBucket: &MinuteBucket{PriceType: TRIGGER_MAX_BALANCE, Price: 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, MinuteBucket: &MinuteBucket{PriceType: TRIGGER_MAX_BALANCE, Price: 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, MinuteBucket: &MinuteBucket{PriceType: TRIGGER_MAX_BALANCE, Price: 2}} + if at.Match(a) { + t.Errorf("Action trigger [%v] does not match action [%v]", at, a) + } +} + +func TestActionTriggerMatcMinuteBucketFalse(t *testing.T) { + at := &ActionTrigger{ + Direction: OUTBOUND, + BalanceId: CREDIT, + ThresholdType: TRIGGER_MAX_BALANCE, + ThresholdValue: 2, + } + a := &Action{Direction: OUTBOUND, BalanceId: CREDIT, MinuteBucket: &MinuteBucket{PriceType: TRIGGER_MAX_BALANCE, Price: 3}} + 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, MinuteBucket: &MinuteBucket{PriceType: TRIGGER_MAX_COUNTER, Price: 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} diff --git a/engine/balances.go b/engine/balances.go new file mode 100644 index 000000000..cffdf95a1 --- /dev/null +++ b/engine/balances.go @@ -0,0 +1,119 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2013 ITsysCOM + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package engine + +import ( + "sort" + "time" +) + +type Balance struct { + Id string + Value float64 + ExpirationDate time.Time + Weight float64 + GroupIds []string + SpecialPercent 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 +} diff --git a/engine/calldesc.go b/engine/calldesc.go index 26904dc62..7b7551698 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -369,7 +369,9 @@ func (cd *CallDescriptor) Debit() (cc *CallCost, err error) { } for _, ts := range cc.Timespans { if ts.MinuteInfo != nil { - userBalance.debitMinutesBalance(ts.MinuteInfo.Quantity, cd.Destination, true) + if err = userBalance.debitMinutesBalance(ts.MinuteInfo.Quantity, cd.Destination, true); err != nil { + return cc, err + } } } } @@ -387,8 +389,7 @@ 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() } diff --git a/engine/groups.go b/engine/groups.go new file mode 100644 index 000000000..baa650fc5 --- /dev/null +++ b/engine/groups.go @@ -0,0 +1,46 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2013 ITsysCOM + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package engine + +import ( + "sort" +) + +type GroupLink struct { + Id string + Weight float64 +} + +type GroupLinks []*GroupLink + +func (gls GroupLinks) Len() int { + return len(gls) +} + +func (gls GroupLinks) Swap(i, j int) { + gls[i], gls[j] = gls[j], gls[i] +} + +func (gls GroupLinks) Less(j, i int) bool { + return gls[i].Weight < gls[j].Weight +} + +func (gls GroupLinks) Sort() { + sort.Sort(gls) +} diff --git a/engine/userbalance.go b/engine/userbalance.go index 9aa25b2df..d43f7ca50 100644 --- a/engine/userbalance.go +++ b/engine/userbalance.go @@ -21,9 +21,7 @@ package engine import ( "errors" "github.com/cgrates/cgrates/utils" - "sort" "strings" - "time" ) const ( @@ -41,6 +39,11 @@ const ( // 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" ) var ( @@ -49,6 +52,7 @@ 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 @@ -57,104 +61,10 @@ type UserBalance struct { MinuteBuckets []*MinuteBucket UnitCounters []*UnitsCounter ActionTriggers ActionTriggerPriotityList + + Groups GroupLinks // user info about groups // group information - GroupIds []string - UserIds []string -} - -type Balance struct { - Id string - Value float64 - ExpirationDate time.Time - Weight float64 - GroupIds []string - Percent 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 + UserIds []string // group info about users } /* @@ -309,11 +219,7 @@ 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") { @@ -386,11 +292,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