mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-12 10:36:24 +05:00
458 lines
15 KiB
Go
458 lines
15 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"
|
|
"fmt"
|
|
"github.com/cgrates/cgrates/cache2go"
|
|
"github.com/cgrates/cgrates/history"
|
|
"github.com/cgrates/cgrates/utils"
|
|
"log/syslog"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func init() {
|
|
var err error
|
|
Logger, err = syslog.New(syslog.LOG_INFO, "CGRateS")
|
|
if err != nil {
|
|
Logger = new(utils.StdLogger)
|
|
Logger.Err(fmt.Sprintf("Could not connect to syslog: %v", err))
|
|
}
|
|
}
|
|
|
|
const (
|
|
RECURSION_MAX_DEPTH = 10
|
|
FALLBACK_SUBJECT = "*any"
|
|
FALLBACK_SEP = ";"
|
|
)
|
|
|
|
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
|
|
debitPeriod = 10 * time.Second
|
|
roundingMethod = "*middle"
|
|
roundingDecimals = 4
|
|
historyScribe history.Scribe
|
|
//historyScribe, _ = history.NewMockScribe()
|
|
)
|
|
|
|
// Exported method to set the storage getter.
|
|
func SetDataStorage(sg DataStorage) {
|
|
storageGetter = sg
|
|
}
|
|
|
|
// Sets the global rounding method and decimal precision for GetCost method
|
|
func SetRoundingMethodAndDecimals(rm string, rd int) {
|
|
roundingMethod = rm
|
|
roundingDecimals = rd
|
|
}
|
|
|
|
/*
|
|
Sets the database for logging (can be de same as storage getter or different db)
|
|
*/
|
|
func SetStorageLogger(sg DataStorage) {
|
|
storageLogger = sg
|
|
}
|
|
|
|
/*
|
|
Exported method to set the debit period for caching purposes.
|
|
*/
|
|
func SetDebitPeriod(d time.Duration) {
|
|
debitPeriod = d
|
|
}
|
|
|
|
// Exported method to set the history scribe.
|
|
func SetHistoryScribe(scribe history.Scribe) {
|
|
historyScribe = scribe
|
|
}
|
|
|
|
/*
|
|
The input stucture that contains call information.
|
|
*/
|
|
type CallDescriptor struct {
|
|
Direction string
|
|
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 float64 // the call duration so far (partial or final)
|
|
Amount float64
|
|
FallbackSubject string // the subject to check for destination if not found on primary subject
|
|
ActivationPeriods []*ActivationPeriod
|
|
userBalance *UserBalance
|
|
}
|
|
|
|
// Adds an activation period that applyes to current call descriptor.
|
|
func (cd *CallDescriptor) AddActivationPeriod(aps ...*ActivationPeriod) {
|
|
cd.ActivationPeriods = append(cd.ActivationPeriods, aps...)
|
|
}
|
|
|
|
// Returns the key used to retrive the user balance involved in this call
|
|
func (cd *CallDescriptor) GetUserBalanceKey() string {
|
|
subj := cd.Subject
|
|
if cd.Account != "" {
|
|
subj = cd.Account
|
|
}
|
|
return fmt.Sprintf("%s:%s:%s", cd.Direction, cd.Tenant, subj)
|
|
}
|
|
|
|
// Gets and caches the user balance information.
|
|
func (cd *CallDescriptor) getUserBalance() (ub *UserBalance, err error) {
|
|
if cd.userBalance == nil {
|
|
cd.userBalance, err = storageGetter.GetUserBalance(cd.GetUserBalanceKey())
|
|
}
|
|
return cd.userBalance, err
|
|
}
|
|
|
|
/*
|
|
Restores the activation periods for the specified prefix from storage.
|
|
*/
|
|
func (cd *CallDescriptor) LoadActivationPeriods() (destPrefix string, err error) {
|
|
if val, err := cache2go.GetXCached(cd.GetKey() + cd.Destination); err == nil {
|
|
xaps := val.(xCachedActivationPeriods)
|
|
cd.ActivationPeriods = xaps.aps
|
|
return xaps.destPrefix, nil
|
|
}
|
|
destPrefix, values, err := cd.getActivationPeriodsForPrefix(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)
|
|
}
|
|
//load the activation preriods
|
|
if err == nil && len(values) > 0 {
|
|
xaps := xCachedActivationPeriods{destPrefix, values, new(cache2go.XEntry)}
|
|
xaps.XCache(cd.GetKey()+cd.Destination, debitPeriod+5*time.Second, xaps)
|
|
cd.ActivationPeriods = values
|
|
}
|
|
return
|
|
}
|
|
|
|
func (cd *CallDescriptor) getActivationPeriodsForPrefix(key string, recursionDepth int) (foundPrefix string, aps []*ActivationPeriod, err error) {
|
|
if recursionDepth > RECURSION_MAX_DEPTH {
|
|
err = errors.New("Max fallback recursion depth reached!" + key)
|
|
return
|
|
}
|
|
rp, err := storageGetter.GetRatingProfile(key)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
foundPrefix, aps, err = rp.GetActivationPeriodsForPrefix(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 {
|
|
return destPrefix, values, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
Constructs the key for the storage lookup.
|
|
The prefixLen is limiting the length of the destination prefix.
|
|
*/
|
|
func (cd *CallDescriptor) GetKey() string {
|
|
return fmt.Sprintf("%s:%s:%s:%s", cd.Direction, cd.Tenant, cd.TOR, cd.Subject)
|
|
}
|
|
|
|
/*
|
|
Splits the received timespan into sub time spans according to the activation periods intervals.
|
|
*/
|
|
func (cd *CallDescriptor) splitInTimeSpans(firstSpan *TimeSpan) (timespans []*TimeSpan) {
|
|
if firstSpan == nil {
|
|
firstSpan = &TimeSpan{TimeStart: cd.TimeStart, TimeEnd: cd.TimeEnd}
|
|
}
|
|
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 {
|
|
return
|
|
}
|
|
firstSpan.ActivationPeriod = cd.ActivationPeriods[0]
|
|
|
|
// split on activation periods
|
|
afterStart, afterEnd := false, false //optimization for multiple activation periods
|
|
for _, ap := range cd.ActivationPeriods {
|
|
if !afterStart && !afterEnd && ap.ActivationTime.Before(cd.TimeStart) {
|
|
firstSpan.ActivationPeriod = ap
|
|
} else {
|
|
afterStart = true
|
|
for i := 0; i < len(timespans); i++ {
|
|
if timespans[i].MinuteInfo != nil {
|
|
continue
|
|
}
|
|
newTs := timespans[i].SplitByActivationPeriod(ap)
|
|
if newTs != nil {
|
|
timespans = append(timespans, newTs)
|
|
} else {
|
|
afterEnd = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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 {
|
|
continue // if the timespan has an interval than it already has a heigher weight
|
|
}
|
|
newTs := timespans[i].SplitByInterval(interval)
|
|
if newTs != nil {
|
|
newTs.ActivationPeriod = ap
|
|
timespans = append(timespans, newTs)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
/*
|
|
Creates a CallCost structure with the cost information calculated for the received CallDescriptor.
|
|
*/
|
|
func (cd *CallDescriptor) GetCost() (*CallCost, error) {
|
|
destPrefix, err := cd.LoadActivationPeriods()
|
|
if err != nil {
|
|
Logger.Err(fmt.Sprintf("error getting cost for key %v: %v", cd.GetUserBalanceKey(), err))
|
|
return &CallCost{Cost: -1}, err
|
|
}
|
|
timespans := cd.splitInTimeSpans(nil)
|
|
cost := 0.0
|
|
connectionFee := 0.0
|
|
|
|
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
|
|
}
|
|
cost += ts.getCost(cd)
|
|
}
|
|
cost = utils.Round(cost, roundingDecimals, roundingMethod)
|
|
cc := &CallCost{
|
|
Direction: cd.Direction,
|
|
TOR: cd.TOR,
|
|
Tenant: cd.Tenant,
|
|
Subject: cd.Subject,
|
|
Account: cd.Account,
|
|
Destination: destPrefix,
|
|
Cost: cost,
|
|
ConnectFee: connectionFee,
|
|
Timespans: timespans}
|
|
Logger.Info(fmt.Sprintf("<Rater> Get Cost: %s => %v", cd.GetKey(), cc))
|
|
return cc, err
|
|
}
|
|
|
|
/*
|
|
Returns the approximate max allowed session for user balance. It will try the max amount received in the call descriptor
|
|
and will decrease it by 10% for nine times. So if the user has little credit it will still allow 10% of the initial amount.
|
|
If the user has no credit then it will return 0.
|
|
If the user has postpayied plan it returns -1.
|
|
*/
|
|
func (cd *CallDescriptor) GetMaxSessionTime() (seconds float64, err error) {
|
|
_, err = cd.LoadActivationPeriods()
|
|
if err != nil {
|
|
Logger.Err(fmt.Sprintf("error getting cost for key %v: %v", cd.GetUserBalanceKey(), err))
|
|
return 0, err
|
|
}
|
|
now := time.Now()
|
|
availableCredit, availableSeconds := 0.0, 0.0
|
|
Logger.Debug(fmt.Sprintf("cd: %+v", cd))
|
|
if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil {
|
|
if userBalance.Type == UB_TYPE_POSTPAID {
|
|
return -1, nil
|
|
} else {
|
|
availableSeconds, availableCredit, _ = userBalance.getSecondsForPrefix(cd.Destination)
|
|
Logger.Debug(fmt.Sprintf("available sec: %v credit: %v", availableSeconds, availableCredit))
|
|
}
|
|
} else {
|
|
Logger.Err(fmt.Sprintf("Could not get user balance for %s.", cd.GetUserBalanceKey()))
|
|
return cd.Amount, err
|
|
}
|
|
// check for zero balance
|
|
if availableCredit == 0 {
|
|
return availableSeconds, nil
|
|
}
|
|
// the price of a seccond cannot be determined because all the seconds can have a different cost.
|
|
// therfore we get the cost for the whole period and then if there are not enough money we backout in steps of 10%.
|
|
maxSessionSeconds := cd.Amount
|
|
for i := 0; i < 10; i++ {
|
|
maxDuration, _ := time.ParseDuration(fmt.Sprintf("%vs", maxSessionSeconds-availableSeconds))
|
|
ts := &TimeSpan{TimeStart: now, TimeEnd: now.Add(maxDuration)}
|
|
timespans := cd.splitInTimeSpans(ts)
|
|
|
|
cost := 0.0
|
|
for i, ts := range timespans {
|
|
if i == 0 && ts.MinuteInfo == nil && ts.Interval != nil {
|
|
cost += ts.Interval.ConnectFee
|
|
}
|
|
cost += ts.getCost(cd)
|
|
}
|
|
//logger.Print(availableCredit, availableSeconds, cost)
|
|
if cost < availableCredit {
|
|
return maxSessionSeconds, nil
|
|
} else { //decrease the period by 10% and try again
|
|
maxSessionSeconds -= cd.Amount * 0.1
|
|
}
|
|
}
|
|
Logger.Debug("Even 10% of the max session time is too much!")
|
|
return 0, nil
|
|
}
|
|
|
|
// Interface method used to add/substract an amount of cents or bonus seconds (as returned by GetCost method)
|
|
// from user's money balance.
|
|
func (cd *CallDescriptor) Debit() (cc *CallCost, err error) {
|
|
cc, err = cd.GetCost()
|
|
if err != nil {
|
|
Logger.Err(fmt.Sprintf("<Rater> Error getting cost for account key %v: %v", cd.GetUserBalanceKey(), err))
|
|
return
|
|
}
|
|
if userBalance, err := cd.getUserBalance(); err != nil {
|
|
Logger.Err(fmt.Sprintf("<Rater> Error retrieving user balance: %v", err))
|
|
} else if userBalance == nil {
|
|
Logger.Debug(fmt.Sprintf("<Rater> No user balance defined: %v", cd.GetUserBalanceKey()))
|
|
} else {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Interface method used to add/substract an amount of cents or bonus seconds (as returned by GetCost method)
|
|
// from user's money balance.
|
|
// This methods combines the Debit and GetMaxSessionTime and will debit the max available time as returned
|
|
// by the GetMaxSessionTime method. The amount filed has to be filled in call descriptor.
|
|
func (cd *CallDescriptor) MaxDebit() (cc *CallCost, err error) {
|
|
remainingSeconds, err := cd.GetMaxSessionTime()
|
|
Logger.Debug(fmt.Sprintf("In MaxDebitd remaining seconds: %v", remainingSeconds))
|
|
if err != nil || remainingSeconds == 0 {
|
|
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)
|
|
}
|
|
return cd.Debit()
|
|
}
|
|
|
|
/*
|
|
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.
|
|
*/
|
|
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 0.0, err
|
|
}
|
|
|
|
/*
|
|
Interface method used to add/substract an amount of units from user's sms balance.
|
|
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 0, err
|
|
}
|
|
|
|
/*
|
|
Interface method used to add/substract an amount of seconds from user's minutes balance.
|
|
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 err
|
|
}
|
|
|
|
/*
|
|
Adds the specified amount of seconds to the received call seconds. When the threshold specified
|
|
in the user's tariff plan is reached then the received call balance is reseted and the bonus
|
|
specified in the tariff plan is applied.
|
|
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},
|
|
}
|
|
userBalance.countUnits(a)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Cleans all chached data
|
|
func (cd *CallDescriptor) FlushCache() (err error) {
|
|
cache2go.XFlush()
|
|
cache2go.Flush()
|
|
return nil
|
|
|
|
}
|