Files
cgrates/engine/balance_filter.go
ionutboangiu 31842bf3f5 Add possibility to set/overwrite balance Factors through API
Constructor looks inside the params' balance map for the Factors key.
It expects either a string representing a JSON serialized map or the
map itself.

BalanceFilter Clone function has been updated to set a deep copy of
the original Factors map instead of a shallow one.

BalanceFilter getter function for Factors now returns nil instead of
an empty map. It's slightly more memory efficient and assignment to
this map will not be attempted, so it's panic proof.

BalanceFilter.ModifyBalance now updates Factors only if the key is
found in the request params' Balance map. Setting Factors to null is also
possible as long as the Factors key exists and is set to null.

Note: only *set_balance can overwrite the Factors map, all the others
can only set it if the balance does exist prior to sending the request.

Update balance integration tests.
2024-04-08 13:15:41 +02:00

653 lines
15 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"
"math"
"reflect"
"strconv"
"time"
"github.com/cgrates/cgrates/utils"
)
type BalanceFilter struct {
Uuid *string
ID *string
Type *string
Value *utils.ValueFormula
ExpirationDate *time.Time
Weight *float64
DestinationIDs *utils.StringMap
RatingSubject *string
Categories *utils.StringMap
SharedGroups *utils.StringMap
TimingIDs *utils.StringMap
Timings []*RITiming
Disabled *bool
Factors *ValueFactors
Blocker *bool
}
// NewBalanceFilter creates a new BalanceFilter based on given filter
func NewBalanceFilter(filter map[string]any, defaultTimezone string) (*BalanceFilter, error) {
bf := new(BalanceFilter)
if id, has := filter[utils.ID]; has {
bf.ID = utils.StringPointer(utils.IfaceAsString(id))
}
if uuid, has := filter[utils.UUID]; has {
bf.Uuid = utils.StringPointer(utils.IfaceAsString(uuid))
}
// if ty, has := filter[utils.Type]; has {
// bf.Type = utils.StringPointer(utils.IfaceAsString(ty))
// }
if val, has := filter[utils.Value]; has {
value, err := utils.IfaceAsTFloat64(val)
if err != nil {
return nil, err
}
bf.Value = &utils.ValueFormula{Static: math.Abs(value)}
}
if exp, has := filter[utils.ExpiryTime]; has {
expTime, err := utils.IfaceAsTime(exp, defaultTimezone)
if err != nil {
return nil, err
}
bf.ExpirationDate = utils.TimePointer(expTime)
}
if weight, has := filter[utils.Weight]; has {
value, err := utils.IfaceAsFloat64(weight)
if err != nil {
return nil, err
}
bf.Weight = utils.Float64Pointer(value)
}
if dst, has := filter[utils.DestinationIDs]; has {
bf.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(utils.IfaceAsString(dst)))
}
if rs, has := filter[utils.RatingSubject]; has {
bf.RatingSubject = utils.StringPointer(utils.IfaceAsString(rs))
}
if cat, has := filter[utils.Categories]; has {
bf.Categories = utils.StringMapPointer(utils.ParseStringMap(utils.IfaceAsString(cat)))
}
if grps, has := filter[utils.SharedGroups]; has {
bf.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(utils.IfaceAsString(grps)))
}
if tim, has := filter[utils.TimingIDs]; has {
bf.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(utils.IfaceAsString(tim)))
}
if dis, has := filter[utils.Disabled]; has {
value, err := utils.IfaceAsBool(dis)
if err != nil {
return nil, err
}
bf.Disabled = utils.BoolPointer(value)
}
if blk, has := filter[utils.Blocker]; has {
value, err := utils.IfaceAsBool(blk)
if err != nil {
return nil, err
}
bf.Blocker = utils.BoolPointer(value)
}
if facVal, has := filter[utils.Factors]; has {
var err error
var facBytes []byte
switch v := facVal.(type) {
case string:
facBytes = []byte(v)
default:
facBytes, err = json.Marshal(v)
if err != nil {
return nil, err
}
}
var vf ValueFactors
if err := json.Unmarshal(facBytes, &vf); err != nil {
return nil, err
}
bf.Factors = &vf
}
return bf, nil
}
func (bp *BalanceFilter) CreateBalance() *Balance {
b := &Balance{
Uuid: bp.GetUuid(),
ID: bp.GetID(),
Value: bp.GetValue(),
ExpirationDate: bp.GetExpirationDate(),
Weight: bp.GetWeight(),
DestinationIDs: bp.GetDestinationIDs(),
RatingSubject: bp.GetRatingSubject(),
Categories: bp.GetCategories(),
SharedGroups: bp.GetSharedGroups(),
Timings: bp.Timings,
TimingIDs: bp.GetTimingIDs(),
Disabled: bp.GetDisabled(),
Factors: bp.GetFactors(),
Blocker: bp.GetBlocker(),
}
return b.Clone()
}
func (bf *BalanceFilter) Clone() *BalanceFilter {
if bf == nil {
return nil
}
result := &BalanceFilter{}
if bf.Uuid != nil {
result.Uuid = new(string)
*result.Uuid = *bf.Uuid
}
if bf.ID != nil {
result.ID = new(string)
*result.ID = *bf.ID
}
if bf.Type != nil {
result.Type = new(string)
*result.Type = *bf.Type
}
if bf.Value != nil {
result.Value = new(utils.ValueFormula)
*result.Value = *bf.Value
}
if bf.ExpirationDate != nil {
result.ExpirationDate = new(time.Time)
*result.ExpirationDate = *bf.ExpirationDate
}
if bf.Weight != nil {
result.Weight = new(float64)
*result.Weight = *bf.Weight
}
if bf.DestinationIDs != nil {
result.DestinationIDs = utils.StringMapPointer(bf.DestinationIDs.Clone())
}
if bf.RatingSubject != nil {
result.RatingSubject = new(string)
*result.RatingSubject = *bf.RatingSubject
}
if bf.Categories != nil {
result.Categories = utils.StringMapPointer(bf.Categories.Clone())
}
if bf.SharedGroups != nil {
result.SharedGroups = utils.StringMapPointer(bf.SharedGroups.Clone())
}
if bf.TimingIDs != nil {
result.TimingIDs = utils.StringMapPointer(bf.TimingIDs.Clone())
}
if bf.Timings != nil {
result.Timings = make([]*RITiming, len(bf.Timings))
for i, rit := range bf.Timings {
result.Timings[i] = rit.Clone()
}
}
if bf.Disabled != nil {
result.Disabled = new(bool)
*result.Disabled = *bf.Disabled
}
if bf.Factors != nil {
cln := make(ValueFactors, len(*bf.Factors))
for key, value := range *bf.Factors {
cln[key] = value
}
result.Factors = &cln
}
if bf.Blocker != nil {
result.Blocker = new(bool)
*result.Blocker = *bf.Blocker
}
return result
}
func (bf *BalanceFilter) LoadFromBalance(b *Balance) *BalanceFilter {
if b.Uuid != "" {
bf.Uuid = &b.Uuid
}
if b.ID != "" {
bf.ID = &b.ID
}
if b.Value != 0 {
bf.Value.Static = b.Value
}
if !b.ExpirationDate.IsZero() {
bf.ExpirationDate = &b.ExpirationDate
}
if b.Weight != 0 {
bf.Weight = &b.Weight
}
if !b.DestinationIDs.IsEmpty() {
bf.DestinationIDs = &b.DestinationIDs
}
if b.RatingSubject != "" {
bf.RatingSubject = &b.RatingSubject
}
if !b.Categories.IsEmpty() {
bf.Categories = &b.Categories
}
if !b.SharedGroups.IsEmpty() {
bf.SharedGroups = &b.SharedGroups
}
if !b.TimingIDs.IsEmpty() {
bf.TimingIDs = &b.TimingIDs
}
if len(b.Timings) != 0 {
bf.Timings = make([]*RITiming, len(b.Timings))
copy(bf.Timings, b.Timings)
}
if len(b.Factors) != 0 {
bf.Factors = &b.Factors
}
if b.Disabled {
bf.Disabled = &b.Disabled
}
if b.Blocker {
bf.Blocker = &b.Blocker
}
bf.Timings = b.Timings
return bf
}
func (bp *BalanceFilter) Equal(o *BalanceFilter) bool {
if bp.ID != nil && o.ID != nil {
return *bp.ID == *o.ID
}
return reflect.DeepEqual(bp, o)
}
func (bp *BalanceFilter) GetType() string {
if bp == nil || bp.Type == nil {
return ""
}
return *bp.Type
}
func (bp *BalanceFilter) GetValue() float64 {
if bp == nil || bp.Value == nil {
return 0.0
}
if bp.Value.Method == "" {
return bp.Value.Static
}
// calculate using formula
formula, exists := utils.ValueFormulas[bp.Value.Method]
if !exists {
return 0.0
}
return formula(bp.Value.Params)
}
func (bp *BalanceFilter) SetValue(v float64) {
if bp.Value == nil {
bp.Value = new(utils.ValueFormula)
}
bp.Value.Static = v
}
func (bp *BalanceFilter) GetUuid() string {
if bp == nil || bp.Uuid == nil {
return ""
}
return *bp.Uuid
}
func (bp *BalanceFilter) GetID() string {
if bp == nil || bp.ID == nil {
return ""
}
return *bp.ID
}
func (bp *BalanceFilter) GetDestinationIDs() utils.StringMap {
if bp == nil || bp.DestinationIDs == nil {
return utils.StringMap{}
}
return *bp.DestinationIDs
}
func (bp *BalanceFilter) GetCategories() utils.StringMap {
if bp == nil || bp.Categories == nil {
return utils.StringMap{}
}
return *bp.Categories
}
func (bp *BalanceFilter) GetTimingIDs() utils.StringMap {
if bp == nil || bp.TimingIDs == nil {
return utils.StringMap{}
}
return *bp.TimingIDs
}
func (bp *BalanceFilter) GetSharedGroups() utils.StringMap {
if bp == nil || bp.SharedGroups == nil {
return utils.StringMap{}
}
return *bp.SharedGroups
}
func (bp *BalanceFilter) GetWeight() float64 {
if bp == nil || bp.Weight == nil {
return 0.0
}
return *bp.Weight
}
func (bp *BalanceFilter) GetRatingSubject() string {
if bp == nil || bp.RatingSubject == nil {
return ""
}
return *bp.RatingSubject
}
func (bp *BalanceFilter) GetDisabled() bool {
if bp == nil || bp.Disabled == nil {
return false
}
return *bp.Disabled
}
func (bp *BalanceFilter) GetBlocker() bool {
if bp == nil || bp.Blocker == nil {
return false
}
return *bp.Blocker
}
func (bp *BalanceFilter) GetExpirationDate() time.Time {
if bp == nil || bp.ExpirationDate == nil {
return time.Time{}
}
return *bp.ExpirationDate
}
func (bp *BalanceFilter) GetFactors() ValueFactors {
if bp == nil || bp.Factors == nil {
return nil
}
return *bp.Factors
}
func (bp *BalanceFilter) EmptyExpirationDate() bool {
if bp.ExpirationDate == nil {
return true
}
return (*bp.ExpirationDate).IsZero()
}
func (bf *BalanceFilter) ModifyBalance(b *Balance) {
if b == nil {
return
}
if bf.ID != nil {
b.ID = *bf.ID
}
if bf.Value != nil {
b.Value = bf.GetValue()
}
if bf.ExpirationDate != nil {
b.ExpirationDate = *bf.ExpirationDate
}
if bf.RatingSubject != nil {
b.RatingSubject = *bf.RatingSubject
}
if bf.Categories != nil {
b.Categories = *bf.Categories
}
if bf.DestinationIDs != nil {
b.DestinationIDs = *bf.DestinationIDs
}
if bf.SharedGroups != nil {
b.SharedGroups = *bf.SharedGroups
}
if bf.TimingIDs != nil {
b.TimingIDs = *bf.TimingIDs
}
if bf.Timings != nil && len(bf.Timings) != 0 {
b.Timings = make([]*RITiming, len(bf.Timings))
copy(b.Timings, bf.Timings)
}
if bf.Weight != nil {
b.Weight = *bf.Weight
}
if bf.Factors != nil {
b.Factors = *bf.Factors
}
if bf.Blocker != nil {
b.Blocker = *bf.Blocker
}
if bf.Disabled != nil {
b.Disabled = *bf.Disabled
}
b.SetDirty() // Mark the balance as dirty since we have modified and it should be checked by action triggers
}
func (bp *BalanceFilter) String() string {
return utils.ToJSON(bp)
}
func (bp *BalanceFilter) FieldAsInterface(fldPath []string) (val any, err error) {
if bp == nil || len(fldPath) == 0 {
return nil, utils.ErrNotFound
}
switch fldPath[0] {
default:
opath, indx := utils.GetPathIndexString(fldPath[0])
if indx != nil {
switch opath {
case utils.DestinationIDs:
if bp.DestinationIDs == nil {
return nil, utils.ErrNotFound
}
val, has := (*bp.DestinationIDs)[*indx]
if !has || len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
return val, nil
case utils.Categories:
if bp.Categories == nil {
return nil, utils.ErrNotFound
}
val, has := (*bp.Categories)[*indx]
if !has || len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
return val, nil
case utils.SharedGroups:
if bp.SharedGroups == nil {
return nil, utils.ErrNotFound
}
val, has := (*bp.SharedGroups)[*indx]
if !has || len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
return val, nil
case utils.TimingIDs:
if bp.TimingIDs == nil {
return nil, utils.ErrNotFound
}
val, has := (*bp.TimingIDs)[*indx]
if !has || len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
return val, nil
case utils.Timings:
var idx int
if idx, err = strconv.Atoi(*indx); err != nil {
return
}
if len(bp.Timings) <= idx {
return nil, utils.ErrNotFound
}
tm := bp.Timings[idx]
if len(fldPath) == 1 {
return tm, nil
}
return tm.FieldAsInterface(fldPath[1:])
case utils.Factors:
if bp.Factors == nil {
return nil, utils.ErrNotFound
}
val, has := (*bp.Factors)[*indx]
if !has || len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
return val, nil
}
}
return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
case utils.Uuid:
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if bp.Uuid == nil {
return
}
return *bp.Uuid, nil
case utils.ID:
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if bp.ID == nil {
return
}
return *bp.ID, nil
case utils.Type:
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if bp.Type == nil {
return
}
return *bp.Type, nil
case utils.ExpirationDate:
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if bp.ExpirationDate == nil {
return
}
return *bp.ExpirationDate, nil
case utils.Weight:
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if bp.Weight == nil {
return
}
return *bp.Weight, nil
case utils.RatingSubject:
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if bp.RatingSubject == nil {
return
}
return *bp.RatingSubject, nil
case utils.Disabled:
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if bp.Disabled == nil {
return
}
return *bp.Disabled, nil
case utils.Blocker:
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if bp.Blocker == nil {
return
}
return *bp.Blocker, nil
case utils.DestinationIDs:
if len(fldPath) == 1 {
return bp.DestinationIDs, nil
}
if bp.DestinationIDs == nil {
return nil, utils.ErrNotFound
}
return bp.DestinationIDs.FieldAsInterface(fldPath[1:])
case utils.Categories:
if len(fldPath) == 1 {
return bp.Categories, nil
}
if bp.Categories == nil {
return nil, utils.ErrNotFound
}
return bp.Categories.FieldAsInterface(fldPath[1:])
case utils.SharedGroups:
if len(fldPath) == 1 {
return bp.SharedGroups, nil
}
if bp.SharedGroups == nil {
return nil, utils.ErrNotFound
}
return bp.SharedGroups.FieldAsInterface(fldPath[1:])
case utils.Timings:
if len(fldPath) == 1 {
return bp.Timings, nil
}
for _, tm := range bp.Timings {
if tm.ID == fldPath[1] {
if len(fldPath) == 2 {
return tm, nil
}
return tm.FieldAsInterface(fldPath[2:])
}
}
return nil, utils.ErrNotFound
case utils.TimingIDs:
if len(fldPath) == 1 {
return bp.TimingIDs, nil
}
if bp.TimingIDs == nil {
return nil, utils.ErrNotFound
}
return bp.TimingIDs.FieldAsInterface(fldPath[1:])
case utils.Factors:
if len(fldPath) == 1 {
return bp.Factors, nil
}
if bp.Factors == nil {
return nil, utils.ErrNotFound
}
return bp.Factors.FieldAsInterface(fldPath[1:])
case utils.Value:
if len(fldPath) == 1 {
return bp.Value, nil
}
return bp.Value.FieldAsInterface(fldPath[1:])
}
}
func (bp *BalanceFilter) FieldAsString(fldPath []string) (val string, err error) {
var iface any
iface, err = bp.FieldAsInterface(fldPath)
if err != nil {
return
}
return utils.IfaceAsString(iface), nil
}