Files
cgrates/timespans/calldesc.go
2012-07-09 19:30:45 +03:00

386 lines
12 KiB
Go

/*
Rating system designed to be used in VoIP Carriers World
Copyright (C) 2012 Radu Ioan Fericean
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 timespans
import (
"errors"
"fmt"
"log"
"math"
"time"
)
const (
// the minimum length for a destination prefix to be matched.
MinPrefixLength = 2
RecursionMaxDepth = 4
FallbackDestination = "fallback" // the string to be used to mark the fallback destination
)
/*
Utility function for rounding a float to a certain number of decimals (not present in math).
*/
func round(val float64, prec int) float64 {
var rounder float64
intermed := val * math.Pow(10, float64(prec))
if val >= 0.5 {
rounder = math.Ceil(intermed)
} else {
rounder = math.Floor(intermed)
}
return rounder / math.Pow(10, float64(prec))
}
/*
The input stucture that contains call information.
*/
type CallDescriptor struct {
Direction string
TOR string
Tenant, Subject, Destination string
TimeStart, TimeEnd time.Time
Amount float64
FallbackSubject string // the subject to check for destination if not found on primary subject
ActivationPeriods []*ActivationPeriod
FallbackKey string
userBalance *UserBalance
}
// Adds an activation period that applyes to current call descriptor.
func (cd *CallDescriptor) AddActivationPeriod(aps ...*ActivationPeriod) {
cd.ActivationPeriods = append(cd.ActivationPeriods, aps...)
}
// Adds an activation period that applyes to current call descriptor if not already present.
func (cd *CallDescriptor) AddActivationPeriodIfNotPresent(aps ...*ActivationPeriod) {
for _, ap := range aps {
found := false
for _, eap := range cd.ActivationPeriods {
if ap.Equal(eap) {
found = true
break
}
}
if !found {
cd.ActivationPeriods = append(cd.ActivationPeriods, ap)
}
}
}
/*
Gets and caches the user balance information.
*/
func (cd *CallDescriptor) getUserBalance() (ub *UserBalance, err error) {
if cd.userBalance == nil {
key := fmt.Sprintf("%s:%s:%s", cd.Direction, cd.Tenant, cd.Subject)
cd.userBalance, err = storageGetter.GetUserBalance(key)
}
return cd.userBalance, err
}
/*
Exported method to set the storage getter.
*/
func SetStorageGetter(sg StorageGetter) {
storageGetter = sg
}
/*
Restores the activation periods for the specified prefix from storage.
*/
func (cd *CallDescriptor) SearchStorageForPrefix() (destPrefix string, err error) {
cd.ActivationPeriods = make([]*ActivationPeriod, 0)
base := fmt.Sprintf("%s:%s:%s:%s:", cd.Direction, cd.Tenant, cd.TOR, cd.Subject)
destPrefix = cd.Destination
key := base + destPrefix
values, err := cd.getActivationPeriodsOrFallback(key, base, destPrefix, 1)
if err != nil {
key := base + FallbackDestination
values, err = cd.getActivationPeriodsOrFallback(key, base, destPrefix, 1)
}
//load the activation preriods
if err == nil && len(values) > 0 {
cd.ActivationPeriods = values
}
return
}
func (cd *CallDescriptor) getActivationPeriodsOrFallback(key, base, destPrefix string, recursionDepth int) (values []*ActivationPeriod, err error) {
if recursionDepth > RecursionMaxDepth {
err = errors.New("Max fallback recursion depth reached!" + key)
return
}
values, fallbackKey, err := storageGetter.GetActivationPeriodsOrFallback(key)
if fallbackKey != "" {
base = fallbackKey + ":"
key = base + destPrefix
recursionDepth++
return cd.getActivationPeriodsOrFallback(key, base, destPrefix, recursionDepth)
}
//get for a smaller prefix if the orignal one was not found
for i := len(cd.Destination); err != nil || fallbackKey != ""; {
if fallbackKey != "" {
base = fallbackKey + ":"
key = base + destPrefix
recursionDepth++
return cd.getActivationPeriodsOrFallback(key, base, destPrefix, recursionDepth)
}
i--
if i >= MinPrefixLength {
destPrefix = cd.Destination[:i]
key = base + destPrefix
} else {
break
}
values, fallbackKey, err = storageGetter.GetActivationPeriodsOrFallback(key)
}
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:%s", cd.Direction, cd.Tenant, cd.TOR, cd.Subject, cd.Destination)
}
/*
Splits the call descriptor timespan into sub time spans according to the activation periods intervals.
*/
func (cd *CallDescriptor) splitInTimeSpans() (timespans []*TimeSpan) {
return cd.splitTimeSpan(&TimeSpan{TimeStart: cd.TimeStart, TimeEnd: cd.TimeEnd})
}
/*
Splits the received timespan into sub time spans according to the activation periods intervals.
*/
func (cd *CallDescriptor) splitTimeSpan(firstSpan *TimeSpan) (timespans []*TimeSpan) {
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
}
ap := timespans[i].ActivationPeriod
//timespans[i].ActivationPeriod = nil
for _, interval := range ap.Intervals {
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.SearchStorageForPrefix()
timespans := cd.splitInTimeSpans()
cost := 0.0
connectionFee := 0.0
for i, ts := range timespans {
if i == 0 && ts.MinuteInfo == nil && ts.Interval != nil {
connectionFee = ts.Interval.ConnectFee
}
cost += ts.getCost(cd)
}
cc := &CallCost{TOR: cd.TOR,
Tenant: cd.Tenant,
Subject: cd.Subject,
Destination: destPrefix,
Cost: cost,
ConnectFee: connectionFee,
Timespans: timespans}
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.
*/
func (cd *CallDescriptor) GetMaxSessionTime() (seconds float64, err error) {
_, err = cd.SearchStorageForPrefix()
now := time.Now()
availableCredit, availableSeconds := 0.0, 0.0
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)
}
} else {
return cd.Amount, err
}
// check for zero balance
if availableCredit == 0 {
return availableSeconds, nil
}
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.splitTimeSpan(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)
}
//log.Print(availableCredit, availableSeconds, cost)
if cost < availableCredit {
return maxSessionSeconds, nil
} else { //decrease the period by 10% and try again
maxSessionSeconds -= cd.Amount * 0.1
}
}
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 {
log.Printf("error getting cost %v", err)
}
if userBalance, err := cd.getUserBalance(); err == nil && userBalance != nil {
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 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{
MinuteBucket: &MinuteBucket{Seconds: cd.Amount, DestinationId: cd.Destination},
}
userBalance.countUnits(a)
return nil
}
return err
}