Files
cgrates/engine/libeventcost.go
ionutboangiu 50d0103c7b Move BalanceFactor value from Increments to Accounting
Retrieve the Accounting BalanceCharge inside a new variable and reuse
that instead of repeatedly indexing the map. It is always assumed that
the BalanceCharge exists in the map and is non-nil. A comment was
added as a reminder.

TotalCost cannot be calculated anymore from ChargingIncrement alone.
The function is not used, so it will not be causing any issues for
now.

BalanceCharge clone function now handles the case where it is nil
to avoid any possible nil pointer dereference.
2024-04-01 20:48:57 +03:00

699 lines
18 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 (
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/cgrates/cgrates/utils"
)
// ChargingInterval represents one interval out of Usage providing charging info
// eg: PEAK vs OFFPEAK
type ChargingInterval struct {
RatingID string // reference to RatingUnit
Increments []*ChargingIncrement // specific increments applied to this interval
CompressFactor int
usage *time.Duration // cache usage computation for this interval
ecUsageIdx *time.Duration // computed value of totalUsage at the starting of the interval
cost *float64 // cache cost calculation on this interval
}
// PartiallyEquals does not compare CompressFactor, usefull for Merge
func (cIl *ChargingInterval) PartiallyEquals(oCIl *ChargingInterval) bool {
if equals := cIl.RatingID == oCIl.RatingID &&
len(cIl.Increments) == len(oCIl.Increments); !equals {
return false
}
for i := range cIl.Increments {
if !cIl.Increments[i].Equals(oCIl.Increments[i]) {
return false
}
}
return true
}
// Usage computes the total usage of this ChargingInterval, ignoring CompressFactor
func (cIl *ChargingInterval) Usage() *time.Duration {
if cIl.usage == nil {
var usage time.Duration
for _, incr := range cIl.Increments {
usage += incr.TotalUsage()
}
cIl.usage = &usage
}
return cIl.usage
}
// TotalUsage returns the total usage of this interval, considering compress factor
func (cIl *ChargingInterval) TotalUsage() (tu *time.Duration) {
usage := cIl.Usage()
if usage == nil {
return
}
tu = new(time.Duration)
*tu = time.Duration(usage.Nanoseconds() * int64(cIl.CompressFactor))
return
}
// EventCostUsageIndex publishes the value of ecUsageIdx
func (cIl *ChargingInterval) EventCostUsageIndex() *time.Duration {
return cIl.ecUsageIdx
}
// StartTime computes a StartTime based on EventCost.Start time and ecUsageIdx
func (cIl *ChargingInterval) StartTime(ecST time.Time) (st time.Time) {
if cIl.ecUsageIdx != nil {
st = ecST.Add(*cIl.ecUsageIdx)
}
return
}
// EndTime computes an EndTime based on ChargingInterval StartTime value and usage
func (cIl *ChargingInterval) EndTime(cIlST time.Time) (et time.Time) {
return cIlST.Add(time.Duration(cIl.Usage().Nanoseconds() * int64(cIl.CompressFactor)))
}
// Cost computes the total cost on this ChargingInterval
func (cIl *ChargingInterval) Cost() float64 {
if cIl.cost == nil {
var cost float64
for _, incr := range cIl.Increments {
cost += incr.Cost * float64(incr.CompressFactor)
}
cost = utils.Round(cost, globalRoundingDecimals, utils.MetaRoundingMiddle)
cIl.cost = &cost
}
return *cIl.cost
}
// TotalCost returns the cost of charges
func (cIl *ChargingInterval) TotalCost() float64 {
return utils.Round((cIl.Cost() * float64(cIl.CompressFactor)),
globalRoundingDecimals, utils.MetaRoundingMiddle)
}
// Clone returns a new instance of ChargingInterval with independent data
func (cIl *ChargingInterval) Clone() (cln *ChargingInterval) {
cln = new(ChargingInterval)
cln.RatingID = cIl.RatingID
cln.CompressFactor = cIl.CompressFactor
cln.Increments = make([]*ChargingIncrement, len(cIl.Increments))
for i, cIt := range cIl.Increments {
cln.Increments[i] = cIt.Clone()
}
return
}
// ChargingIncrement represents one unit charged inside an interval
type ChargingIncrement struct {
Usage time.Duration
Cost float64
AccountingID string
CompressFactor int
}
// Equals returns if the structure has the same value
func (cIt *ChargingIncrement) Equals(oCIt *ChargingIncrement) bool {
return cIt.Usage == oCIt.Usage &&
cIt.Cost == oCIt.Cost &&
cIt.AccountingID == oCIt.AccountingID &&
cIt.CompressFactor == oCIt.CompressFactor
}
// PartiallyEquals ignores the CompressFactor when comparing
func (cIt *ChargingIncrement) PartiallyEquals(oCIt *ChargingIncrement) bool {
return cIt.Usage == oCIt.Usage &&
cIt.Cost == oCIt.Cost &&
cIt.AccountingID == oCIt.AccountingID
}
// Clone creates a copy of ChargingIncrement
func (cIt *ChargingIncrement) Clone() (cln *ChargingIncrement) {
cln = new(ChargingIncrement)
*cln = *cIt
return
}
// TotalUsage returns the total usage of the increment, considering compress factor
func (cIt *ChargingIncrement) TotalUsage() time.Duration {
return time.Duration(cIt.Usage.Nanoseconds() * int64(cIt.CompressFactor))
}
// TotalCost returns the cost of the increment. DOES NOT have
// access to Accounting map to extract Factor. Therefore,
// TotalCost cannot be calculated.
func (cIt *ChargingIncrement) TotalCost() float64 {
return cIt.Cost * float64(cIt.CompressFactor)
}
// FieldAsInterface func to help EventCost FieldAsInterface
func (cIt *ChargingIncrement) FieldAsInterface(fldPath []string) (val any, err error) {
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
switch fldPath[0] {
default:
return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
case utils.Usage:
return cIt.Usage, nil
case utils.Cost:
return cIt.Cost, nil
case utils.AccountingID:
return cIt.AccountingID, nil
case utils.CompressFactor:
return cIt.CompressFactor, nil
}
}
// BalanceCharge represents one unit charged to a balance
type BalanceCharge struct {
AccountID string // reference for shared balances
BalanceUUID string // balance charged
RatingID string // special price applied on this balance
Units float64 // number of units charged
Factor float64 // calculation multiplier for units; always 1 for *monetary balances
ExtraChargeID string // used in cases when paying *voice with *monetary
}
// FieldAsInterface func to help EventCost FieldAsInterface
func (bc *BalanceCharge) FieldAsInterface(fldPath []string) (val any, err error) {
if bc == nil || len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
switch fldPath[0] {
default:
return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
case utils.AccountID:
return bc.AccountID, nil
case utils.BalanceUUID:
return bc.BalanceUUID, nil
case utils.RatingID:
return bc.RatingID, nil
case utils.Units:
return bc.Units, nil
case utils.Factor:
return bc.Factor, nil
case utils.ExtraChargeID:
return bc.ExtraChargeID, nil
}
}
// Equals checks if two BalanceCharge instances are equivalent,
// comparing all fields directly, while treating empty ExtraChargeID
// as utils.MetaNone.
func (bc *BalanceCharge) Equals(oBC *BalanceCharge) bool {
bcExtraChargeID := bc.ExtraChargeID
if bcExtraChargeID == "" {
bcExtraChargeID = utils.MetaNone
}
oBCExtraChargeID := oBC.ExtraChargeID
if oBCExtraChargeID == "" { // so we can compare them properly
oBCExtraChargeID = utils.MetaNone
}
return bc.AccountID == oBC.AccountID &&
bc.BalanceUUID == oBC.BalanceUUID &&
bc.RatingID == oBC.RatingID &&
bc.Units == oBC.Units &&
bc.Factor == oBC.Factor &&
bcExtraChargeID == oBCExtraChargeID
}
// Clone creates a copy of BalanceCharge
func (bc *BalanceCharge) Clone() *BalanceCharge {
if bc == nil {
return nil
}
cln := new(BalanceCharge)
*cln = *bc
return cln
}
// RatingMatchedFilters a rating filter
type RatingMatchedFilters map[string]any
// Equals returns if the RatingMatchedFilters are equal
func (rf RatingMatchedFilters) Equals(oRF RatingMatchedFilters) bool {
for k := range rf {
if rf[k] != oRF[k] {
return false
}
}
return true
}
// Clone creates a copy of RatingMatchedFilters
func (rf RatingMatchedFilters) Clone() (cln map[string]any) {
if rf == nil {
return nil
}
cln = make(map[string]any)
for key, value := range rf {
cln[key] = value
}
return
}
// FieldAsInterface func to help EventCost FieldAsInterface
func (rf RatingMatchedFilters) FieldAsInterface(fldPath []string) (val any, err error) {
if rf == nil || len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
ct, has := rf[fldPath[0]]
if !has || ct == nil {
return nil, utils.ErrNotFound
}
return ct, nil
}
// ChargedTiming represents one timing attached to a charge
type ChargedTiming struct {
Years utils.Years
Months utils.Months
MonthDays utils.MonthDays
WeekDays utils.WeekDays
StartTime string
}
// Equals returns if the timings are equal
func (ct *ChargedTiming) Equals(oCT *ChargedTiming) bool {
return ct.Years.Equals(oCT.Years) &&
ct.Months.Equals(oCT.Months) &&
ct.MonthDays.Equals(oCT.MonthDays) &&
ct.WeekDays.Equals(oCT.WeekDays) &&
ct.StartTime == oCT.StartTime
}
// Clone creates a copy of ChargedTiming
func (ct *ChargedTiming) Clone() (cln *ChargedTiming) {
cln = new(ChargedTiming)
*cln = *ct
return
}
// FieldAsInterface func to help EventCost FieldAsInterface
func (ct ChargedTiming) FieldAsInterface(fldPath []string) (val any, err error) {
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
switch fldPath[0] {
default:
return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
case utils.YearsFieldName:
return ct.Years, nil
case utils.MonthsFieldName:
return ct.Months, nil
case utils.MonthDaysFieldName:
return ct.MonthDays, nil
case utils.WeekDaysFieldName:
return ct.WeekDays, nil
case utils.StartTime:
return ct.StartTime, nil
}
}
// RatingUnit represents one unit out of RatingPlan matching for an event
type RatingUnit struct {
ConnectFee float64
RoundingMethod string
RoundingDecimals int
MaxCost float64
MaxCostStrategy string
TimingID string // This RatingUnit is bounded to specific timing profile
RatesID string
RatingFiltersID string
}
// Equals returns if RatingUnit is equal to the other
func (ru *RatingUnit) Equals(oRU *RatingUnit) bool {
return ru.ConnectFee == oRU.ConnectFee &&
ru.RoundingMethod == oRU.RoundingMethod &&
ru.RoundingDecimals == oRU.RoundingDecimals &&
ru.MaxCost == oRU.MaxCost &&
ru.MaxCostStrategy == oRU.MaxCostStrategy &&
ru.TimingID == oRU.TimingID &&
ru.RatesID == oRU.RatesID &&
ru.RatingFiltersID == oRU.RatingFiltersID
}
// Clone creates a copy of RatingUnit
func (ru *RatingUnit) Clone() (cln *RatingUnit) {
cln = new(RatingUnit)
*cln = *ru
return
}
// FieldAsInterface func to help EventCost FieldAsInterface
func (ru RatingUnit) FieldAsInterface(fldPath []string) (val any, err error) {
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
switch fldPath[0] {
default:
return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
case utils.ConnectFee:
return ru.ConnectFee, nil
case utils.RoundingMethod:
return ru.RoundingMethod, nil
case utils.RoundingDecimals:
return ru.RoundingDecimals, nil
case utils.MaxCost:
return ru.MaxCost, nil
case utils.MaxCostStrategy:
return ru.MaxCostStrategy, nil
case utils.TimingID:
return ru.TimingID, nil
case utils.RatesID:
return ru.RatesID, nil
case utils.RatingFiltersID:
return ru.RatingFiltersID, nil
}
}
// RatingFilters the map of rating filters
type RatingFilters map[string]RatingMatchedFilters // so we can define search methods
// GetIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (rfs RatingFilters) GetIDWithSet(rmf RatingMatchedFilters) string {
if len(rmf) == 0 {
return ""
}
for k, v := range rfs {
if v.Equals(rmf) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
rfs[uuid] = rmf
return uuid
}
// Clone creates a copy of RatingFilters
func (rfs RatingFilters) Clone() (cln RatingFilters) {
cln = make(RatingFilters, len(rfs))
for k, v := range rfs {
cln[k] = v.Clone()
}
return
}
// FieldAsInterface func to help EventCost FieldAsInterface
func (rfs RatingFilters) FieldAsInterface(fldPath []string) (val any, err error) {
if rfs == nil || len(fldPath) == 0 {
return nil, utils.ErrNotFound
}
ct, has := rfs[fldPath[0]]
if !has || ct == nil {
return nil, utils.ErrNotFound
}
if len(fldPath) == 1 {
return ct, nil
}
return ct.FieldAsInterface(fldPath[1:])
}
// Rating the map of rating units
type Rating map[string]*RatingUnit
// GetIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (crus Rating) GetIDWithSet(cru *RatingUnit) string {
if cru == nil {
return ""
}
for k, v := range crus {
if v.Equals(cru) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
crus[uuid] = cru
return uuid
}
// Clone creates a copy of Rating
func (crus Rating) Clone() (cln Rating) {
cln = make(Rating, len(crus))
for k, v := range crus {
cln[k] = v.Clone()
}
return
}
// FieldAsInterface func to help EventCost FieldAsInterface
func (crus Rating) FieldAsInterface(fldPath []string) (val any, err error) {
if crus == nil || len(fldPath) == 0 {
return nil, utils.ErrNotFound
}
rt, has := crus[fldPath[0]]
if !has || rt == nil {
return nil, utils.ErrNotFound
}
if len(fldPath) == 1 {
return rt, nil
}
return rt.FieldAsInterface(fldPath[1:])
}
// ChargedRates the map with rateGroups
type ChargedRates map[string]RateGroups
// FieldAsInterface func to help EventCost FieldAsInterface
func (crs ChargedRates) FieldAsInterface(fldPath []string) (val any, err error) {
if crs == nil || len(fldPath) == 0 {
return nil, utils.ErrNotFound
}
opath, indx := utils.GetPathIndex(fldPath[0])
cr, has := crs[opath]
if !has || cr == nil {
return nil, utils.ErrNotFound
}
if indx != nil {
if len(cr) <= *indx {
return nil, utils.ErrNotFound
}
rg := cr[*indx]
if len(fldPath) == 1 {
return rg, nil
}
return rg.FieldAsInterface(fldPath[1:])
}
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
return cr, nil
}
// GetIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (crs ChargedRates) GetIDWithSet(rg RateGroups) string {
if len(rg) == 0 {
return ""
}
for k, v := range crs {
if v.Equals(rg) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
crs[uuid] = rg
return uuid
}
// Clone creates a copy of ChargedRates
func (crs ChargedRates) Clone() (cln ChargedRates) {
cln = make(ChargedRates, len(crs))
for k, v := range crs {
cln[k] = v.Clone()
}
return
}
// ChargedTimings the map of ChargedTiming
type ChargedTimings map[string]*ChargedTiming
// FieldAsInterface func to help EventCost FieldAsInterface
func (cts ChargedTimings) FieldAsInterface(fldPath []string) (val any, err error) {
if cts == nil || len(fldPath) == 0 {
return nil, utils.ErrNotFound
}
ct, has := cts[fldPath[0]]
if !has || ct == nil {
return nil, utils.ErrNotFound
}
if len(fldPath) == 1 {
return ct, nil
}
return ct.FieldAsInterface(fldPath[1:])
}
// GetIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (cts ChargedTimings) GetIDWithSet(ct *ChargedTiming) string {
if ct == nil {
return ""
}
for k, v := range cts {
if v.Equals(ct) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
cts[uuid] = ct
return uuid
}
// Clone creates a copy of ChargedTimings
func (cts ChargedTimings) Clone() (cln ChargedTimings) {
cln = make(ChargedTimings, len(cts))
for k, v := range cts {
cln[k] = v.Clone()
}
return
}
// Accounting the map of debited balances
type Accounting map[string]*BalanceCharge
// GetIDWithSet attempts to retrieve the UUID of a matching data or create a new one
func (cbs Accounting) GetIDWithSet(cb *BalanceCharge) string {
if cb == nil {
return ""
}
for k, v := range cbs {
if v.Equals(cb) {
return k
}
}
// not found, set it here
uuid := utils.UUIDSha1Prefix()
cbs[uuid] = cb
return uuid
}
// Clone creates a copy of Accounting
func (cbs Accounting) Clone() (cln Accounting) {
cln = make(Accounting, len(cbs))
for k, v := range cbs {
cln[k] = v.Clone()
}
return
}
// FieldAsInterface func to help EventCost FieldAsInterface
func (cbs Accounting) FieldAsInterface(fldPath []string) (val any, err error) {
if cbs == nil || len(fldPath) == 0 {
return nil, utils.ErrNotFound
}
ac, has := cbs[fldPath[0]]
if !has || ac == nil {
return nil, utils.ErrNotFound
}
if len(fldPath) == 1 {
return ac, nil
}
return ac.FieldAsInterface(fldPath[1:])
}
// IfaceAsEventCost converts an interface to EventCost
func IfaceAsEventCost(itm any) (ec *EventCost, err error) {
switch otm := itm.(type) {
case nil:
case *EventCost:
ec = otm
case string:
var rawEC EventCost
if errUnmarshal := json.Unmarshal([]byte(otm), &rawEC); errUnmarshal != nil {
return nil, fmt.Errorf("JSON cannot unmarshal to *EventCost, err: %s", errUnmarshal.Error())
}
ec = &rawEC
case map[string]any:
ec, err = IfaceAsEventCost(utils.ToJSON(otm))
default:
err = utils.ErrNotConvertibleTF(reflect.TypeOf(otm).String(), "*EventCost")
}
return
}
// NewFreeEventCost returns an EventCost of given duration that it's free
func NewFreeEventCost(cgrID, runID, account string, tStart time.Time, usage time.Duration) *EventCost {
return &EventCost{
CGRID: cgrID,
RunID: runID,
StartTime: tStart,
Cost: utils.Float64Pointer(0),
Charges: []*ChargingInterval{{
RatingID: utils.MetaPause,
Increments: []*ChargingIncrement{
{
Usage: usage,
AccountingID: utils.MetaPause,
CompressFactor: 1,
},
},
CompressFactor: 1,
}},
Rating: Rating{
utils.MetaPause: {
RoundingMethod: "*up",
RoundingDecimals: 5,
RatesID: utils.MetaPause,
RatingFiltersID: utils.MetaPause,
TimingID: utils.MetaPause,
},
},
Accounting: Accounting{
utils.MetaPause: {
AccountID: account,
// BalanceUUID: "",
RatingID: utils.MetaPause,
Factor: 1,
},
},
RatingFilters: RatingFilters{
utils.MetaPause: {
utils.Subject: "",
utils.DestinationPrefixName: "",
utils.DestinationID: "",
utils.RatingPlanID: utils.MetaPause,
},
},
Rates: ChargedRates{
utils.MetaPause: {
{
RateIncrement: 1,
RateUnit: 1,
},
},
},
Timings: ChargedTimings{
utils.MetaPause: {
StartTime: "00:00:00",
},
},
cache: utils.NewSecureMapStorage(),
}
}