mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
It will be added to CallDescriptor in ExtraFields. Ensure CDR ExtraFields are passed to CallDescriptor before sending it to RALs. Ensure Clone function of CallDescriptor also clones the ExtraFields map.
1047 lines
33 KiB
Go
1047 lines
33 KiB
Go
/*
|
|
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
|
|
Copyright (C) ITsysCOM GmbH
|
|
|
|
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"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cgrates/cgrates/config"
|
|
"github.com/cgrates/cgrates/guardian"
|
|
"github.com/cgrates/cgrates/utils"
|
|
)
|
|
|
|
const (
|
|
// these might be better in the confs under optimizations section
|
|
MIN_PREFIX_MATCH = 1
|
|
FALLBACK_SUBJECT = utils.MetaAny
|
|
)
|
|
|
|
var (
|
|
globalRoundingDecimals = 6
|
|
rpSubjectPrefixMatching bool
|
|
rpSubjectPrefixMatchingMutex sync.RWMutex // used to reload rpSubjectPrefixMatching
|
|
)
|
|
|
|
// SetRoundingDecimals sets the global rounding method and decimal precision for GetCost method
|
|
func SetRoundingDecimals(rd int) {
|
|
globalRoundingDecimals = rd
|
|
}
|
|
|
|
// SetRpSubjectPrefixMatching sets rpSubjectPrefixMatching (is thread safe)
|
|
func SetRpSubjectPrefixMatching(flag bool) {
|
|
rpSubjectPrefixMatchingMutex.Lock()
|
|
rpSubjectPrefixMatching = flag
|
|
rpSubjectPrefixMatchingMutex.Unlock()
|
|
}
|
|
|
|
// getRpSubjectPrefixMatching returns rpSubjectPrefixMatching (is thread safe)
|
|
func getRpSubjectPrefixMatching() (flag bool) {
|
|
rpSubjectPrefixMatchingMutex.RLock()
|
|
flag = rpSubjectPrefixMatching
|
|
rpSubjectPrefixMatchingMutex.RUnlock()
|
|
return
|
|
}
|
|
|
|
// NewCallDescriptorFromCGREvent converts a CGREvent into CallDescriptor
|
|
func NewCallDescriptorFromCGREvent(cgrEv *utils.CGREvent,
|
|
timezone string) (cd *CallDescriptor, err error) {
|
|
cd = &CallDescriptor{Tenant: cgrEv.Tenant}
|
|
if _, has := cgrEv.Event[utils.Category]; has {
|
|
if cd.Category, err = cgrEv.FieldAsString(utils.Category); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if cd.Account, err = cgrEv.FieldAsString(utils.AccountField); err != nil {
|
|
return
|
|
}
|
|
if cd.Subject, err = cgrEv.FieldAsString(utils.Subject); err != nil {
|
|
if err != utils.ErrNotFound {
|
|
return
|
|
}
|
|
cd.Subject = cd.Account
|
|
}
|
|
if cd.Destination, err = cgrEv.FieldAsString(utils.Destination); err != nil {
|
|
return nil, err
|
|
}
|
|
if cd.TimeStart, err = cgrEv.FieldAsTime(utils.SetupTime,
|
|
timezone); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, has := cgrEv.Event[utils.AnswerTime]; has { // AnswerTime takes precendence for TimeStart
|
|
if aTime, err := cgrEv.FieldAsTime(utils.AnswerTime,
|
|
timezone); err != nil {
|
|
return nil, err
|
|
} else if !aTime.IsZero() {
|
|
cd.TimeStart = aTime
|
|
}
|
|
}
|
|
if usage, err := cgrEv.FieldAsDuration(utils.Usage); err != nil {
|
|
return nil, err
|
|
} else {
|
|
cd.TimeEnd = cd.TimeStart.Add(usage)
|
|
}
|
|
if _, has := cgrEv.Event[utils.ToR]; has {
|
|
if cd.ToR, err = cgrEv.FieldAsString(utils.ToR); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
/*
|
|
The input stucture that contains call information.
|
|
*/
|
|
type CallDescriptor struct {
|
|
Category string
|
|
Tenant string
|
|
Subject string
|
|
Account string
|
|
Destination string
|
|
TimeStart time.Time
|
|
TimeEnd time.Time
|
|
LoopIndex float64 // indicates the position of this segment in a cost request loop
|
|
DurationIndex time.Duration // the call duration so far (till TimeEnd)
|
|
FallbackSubject string // the subject to check for destination if not found on primary subject
|
|
RatingInfos RatingInfos
|
|
Increments Increments
|
|
ToR string // used unit balances selector
|
|
ExtraFields map[string]string // Extra fields, mostly used for user profile matching
|
|
// session limits
|
|
MaxRate float64
|
|
MaxRateUnit time.Duration
|
|
MaxCostSoFar float64
|
|
CgrID string
|
|
RunID string
|
|
ForceDuration bool // for Max debit if less than duration return err
|
|
PerformRounding bool // flag for rating info rounding
|
|
DenyNegativeAccount bool // prevent account going on negative during debit
|
|
account *Account
|
|
testCallcost *CallCost // testing purpose only!
|
|
DryRun bool
|
|
}
|
|
|
|
// AsCGREvent converts the CallDescriptor into CGREvent
|
|
func (cd *CallDescriptor) AsCGREvent(opts map[string]any) *utils.CGREvent {
|
|
cgrEv := &utils.CGREvent{
|
|
Tenant: cd.Tenant,
|
|
ID: utils.UUIDSha1Prefix(), // make it unique
|
|
Event: make(map[string]any),
|
|
APIOpts: opts,
|
|
}
|
|
for k, v := range cd.ExtraFields {
|
|
cgrEv.Event[k] = v
|
|
}
|
|
cgrEv.Event[utils.ToR] = cd.ToR
|
|
cgrEv.Event[utils.Tenant] = cd.Tenant
|
|
cgrEv.Event[utils.Category] = cd.Category
|
|
cgrEv.Event[utils.AccountField] = cd.Account
|
|
cgrEv.Event[utils.Subject] = cd.Subject
|
|
cgrEv.Event[utils.Destination] = cd.Destination
|
|
cgrEv.Event[utils.AnswerTime] = cd.TimeStart
|
|
cgrEv.Event[utils.Usage] = cd.TimeEnd.Sub(cd.TimeStart)
|
|
return cgrEv
|
|
}
|
|
|
|
// UpdateFromCGREvent will update CallDescriptor with fields from CGREvent
|
|
// cgrEv contains both fields and their values
|
|
// fields represent fields needing update
|
|
func (cd *CallDescriptor) UpdateFromCGREvent(cgrEv *utils.CGREvent, fields []string) (err error) {
|
|
for _, fldName := range fields {
|
|
switch fldName {
|
|
case utils.ToR:
|
|
if cd.ToR, err = cgrEv.FieldAsString(fldName); err != nil {
|
|
return
|
|
}
|
|
case utils.Tenant:
|
|
if cd.Tenant, err = cgrEv.FieldAsString(fldName); err != nil {
|
|
return
|
|
}
|
|
case utils.Category:
|
|
if cd.Category, err = cgrEv.FieldAsString(fldName); err != nil {
|
|
return
|
|
}
|
|
case utils.AccountField:
|
|
if cd.Account, err = cgrEv.FieldAsString(fldName); err != nil {
|
|
return
|
|
}
|
|
case utils.Subject:
|
|
if cd.Subject, err = cgrEv.FieldAsString(fldName); err != nil {
|
|
return
|
|
}
|
|
case utils.Destination:
|
|
if cd.Destination, err = cgrEv.FieldAsString(fldName); err != nil {
|
|
return
|
|
}
|
|
case utils.AnswerTime:
|
|
if cd.TimeStart, err = cgrEv.FieldAsTime(fldName,
|
|
config.CgrConfig().GeneralCfg().DefaultTimezone); err != nil {
|
|
return
|
|
}
|
|
case utils.Usage:
|
|
usage, err := cgrEv.FieldAsDuration(fldName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cd.TimeEnd = cd.TimeStart.Add(usage)
|
|
default:
|
|
fldVal, err := cgrEv.FieldAsString(fldName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cd.ExtraFields[fldName] = fldVal
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (cd *CallDescriptor) ValidateCallData() error {
|
|
if cd.TimeStart.After(cd.TimeEnd) || cd.TimeStart.Equal(cd.TimeEnd) {
|
|
return errors.New("TimeStart must be strctly before TimeEnd")
|
|
}
|
|
if cd.TimeEnd.Sub(cd.TimeStart) < cd.DurationIndex {
|
|
return errors.New("DurationIndex must be equal or greater than TimeEnd - TimeStart")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Adds a rating plan that applyes to current call descriptor.
|
|
func (cd *CallDescriptor) AddRatingInfo(ris ...*RatingInfo) {
|
|
cd.RatingInfos = append(cd.RatingInfos, ris...)
|
|
}
|
|
|
|
// Gets and caches the user balance information.
|
|
func (cd *CallDescriptor) getAccount() (ub *Account, err error) {
|
|
if cd.account == nil {
|
|
cd.account, err = dm.GetAccount(cd.GetAccountKey())
|
|
}
|
|
if cd.account != nil && cd.account.Disabled {
|
|
return nil, utils.ErrAccountDisabled
|
|
}
|
|
if err != nil || cd.account == nil {
|
|
utils.Logger.Warning(fmt.Sprintf("Account: %s, not found (%v)", cd.GetAccountKey(), err))
|
|
return nil, utils.ErrAccountNotFound
|
|
}
|
|
return cd.account, err
|
|
}
|
|
|
|
/*
|
|
Restores the activation periods for the specified prefix from storage.
|
|
*/
|
|
func (cd *CallDescriptor) LoadRatingPlans() (err error) {
|
|
var rec int
|
|
rec, err = cd.getRatingPlansForPrefix(cd.GetKey(cd.Subject), 1)
|
|
if err == utils.ErrNotFound && rec == 1 {
|
|
//if err != nil || !cd.continousRatingInfos() {
|
|
// use the default subject only if the initial one was not found
|
|
_, err = cd.getRatingPlansForPrefix(cd.GetKey(FALLBACK_SUBJECT), 1)
|
|
}
|
|
//load the rating plans
|
|
if err != nil {
|
|
utils.Logger.Err(fmt.Sprintf("Rating plan not found for destination %s and account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject)))
|
|
return utils.ErrRatingPlanNotFound
|
|
|
|
}
|
|
if !cd.continousRatingInfos() {
|
|
utils.Logger.Err(fmt.Sprintf("Destination %s not authorized for account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject)))
|
|
return utils.ErrUnauthorizedDestination
|
|
}
|
|
return
|
|
}
|
|
|
|
// FIXME: this method is not exhaustive but will cover 99% of cases just good
|
|
// it will not cover very long calls with very short activation periods for rates
|
|
func (cd *CallDescriptor) getRatingPlansForPrefix(key string, recursionDepth int) (int, error) {
|
|
if recursionDepth > config.CgrConfig().RalsCfg().FallbackDepth {
|
|
return recursionDepth, utils.ErrMaxRecursionDepth
|
|
}
|
|
rpf, err := RatingProfileSubjectPrefixMatching(key)
|
|
if err != nil || rpf == nil {
|
|
return recursionDepth, utils.ErrNotFound
|
|
}
|
|
if err = rpf.GetRatingPlansForPrefix(cd); err != nil || !cd.continousRatingInfos() {
|
|
// try rating profile fallback
|
|
recursionDepth++
|
|
for index := 0; index < len(cd.RatingInfos); index++ {
|
|
ri := cd.RatingInfos[index]
|
|
if len(ri.RateIntervals) > 0 {
|
|
// go to next rate info
|
|
continue
|
|
}
|
|
if len(ri.FallbackKeys) > 0 {
|
|
tempCD := &CallDescriptor{
|
|
Category: cd.Category,
|
|
Tenant: cd.Tenant,
|
|
Destination: cd.Destination,
|
|
}
|
|
if index == 0 {
|
|
tempCD.TimeStart = cd.TimeStart
|
|
} else {
|
|
tempCD.TimeStart = ri.ActivationTime
|
|
}
|
|
if index == len(cd.RatingInfos)-1 {
|
|
tempCD.TimeEnd = cd.TimeEnd
|
|
} else {
|
|
tempCD.TimeEnd = cd.RatingInfos[index+1].ActivationTime
|
|
}
|
|
for _, fbk := range ri.FallbackKeys {
|
|
if _, err := tempCD.getRatingPlansForPrefix(fbk, recursionDepth); err != nil {
|
|
continue
|
|
}
|
|
// extract the rate infos and break
|
|
for newIndex, newRI := range tempCD.RatingInfos {
|
|
// check if the new ri is filled
|
|
if len(newRI.RateIntervals) == 0 {
|
|
continue
|
|
}
|
|
if newIndex == 0 {
|
|
cd.RatingInfos[index] = newRI
|
|
} else {
|
|
// insert extra data
|
|
i := index + newIndex
|
|
cd.RatingInfos = append(cd.RatingInfos, nil)
|
|
copy(cd.RatingInfos[i+1:], cd.RatingInfos[i:])
|
|
cd.RatingInfos[i] = newRI
|
|
}
|
|
}
|
|
// if this fallbackey covered the interval than skip
|
|
// the other fallback keys
|
|
if tempCD.continousRatingInfos() {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return recursionDepth, nil
|
|
}
|
|
|
|
// checks if there is rating info for the entire call duration
|
|
func (cd *CallDescriptor) continousRatingInfos() bool {
|
|
if len(cd.RatingInfos) == 0 || cd.RatingInfos[0].ActivationTime.After(cd.TimeStart) {
|
|
return false
|
|
}
|
|
for _, ri := range cd.RatingInfos {
|
|
if ri.RateIntervals == nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// adds a rating infos only if that call period is not already covered
|
|
// returns true if added
|
|
func (cd *CallDescriptor) addRatingInfos(ris RatingInfos) bool {
|
|
if len(cd.RatingInfos) == 0 {
|
|
cd.RatingInfos = append(cd.RatingInfos, ris...)
|
|
return true
|
|
}
|
|
cd.RatingInfos.Sort()
|
|
// check if we dont have the start covered
|
|
if cd.RatingInfos[0].ActivationTime.After(cd.TimeStart) {
|
|
if ris[0].ActivationTime.Before(cd.RatingInfos[0].ActivationTime) {
|
|
cd.RatingInfos = append(cd.RatingInfos, ris[0])
|
|
cd.RatingInfos.Sort()
|
|
}
|
|
}
|
|
for _, ri := range cd.RatingInfos {
|
|
if ri.RateIntervals == nil {
|
|
for i, newRi := range ris {
|
|
_ = i
|
|
_ = newRi
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// GetKey constructs the key for the storage lookup.
|
|
// The prefixLen is limiting the length of the destination prefix.
|
|
func (cd *CallDescriptor) GetKey(subject string) string {
|
|
return utils.ConcatenatedKey(utils.MetaOut, cd.Tenant, cd.Category, subject)
|
|
}
|
|
|
|
// GetAccountKey returns the key used to retrive the user balance involved in this call
|
|
func (cd *CallDescriptor) GetAccountKey() string {
|
|
subj := cd.Subject
|
|
if cd.Account != "" {
|
|
subj = cd.Account
|
|
}
|
|
return utils.ConcatenatedKey(cd.Tenant, subj)
|
|
}
|
|
|
|
// Splits the received timespan into sub time spans according to the activation periods intervals.
|
|
func (cd *CallDescriptor) splitInTimeSpans() (timespans []*TimeSpan) {
|
|
firstSpan := &TimeSpan{TimeStart: cd.TimeStart, TimeEnd: cd.TimeEnd,
|
|
DurationIndex: cd.DurationIndex}
|
|
|
|
timespans = append(timespans, firstSpan)
|
|
if len(cd.RatingInfos) == 0 {
|
|
return
|
|
}
|
|
firstSpan.setRatingInfo(cd.RatingInfos[0])
|
|
if cd.ToR == utils.MetaVoice {
|
|
// split on rating plans
|
|
afterStart, afterEnd := false, false //optimization for multiple activation periods
|
|
for _, rp := range cd.RatingInfos {
|
|
//log.Print("RP: ", utils.ToJSON(rp))
|
|
if !afterStart && !afterEnd && rp.ActivationTime.Before(cd.TimeStart) {
|
|
firstSpan.setRatingInfo(rp)
|
|
} else {
|
|
afterStart = true
|
|
for i := 0; i < len(timespans); i++ {
|
|
newTs := timespans[i].SplitByRatingPlan(rp)
|
|
if newTs != nil {
|
|
timespans = append(timespans, newTs)
|
|
} else {
|
|
afterEnd = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//log.Printf("After SplitByRatingPlan: %+v", utils.ToJSON(timespans))
|
|
// split on days
|
|
|
|
for i := 0; i < len(timespans); i++ {
|
|
rp := timespans[i].ratingInfo
|
|
newTs := timespans[i].SplitByDay()
|
|
if newTs != nil {
|
|
//log.Print("NEW TS: ", newTs.TimeStart, newTs.TimeEnd)
|
|
newTs.setRatingInfo(rp)
|
|
// insert the new timespan
|
|
index := i + 1
|
|
timespans = append(timespans, nil)
|
|
copy(timespans[index+1:], timespans[index:])
|
|
timespans[index] = newTs
|
|
}
|
|
}
|
|
}
|
|
//log.Printf("After SplitByDay: %+v", utils.ToJSON(timespans))
|
|
// split on rate intervals
|
|
|
|
for i := 0; i < len(timespans); i++ {
|
|
//log.Printf("==============%v==================", i)
|
|
//log.Printf("TS: %+v", timespans[i])
|
|
rp := timespans[i].ratingInfo
|
|
//timespans[i].RatingPlan = nil
|
|
rateIntervals := rp.SelectRatingIntevalsForTimespan(timespans[i])
|
|
//log.Print("RIs: ", utils.ToJSON(rateIntervals))
|
|
/*for _, interval := range rp.RateIntervals {
|
|
if !timespans[i].hasBetterRateIntervalThan(interval) {
|
|
timespans[i].SetRateInterval(interval)
|
|
}
|
|
}*/
|
|
//log.Print("ORIG TS: ", timespans[i].TimeStart, timespans[i].TimeEnd)
|
|
//log.Print(timespans[i].RateInterval)
|
|
for _, interval := range rateIntervals {
|
|
//log.Printf("\tINTERVAL: %+v", interval.Timing)
|
|
newTs := timespans[i].SplitByRateInterval(interval, cd.ToR != utils.MetaVoice)
|
|
//utils.PrintFull(timespans[i])
|
|
//utils.PrintFull(newTs)
|
|
if newTs != nil {
|
|
//log.Print("NEW TS: ", newTs.TimeStart, newTs.TimeEnd)
|
|
newTs.setRatingInfo(rp)
|
|
// insert the new timespan
|
|
index := i + 1
|
|
timespans = append(timespans, nil)
|
|
copy(timespans[index+1:], timespans[index:])
|
|
timespans[index] = newTs
|
|
if timespans[i].RateInterval != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
//log.Print("TS: ", timespans[i].TimeStart, timespans[i].TimeEnd)
|
|
//log.Print(timespans[i].RateInterval.Timing)
|
|
}
|
|
|
|
//log.Printf("After SplitByRateInterval: %+v", timespans[0].RateInterval.Timing)
|
|
timespans = cd.roundTimeSpansToIncrement(timespans)
|
|
//log.Printf("After round: %+v", timespans[0].RateInterval.Timing)
|
|
return
|
|
}
|
|
|
|
// if the rate interval for any timespan has a RatingIncrement larger than the timespan duration
|
|
// the timespan must expand potentially overlaping folowing timespans and may exceed call
|
|
// descriptor's initial duration
|
|
func (cd *CallDescriptor) roundTimeSpansToIncrement(timespans TimeSpans) []*TimeSpan {
|
|
for i := 0; i < len(timespans); i++ {
|
|
ts := timespans[i]
|
|
if ts.RateInterval != nil {
|
|
_, rateIncrement, _ := ts.RateInterval.GetRateParameters(ts.GetGroupStart())
|
|
// if the timespan duration is larger than the rate increment make sure it is a multiple of it
|
|
if rateIncrement < ts.GetDuration() {
|
|
rateIncrement = utils.RoundDuration(rateIncrement, ts.GetDuration())
|
|
}
|
|
if rateIncrement > ts.GetDuration() {
|
|
initialDuration := ts.GetDuration()
|
|
ts.TimeEnd = ts.TimeStart.Add(rateIncrement)
|
|
ts.DurationIndex = ts.DurationIndex + (rateIncrement - initialDuration)
|
|
timespans.RemoveOverlapedFromIndex(i)
|
|
}
|
|
}
|
|
}
|
|
|
|
return timespans
|
|
}
|
|
|
|
// Returns call descripor's total duration
|
|
func (cd *CallDescriptor) GetDuration() time.Duration {
|
|
return cd.TimeEnd.Sub(cd.TimeStart)
|
|
}
|
|
|
|
/*
|
|
Creates a CallCost structure with the cost information calculated for the received CallDescriptor.
|
|
*/
|
|
func (cd *CallDescriptor) GetCost() (*CallCost, error) {
|
|
cd.account = nil // make sure it's not cached
|
|
cc, err := cd.getCost()
|
|
if err != nil || cd.GetDuration() == 0 {
|
|
return cc, err
|
|
}
|
|
cost := 0.0
|
|
for i, ts := range cc.Timespans {
|
|
// only add connect fee if this is the first/only call cost request
|
|
if cd.LoopIndex == 0 && i == 0 && ts.RateInterval != nil {
|
|
//Add the ConnectFee increment at the beggining
|
|
ts.Increments = append(Increments{&Increment{
|
|
Duration: 0,
|
|
Cost: ts.RateInterval.Rating.ConnectFee,
|
|
CompressFactor: 1,
|
|
BalanceInfo: &DebitInfo{
|
|
Monetary: nil,
|
|
Unit: nil,
|
|
AccountID: "",
|
|
},
|
|
}}, ts.Increments...)
|
|
//Add the cost from ConnectFee to TimeSpan
|
|
ts.Cost = ts.Cost + ts.RateInterval.Rating.ConnectFee
|
|
}
|
|
// handle max cost
|
|
maxCost, strategy := ts.RateInterval.GetMaxCost()
|
|
|
|
ts.Cost = ts.CalculateCost()
|
|
cost += ts.Cost
|
|
cd.MaxCostSoFar += cost
|
|
|
|
if strategy != "" && maxCost > 0 {
|
|
//log.Print("HERE: ", strategy, maxCost)
|
|
if strategy == utils.MetaMaxCostFree && cd.MaxCostSoFar >= maxCost {
|
|
cost = maxCost
|
|
cd.MaxCostSoFar = maxCost
|
|
}
|
|
}
|
|
}
|
|
cc.Cost = cost
|
|
|
|
// global rounding
|
|
roundingDecimals, roundingMethod := cc.GetLongestRounding()
|
|
cc.Cost = utils.Round(cc.Cost, roundingDecimals, roundingMethod)
|
|
return cc, nil
|
|
}
|
|
|
|
func (cd *CallDescriptor) getCost() (*CallCost, error) {
|
|
// check for 0 duration
|
|
if cd.GetDuration() == 0 {
|
|
cc := cd.CreateCallCost()
|
|
// add RatingInfo
|
|
err := cd.LoadRatingPlans()
|
|
if err == nil && len(cd.RatingInfos) > 0 {
|
|
ts := &TimeSpan{
|
|
TimeStart: cd.TimeStart,
|
|
TimeEnd: cd.TimeEnd,
|
|
}
|
|
ts.setRatingInfo(cd.RatingInfos[0])
|
|
cc.Timespans = append(cc.Timespans, ts)
|
|
}
|
|
return cc, nil
|
|
}
|
|
if cd.DurationIndex < cd.TimeEnd.Sub(cd.TimeStart) {
|
|
cd.DurationIndex = cd.TimeEnd.Sub(cd.TimeStart)
|
|
}
|
|
if cd.ToR == "" {
|
|
cd.ToR = utils.MetaVoice
|
|
}
|
|
err := cd.LoadRatingPlans()
|
|
if err != nil {
|
|
return &CallCost{Cost: -1}, err
|
|
}
|
|
timespans := cd.splitInTimeSpans()
|
|
cost := 0.0
|
|
|
|
for i, ts := range timespans {
|
|
ts.createIncrementsSlice()
|
|
// only add connect fee if this is the first/only call cost request
|
|
if cd.LoopIndex == 0 && i == 0 && ts.RateInterval != nil {
|
|
cost += ts.RateInterval.Rating.ConnectFee
|
|
}
|
|
cost += ts.CalculateCost()
|
|
}
|
|
|
|
cc := cd.CreateCallCost()
|
|
cc.Cost = cost
|
|
cc.Timespans = timespans
|
|
|
|
// global rounding
|
|
roundingDecimals, roundingMethod := cc.GetLongestRounding()
|
|
cc.Cost = utils.Round(cc.Cost, roundingDecimals, roundingMethod)
|
|
cc.Timespans.Compress()
|
|
cc.UpdateRatedUsage()
|
|
return cc, err
|
|
}
|
|
|
|
/*
|
|
Returns the approximate max allowed session for user balance. It will try the max amount received in the call descriptor
|
|
If the user has no credit then it will return 0.
|
|
If the user has postpayed plan it returns -1.
|
|
*/
|
|
func (origCD *CallDescriptor) getMaxSessionDuration(origAcc *Account, fltrS *FilterS) (time.Duration, error) {
|
|
// clone the account for discarding chenges on debit dry run
|
|
account := origAcc.Clone()
|
|
if account.AllowNegative {
|
|
return -1, nil
|
|
}
|
|
// for zero duration index
|
|
if origCD.DurationIndex < origCD.TimeEnd.Sub(origCD.TimeStart) {
|
|
origCD.DurationIndex = origCD.TimeEnd.Sub(origCD.TimeStart)
|
|
}
|
|
if origCD.ToR == "" {
|
|
origCD.ToR = utils.MetaVoice
|
|
}
|
|
cd := origCD.Clone()
|
|
initialDuration := cd.TimeEnd.Sub(cd.TimeStart)
|
|
defaultBalance := account.GetDefaultMoneyBalance()
|
|
|
|
//use this to check what increment was payed with debt
|
|
initialDefaultBalanceValue := defaultBalance.GetValue()
|
|
|
|
cc, err := cd.debit(account, true, false, fltrS)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// not enough credit for connect fee
|
|
if cc.negativeConnectFee {
|
|
return 0, nil
|
|
}
|
|
|
|
var totalCost float64
|
|
var totalDuration time.Duration
|
|
cc.Timespans.Decompress()
|
|
for _, ts := range cc.Timespans {
|
|
if cd.MaxRate > 0 && cd.MaxRateUnit > 0 {
|
|
rate, _, rateUnit := ts.RateInterval.GetRateParameters(ts.GetGroupStart())
|
|
if rate/float64(rateUnit.Nanoseconds()) > cd.MaxRate/float64(cd.MaxRateUnit.Nanoseconds()) {
|
|
return utils.MinDuration(initialDuration, totalDuration), nil
|
|
}
|
|
}
|
|
if ts.Increments == nil {
|
|
ts.createIncrementsSlice()
|
|
}
|
|
for _, incr := range ts.Increments {
|
|
totalCost += incr.Cost
|
|
if incr.BalanceInfo.Monetary != nil && incr.BalanceInfo.Monetary.UUID == defaultBalance.Uuid {
|
|
initialDefaultBalanceValue -= incr.Cost
|
|
if initialDefaultBalanceValue < 0 {
|
|
// this increment was payed with debt
|
|
// TODO: improve this check
|
|
return utils.MinDuration(initialDuration, totalDuration), nil
|
|
|
|
}
|
|
}
|
|
totalDuration += incr.Duration
|
|
if totalDuration >= initialDuration {
|
|
// we have enough, return
|
|
return initialDuration, nil
|
|
}
|
|
}
|
|
}
|
|
return utils.MinDuration(initialDuration, totalDuration), nil
|
|
}
|
|
|
|
func (cd *CallDescriptor) GetMaxSessionDuration(fltrS *FilterS) (duration time.Duration, err error) {
|
|
cd.account = nil // make sure it's not cached
|
|
err = guardian.Guardian.Guard(func() (_ error) {
|
|
account, err := cd.getAccount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
acntIDs, err := account.GetUniqueSharedGroupMembers(cd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var lkIDs []string
|
|
for acntID := range acntIDs {
|
|
if acntID != cd.GetAccountKey() {
|
|
lkIDs = append(lkIDs, utils.AccountPrefix+acntID)
|
|
}
|
|
}
|
|
return guardian.Guardian.Guard(func() error {
|
|
duration, err = cd.getMaxSessionDuration(account, fltrS)
|
|
return err
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, lkIDs...)
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, utils.AccountPrefix+cd.GetAccountKey())
|
|
return
|
|
}
|
|
|
|
// 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(account *Account, dryRun bool, goNegative bool, fltrS *FilterS) (cc *CallCost, err error) {
|
|
if cd.GetDuration() == 0 {
|
|
cc = cd.CreateCallCost()
|
|
// add RatingInfo
|
|
err := cd.LoadRatingPlans()
|
|
if err == nil && len(cd.RatingInfos) > 0 {
|
|
ts := &TimeSpan{
|
|
TimeStart: cd.TimeStart,
|
|
TimeEnd: cd.TimeEnd,
|
|
}
|
|
ts.setRatingInfo(cd.RatingInfos[0])
|
|
cc.Timespans = append(cc.Timespans, ts)
|
|
}
|
|
return cc, nil
|
|
}
|
|
if cd.ToR == "" {
|
|
cd.ToR = utils.MetaVoice
|
|
}
|
|
//log.Printf("Debit CD: %+v", cd)
|
|
cc, err = account.debitCreditBalance(cd, !dryRun, dryRun, goNegative, fltrS)
|
|
//log.Printf("HERE: %+v %v", cc, err)
|
|
if err != nil {
|
|
utils.Logger.Err(fmt.Sprintf("<Rater> Error getting cost for account key <%s>: %s", cd.GetAccountKey(), err.Error()))
|
|
return nil, err
|
|
}
|
|
cc.updateCost()
|
|
cc.UpdateRatedUsage()
|
|
cc.Timespans.Compress()
|
|
if !dryRun {
|
|
dm.SetAccount(account)
|
|
}
|
|
if cd.PerformRounding {
|
|
cc.Round()
|
|
roundIncrements := cc.GetRoundIncrements()
|
|
if len(roundIncrements) != 0 {
|
|
rcd := cc.CreateCallDescriptor()
|
|
rcd.Increments = roundIncrements
|
|
rcd.refundRounding(cd.account, fltrS)
|
|
}
|
|
}
|
|
//log.Printf("OUT CC: ", cc)
|
|
return
|
|
}
|
|
|
|
func (cd *CallDescriptor) Debit(fltrS *FilterS) (cc *CallCost, err error) {
|
|
cd.account = nil // make sure it's not cached
|
|
err = guardian.Guardian.Guard(func() (_ error) {
|
|
// lock all group members
|
|
account, err := cd.getAccount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
initialAcnt := account.AsAccountSummary()
|
|
acntIDs, sgerr := account.GetUniqueSharedGroupMembers(cd)
|
|
if sgerr != nil {
|
|
return sgerr
|
|
}
|
|
var lkIDs []string
|
|
for acntID := range acntIDs {
|
|
if acntID != cd.GetAccountKey() {
|
|
lkIDs = append(lkIDs, utils.AccountPrefix+acntID)
|
|
}
|
|
}
|
|
return guardian.Guardian.Guard(func() (err error) {
|
|
cc, err = cd.debit(account, cd.DryRun, !cd.DenyNegativeAccount, fltrS)
|
|
if err == nil {
|
|
cc.AccountSummary = cd.AccountSummary(initialAcnt)
|
|
}
|
|
return err
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, lkIDs...)
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, utils.AccountPrefix+cd.GetAccountKey())
|
|
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 GetMaxSessionDuration and will debit the max available time as returned
|
|
// by the GetMaxSessionDuration method. The amount filed has to be filled in call descriptor.
|
|
func (cd *CallDescriptor) MaxDebit(fltrS *FilterS) (cc *CallCost, err error) {
|
|
cd.account = nil // make sure it's not cached
|
|
err = guardian.Guardian.Guard(func() (err error) {
|
|
account, err := cd.getAccount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
initialAcnt := account.AsAccountSummary()
|
|
acntIDs, err := account.GetUniqueSharedGroupMembers(cd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var lkIDs []string
|
|
for acntID := range acntIDs {
|
|
if acntID != cd.GetAccountKey() {
|
|
lkIDs = append(lkIDs, utils.AccountPrefix+acntID)
|
|
}
|
|
}
|
|
return guardian.Guardian.Guard(func() (err error) {
|
|
remainingDuration, err := cd.getMaxSessionDuration(account, fltrS)
|
|
if err != nil && cd.GetDuration() > 0 {
|
|
return err
|
|
}
|
|
// check ForceDuartion
|
|
if cd.ForceDuration && !account.AllowNegative && remainingDuration < cd.GetDuration() {
|
|
return utils.ErrInsufficientCredit
|
|
}
|
|
if err != nil || remainingDuration == 0 {
|
|
cc = cd.CreateCallCost()
|
|
cc.AccountSummary = cd.AccountSummary(initialAcnt)
|
|
if cd.GetDuration() == 0 {
|
|
// add RatingInfo
|
|
err = cd.LoadRatingPlans()
|
|
if err == nil && len(cd.RatingInfos) > 0 {
|
|
ts := &TimeSpan{
|
|
TimeStart: cd.TimeStart,
|
|
TimeEnd: cd.TimeEnd,
|
|
}
|
|
ts.setRatingInfo(cd.RatingInfos[0])
|
|
cc.Timespans = append(cc.Timespans, ts)
|
|
}
|
|
return
|
|
}
|
|
return
|
|
}
|
|
if remainingDuration > 0 { // for postpaying client returns -1
|
|
initialDuration := cd.GetDuration()
|
|
cd.TimeEnd = cd.TimeStart.Add(remainingDuration)
|
|
cd.DurationIndex -= initialDuration - remainingDuration
|
|
}
|
|
cc, err = cd.debit(account, false, !cd.DenyNegativeAccount, fltrS)
|
|
if err == nil {
|
|
cc.AccountSummary = cd.AccountSummary(initialAcnt)
|
|
}
|
|
return err
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, lkIDs...)
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, utils.AccountPrefix+cd.GetAccountKey())
|
|
return cc, err
|
|
}
|
|
|
|
// refundIncrements has no locks
|
|
// returns the updated account referenced by the CallDescriptor
|
|
func (cd *CallDescriptor) refundIncrements(fltrS *FilterS) (acnt *Account, err error) {
|
|
accountsCache := make(map[string]*Account)
|
|
for _, increment := range cd.Increments {
|
|
// work around for the refund from CDRServer:
|
|
// for the calls with Cost 0 but with at least a TimeSpan it will make the information
|
|
// from BalanceInfo nil so here we can ignore all increments with BalanceInfo nil
|
|
if increment.BalanceInfo == nil {
|
|
continue
|
|
}
|
|
|
|
account, found := accountsCache[increment.BalanceInfo.AccountID]
|
|
if !found {
|
|
if acc, err := dm.GetAccount(increment.BalanceInfo.AccountID); err == nil && acc != nil {
|
|
account = acc
|
|
accountsCache[increment.BalanceInfo.AccountID] = account
|
|
// will save the account only once at the end of the function
|
|
defer dm.SetAccount(account)
|
|
}
|
|
}
|
|
if account == nil {
|
|
utils.Logger.Warning(fmt.Sprintf("Could not get the account to be refunded: %s", increment.BalanceInfo.AccountID))
|
|
continue
|
|
}
|
|
//utils.Logger.Info(fmt.Sprintf("Refunding increment %+v", increment))
|
|
var balance *Balance
|
|
unitType := cd.ToR
|
|
cc := cd.CreateCallCost()
|
|
if increment.BalanceInfo.Unit != nil && increment.BalanceInfo.Unit.UUID != "" {
|
|
if balance = account.BalanceMap[unitType].GetBalance(increment.BalanceInfo.Unit.UUID); balance == nil {
|
|
utils.Logger.Warning(fmt.Sprintf("Could not get the balnce: <%s> to be refunded for account: <%s>", increment.BalanceInfo.Unit.UUID, increment.BalanceInfo.AccountID))
|
|
continue
|
|
}
|
|
balance.AddValue(float64(increment.Duration.Nanoseconds()))
|
|
account.countUnits(-float64(increment.Duration.Nanoseconds()), unitType, cc, balance, fltrS)
|
|
}
|
|
// check money too
|
|
if increment.BalanceInfo.Monetary != nil && increment.BalanceInfo.Monetary.UUID != "" {
|
|
if balance = account.BalanceMap[utils.MetaMonetary].GetBalance(increment.BalanceInfo.Monetary.UUID); balance == nil {
|
|
utils.Logger.Warning(fmt.Sprintf("Could not get the balnce: <%s> to be refunded for account: <%s>", increment.BalanceInfo.Monetary.UUID, increment.BalanceInfo.AccountID))
|
|
continue
|
|
}
|
|
balance.AddValue(increment.Cost)
|
|
account.countUnits(-increment.Cost, utils.MetaMonetary, cc, balance, fltrS)
|
|
}
|
|
}
|
|
acnt = accountsCache[utils.ConcatenatedKey(cd.Tenant, cd.Account)]
|
|
return
|
|
|
|
}
|
|
|
|
func (cd *CallDescriptor) RefundIncrements(fltrS *FilterS) (acnt *Account, err error) {
|
|
// get account list for locking
|
|
// all must be locked in order to use cache
|
|
cd.Increments.Decompress()
|
|
accMap := make(utils.StringMap)
|
|
for _, increment := range cd.Increments {
|
|
if increment.BalanceInfo == nil {
|
|
continue
|
|
}
|
|
if increment.BalanceInfo.Monetary != nil || increment.BalanceInfo.Unit != nil {
|
|
accMap[utils.AccountPrefix+increment.BalanceInfo.AccountID] = true
|
|
}
|
|
}
|
|
guardian.Guardian.Guard(func() (_ error) {
|
|
acnt, err = cd.refundIncrements(fltrS)
|
|
return
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, accMap.Slice()...)
|
|
return
|
|
}
|
|
|
|
func (cd *CallDescriptor) refundRounding(old *Account, fltrS *FilterS) (accountsCache map[string]*Account, err error) {
|
|
// get account list for locking
|
|
// all must be locked in order to use cache
|
|
accountsCache = make(map[string]*Account)
|
|
if old != nil {
|
|
accountsCache[old.ID] = old
|
|
defer dm.SetAccount(old)
|
|
}
|
|
for _, increment := range cd.Increments {
|
|
account, found := accountsCache[increment.BalanceInfo.AccountID]
|
|
if !found {
|
|
if acc, err := dm.GetAccount(increment.BalanceInfo.AccountID); err == nil && acc != nil {
|
|
account = acc
|
|
accountsCache[increment.BalanceInfo.AccountID] = account
|
|
// will save the account only once at the end of the function
|
|
defer dm.SetAccount(account)
|
|
}
|
|
}
|
|
if account == nil {
|
|
utils.Logger.Warning(fmt.Sprintf("Could not get the account to be refunded: %s", increment.BalanceInfo.AccountID))
|
|
continue
|
|
}
|
|
cc := cd.CreateCallCost()
|
|
if increment.BalanceInfo.Monetary != nil {
|
|
var balance *Balance
|
|
if balance = account.BalanceMap[utils.MetaMonetary].GetBalance(increment.BalanceInfo.Monetary.UUID); balance == nil {
|
|
return
|
|
}
|
|
balance.AddValue(-increment.Cost)
|
|
account.countUnits(increment.Cost, utils.MetaMonetary, cc, balance, fltrS)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (cd *CallDescriptor) RefundRounding(fltrS *FilterS) (acc *Account, err error) {
|
|
accMap := make(utils.StringMap)
|
|
for _, inc := range cd.Increments {
|
|
accMap[utils.AccountPrefix+inc.BalanceInfo.AccountID] = true
|
|
}
|
|
guardian.Guardian.Guard(func() (_ error) {
|
|
var accCache map[string]*Account
|
|
if accCache, err = cd.refundRounding(nil, fltrS); err != nil {
|
|
return
|
|
}
|
|
acc = accCache[utils.ConcatenatedKey(cd.Tenant, cd.Account)]
|
|
return
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, accMap.Slice()...)
|
|
return
|
|
}
|
|
|
|
// Creates a CallCost structure copying related data from CallDescriptor
|
|
func (cd *CallDescriptor) CreateCallCost() *CallCost {
|
|
return &CallCost{
|
|
Category: cd.Category,
|
|
Tenant: cd.Tenant,
|
|
Subject: cd.Subject,
|
|
Account: cd.Account,
|
|
Destination: cd.Destination,
|
|
ToR: cd.ToR,
|
|
deductConnectFee: cd.LoopIndex == 0,
|
|
}
|
|
}
|
|
|
|
func (cd *CallDescriptor) Clone() *CallDescriptor {
|
|
cln := &CallDescriptor{
|
|
Category: cd.Category,
|
|
Tenant: cd.Tenant,
|
|
Subject: cd.Subject,
|
|
Account: cd.Account,
|
|
Destination: cd.Destination,
|
|
TimeStart: cd.TimeStart,
|
|
TimeEnd: cd.TimeEnd,
|
|
LoopIndex: cd.LoopIndex,
|
|
DurationIndex: cd.DurationIndex,
|
|
MaxRate: cd.MaxRate,
|
|
MaxRateUnit: cd.MaxRateUnit,
|
|
MaxCostSoFar: cd.MaxCostSoFar,
|
|
FallbackSubject: cd.FallbackSubject,
|
|
ToR: cd.ToR,
|
|
ForceDuration: cd.ForceDuration,
|
|
PerformRounding: cd.PerformRounding,
|
|
CgrID: cd.CgrID,
|
|
RunID: cd.RunID,
|
|
}
|
|
if cd.ExtraFields != nil {
|
|
cln.ExtraFields = make(map[string]string, len(cd.ExtraFields))
|
|
for key, value := range cd.ExtraFields {
|
|
cln.ExtraFields[key] = value
|
|
}
|
|
}
|
|
return cln
|
|
}
|
|
|
|
// AccountSummary returns the AccountSummary for cached account
|
|
func (cd *CallDescriptor) AccountSummary(initialAcnt *AccountSummary) *AccountSummary {
|
|
if cd.account == nil {
|
|
return nil
|
|
}
|
|
acntSummary := cd.account.AsAccountSummary()
|
|
acntSummary.SetInitialValue(initialAcnt)
|
|
return acntSummary
|
|
}
|
|
|
|
// FieldAsInterface is part of utils.DataProvider
|
|
func (cd *CallDescriptor) FieldAsInterface(fldPath []string) (fldVal any, err error) {
|
|
if len(fldPath) == 0 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return utils.ReflectFieldInterface(cd, fldPath[0], utils.ExtraFields)
|
|
}
|
|
|
|
// FieldAsString is part of utils.DataProvider
|
|
func (cd *CallDescriptor) FieldAsString(fldPath []string) (fldVal string, err error) {
|
|
if len(fldPath) == 0 {
|
|
return "", utils.ErrNotFound
|
|
}
|
|
return utils.ReflectFieldAsString(cd, fldPath[0], utils.ExtraFields)
|
|
}
|
|
|
|
// String is part of utils.DataProvider
|
|
func (cd *CallDescriptor) String() string {
|
|
return utils.ToJSON(cd)
|
|
}
|
|
|
|
type CallDescriptorWithAPIOpts struct {
|
|
*CallDescriptor
|
|
APIOpts map[string]any
|
|
}
|