mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
424 lines
14 KiB
Go
424 lines
14 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 Affero 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 Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
package cdrs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/cgrates/birpc/context"
|
|
"github.com/cgrates/cgrates/attributes"
|
|
"github.com/cgrates/cgrates/chargers"
|
|
"github.com/cgrates/cgrates/config"
|
|
"github.com/cgrates/cgrates/engine"
|
|
"github.com/cgrates/cgrates/utils"
|
|
"github.com/cgrates/guardian"
|
|
)
|
|
|
|
func newMapEventFromReqForm(r *http.Request) (mp engine.MapEvent, err error) {
|
|
if r.Form == nil {
|
|
if err = r.ParseForm(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
mp = engine.MapEvent{utils.Source: r.RemoteAddr}
|
|
for k, vals := range r.Form {
|
|
mp[k] = vals[0] // We only support the first value for now, if more are provided it is considered remote's fault
|
|
}
|
|
return
|
|
}
|
|
|
|
// NewCDRServer is a constructor for CDRServer
|
|
func NewCDRServer(cfg *config.CGRConfig, dm *engine.DataManager, filterS *engine.FilterS, connMgr *engine.ConnManager) *CDRServer {
|
|
return &CDRServer{
|
|
cfg: cfg,
|
|
dm: dm,
|
|
guard: guardian.Guardian,
|
|
fltrS: filterS,
|
|
connMgr: connMgr,
|
|
}
|
|
}
|
|
|
|
// CDRServer stores and rates CDRs
|
|
type CDRServer struct {
|
|
cfg *config.CGRConfig
|
|
dm *engine.DataManager
|
|
guard *guardian.GuardianLocker
|
|
fltrS *engine.FilterS
|
|
connMgr *engine.ConnManager
|
|
}
|
|
|
|
// chrgrSProcessEvent forks CGREventWithOpts into multiples based on matching ChargerS profiles
|
|
func (cdrS *CDRServer) chrgrSProcessEvent(ctx *context.Context, cgrEv *utils.CGREvent) (cgrEvs []*utils.CGREvent, err error) {
|
|
var chrgrs []*chargers.ChrgSProcessEventReply
|
|
if err = cdrS.connMgr.Call(ctx, cdrS.cfg.CdrsCfg().ChargerSConns,
|
|
utils.ChargerSv1ProcessEvent,
|
|
cgrEv, &chrgrs); err != nil {
|
|
return
|
|
}
|
|
if len(chrgrs) == 0 {
|
|
return
|
|
}
|
|
cgrEvs = make([]*utils.CGREvent, len(chrgrs))
|
|
for i, cgrPrfl := range chrgrs {
|
|
cgrEvs[i] = cgrPrfl.CGREvent
|
|
}
|
|
return
|
|
}
|
|
|
|
// attrSProcessEvent will send the event to StatS if the connection is configured
|
|
func (cdrS *CDRServer) attrSProcessEvent(ctx *context.Context, cgrEv *utils.CGREvent) (err error) {
|
|
var rplyEv attributes.AttrSProcessEventReply
|
|
if cgrEv.APIOpts == nil {
|
|
cgrEv.APIOpts = make(map[string]any)
|
|
}
|
|
cgrEv.APIOpts[utils.MetaSubsys] = utils.MetaCDRs
|
|
cgrEv.APIOpts[utils.OptsContext] = utils.FirstNonEmpty(
|
|
utils.IfaceAsString(cgrEv.APIOpts[utils.OptsContext]),
|
|
utils.MetaCDRs)
|
|
if err = cdrS.connMgr.Call(ctx, cdrS.cfg.CdrsCfg().AttributeSConns,
|
|
utils.AttributeSv1ProcessEvent,
|
|
cgrEv, &rplyEv); err != nil {
|
|
if err.Error() == utils.ErrNotFound.Error() {
|
|
err = nil
|
|
}
|
|
return
|
|
}
|
|
if len(rplyEv.AlteredFields) != 0 {
|
|
*cgrEv = *rplyEv.CGREvent
|
|
}
|
|
return
|
|
}
|
|
|
|
// rateSProcessEvent will send the event to rateS and attach the cost received back to event
|
|
func (cdrS *CDRServer) rateSCostForEvent(ctx *context.Context, cgrEv *utils.CGREvent) (err error) {
|
|
var rpCost utils.RateProfileCost
|
|
if err = cdrS.connMgr.Call(ctx, cdrS.cfg.CdrsCfg().RateSConns,
|
|
utils.RateSv1CostForEvent,
|
|
cgrEv, &rpCost); err != nil {
|
|
return
|
|
}
|
|
cgrEv.APIOpts[utils.MetaRateSCost] = rpCost
|
|
return
|
|
}
|
|
|
|
// accountSDebitEvent will send the event to accountS and attach the cost received back to event
|
|
func (cdrS *CDRServer) accountSDebitEvent(ctx *context.Context, cgrEv *utils.CGREvent) (err error) {
|
|
acntCost := new(utils.EventCharges)
|
|
if err = cdrS.connMgr.Call(ctx, cdrS.cfg.CdrsCfg().AccountSConns,
|
|
utils.AccountSv1DebitAbstracts, cgrEv, acntCost); err != nil {
|
|
return
|
|
}
|
|
cgrEv.APIOpts[utils.MetaAccountsCost] = acntCost
|
|
return
|
|
}
|
|
|
|
// accountSRefundCharges will refund the charges into Account
|
|
func (cdrS *CDRServer) accountSRefundCharges(ctx *context.Context, tnt string, eChrgs *utils.EventCharges,
|
|
apiOpts map[string]any) (err error) {
|
|
var rply string
|
|
argsRefund := &utils.APIEventCharges{
|
|
Tenant: tnt,
|
|
APIOpts: apiOpts,
|
|
EventCharges: eChrgs,
|
|
}
|
|
if err = cdrS.connMgr.Call(ctx, cdrS.cfg.CdrsCfg().AccountSConns,
|
|
utils.AccountSv1RefundCharges, argsRefund, &rply); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// thdSProcessEvent will send the event to ThresholdS
|
|
func (cdrS *CDRServer) thdSProcessEvent(ctx *context.Context, cgrEv *utils.CGREvent) (err error) {
|
|
var tIDs []string
|
|
// we clone the CGREvent so we can add EventType without being propagated
|
|
cgrEv = cgrEv.Clone()
|
|
cgrEv.APIOpts[utils.MetaEventType] = utils.CDRKey
|
|
if err = cdrS.connMgr.Call(ctx, cdrS.cfg.CdrsCfg().ThresholdSConns,
|
|
utils.ThresholdSv1ProcessEvent,
|
|
cgrEv, &tIDs); err != nil &&
|
|
err.Error() == utils.ErrNotFound.Error() {
|
|
err = nil // NotFound is not considered error
|
|
}
|
|
return
|
|
}
|
|
|
|
// statSProcessEvent will send the event to StatS
|
|
func (cdrS *CDRServer) statSProcessEvent(ctx *context.Context, cgrEv *utils.CGREvent) (err error) {
|
|
var reply []string
|
|
if err = cdrS.connMgr.Call(ctx, cdrS.cfg.CdrsCfg().StatSConns,
|
|
utils.StatSv1ProcessEvent,
|
|
cgrEv.Clone(), &reply); err != nil &&
|
|
err.Error() == utils.ErrNotFound.Error() {
|
|
err = nil // NotFound is not considered error
|
|
}
|
|
return
|
|
}
|
|
|
|
// eeSProcessEvent will process the event with the EEs component
|
|
func (cdrS *CDRServer) eeSProcessEvent(ctx *context.Context, cgrEv *utils.CGREventWithEeIDs) (err error) {
|
|
var reply map[string]map[string]any
|
|
if err = cdrS.connMgr.Call(ctx, cdrS.cfg.CdrsCfg().EEsConns,
|
|
utils.EeSv1ProcessEvent,
|
|
cgrEv, &reply); err != nil &&
|
|
err.Error() == utils.ErrNotFound.Error() {
|
|
err = nil // NotFound is not considered error
|
|
}
|
|
return
|
|
}
|
|
|
|
// processEvents processes CGREvents based on arguments.
|
|
// In case of partially executed, both the error and the events will be returned.
|
|
func (cdrS *CDRServer) processEvents(ctx *context.Context, evs []*utils.CGREvent) ([]*utils.EventsWithOpts, error) {
|
|
for _, ev := range evs {
|
|
attrS, err := engine.GetBoolOpts(ctx, ev.Tenant, ev.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Attributes,
|
|
utils.MetaAttributes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.MetaAttributes, err)
|
|
}
|
|
if !attrS {
|
|
|
|
continue
|
|
}
|
|
if err = cdrS.attrSProcessEvent(ctx, ev); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%v> processing event %s with %s",
|
|
utils.CDRs, err, utils.ToJSON(ev), utils.AttributeS))
|
|
return nil, utils.NewErrAttributeS(err)
|
|
}
|
|
}
|
|
|
|
// Allocate capacity equal to the number of events for slight optimization.
|
|
cgrEvs := make([]*utils.CGREvent, 0, len(evs))
|
|
|
|
for _, ev := range evs {
|
|
chrgS, err := engine.GetBoolOpts(ctx, ev.Tenant, ev.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Chargers,
|
|
utils.MetaChargers)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.MetaChargers, err)
|
|
}
|
|
if !chrgS {
|
|
cgrEvs = append(cgrEvs, ev)
|
|
continue
|
|
}
|
|
chrgEvs, err := cdrS.chrgrSProcessEvent(ctx, ev)
|
|
if err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%v> processing event %s with %s",
|
|
utils.CDRs, err, utils.ToJSON(ev), utils.ChargerS))
|
|
return nil, utils.NewErrChargerS(err)
|
|
}
|
|
cgrEvs = append(cgrEvs, chrgEvs...)
|
|
}
|
|
|
|
var partiallyExecuted bool // from here actions are optional and a general error is returned
|
|
|
|
for _, cgrEv := range cgrEvs {
|
|
rateS, err := engine.GetBoolOpts(ctx, cgrEv.Tenant, cgrEv.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Rates,
|
|
utils.MetaRates)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.MetaRates, err)
|
|
}
|
|
if !rateS {
|
|
continue
|
|
}
|
|
if err := cdrS.rateSCostForEvent(ctx, cgrEv); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%v> processing event %s with %s",
|
|
utils.CDRs, err, utils.ToJSON(cgrEv), utils.RateS))
|
|
partiallyExecuted = true
|
|
}
|
|
}
|
|
|
|
for _, cgrEv := range cgrEvs {
|
|
acntS, err := engine.GetBoolOpts(ctx, cgrEv.Tenant, cgrEv.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Accounts,
|
|
utils.MetaAccounts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.MetaAccounts, err)
|
|
}
|
|
if !acntS {
|
|
continue
|
|
}
|
|
if ecCostIface, wasCharged := cgrEv.APIOpts[utils.MetaAccountsCost]; wasCharged {
|
|
ecCostMap, ok := ecCostIface.(*utils.EventCharges)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected %s to be a *utils.EventCharges, got %T", utils.MetaAccountsCost, ecCostMap)
|
|
}
|
|
|
|
// before converting into EventChargers, we must get the JSON encoding and Unmarshal it into an EventChargers
|
|
btsEvCh, err := json.Marshal(ecCostMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ecCost utils.EventCharges
|
|
if err = json.Unmarshal(btsEvCh, &ecCost); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := cdrS.accountSRefundCharges(ctx, cgrEv.Tenant, &ecCost, cgrEv.APIOpts); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%v> processing event %s with %s",
|
|
utils.CDRs, err, utils.ToJSON(cgrEv), utils.AccountS))
|
|
partiallyExecuted = true
|
|
}
|
|
}
|
|
if err := cdrS.accountSDebitEvent(ctx, cgrEv); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%v> processing event %s with %s",
|
|
utils.CDRs, err, utils.ToJSON(cgrEv), utils.AccountS))
|
|
partiallyExecuted = true
|
|
}
|
|
}
|
|
|
|
// populate cost from accounts or rates for every event
|
|
for _, cgrEv := range cgrEvs {
|
|
if cost := populateCost(cgrEv.APIOpts); cost != nil {
|
|
cgrEv.APIOpts[utils.MetaCost] = cost
|
|
}
|
|
}
|
|
|
|
for _, cgrEv := range cgrEvs {
|
|
store, err := engine.GetBoolOpts(ctx, cgrEv.Tenant, cgrEv.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Store,
|
|
utils.MetaStore)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.MetaStore, err)
|
|
}
|
|
if !store {
|
|
continue
|
|
}
|
|
rerate, err := engine.GetBoolOpts(ctx, cgrEv.Tenant, cgrEv.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Rerate,
|
|
utils.MetaRerate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.MetaRerate, err)
|
|
}
|
|
|
|
// Prevent 'assignment to entry in nil map' panic.
|
|
if cgrEv.APIOpts == nil {
|
|
cgrEv.APIOpts = make(map[string]any)
|
|
}
|
|
|
|
// Make sure *cdrID key exists in opts, as it's needed to identify CDRs during CRUD operations.
|
|
if _, ok := cgrEv.APIOpts[utils.MetaCDRID]; !ok {
|
|
cgrEv.APIOpts[utils.MetaCDRID] = utils.GetUniqueCDRID(cgrEv)
|
|
}
|
|
|
|
if err := cdrS.dm.SetCDR(ctx, cgrEv, false); err != nil {
|
|
if err != utils.ErrExists || !rerate {
|
|
|
|
// ToDo: add refund logic
|
|
return nil, fmt.Errorf("storing CDR %s failed: %w", utils.ToJSON(cgrEv), err)
|
|
}
|
|
if err = cdrS.dm.SetCDR(ctx, cgrEv, true); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%s> updating CDR %+v",
|
|
utils.CDRs, err.Error(), utils.ToJSON(cgrEv)))
|
|
return nil, utils.ErrPartiallyExecuted
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, cgrEv := range cgrEvs {
|
|
export, err := engine.GetBoolOpts(ctx, cgrEv.Tenant, cgrEv.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Export,
|
|
utils.OptsCDRsExport)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.OptsCDRsExport, err)
|
|
}
|
|
if !export {
|
|
continue
|
|
}
|
|
evWithOpts := &utils.CGREventWithEeIDs{
|
|
CGREvent: cgrEv,
|
|
EeIDs: cdrS.cfg.CdrsCfg().OnlineCDRExports,
|
|
}
|
|
if err := cdrS.eeSProcessEvent(ctx, evWithOpts); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%v> exporting cdr %s",
|
|
utils.CDRs, err, utils.ToJSON(evWithOpts)))
|
|
partiallyExecuted = true
|
|
}
|
|
}
|
|
|
|
for _, cgrEv := range cgrEvs {
|
|
thdS, err := engine.GetBoolOpts(ctx, cgrEv.Tenant, cgrEv.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Thresholds,
|
|
utils.MetaThresholds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.MetaThresholds, err)
|
|
}
|
|
if !thdS {
|
|
continue
|
|
}
|
|
if err := cdrS.thdSProcessEvent(ctx, cgrEv); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%v> processing event %s with %s",
|
|
utils.CDRs, err, utils.ToJSON(cgrEv), utils.ThresholdS))
|
|
partiallyExecuted = true
|
|
}
|
|
}
|
|
|
|
for _, cgrEv := range cgrEvs {
|
|
stS, err := engine.GetBoolOpts(ctx, cgrEv.Tenant, cgrEv.AsDataProvider(), nil, cdrS.fltrS, cdrS.cfg.CdrsCfg().Opts.Stats,
|
|
utils.MetaStats)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving %s option failed: %w", utils.MetaStats, err)
|
|
}
|
|
if !stS {
|
|
continue
|
|
}
|
|
if err := cdrS.statSProcessEvent(ctx, cgrEv); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf("<%s> error: <%v> processing event %s with %s",
|
|
utils.CDRs, err, utils.ToJSON(cgrEv), utils.StatS))
|
|
partiallyExecuted = true
|
|
}
|
|
}
|
|
|
|
// now that we did all the requested processed events, we have to build our EventsWithOpts
|
|
outEvs := make([]*utils.EventsWithOpts, len(cgrEvs))
|
|
for i, cgrEv := range cgrEvs {
|
|
outEvs[i] = &utils.EventsWithOpts{
|
|
Event: cgrEv.Event,
|
|
Opts: cgrEv.APIOpts,
|
|
}
|
|
}
|
|
|
|
if partiallyExecuted {
|
|
return outEvs, utils.ErrPartiallyExecuted
|
|
}
|
|
return outEvs, nil
|
|
}
|
|
|
|
func populateCost(cgrOpts map[string]any) *utils.Decimal {
|
|
// if the cost is already present, get out
|
|
if _, has := cgrOpts[utils.MetaCost]; has {
|
|
return nil
|
|
}
|
|
// check firstly in accounts
|
|
if accCost, has := cgrOpts[utils.MetaAccountsCost]; has {
|
|
return accCost.(*utils.EventCharges).Concretes
|
|
}
|
|
// after check in rates
|
|
if rtCost, has := cgrOpts[utils.MetaRateSCost]; has {
|
|
return rtCost.(utils.RateProfileCost).Cost
|
|
}
|
|
return nil
|
|
}
|