Files
cgrates/engine/userbalance.go
2013-07-24 13:38:45 +03:00

460 lines
12 KiB
Go

/*
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 (
"errors"
"github.com/cgrates/cgrates/utils"
"sort"
"strings"
"time"
)
const (
UB_TYPE_POSTPAID = "*postpaid"
UB_TYPE_PREPAID = "*prepaid"
// Direction type
INBOUND = "*in"
OUTBOUND = "*out"
// Balance types
CREDIT = "*monetary"
SMS = "*sms"
TRAFFIC = "*internet"
TRAFFIC_TIME = "*internet_time"
MINUTES = "*minutes"
)
/*
Structure containing information about user's credit (minutes, cents, sms...).'
*/
type UserBalance struct {
Id string
Type string // prepaid-postpaid
BalanceMap map[string]BalanceChain
MinuteBuckets []*MinuteBucket
UnitCounters []*UnitsCounter
ActionTriggers ActionTriggerPriotityList
}
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())
}
/*
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
}
/*
Error type for overflowed debit methods.
*/
type AmountTooBig struct{}
func (a AmountTooBig) Error() string {
return "Amount excedes balance!"
}
/*
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)
if err != nil {
continue
}
if precision, ok := d.containsPrefix(prefix); ok {
mb.precision = precision
if mb.Seconds > 0 {
bucketList = append(bucketList, mb)
}
}
}
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!")
}
found := false
for _, mb := range ub.MinuteBuckets {
if mb.IsExpired() {
continue
}
if mb.Equal(newMb) {
mb.Seconds -= newMb.Seconds
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)
}
return nil
}
/*
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.
*/
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 new(AmountTooBig)
}
credit := ub.BalanceMap[CREDIT+OUTBOUND]
// calculating money debit
// this is needed because if the credit is less then the amount needed to be debited
// we need to keep everything in place and return an error
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)
}
} else {
if mb.Price > 0 { // debit the money if the bucket has price
credit.Debit(amount * mb.Price)
}
break
}
if credit.GetTotalValue() < 0 {
break
}
}
if credit.GetTotalValue() < 0 {
return new(AmountTooBig)
}
ub.BalanceMap[CREDIT+OUTBOUND] = credit // credit is > 0
for _, mb := range bucketList {
if mb.Seconds < amount {
amount -= mb.Seconds
mb.Seconds = 0
} else {
mb.Seconds -= amount
break
}
}
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.
*/
func (ub *UserBalance) debitBalance(balanceId string, amount float64, count bool) float64 {
if count {
ub.countUnits(&Action{BalanceId: balanceId, Direction: OUTBOUND, Units: amount})
}
ub.BalanceMap[balanceId+OUTBOUND].Debit(amount)
return ub.BalanceMap[balanceId+OUTBOUND].GetTotalValue()
}
// Scans the action trigers and execute the actions for which trigger is met
func (ub *UserBalance) executeActionTriggers(a *Action) {
ub.ActionTriggers.Sort()
for _, at := range ub.ActionTriggers {
if at.Executed {
// trigger is marked as executed, so skipp it until
// 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))) {
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 strings.Contains(at.ThresholdType, "*max") {
if mb.DestinationId == at.DestinationId && mb.Seconds >= at.ThresholdValue {
// run the actions
at.Execute(ub)
}
} else { //MIN
if mb.DestinationId == at.DestinationId && mb.Seconds <= at.ThresholdValue {
// run the actions
at.Execute(ub)
}
}
}
} else {
if strings.Contains(at.ThresholdType, "*max") {
if uc.Units >= at.ThresholdValue {
// run the actions
at.Execute(ub)
}
} else { //MIN
if uc.Units <= at.ThresholdValue {
// run the actions
at.Execute(ub)
}
}
}
}
}
} 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 strings.Contains(at.ThresholdType, "*max") {
if mb.DestinationId == at.DestinationId && mb.Seconds >= at.ThresholdValue {
// run the actions
at.Execute(ub)
}
} else { //MIN
if mb.DestinationId == at.DestinationId && mb.Seconds <= at.ThresholdValue {
// run the actions
at.Execute(ub)
}
}
}
} else {
if strings.Contains(at.ThresholdType, "*max") {
if b.Value >= at.ThresholdValue {
// run the actions
at.Execute(ub)
}
} else { //MIN
if b.Value <= at.ThresholdValue {
// run the actions
at.Execute(ub)
}
}
}
}
}
}
}
// Mark all action trigers as ready for execution
// 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))) {
continue
}
at.Executed = false
}
ub.executeActionTriggers(a)
}
// Returns the unit counter that matches the specified action type
func (ub *UserBalance) getUnitCounter(a *Action) *UnitsCounter {
for _, uc := range ub.UnitCounters {
direction := a.Direction
if direction == "" {
direction = OUTBOUND
}
if uc.BalanceId == a.BalanceId && uc.Direction == direction {
return uc
}
}
return nil
}
// Increments the counter for the type specified in the received Action
// with the actions values
func (ub *UserBalance) countUnits(a *Action) {
unitsCounter := ub.getUnitCounter(a)
// if not found add the counter
if unitsCounter == nil {
direction := a.Direction
if direction == "" {
direction = OUTBOUND
}
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)
} else {
unitsCounter.Units += a.Units
}
ub.executeActionTriggers(nil)
}
// Create minute counters for all triggered actions that have actions operating on minute buckets
func (ub *UserBalance) initMinuteCounters() {
ucTempMap := make(map[string]*UnitsCounter, 2)
for _, at := range ub.ActionTriggers {
acs, err := storageGetter.GetActions(at.ActionsId)
if err != nil {
continue
}
for _, a := range acs {
if a.MinuteBucket != nil {
direction := at.Direction
if direction == "" {
direction = OUTBOUND
}
uc, exists := ucTempMap[direction]
if !exists {
uc = &UnitsCounter{BalanceId: MINUTES, Direction: direction}
ucTempMap[direction] = uc
uc.MinuteBuckets = bucketsorter{}
ub.UnitCounters = append(ub.UnitCounters, uc)
}
uc.MinuteBuckets = append(uc.MinuteBuckets, a.MinuteBucket.Clone())
uc.MinuteBuckets.Sort()
}
}
}
}
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:]...)
}
}
}