mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
`ClientConnector` is no longer defined within `rpcclient` in its latest
version. It has been changed to be obtained from the `cgrates/birpc`
library instead.
Replaced `net/rpc` with `cgrates/birpc` and `net/rpc/jsonrpc` with
`cgrates/birpc/jsonrpc` libraries.
The implementations of `CallBiRPC()` and `Handlers()` were removed,
along with the methods associated with them.
The `rpcclient.BIRPCConector` and the methods prefixed with `BiRPC` were
removed from the `BiRPClient` interface.
The `BiRPClient` interface was renamed to `BIRPCClient`, although not
sure if needed (seems useful just to test if the structure is correct).
`rpcclient.BiRPCConector` has been replaced with `context.ClientConnector`,
which is now passed alongside `context.Context` within the same struct
(`cgrates/birpc/context.Context`). Consequently, functions that were
previously relying on it are now receiving the context instead. The
changes were made in the following functions:
- `engine/connmanager.go` - `*ConnManager.Call`
- `engine/connmanager.go` - `*ConnManager.getConn`
- `engine/connmanager.go` - `*ConnManager.getConnWithConfig`
- `engine/libengine.go` - `NewRPCPool`
- `engine/libengine.go` - `NewRPCConnection`
- `agents/libagents.go` - `processRequest`
Compilation errors related to the `rpcclient.NewRPCClient` function were
resolved by adding the missing `context`, `max_reconnect_interval`, and
`delayFunc` parameters. Additionally, context was added to all calls made
by the client. An effort was made to avoid passing hardcoded values as
much as possible, and extra flags were added where necessary for cgr
binaries.
The `max_reconnect_interval` parameter is now passed from parent
functions, which required adjustments to the function signature.
A new context field was added to all agent objects to ensure access to
it before sending it to the `connmanager's Call`, effectively replacing
`birpcclient`. Although an alternative would have been to create the
new service and add it to the context right before passing it to the
handlers, the chosen approach is definitely more comfortable.
With the addition of a context field for the SIP servers agents, an
additional error needed to be handled, coming from the creation of the
service. Agent constructors within the services package log errors as
they occur and return. Alternate solutions considered were either
shutting down the engine instead of returning, or just logging the
occurrence as a warning, particularly when the `ctx.Client` isn't
required, especially in cases where bidirectional connections are not
needed. For the latter option, it's crucial to return the object with
the error rather than nil or to make the error nil immediately after
logging.
Context has been integrated into all internal Call implementations to
ensure the objects conform to the `birpc.ClientConnector` interface.
These implementations will be removed in the near future as all service
objects are being wrapped in a `birpc.Service` type that satisfies the
`birpc.ClientConnector` interface. Currently, they are being retained
as a reference in case of any unexpected issues that arise.
Ensured that the `birpc.Service` wrapped service objects are passed to
the internal channel getters rather than the objects themselves.
Add context.TODO() to all \*ConnManager.Call function calls. To be
replaced with the context passed to the Method, when available.
For all `*ConnManager.Call` function calls, `context.TODO()` has been
added. This will be replaced with the context passed to the method when
it becomes available.
The value returned by StringGetOpts is now passed directly to the
FirstNonEmpty function, instead of being assigned to a variable
first.
The implementation of the `*AnalyzerService.GetInternalBiRPCCodec`
function has been removed from the services package. Additionally,
the AnalyzerBiRPCConnector type definition and its associated methods
have been removed.
The codec implementation has been revised to include the following
changes:
- `rpc.ServerCodec` -> `birpc.ServerCodec`;
- `rpc2.ServerCodec` -> `birpc.BirpcCodec`;
- `rpc2.Request` -> `birpc.Request`;
- `rpc2.Response` -> `birpc.Response`;
- The constructors for json and gob birpc codecs in `cenkalti/rpc`
have been replaced with ones from the `birpc/jsonrpc` library;
- The gob codec implementation has been removed in favor of the
version already implemented in the birpc external library.
The server implementation has been updated with the following changes:
- A field that represents a simple RPC server has been added to the
Server struct;
- Both the simple and bidirectional RPC servers are now initialized
inside the Server constructor, eliminating the need for nil checks;
- Usage of `net/rpc` and `cenkalti/rpc2` has been replaced with
`cgrates/birpc`;
- Additional `(Bi)RPCUnregisterName` methods have been added;
- The implementations for (bi)json/gob servers have been somewhat
simplified.
Before deleting the Call functions and using the `birpc.NewService`
method to register the methods for all cgrates components, update the
Call functions to satisfy the `birpc.ClientConnector` interface. This
way it will be a bit safer. Had to be done for SessionS though.
The `BiRPCCall` method has been removed from coreutils.go. The
`RPCCall` and `APIerRPCCall` methods are also to be removed in the
future.
Ensured that all methods for `SessionSv1` and `SessionS` have the
correct function signature with context. The same adjustments were made
for the session dispatcher methods and for the `SessionSv1Interface`.
Also removed sessionsbirpc.go and smgbirpc.go files.
Implemented the following methods to help with the registration of
methods across all subsystems:
- `NewServiceWithName`;
- `NewDispatcherService` for all dispatcher methods;
- `NewService` for the remaining methods that are already named
correctly.
Compared to the constructor from the external library, these also make
sure that the naming of the methods is consistent with our constants.
Added context to the Call methods for the mock client connectors (used
in tests).
Removed unused rpc fields from inside the following services:
- EeS
- LoaderS
- ResourceS
- RouteS
- StatS
- ThresholdS
- SessionS
- CoreS
Updated the methods implementing the logic for API methods to align
with the latest changes, ensuring consistency and correctness. The
modifications include:
- Adjusting the function signature to the new format
(ctx, args, reply).
- Prefixing names with 'V*' to indicate that they are utilized by
or registered as APIs.
- Containing the complete logic within the methods, enabling APIs
to call them and return their reply directly.
The subsystems affected by these changes are detailed as follows:
- CoreS: Additional methods were implementing utilizing the
existing ones. Though modifying them directly was possible, certain
methods (e.g., StopCPUProfiling()) were used elsewhere and not as
RPC requests.
- CDRs: Renamed V1CountCDRs to V1GetCDRsCount.
- StatS: V1GetQueueFloatMetrics, V1GetQueueStringMetrics,
V1GetStatQueue accept different arguments compared to API functions
(opted to register StatSv1 instead).
- ResourceS: Renamed V1ResourcesForEvent to V1GetResourcesForEvent
to align with API naming.
- DispatcherS: Renamed V1GetProfilesForEvent to
DispatcherSv1GetProfilesForEvent.
- For the rest, adding context to the function signature was enough.
In the unit tests, wrapping the object within a biprc.Service is now
ensured before passing it to the internal connections map under the
corresponding key.
Some tests that are covering error cases, are also checking the other
return value besides the error. That check has been removed since it
is redundant.
Revised the RPC/BiRPC clients' constructors (for use in tests)
A different approach has been chosen for the handling of ping methods
within subsystems. Instead of defining the same structure in every file,
the ping methods were added inside the Service constructor function.
Though the existing Ping methods were left as they were, they will be
removed in the future.
An additional method has been implemented to register the Ping method
from outside of the engine package.
Implemented Sleep and CapsError methods for SessionS (before they were
exclusively for bidirectional use, I believe).
A specific issue has been fixed within the CapsError SessionSv1 API
implementation, which is designed to overwrite methods that cannot be
allocated due to the threshold limit being reached. Previously, it was
deallocating when writing the response, even when a spot hadn't been
allocated in the first place (due to the cap being hit). The reason
behind this, especially why the test was passing before, still needs
to be looked into, as the problem should have occurred from before.
Implement `*SessionSv1.RegisterInternalBiJSONConn` method in apier.
All agent methods have been registered under the SessionSv1 name. For
the correct method names, the leading "V1" prefix has been trimmed
using the `birpc.NewServiceWithMethodsRename` function.
Revise the RegisterRpcParams function to populate the parameters
while relying on the `*birpc.Service` type instead. This will
automatically also deal with the validation. At the moment,
any error encountered is logged without being returned. Might
be changed in the future.
Inside the cgrRPCAction function, `mapstructure.Decode`'s output parameter
is now guaranteed to always be a pointer.
Updated go.mod and go.sum.
Fixed some typos.
1414 lines
41 KiB
Go
1414 lines
41 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"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cgrates/birpc/context"
|
|
"github.com/cgrates/cgrates/config"
|
|
"github.com/cgrates/cgrates/guardian"
|
|
"github.com/cgrates/cgrates/utils"
|
|
)
|
|
|
|
// Account structure containing information about user's credit (minutes, cents, sms...).'
|
|
// This can represent a user or a shared group.
|
|
type Account struct {
|
|
ID string
|
|
BalanceMap map[string]Balances
|
|
UnitCounters UnitCounters
|
|
ActionTriggers ActionTriggers
|
|
AllowNegative bool
|
|
Disabled bool
|
|
UpdateTime time.Time
|
|
executingTriggers bool
|
|
}
|
|
|
|
type AccountWithAPIOpts struct {
|
|
*Account
|
|
APIOpts map[string]any
|
|
}
|
|
|
|
// User's available minutes for the specified destination
|
|
func (acc *Account) getCreditForPrefix(cd *CallDescriptor) (duration time.Duration, credit float64, balances Balances) {
|
|
creditBalances := acc.getBalancesForPrefix(cd.Destination, cd.Category, utils.MetaMonetary, "", cd.TimeStart)
|
|
|
|
unitBalances := acc.getBalancesForPrefix(cd.Destination, cd.Category, cd.ToR, "", cd.TimeStart)
|
|
// gather all balances from shared groups
|
|
var extendedCreditBalances Balances
|
|
for _, cb := range creditBalances {
|
|
if len(cb.SharedGroups) > 0 {
|
|
for sg := range cb.SharedGroups {
|
|
if sharedGroup, _ := dm.GetSharedGroup(sg, false, utils.NonTransactional); sharedGroup != nil {
|
|
sgb := sharedGroup.GetBalances(cd.Destination, cd.Category, utils.MetaMonetary, acc, cd.TimeStart)
|
|
sgb = sharedGroup.SortBalancesByStrategy(cb, sgb)
|
|
extendedCreditBalances = append(extendedCreditBalances, sgb...)
|
|
}
|
|
}
|
|
} else {
|
|
extendedCreditBalances = append(extendedCreditBalances, cb)
|
|
}
|
|
}
|
|
var extendedMinuteBalances Balances
|
|
for _, mb := range unitBalances {
|
|
if len(mb.SharedGroups) > 0 {
|
|
for sg := range mb.SharedGroups {
|
|
if sharedGroup, _ := dm.GetSharedGroup(sg, false, utils.NonTransactional); sharedGroup != nil {
|
|
sgb := sharedGroup.GetBalances(cd.Destination, cd.Category, cd.ToR, acc, cd.TimeStart)
|
|
sgb = sharedGroup.SortBalancesByStrategy(mb, sgb)
|
|
extendedMinuteBalances = append(extendedMinuteBalances, sgb...)
|
|
}
|
|
}
|
|
} else {
|
|
extendedMinuteBalances = append(extendedMinuteBalances, mb)
|
|
}
|
|
}
|
|
credit = extendedCreditBalances.GetTotalValue()
|
|
balances = extendedMinuteBalances
|
|
for _, b := range balances {
|
|
d, c := b.GetMinutesForCredit(cd, credit)
|
|
credit = c
|
|
duration += d
|
|
}
|
|
return
|
|
}
|
|
|
|
// sets all the fields of the balance
|
|
func (acc *Account) setBalanceAction(a *Action, fltrS *FilterS) error {
|
|
if a == nil {
|
|
return errors.New("nil action")
|
|
}
|
|
if acc.BalanceMap == nil {
|
|
acc.BalanceMap = make(map[string]Balances)
|
|
}
|
|
var balance *Balance
|
|
var found bool
|
|
var previousSharedGroups utils.StringMap // kept for comparison
|
|
if a.Balance.Uuid != nil && *a.Balance.Uuid != "" { // balance uuid match
|
|
for balanceType := range acc.BalanceMap {
|
|
for _, b := range acc.BalanceMap[balanceType] {
|
|
if b.Uuid == *a.Balance.Uuid && !b.IsExpiredAt(time.Now()) {
|
|
previousSharedGroups = b.SharedGroups
|
|
balance = b
|
|
found = true
|
|
break // only set one balance
|
|
}
|
|
}
|
|
if found {
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("cannot find balance with uuid: <%s>", *a.Balance.Uuid)
|
|
}
|
|
} else { // balance id match
|
|
for balanceType := range acc.BalanceMap {
|
|
for _, b := range acc.BalanceMap[balanceType] {
|
|
if a.Balance.ID != nil && b.ID == *a.Balance.ID && !b.IsExpiredAt(time.Now()) {
|
|
previousSharedGroups = b.SharedGroups
|
|
balance = b
|
|
found = true
|
|
break // only set one balance
|
|
}
|
|
}
|
|
if found {
|
|
break
|
|
}
|
|
}
|
|
// if it is not found then we create it
|
|
if !found {
|
|
if a.Balance.Type == nil { // cannot create the entry in the balance map without this info
|
|
return errors.New("missing balance type")
|
|
}
|
|
balance = &Balance{}
|
|
balance.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency
|
|
acc.BalanceMap[*a.Balance.Type] = append(acc.BalanceMap[*a.Balance.Type], balance)
|
|
}
|
|
}
|
|
if a.Balance.ID != nil && *a.Balance.ID == utils.MetaDefault { // treat it separately since modifyBalance sets expiry and others parameters, not specific for *default
|
|
if a.Balance.Value != nil {
|
|
balance.ID = *a.Balance.ID
|
|
balance.Value = a.Balance.GetValue()
|
|
balance.SetDirty() // Mark the balance as dirty since we have modified and it should be checked by action triggers
|
|
}
|
|
} else {
|
|
a.Balance.ModifyBalance(balance)
|
|
}
|
|
// modify if necessary the shared groups here
|
|
if !found || !previousSharedGroups.Equal(balance.SharedGroups) {
|
|
err := guardian.Guardian.Guard(func() error {
|
|
i := 0
|
|
for sgID := range balance.SharedGroups {
|
|
// add shared group member
|
|
sg, err := dm.GetSharedGroup(sgID, false, utils.NonTransactional)
|
|
if err != nil || sg == nil {
|
|
//than is problem
|
|
utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID))
|
|
} else {
|
|
if _, found := sg.MemberIds[acc.ID]; !found {
|
|
// add member and save
|
|
if sg.MemberIds == nil {
|
|
sg.MemberIds = make(utils.StringMap)
|
|
}
|
|
sg.MemberIds[acc.ID] = true
|
|
dm.SetSharedGroup(sg)
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
return nil
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, balance.SharedGroups.Slice()...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
acc.InitCounters()
|
|
acc.ExecuteActionTriggers(nil, fltrS)
|
|
return nil
|
|
}
|
|
|
|
// Debits some amount of user's specified balance adding the balance if it does not exists.
|
|
// Returns the remaining credit in user's balance.
|
|
func (acc *Account) debitBalanceAction(a *Action, reset, resetIfNegative bool, fltrS *FilterS) error {
|
|
if a == nil {
|
|
return errors.New("nil action")
|
|
}
|
|
bClone := a.Balance.CreateBalance()
|
|
//log.Print("Bclone: ", utils.ToJSON(a.Balance))
|
|
if bClone == nil {
|
|
return errors.New("nil balance in action")
|
|
}
|
|
if acc.BalanceMap == nil {
|
|
acc.BalanceMap = make(map[string]Balances)
|
|
}
|
|
found := false
|
|
balanceType := a.Balance.GetType()
|
|
for _, b := range acc.BalanceMap[balanceType] {
|
|
if b.IsExpiredAt(time.Now()) {
|
|
continue // just to be safe (cleaned expired balances above)
|
|
}
|
|
b.account = acc
|
|
if b.MatchFilter(a.Balance, false, false) {
|
|
if reset || (resetIfNegative && b.Value < 0) {
|
|
b.SetValue(0)
|
|
}
|
|
b.SubstractValue(bClone.GetValue())
|
|
b.dirty = true
|
|
found = true
|
|
a.balanceValue = b.GetValue()
|
|
}
|
|
}
|
|
// if it is not found then we add it to the list
|
|
if !found {
|
|
// check if the Id is *default (user trying to create the default balance)
|
|
// use only it's value value
|
|
if bClone.ID == utils.MetaDefault {
|
|
bClone = &Balance{
|
|
ID: utils.MetaDefault,
|
|
Value: -bClone.GetValue(),
|
|
}
|
|
} else {
|
|
if bClone.GetValue() != 0 {
|
|
bClone.SetValue(-bClone.GetValue())
|
|
}
|
|
}
|
|
bClone.dirty = true // Mark the balance as dirty since we have modified and it should be checked by action triggers
|
|
a.balanceValue = bClone.GetValue()
|
|
bClone.Uuid = utils.GenUUID() // alway overwrite the uuid for consistency
|
|
// load ValueFactor if defined in extra parametrs
|
|
if a.ExtraParameters != "" {
|
|
vf := ValueFactor{}
|
|
err := json.Unmarshal([]byte(a.ExtraParameters), &vf)
|
|
if err == nil {
|
|
bClone.Factor = vf
|
|
} else {
|
|
utils.Logger.Warning(fmt.Sprintf("Could load value factor from actions: extra parametrs: %s", a.ExtraParameters))
|
|
}
|
|
}
|
|
acc.BalanceMap[balanceType] = append(acc.BalanceMap[balanceType], bClone)
|
|
err := guardian.Guardian.Guard(func() error {
|
|
sgs := make([]string, len(bClone.SharedGroups))
|
|
i := 0
|
|
for sgID := range bClone.SharedGroups {
|
|
// add shared group member
|
|
sg, err := dm.GetSharedGroup(sgID, false, utils.NonTransactional)
|
|
if err != nil || sg == nil {
|
|
//than is problem
|
|
utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID))
|
|
} else {
|
|
if _, found := sg.MemberIds[acc.ID]; !found {
|
|
// add member and save
|
|
if sg.MemberIds == nil {
|
|
sg.MemberIds = make(utils.StringMap)
|
|
}
|
|
sg.MemberIds[acc.ID] = true
|
|
dm.SetSharedGroup(sg)
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
dm.CacheDataFromDB(utils.SharedGroupPrefix, sgs, true)
|
|
return nil
|
|
}, config.CgrConfig().GeneralCfg().LockingTimeout, bClone.SharedGroups.Slice()...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
acc.InitCounters()
|
|
acc.ExecuteActionTriggers(nil, fltrS)
|
|
return nil
|
|
}
|
|
|
|
func (acc *Account) getBalancesForPrefix(prefix, category, tor,
|
|
sharedGroup string, aTime time.Time) Balances {
|
|
var balances Balances
|
|
balances = append(balances, acc.BalanceMap[tor]...)
|
|
if tor != utils.MetaMonetary && tor != utils.MetaGeneric {
|
|
balances = append(balances, acc.BalanceMap[utils.MetaGeneric]...)
|
|
}
|
|
|
|
var usefulBalances Balances
|
|
for _, b := range balances {
|
|
if b.Disabled {
|
|
continue
|
|
}
|
|
if b.IsExpiredAt(aTime) || (len(b.SharedGroups) == 0 && b.GetValue() <= 0 && !b.Blocker) {
|
|
continue
|
|
}
|
|
if sharedGroup != "" && !b.SharedGroups[sharedGroup] {
|
|
continue
|
|
}
|
|
if !b.MatchCategory(category) {
|
|
continue
|
|
}
|
|
b.account = acc
|
|
|
|
if len(b.DestinationIDs) > 0 && !b.DestinationIDs[utils.MetaAny] {
|
|
for _, p := range utils.SplitPrefix(prefix, MIN_PREFIX_MATCH) {
|
|
if destIDs, err := dm.GetReverseDestination(p, true, true, utils.NonTransactional); err == nil {
|
|
foundResult := false
|
|
allInclude := true // whether it is excluded or included
|
|
for _, dID := range destIDs {
|
|
inclDest, found := b.DestinationIDs[dID]
|
|
if found {
|
|
foundResult = true
|
|
allInclude = allInclude && inclDest
|
|
}
|
|
}
|
|
// check wheter all destination ids in the balance were exclusions
|
|
allExclude := true
|
|
for _, inclDest := range b.DestinationIDs {
|
|
if inclDest {
|
|
allExclude = false
|
|
break
|
|
}
|
|
}
|
|
if foundResult || allExclude {
|
|
if allInclude {
|
|
b.precision = len(p)
|
|
usefulBalances = append(usefulBalances, b)
|
|
} else {
|
|
b.precision = 1 // fake to exit the outer loop
|
|
}
|
|
}
|
|
}
|
|
if b.precision > 0 {
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
usefulBalances = append(usefulBalances, b)
|
|
}
|
|
}
|
|
|
|
// resort by precision
|
|
usefulBalances.Sort()
|
|
// clear precision
|
|
for _, b := range usefulBalances {
|
|
b.precision = 0
|
|
}
|
|
return usefulBalances
|
|
}
|
|
|
|
// like getBalancesForPrefix but expanding shared balances
|
|
func (acc *Account) getAlldBalancesForPrefix(destination, category,
|
|
balanceType string, aTime time.Time) (bc Balances) {
|
|
balances := acc.getBalancesForPrefix(destination, category, balanceType, "", aTime)
|
|
for _, b := range balances {
|
|
if len(b.SharedGroups) > 0 {
|
|
for sgID := range b.SharedGroups {
|
|
sharedGroup, err := dm.GetSharedGroup(sgID, false, utils.NonTransactional)
|
|
if err != nil || sharedGroup == nil {
|
|
utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID))
|
|
continue
|
|
}
|
|
sharedBalances := sharedGroup.GetBalances(destination, category, balanceType, acc, aTime)
|
|
sharedBalances = sharedGroup.SortBalancesByStrategy(b, sharedBalances)
|
|
bc = append(bc, sharedBalances...)
|
|
}
|
|
} else {
|
|
bc = append(bc, b)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (acc *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun bool, goNegative bool, fltrS *FilterS) (cc *CallCost, err error) {
|
|
usefulUnitBalances := acc.getAlldBalancesForPrefix(cd.Destination, cd.Category, cd.ToR, cd.TimeStart)
|
|
usefulMoneyBalances := acc.getAlldBalancesForPrefix(cd.Destination, cd.Category, utils.MetaMonetary, cd.TimeStart)
|
|
// intiValues map[UUID]float64 and pass them to publish updating initial value
|
|
initUnitBal, initMoneyBal := balancesValues(usefulUnitBalances), balancesValues(usefulMoneyBalances)
|
|
|
|
var leftCC *CallCost
|
|
cc = cd.CreateCallCost()
|
|
var hadBalanceSubj bool
|
|
generalBalanceChecker := true
|
|
for generalBalanceChecker {
|
|
generalBalanceChecker = false
|
|
|
|
// debit minutes
|
|
unitBalanceChecker := true
|
|
for unitBalanceChecker {
|
|
// try every balance multiple times in case one becomes active or ratig changes
|
|
unitBalanceChecker = false
|
|
for _, balance := range usefulUnitBalances {
|
|
partCC, debitErr := balance.debit(cd, balance.account,
|
|
usefulMoneyBalances, count, dryRun, len(cc.Timespans) == 0, true, fltrS)
|
|
if debitErr != nil {
|
|
return nil, debitErr
|
|
}
|
|
if balance.RatingSubject != "" &&
|
|
!strings.HasPrefix(balance.RatingSubject, utils.MetaRatingSubjectPrefix) {
|
|
hadBalanceSubj = true
|
|
}
|
|
if partCC != nil {
|
|
cc.Timespans = append(cc.Timespans, partCC.Timespans...)
|
|
cc.negativeConnectFee = partCC.negativeConnectFee
|
|
cd.TimeStart = cc.GetEndTime()
|
|
// check if the calldescriptor is covered
|
|
if cd.GetDuration() <= 0 {
|
|
goto COMMIT
|
|
}
|
|
unitBalanceChecker = true
|
|
generalBalanceChecker = true
|
|
// check for max cost disconnect
|
|
if dryRun && partCC.maxCostDisconect {
|
|
// only return if we are in dry run (max call duration)
|
|
return
|
|
}
|
|
}
|
|
// check for blocker
|
|
if dryRun && balance.Blocker {
|
|
return // don't go to next balances
|
|
}
|
|
}
|
|
}
|
|
// debit money
|
|
moneyBalanceChecker := true
|
|
for moneyBalanceChecker {
|
|
// try every balance multiple times in case one becomes active or ratig changes
|
|
moneyBalanceChecker = false
|
|
for _, balance := range usefulMoneyBalances {
|
|
partCC, debitErr := balance.debit(cd, balance.account,
|
|
usefulMoneyBalances, count, dryRun, len(cc.Timespans) == 0, false, fltrS)
|
|
if debitErr != nil {
|
|
return nil, debitErr
|
|
}
|
|
if partCC != nil {
|
|
cc.Timespans = append(cc.Timespans, partCC.Timespans...)
|
|
cc.negativeConnectFee = partCC.negativeConnectFee
|
|
|
|
cd.TimeStart = cc.GetEndTime()
|
|
// check if the calldescriptor is covered
|
|
if cd.GetDuration() <= 0 {
|
|
goto COMMIT
|
|
}
|
|
moneyBalanceChecker = true
|
|
generalBalanceChecker = true
|
|
if dryRun && partCC.maxCostDisconect {
|
|
// only return if we are in dry run (max call duration)
|
|
return
|
|
}
|
|
}
|
|
// check for blocker
|
|
if dryRun && balance.Blocker {
|
|
return // don't go to next balances
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if hadBalanceSubj {
|
|
cd.RatingInfos = nil
|
|
}
|
|
leftCC, err = cd.getCost()
|
|
if err != nil {
|
|
utils.Logger.Err(fmt.Sprintf("Error getting new cost for balance subject: %v", err))
|
|
}
|
|
if leftCC.Cost == 0 && len(leftCC.Timespans) > 0 {
|
|
// put AccountID ubformation in increments
|
|
for _, ts := range leftCC.Timespans {
|
|
for _, inc := range ts.Increments {
|
|
if inc.BalanceInfo == nil {
|
|
inc.BalanceInfo = &DebitInfo{}
|
|
}
|
|
inc.BalanceInfo.AccountID = acc.ID
|
|
}
|
|
}
|
|
cc.Timespans = append(cc.Timespans, leftCC.Timespans...)
|
|
}
|
|
|
|
if leftCC.Cost > 0 && goNegative {
|
|
initialLength := len(cc.Timespans)
|
|
cc.Timespans = append(cc.Timespans, leftCC.Timespans...)
|
|
|
|
var debitedConnectFeeBalance Balance
|
|
var ok bool
|
|
|
|
if initialLength == 0 {
|
|
// this is the first add, debit the connect fee
|
|
ok, debitedConnectFeeBalance = acc.DebitConnectionFee(cc, usefulMoneyBalances, count, true, fltrS)
|
|
}
|
|
// get the default money balance
|
|
// and go negative on it with the amount still unpaid
|
|
if len(leftCC.Timespans) > 0 && leftCC.Cost > 0 && !acc.AllowNegative && !dryRun {
|
|
utils.Logger.Warning(fmt.Sprintf("<Rater> Going negative on account %s with AllowNegative: false", cd.GetAccountKey()))
|
|
}
|
|
leftCC.Timespans.Decompress()
|
|
for tsIndex, ts := range leftCC.Timespans {
|
|
if ts.Increments == nil {
|
|
ts.createIncrementsSlice()
|
|
}
|
|
|
|
if tsIndex == 0 && ts.RateInterval.Rating.ConnectFee > 0 && cc.deductConnectFee && ok {
|
|
|
|
inc := &Increment{
|
|
Duration: 0,
|
|
Cost: ts.RateInterval.Rating.ConnectFee,
|
|
BalanceInfo: &DebitInfo{
|
|
Monetary: &MonetaryInfo{
|
|
UUID: debitedConnectFeeBalance.Uuid,
|
|
ID: debitedConnectFeeBalance.ID,
|
|
Value: debitedConnectFeeBalance.Value,
|
|
},
|
|
AccountID: acc.ID,
|
|
},
|
|
}
|
|
|
|
incs := []*Increment{inc}
|
|
ts.Increments = append(incs, ts.Increments...)
|
|
}
|
|
|
|
for incIndex, increment := range ts.Increments {
|
|
// connect fee was processed and skip it
|
|
if tsIndex == 0 && incIndex == 0 && ts.RateInterval.Rating.ConnectFee > 0 && cc.deductConnectFee && ok {
|
|
continue
|
|
}
|
|
cost := increment.Cost
|
|
defaultBalance := acc.GetDefaultMoneyBalance()
|
|
defaultBalance.SubstractValue(cost)
|
|
|
|
increment.BalanceInfo.Monetary = &MonetaryInfo{
|
|
UUID: defaultBalance.Uuid,
|
|
ID: defaultBalance.ID,
|
|
Value: defaultBalance.Value,
|
|
}
|
|
increment.BalanceInfo.AccountID = acc.ID
|
|
if count {
|
|
acc.countUnits(
|
|
cost,
|
|
utils.MetaMonetary,
|
|
leftCC,
|
|
&Balance{
|
|
Value: cost,
|
|
DestinationIDs: utils.NewStringMap(leftCC.Destination),
|
|
}, fltrS)
|
|
}
|
|
}
|
|
}
|
|
|
|
// in case of going to negative we send the default balance to thresholdS to be processed
|
|
if len(config.CgrConfig().RalsCfg().ThresholdSConns) != 0 {
|
|
defaultBalance := acc.GetDefaultMoneyBalance()
|
|
acntTnt := utils.NewTenantID(acc.ID)
|
|
thEv := &utils.CGREvent{
|
|
Tenant: acntTnt.Tenant,
|
|
ID: utils.GenUUID(),
|
|
Event: map[string]any{
|
|
utils.EventType: utils.BalanceUpdate,
|
|
utils.EventSource: utils.AccountService,
|
|
utils.AccountField: acntTnt.ID,
|
|
utils.BalanceID: defaultBalance.ID,
|
|
utils.Units: defaultBalance.Value,
|
|
},
|
|
APIOpts: map[string]any{
|
|
utils.MetaEventType: utils.BalanceUpdate,
|
|
},
|
|
}
|
|
var tIDs []string
|
|
if err := connMgr.Call(context.TODO(), config.CgrConfig().RalsCfg().ThresholdSConns,
|
|
utils.ThresholdSv1ProcessEvent, thEv, &tIDs); err != nil &&
|
|
err.Error() != utils.ErrNotFound.Error() {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<AccountS> error: <%s> processing balance event <%+v> with ThresholdS.",
|
|
err.Error(), utils.ToJSON(thEv)))
|
|
}
|
|
}
|
|
}
|
|
|
|
COMMIT:
|
|
if !dryRun {
|
|
// save darty shared balances
|
|
usefulMoneyBalances.SaveDirtyBalances(acc, initMoneyBal)
|
|
usefulUnitBalances.SaveDirtyBalances(acc, initUnitBal)
|
|
}
|
|
//log.Printf("Final CC: %+v", cc)
|
|
return
|
|
}
|
|
|
|
// GetDefaultMoneyBalance returns the defaultmoney balance
|
|
func (acc *Account) GetDefaultMoneyBalance() *Balance {
|
|
for _, balance := range acc.BalanceMap[utils.MetaMonetary] {
|
|
if balance.IsDefault() {
|
|
return balance
|
|
}
|
|
}
|
|
// create default balance
|
|
defaultBalance := &Balance{
|
|
Uuid: utils.GenUUID(),
|
|
ID: utils.MetaDefault,
|
|
} // minimum weight
|
|
if acc.BalanceMap == nil {
|
|
acc.BalanceMap = make(map[string]Balances)
|
|
}
|
|
acc.BalanceMap[utils.MetaMonetary] = append(acc.BalanceMap[utils.MetaMonetary], defaultBalance)
|
|
return defaultBalance
|
|
}
|
|
|
|
// ExecuteActionTriggers scans the action triggers and execute the actions for which trigger is met
|
|
func (acc *Account) ExecuteActionTriggers(a *Action, fltrS *FilterS) {
|
|
if acc.executingTriggers {
|
|
return
|
|
}
|
|
acc.executingTriggers = true
|
|
defer func() {
|
|
acc.executingTriggers = false
|
|
}()
|
|
|
|
acc.ActionTriggers.Sort()
|
|
for _, at := range acc.ActionTriggers {
|
|
// check is effective
|
|
if at.IsExpired(time.Now()) || !at.IsActive(time.Now()) {
|
|
continue
|
|
}
|
|
|
|
// sanity check
|
|
if !strings.Contains(at.ThresholdType, "counter") && !strings.Contains(at.ThresholdType, "balance") {
|
|
continue
|
|
}
|
|
if at.Executed {
|
|
// trigger is marked as executed, so skipp it until
|
|
// the next reset (see RESET_TRIGGERS action type)
|
|
continue
|
|
}
|
|
if !at.Match(a) {
|
|
continue
|
|
}
|
|
if strings.Contains(at.ThresholdType, "counter") {
|
|
if (at.Balance.ID == nil || *at.Balance.ID != "") && at.UniqueID != "" {
|
|
at.Balance.ID = utils.StringPointer(at.UniqueID)
|
|
}
|
|
for _, counters := range acc.UnitCounters {
|
|
for _, uc := range counters {
|
|
if strings.Contains(at.ThresholdType, uc.CounterType[1:]) {
|
|
for _, c := range uc.Counters {
|
|
//log.Print("C: ", utils.ToJSON(c))
|
|
if strings.HasPrefix(at.ThresholdType, "*max") {
|
|
if c.Filter.Equal(at.Balance) && c.Value >= at.ThresholdValue {
|
|
//log.Print("HERE")
|
|
at.Execute(acc, fltrS)
|
|
}
|
|
} else { //MIN
|
|
if c.Filter.Equal(at.Balance) && c.Value <= at.ThresholdValue {
|
|
at.Execute(acc, fltrS)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else { // BALANCE
|
|
for _, b := range acc.BalanceMap[at.Balance.GetType()] {
|
|
if !b.dirty && at.ThresholdType != utils.TriggerBalanceExpired { // do not check clean balances
|
|
continue
|
|
}
|
|
switch at.ThresholdType {
|
|
case utils.TriggerMaxBalance:
|
|
if b.MatchActionTrigger(at) && b.GetValue() >= at.ThresholdValue {
|
|
at.Execute(acc, fltrS)
|
|
}
|
|
case utils.TriggerMinBalance:
|
|
if b.MatchActionTrigger(at) && b.GetValue() <= at.ThresholdValue {
|
|
at.Execute(acc, fltrS)
|
|
}
|
|
case utils.TriggerBalanceExpired:
|
|
if b.MatchActionTrigger(at) && b.IsExpiredAt(time.Now()) {
|
|
at.Execute(acc, fltrS)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
acc.CleanExpiredStuff()
|
|
}
|
|
|
|
// ResetActionTriggers marks all action trigers as ready for execution
|
|
// If the action is not nil it acts like a filter
|
|
func (acc *Account) ResetActionTriggers(a *Action, fltrS *FilterS) {
|
|
for _, at := range acc.ActionTriggers {
|
|
if !at.Match(a) {
|
|
continue
|
|
}
|
|
at.Executed = false
|
|
}
|
|
acc.ExecuteActionTriggers(a, fltrS)
|
|
}
|
|
|
|
// SetRecurrent sets/unsets recurrent flag for action triggers
|
|
func (acc *Account) SetRecurrent(a *Action, recurrent bool) {
|
|
for _, at := range acc.ActionTriggers {
|
|
if !at.Match(a) {
|
|
continue
|
|
}
|
|
at.Recurrent = recurrent
|
|
}
|
|
}
|
|
|
|
// Increments the counter for the type
|
|
func (acc *Account) countUnits(amount float64, kind string, cc *CallCost, b *Balance, fltrS *FilterS) {
|
|
acc.UnitCounters.addUnits(amount, kind, cc, b)
|
|
acc.ExecuteActionTriggers(nil, fltrS)
|
|
}
|
|
|
|
// InitCounters creates counters for all triggered actions
|
|
func (acc *Account) InitCounters() {
|
|
oldUcs := acc.UnitCounters
|
|
acc.UnitCounters = make(UnitCounters)
|
|
ucTempMap := make(map[string]*UnitCounter)
|
|
for _, at := range acc.ActionTriggers {
|
|
//log.Print("AT: ", utils.ToJSON(at))
|
|
if !strings.Contains(at.ThresholdType, "counter") {
|
|
continue
|
|
}
|
|
ct := utils.MetaCounterEvent //default
|
|
if strings.Contains(at.ThresholdType, "balance") {
|
|
ct = utils.MetaBalance
|
|
}
|
|
uc, exists := ucTempMap[at.Balance.GetType()+ct]
|
|
//log.Print("CT: ", at.Balance.GetType()+ct)
|
|
if !exists {
|
|
uc = &UnitCounter{
|
|
CounterType: ct,
|
|
}
|
|
ucTempMap[at.Balance.GetType()+ct] = uc
|
|
uc.Counters = make(CounterFilters, 0)
|
|
acc.UnitCounters[at.Balance.GetType()] = append(acc.UnitCounters[at.Balance.GetType()], uc)
|
|
}
|
|
|
|
c := &CounterFilter{Filter: at.Balance.Clone()}
|
|
if (c.Filter.ID == nil || *c.Filter.ID == "") && at.UniqueID != "" {
|
|
c.Filter.ID = utils.StringPointer(at.UniqueID)
|
|
}
|
|
//log.Print("C: ", utils.ToJSON(c))
|
|
if !uc.Counters.HasCounter(c) {
|
|
uc.Counters = append(uc.Counters, c)
|
|
}
|
|
}
|
|
// copy old counter values
|
|
for key, counters := range acc.UnitCounters {
|
|
oldCounters, found := oldUcs[key]
|
|
if !found {
|
|
continue
|
|
}
|
|
for _, uc := range counters {
|
|
for _, oldUc := range oldCounters {
|
|
if uc.CopyCounterValues(oldUc) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(acc.UnitCounters) == 0 {
|
|
acc.UnitCounters = nil // leave it nil if empty
|
|
}
|
|
}
|
|
|
|
// CleanExpiredStuff removed expired balances and actiontriggers
|
|
func (acc *Account) CleanExpiredStuff() {
|
|
if config.CgrConfig().RalsCfg().RemoveExpired {
|
|
for key, bm := range acc.BalanceMap {
|
|
for i := 0; i < len(bm); i++ {
|
|
if bm[i].IsExpiredAt(time.Now()) {
|
|
// delete it
|
|
bm = append(bm[:i], bm[i+1:]...)
|
|
}
|
|
}
|
|
acc.BalanceMap[key] = bm
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(acc.ActionTriggers); i++ {
|
|
if acc.ActionTriggers[i].IsExpired(time.Now()) {
|
|
acc.ActionTriggers = append(acc.ActionTriggers[:i], acc.ActionTriggers[i+1:]...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (acc *Account) allBalancesExpired() bool {
|
|
for _, bm := range acc.BalanceMap {
|
|
for i := 0; i < len(bm); i++ {
|
|
if !bm[i].IsExpiredAt(time.Now()) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// GetSharedGroups returns the shared groups that this user balance belnongs to
|
|
func (acc *Account) GetSharedGroups() (groups []string) {
|
|
for _, balanceChain := range acc.BalanceMap {
|
|
for _, b := range balanceChain {
|
|
for sg := range b.SharedGroups {
|
|
groups = append(groups, sg)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetUniqueSharedGroupMembers returns the acounts from the group
|
|
func (acc *Account) GetUniqueSharedGroupMembers(cd *CallDescriptor) (utils.StringMap, error) { // ToDo: make sure we return accountIDs
|
|
var balances []*Balance
|
|
balances = append(balances, acc.getBalancesForPrefix(cd.Destination, cd.Category, utils.MetaMonetary, "", cd.TimeStart)...)
|
|
balances = append(balances, acc.getBalancesForPrefix(cd.Destination, cd.Category, cd.ToR, "", cd.TimeStart)...)
|
|
// gather all shared group ids
|
|
var sharedGroupIds []string
|
|
for _, b := range balances {
|
|
for sg := range b.SharedGroups {
|
|
sharedGroupIds = append(sharedGroupIds, sg)
|
|
}
|
|
}
|
|
memberIds := make(utils.StringMap)
|
|
for _, sgID := range sharedGroupIds {
|
|
sharedGroup, err := dm.GetSharedGroup(sgID, false, utils.NonTransactional)
|
|
if err != nil {
|
|
utils.Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sgID))
|
|
return nil, err
|
|
}
|
|
for memberID := range sharedGroup.MemberIds {
|
|
memberIds[memberID] = true
|
|
}
|
|
}
|
|
return memberIds, nil
|
|
}
|
|
|
|
// Clone creates a copy of the account
|
|
func (acc *Account) Clone() *Account {
|
|
newAcc := &Account{
|
|
ID: acc.ID,
|
|
UnitCounters: acc.UnitCounters.Clone(),
|
|
AllowNegative: acc.AllowNegative,
|
|
Disabled: acc.Disabled,
|
|
}
|
|
if acc.BalanceMap != nil {
|
|
newAcc.BalanceMap = make(map[string]Balances, len(acc.BalanceMap))
|
|
for key, balanceChain := range acc.BalanceMap {
|
|
newAcc.BalanceMap[key] = balanceChain.Clone()
|
|
}
|
|
}
|
|
if acc.ActionTriggers != nil {
|
|
newAcc.ActionTriggers = make(ActionTriggers, len(acc.ActionTriggers))
|
|
for key, actionTrigger := range acc.ActionTriggers {
|
|
newAcc.ActionTriggers[key] = actionTrigger.Clone()
|
|
}
|
|
}
|
|
return newAcc
|
|
}
|
|
|
|
// DebitConnectionFee debits the connection fee
|
|
func (acc *Account) DebitConnectionFee(cc *CallCost, ufMoneyBalances Balances, count bool, block bool, fltrS *FilterS) (bool, Balance) {
|
|
var debitedBalance Balance
|
|
if !cc.deductConnectFee {
|
|
return true, debitedBalance
|
|
}
|
|
connectFee := cc.GetConnectFee()
|
|
//log.Print("CONNECT FEE: %f", connectFee)
|
|
var connectFeePaid bool
|
|
for _, b := range ufMoneyBalances {
|
|
if b.GetValue() >= connectFee {
|
|
b.SubstractValue(connectFee)
|
|
// the conect fee is not refundable!
|
|
if count {
|
|
acc.countUnits(connectFee, utils.MetaMonetary, cc, b, fltrS)
|
|
}
|
|
connectFeePaid = true
|
|
debitedBalance = *b
|
|
break
|
|
}
|
|
if b.Blocker && block { // stop here
|
|
return false, debitedBalance
|
|
}
|
|
}
|
|
// debit connect fee
|
|
if connectFee > 0 && !connectFeePaid {
|
|
cc.negativeConnectFee = true
|
|
// there are no money for the connect fee; go negative
|
|
b := acc.GetDefaultMoneyBalance()
|
|
b.SubstractValue(connectFee)
|
|
debitedBalance = *b
|
|
// the conect fee is not refundable!
|
|
if count {
|
|
acc.countUnits(connectFee, utils.MetaMonetary, cc, b, fltrS)
|
|
}
|
|
}
|
|
return true, debitedBalance
|
|
}
|
|
|
|
// GetID returns the account ID
|
|
func (acc *Account) GetID() string {
|
|
split := strings.Split(acc.ID, utils.ConcatenatedKeySep)
|
|
if len(split) != 2 {
|
|
return ""
|
|
}
|
|
return split[1]
|
|
}
|
|
|
|
// AsOldStructure used in some api for transition
|
|
func (acc *Account) AsOldStructure() any {
|
|
type Balance struct {
|
|
Uuid string //system wide unique
|
|
Id string // account wide unique
|
|
Value float64
|
|
ExpirationDate time.Time
|
|
Weight float64
|
|
DestinationIds string
|
|
RatingSubject string
|
|
Category string
|
|
SharedGroup string
|
|
Timings []*RITiming
|
|
TimingIDs string
|
|
Disabled bool
|
|
}
|
|
type Balances []*Balance
|
|
type UnitsCounter struct {
|
|
BalanceType string
|
|
// Units float64
|
|
Balances Balances // first balance is the general one (no destination)
|
|
}
|
|
type ActionTrigger struct {
|
|
Id string
|
|
ThresholdType string
|
|
ThresholdValue float64
|
|
Recurrent bool
|
|
MinSleep time.Duration
|
|
BalanceId string
|
|
BalanceType string
|
|
BalanceDestinationIds string
|
|
BalanceWeight float64
|
|
BalanceExpirationDate time.Time
|
|
BalanceTimingTags string
|
|
BalanceRatingSubject string
|
|
BalanceCategory string
|
|
BalanceSharedGroup string
|
|
BalanceDisabled bool
|
|
Weight float64
|
|
ActionsId string
|
|
MinQueuedItems int
|
|
Executed bool
|
|
}
|
|
type ActionTriggers []*ActionTrigger
|
|
type Account struct {
|
|
Id string
|
|
BalanceMap map[string]Balances
|
|
UnitCounters []*UnitsCounter
|
|
ActionTriggers ActionTriggers
|
|
AllowNegative bool
|
|
Disabled bool
|
|
}
|
|
|
|
result := &Account{
|
|
Id: utils.MetaOut + utils.ConcatenatedKeySep + acc.ID,
|
|
BalanceMap: make(map[string]Balances, len(acc.BalanceMap)),
|
|
UnitCounters: make([]*UnitsCounter, len(acc.UnitCounters)),
|
|
ActionTriggers: make(ActionTriggers, len(acc.ActionTriggers)),
|
|
AllowNegative: acc.AllowNegative,
|
|
Disabled: acc.Disabled,
|
|
}
|
|
for balanceType, counters := range acc.UnitCounters {
|
|
for i, uc := range counters {
|
|
if uc == nil {
|
|
continue
|
|
}
|
|
result.UnitCounters[i] = &UnitsCounter{
|
|
BalanceType: balanceType,
|
|
Balances: make(Balances, len(uc.Counters)),
|
|
}
|
|
if len(uc.Counters) > 0 {
|
|
for j, c := range uc.Counters {
|
|
result.UnitCounters[i].Balances[j] = &Balance{
|
|
Uuid: c.Filter.GetUuid(),
|
|
Id: c.Filter.GetID(),
|
|
Value: c.Filter.GetValue(),
|
|
ExpirationDate: c.Filter.GetExpirationDate(),
|
|
Weight: c.Filter.GetWeight(),
|
|
DestinationIds: c.Filter.GetDestinationIDs().String(),
|
|
RatingSubject: c.Filter.GetRatingSubject(),
|
|
Category: c.Filter.GetCategories().String(),
|
|
SharedGroup: c.Filter.GetSharedGroups().String(),
|
|
Timings: c.Filter.Timings,
|
|
TimingIDs: c.Filter.GetTimingIDs().String(),
|
|
Disabled: c.Filter.GetDisabled(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for i, at := range acc.ActionTriggers {
|
|
b := at.Balance.CreateBalance()
|
|
result.ActionTriggers[i] = &ActionTrigger{
|
|
Id: at.ID,
|
|
ThresholdType: at.ThresholdType,
|
|
ThresholdValue: at.ThresholdValue,
|
|
Recurrent: at.Recurrent,
|
|
MinSleep: at.MinSleep,
|
|
BalanceType: at.Balance.GetType(),
|
|
BalanceId: b.ID,
|
|
BalanceDestinationIds: b.DestinationIDs.String(),
|
|
BalanceWeight: b.Weight,
|
|
BalanceExpirationDate: b.ExpirationDate,
|
|
BalanceTimingTags: b.TimingIDs.String(),
|
|
BalanceRatingSubject: b.RatingSubject,
|
|
BalanceCategory: b.Categories.String(),
|
|
BalanceSharedGroup: b.SharedGroups.String(),
|
|
BalanceDisabled: b.Disabled,
|
|
Weight: at.Weight,
|
|
ActionsId: at.ActionsID,
|
|
MinQueuedItems: at.MinQueuedItems,
|
|
Executed: at.Executed,
|
|
}
|
|
}
|
|
for key, values := range acc.BalanceMap {
|
|
if len(values) > 0 {
|
|
key += utils.MetaOut
|
|
result.BalanceMap[key] = make(Balances, len(values))
|
|
for i, b := range values {
|
|
result.BalanceMap[key][i] = &Balance{
|
|
Uuid: b.Uuid,
|
|
Id: b.ID,
|
|
Value: b.Value,
|
|
ExpirationDate: b.ExpirationDate,
|
|
Weight: b.Weight,
|
|
DestinationIds: b.DestinationIDs.String(),
|
|
RatingSubject: b.RatingSubject,
|
|
Category: b.Categories.String(),
|
|
SharedGroup: b.SharedGroups.String(),
|
|
Timings: b.Timings,
|
|
TimingIDs: b.TimingIDs.String(),
|
|
Disabled: b.Disabled,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AsAccountSummary converts the account into AccountSummary
|
|
func (acc *Account) AsAccountSummary() *AccountSummary {
|
|
idSplt := strings.Split(acc.ID, utils.ConcatenatedKeySep)
|
|
ad := &AccountSummary{AllowNegative: acc.AllowNegative, Disabled: acc.Disabled}
|
|
if len(idSplt) == 1 {
|
|
ad.ID = idSplt[0]
|
|
} else if len(idSplt) == 2 {
|
|
ad.Tenant = idSplt[0]
|
|
ad.ID = idSplt[1]
|
|
}
|
|
|
|
for _, balanceType := range []string{utils.MetaData, utils.MetaSMS, utils.MetaMMS, utils.MetaVoice, utils.MetaGeneric, utils.MetaMonetary} {
|
|
balances, has := acc.BalanceMap[balanceType]
|
|
if !has {
|
|
continue
|
|
}
|
|
for _, balance := range balances {
|
|
ad.BalanceSummaries = append(ad.BalanceSummaries, balance.AsBalanceSummary(balanceType))
|
|
}
|
|
}
|
|
return ad
|
|
}
|
|
|
|
// Publish sends the account to stats and threshold
|
|
func (acc *Account) Publish(initBal map[string]float64) {
|
|
acntSummary := acc.AsAccountSummary()
|
|
for _, currentBal := range acntSummary.BalanceSummaries {
|
|
currentBal.Initial = initBal[currentBal.UUID]
|
|
}
|
|
cgrEv := &utils.CGREvent{
|
|
Tenant: acntSummary.Tenant,
|
|
ID: utils.GenUUID(),
|
|
Time: utils.TimePointer(time.Now()),
|
|
Event: acntSummary.AsMapInterface(),
|
|
APIOpts: map[string]any{
|
|
utils.MetaEventType: utils.AccountUpdate,
|
|
},
|
|
}
|
|
if len(config.CgrConfig().RalsCfg().ThresholdSConns) != 0 {
|
|
var tIDs []string
|
|
if err := connMgr.Call(context.TODO(), config.CgrConfig().RalsCfg().ThresholdSConns,
|
|
utils.ThresholdSv1ProcessEvent, cgrEv, &tIDs); err != nil &&
|
|
err.Error() != utils.ErrNotFound.Error() {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<AccountS> error: %s processing account event %+v with ThresholdS.", err.Error(), cgrEv))
|
|
}
|
|
}
|
|
if len(config.CgrConfig().RalsCfg().StatSConns) != 0 {
|
|
var stsIDs []string
|
|
if err := connMgr.Call(context.TODO(), config.CgrConfig().RalsCfg().StatSConns,
|
|
utils.StatSv1ProcessEvent, cgrEv, &stsIDs); err != nil &&
|
|
err.Error() != utils.ErrNotFound.Error() {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<AccountS> error: %s processing account event %+v with StatS.", err.Error(), cgrEv))
|
|
}
|
|
}
|
|
}
|
|
|
|
func balancesValues(bals Balances) (init map[string]float64) {
|
|
init = make(map[string]float64)
|
|
for _, bal := range bals {
|
|
init[bal.Uuid] = bal.Value
|
|
}
|
|
return
|
|
}
|
|
|
|
// NewAccountSummaryFromJSON creates a new AcccountSummary from a json string
|
|
func NewAccountSummaryFromJSON(jsn string) (acntSummary *AccountSummary, err error) {
|
|
if !utils.SliceHasMember([]string{"", "null"}, jsn) { // Unmarshal only when content
|
|
err = json.Unmarshal([]byte(jsn), &acntSummary)
|
|
}
|
|
return
|
|
}
|
|
|
|
// AccountSummary contains compressed information about an Account
|
|
type AccountSummary struct {
|
|
Tenant string
|
|
ID string
|
|
BalanceSummaries BalanceSummaries
|
|
AllowNegative bool
|
|
Disabled bool
|
|
}
|
|
|
|
// Clone creates a copy of the structure
|
|
func (as *AccountSummary) Clone() (cln *AccountSummary) {
|
|
cln = new(AccountSummary)
|
|
cln.Tenant = as.Tenant
|
|
cln.ID = as.ID
|
|
cln.AllowNegative = as.AllowNegative
|
|
cln.Disabled = as.Disabled
|
|
if as.BalanceSummaries != nil {
|
|
cln.BalanceSummaries = make(BalanceSummaries, len(as.BalanceSummaries))
|
|
for i, bs := range as.BalanceSummaries {
|
|
cln.BalanceSummaries[i] = new(BalanceSummary)
|
|
*cln.BalanceSummaries[i] = *bs
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// UpdateInitialValue keeps the old initial balance value
|
|
func (as *AccountSummary) UpdateInitialValue(old *AccountSummary) {
|
|
if old == nil {
|
|
return
|
|
}
|
|
for _, initialBal := range old.BalanceSummaries {
|
|
removed := true
|
|
for _, currentBal := range as.BalanceSummaries {
|
|
if currentBal.UUID == initialBal.UUID {
|
|
currentBal.Initial = initialBal.Initial
|
|
removed = false
|
|
break
|
|
}
|
|
}
|
|
if removed { // add back the expired balances
|
|
initialBal.Value = 0 // it expired so lose all the values
|
|
initialBal.Initial = 0 // only keep track of it in this
|
|
as.BalanceSummaries = append(as.BalanceSummaries, initialBal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetInitialValue set initial balance value
|
|
func (as *AccountSummary) SetInitialValue(old *AccountSummary) {
|
|
if old == nil {
|
|
return
|
|
}
|
|
for _, initialBal := range old.BalanceSummaries {
|
|
removed := true
|
|
for _, currentBal := range as.BalanceSummaries {
|
|
if currentBal.UUID == initialBal.UUID {
|
|
currentBal.Initial = initialBal.Value
|
|
removed = false
|
|
break
|
|
}
|
|
}
|
|
if removed { // add back the expired balances
|
|
initialBal.Value = 0 // it expired so lose all the values
|
|
initialBal.Initial = 0 // only keep track of it in this
|
|
as.BalanceSummaries = append(as.BalanceSummaries, initialBal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetBalanceWithID returns a Balance given balance type and balance ID
|
|
func (acc *Account) GetBalanceWithID(blcType, blcID string) (blc *Balance) {
|
|
for _, blc = range acc.BalanceMap[blcType] {
|
|
if blc.ID == blcID {
|
|
return
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FieldAsInterface func to help EventCost FieldAsInterface
|
|
func (as *AccountSummary) FieldAsInterface(fldPath []string) (val any, err error) {
|
|
if as == nil || len(fldPath) == 0 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
switch fldPath[0] {
|
|
default:
|
|
opath, indx := utils.GetPathIndex(fldPath[0])
|
|
if opath == utils.BalanceSummaries && indx != nil {
|
|
if len(as.BalanceSummaries) <= *indx {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
bl := as.BalanceSummaries[*indx]
|
|
if len(fldPath) == 1 {
|
|
return bl, nil
|
|
}
|
|
return bl.FieldAsInterface(fldPath[1:])
|
|
}
|
|
return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
|
|
case utils.Tenant:
|
|
if len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return as.Tenant, nil
|
|
case utils.ID:
|
|
if len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return as.ID, nil
|
|
case utils.BalanceSummaries:
|
|
if len(fldPath) == 1 {
|
|
return as.BalanceSummaries, nil
|
|
}
|
|
for _, bs := range as.BalanceSummaries {
|
|
if bs.ID == fldPath[1] {
|
|
if len(fldPath) == 2 {
|
|
return bs, nil
|
|
}
|
|
return bs.FieldAsInterface(fldPath[2:])
|
|
}
|
|
}
|
|
return nil, utils.ErrNotFound
|
|
case utils.AllowNegative:
|
|
if len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return as.AllowNegative, nil
|
|
case utils.Disabled:
|
|
if len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return as.Disabled, nil
|
|
}
|
|
}
|
|
|
|
func (as *AccountSummary) FieldAsString(fldPath []string) (val string, err error) {
|
|
var iface any
|
|
iface, err = as.FieldAsInterface(fldPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return utils.IfaceAsString(iface), nil
|
|
}
|
|
|
|
// String implements utils.DataProvider
|
|
func (as *AccountSummary) String() string {
|
|
return utils.ToIJSON(as)
|
|
}
|
|
|
|
func (as *AccountSummary) AsMapInterface() map[string]any {
|
|
return map[string]any{
|
|
utils.Tenant: as.Tenant,
|
|
utils.ID: as.ID,
|
|
utils.AllowNegative: as.AllowNegative,
|
|
utils.Disabled: as.Disabled,
|
|
utils.BalanceSummaries: as.BalanceSummaries,
|
|
}
|
|
}
|
|
|
|
func (acc *Account) String() string {
|
|
return utils.ToJSON(acc)
|
|
}
|
|
|
|
func (acc *Account) FieldAsInterface(fldPath []string) (val any, err error) {
|
|
if len(fldPath) == 0 {
|
|
return acc, nil
|
|
}
|
|
if acc == nil {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
switch fldPath[0] {
|
|
default:
|
|
opath, indx := utils.GetPathIndexString(fldPath[0])
|
|
if indx != nil {
|
|
switch opath {
|
|
case utils.BalanceMap:
|
|
bl, has := acc.BalanceMap[*indx]
|
|
if !has {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
if len(fldPath) == 1 {
|
|
return bl, nil
|
|
}
|
|
return bl.FieldAsInterface(fldPath[1:])
|
|
case utils.UnitCounters:
|
|
uc, has := acc.UnitCounters[*indx]
|
|
if !has || len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return uc, nil
|
|
case utils.ActionTriggers:
|
|
var idx int
|
|
if idx, err = strconv.Atoi(*indx); err != nil {
|
|
return
|
|
}
|
|
if len(acc.ActionTriggers) <= idx {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
at := acc.ActionTriggers[idx]
|
|
if len(fldPath) == 1 {
|
|
return at, nil
|
|
}
|
|
return at.FieldAsInterface(fldPath[1:])
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("unsupported field prefix: <%s>", fldPath[0])
|
|
case utils.ID:
|
|
if len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return acc.ID, nil
|
|
case utils.BalanceMap:
|
|
if len(fldPath) == 1 {
|
|
return acc.BalanceMap, nil
|
|
}
|
|
opath, indx := utils.GetPathIndex(fldPath[1])
|
|
if bc, has := acc.BalanceMap[opath]; has {
|
|
if indx != nil {
|
|
if len(bc) <= *indx {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
c := bc[*indx]
|
|
if len(fldPath) == 2 {
|
|
return c, nil
|
|
}
|
|
return c.FieldAsInterface(fldPath[2:])
|
|
}
|
|
if len(fldPath) == 2 {
|
|
return bc, nil
|
|
}
|
|
return bc.FieldAsInterface(fldPath[2:])
|
|
}
|
|
return nil, utils.ErrNotFound
|
|
case utils.UnitCounters:
|
|
if len(fldPath) == 1 {
|
|
return acc.UnitCounters, nil
|
|
}
|
|
if uc, has := acc.UnitCounters[fldPath[1]]; has {
|
|
if len(fldPath) == 2 {
|
|
return uc, nil
|
|
}
|
|
var indx int
|
|
if indx, err = strconv.Atoi(fldPath[2]); err != nil {
|
|
return
|
|
}
|
|
if len(uc) <= indx {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
c := uc[indx]
|
|
if len(fldPath) == 1 {
|
|
return c, nil
|
|
}
|
|
return c.FieldAsInterface(fldPath[3:])
|
|
}
|
|
return nil, utils.ErrNotFound
|
|
case utils.ActionTriggers:
|
|
if len(fldPath) == 1 {
|
|
return acc.ActionTriggers, nil
|
|
}
|
|
for _, at := range acc.ActionTriggers {
|
|
if at.ID == fldPath[1] {
|
|
if len(fldPath) == 2 {
|
|
return at, nil
|
|
}
|
|
return at.FieldAsInterface(fldPath[2:])
|
|
}
|
|
}
|
|
return nil, utils.ErrNotFound
|
|
case utils.AllowNegative:
|
|
if len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return acc.AllowNegative, nil
|
|
case utils.Disabled:
|
|
if len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return acc.Disabled, nil
|
|
case utils.UpdateTime:
|
|
if len(fldPath) != 1 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
return acc.UpdateTime, nil
|
|
}
|
|
}
|
|
|
|
func (acc *Account) FieldAsString(fldPath []string) (val string, err error) {
|
|
var iface any
|
|
iface, err = acc.FieldAsInterface(fldPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return utils.IfaceAsString(iface), nil
|
|
}
|