diff --git a/ees/ees.go b/ees/ees.go
index 3eb163e1f..cfa0eb5c7 100644
--- a/ees/ees.go
+++ b/ees/ees.go
@@ -116,6 +116,14 @@ func (eeS *EventExporterS) attrSProcessEvent(cgrEv *utils.CGREvent, attrIDs []st
return nil, err
}
if len(rplyEv.AlteredFields) != 0 {
+
+ // Restore original CostDetails in reply to preserve its type, which is lost after
+ // calling AttributeS via a *json connection. Assumes AttributeS does not modify
+ // CostDetails. If it does, we should probably add a type check against *engine.EventCost
+ // before to stay backwards compatible with *gob and *internal connections.
+ if _, has := cgrEv.Event[utils.CostDetails]; has {
+ rplyEv.Event[utils.CostDetails] = cgrEv.Event[utils.CostDetails]
+ }
return rplyEv.CGREvent, nil
}
return cgrEv, nil
diff --git a/ees/ees_test.go b/ees/ees_test.go
index 4bca0bea9..8a7034ab3 100644
--- a/ees/ees_test.go
+++ b/ees/ees_test.go
@@ -103,7 +103,9 @@ func TestAttrSProcessEvent(t *testing.T) {
utils.AttributeSv1ProcessEvent: func(args, reply any) error {
rplyEv := &engine.AttrSProcessEventReply{
AlteredFields: []string{"testcase"},
- CGREvent: &utils.CGREvent{Event: map[string]any{"testcase": 1}},
+ CGREvent: &utils.CGREvent{
+ Event: map[string]any{"testcase": 1},
+ },
}
*reply.(*engine.AttrSProcessEventReply) = *rplyEv
return nil
@@ -131,7 +133,7 @@ func TestAttrSProcessEvent(t *testing.T) {
if rplyEv, err := eeS.attrSProcessEvent(cgrEv, []string{}, utils.EmptyString); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(exp, rplyEv) {
- t.Errorf("Expected %v but received %v", utils.ToJSON(exp), utils.ToJSON(cgrEv))
+ t.Errorf("Expected %v but received %v", utils.ToJSON(exp), utils.ToJSON(rplyEv))
}
}
diff --git a/engine/argees.go b/engine/argees.go
index 444b8c70e..997c1c628 100644
--- a/engine/argees.go
+++ b/engine/argees.go
@@ -25,9 +25,11 @@ import (
"github.com/cgrates/cgrates/utils"
)
-// CGREventWithEeIDs struct is moved in engine due to importing ciclying packages in order to unmarshalling properly for our EventCost type. This is the API struct argument
-
-// CGREventWithEeIDs is CGREvent with EventExporterIDs
+// CGREventWithEeIDs is CGREvent with EventExporterIDs. This
+// struct is used as an API argument. It has been moved into
+// the engine package to avoid import cycling issues that were
+// encountered when trying to properly handle unmarshalling
+// for our EventCost type.
type CGREventWithEeIDs struct {
EeIDs []string
*utils.CGREvent
@@ -54,49 +56,62 @@ func (attr *CGREventWithEeIDs) RPCClone() (any, error) {
return attr.Clone(), nil
}
-func (cgr *CGREventWithEeIDs) UnmarshalJSON(data []byte) (err error) {
- // firstly, we will unamrshall the entire data into raw bytes
- ids := make(map[string]json.RawMessage)
- if err = json.Unmarshal(data, &ids); err != nil {
- return
+// UnmarshalJSON decodes the JSON data into a CGREventWithEeIDs, while
+// ensuring that the CostDetails key of the embedded CGREvent is of
+// type *engine.EventCost.
+func (cgr *CGREventWithEeIDs) UnmarshalJSON(data []byte) error {
+
+ // Define a temporary struct with the same
+ // structure as CGREventWithEeIDs.
+ var temp struct {
+ EeIDs []string
+ *utils.CGREvent
}
- // populate eeids in case of it's existance
- eeIDs := make([]string, len(ids[utils.EeIDs]))
- if err = json.Unmarshal(ids[utils.EeIDs], &eeIDs); err != nil {
- return
+
+ // Unmarshal JSON data into the temporary struct,
+ // using the default unmarshaler.
+ err := json.Unmarshal(data, &temp)
+ if err != nil {
+ return err
}
- cgr.EeIDs = eeIDs
- // populate the entire CGRevent struct in case of it's existance
- var cgrEv *utils.CGREvent
- if err = json.Unmarshal(data, &cgrEv); err != nil {
- return
- }
- cgr.CGREvent = cgrEv
- // check if we have CostDetails and modify it's type (by default it was map[string]any by unrmarshaling, now it will be EventCost)
- if ecEv, has := cgrEv.Event[utils.CostDetails]; has {
- var bts []byte
- switch ecEv.(type) {
+
+ if ecEv, has := temp.Event[utils.CostDetails]; has {
+ var ecBytes []byte
+
+ // CostDetails value can either be a JSON string (which is
+ // the marshaled form of an EventCost) or a map representing
+ // the EventCost directly.
+ switch v := ecEv.(type) {
case string:
- btsToStr, err := json.Marshal(ecEv)
- if err != nil {
- return err
- }
- var toString string
- if err = json.Unmarshal(btsToStr, &toString); err != nil {
- return err
- }
- bts = []byte(toString)
+ // If string, it's assumed to be the JSON
+ // representation of EventCost.
+ ecBytes = []byte(v)
default:
- bts, err = json.Marshal(ecEv)
+ // Otherwise we assume it's a map and we marshal
+ // it back to JSON to prepare for unmarshalling
+ // into EventCost.
+ ecBytes, err = json.Marshal(v)
if err != nil {
return err
}
}
- ec := new(EventCost)
- if err = json.Unmarshal(bts, &ec); err != nil {
+
+ // Unmarshal the JSON (either directly from the string case
+ // or from the marshaled map) into an EventCost struct.
+ var ec EventCost
+ if err := json.Unmarshal(ecBytes, &ec); err != nil {
return err
}
- cgr.Event[utils.CostDetails] = ec
+
+ // Update the Event map with the unmarshalled EventCost,
+ // ensuring the type of CostDetails is *EventCost.
+ temp.Event[utils.CostDetails] = &ec
}
- return
+
+ // Assign the extracted EeIDs and CGREvent
+ // to the main struct fields.
+ cgr.EeIDs = temp.EeIDs
+ cgr.CGREvent = temp.CGREvent
+
+ return nil
}
diff --git a/engine/argees_test.go b/engine/argees_test.go
new file mode 100644
index 000000000..0da1e4e3d
--- /dev/null
+++ b/engine/argees_test.go
@@ -0,0 +1,130 @@
+/*
+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
+*/
+package engine
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/cgrates/cgrates/utils"
+)
+
+func TestArgEEsUnmarshalJSON(t *testing.T) {
+ var testEC = &EventCost{
+ CGRID: "164b0422fdc6a5117031b427439482c6a4f90e41",
+ RunID: utils.MetaDefault,
+ StartTime: time.Date(2017, 1, 9, 16, 18, 21, 0, time.UTC),
+ AccountSummary: &AccountSummary{
+ Tenant: "cgrates.org",
+ ID: "dan",
+ BalanceSummaries: []*BalanceSummary{
+ {
+ UUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
+ ID: "BALANCE_1",
+ Type: utils.MetaMonetary,
+ Value: 50,
+ Initial: 60,
+ Disabled: false,
+ },
+ },
+ AllowNegative: false,
+ Disabled: false,
+ },
+ }
+ cdBytes, err := json.Marshal(testEC)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cgrEvWithIDs := CGREventWithEeIDs{
+ EeIDs: []string{"id1", "id2"},
+ CGREvent: &utils.CGREvent{
+ Tenant: "cgrates.org",
+ ID: "ev1",
+ Event: map[string]any{
+ utils.AccountField: "1001",
+ },
+ },
+ }
+
+ t.Run("UnmarshalFromMap", func(t *testing.T) {
+ var cdMap map[string]any
+ if err = json.Unmarshal(cdBytes, &cdMap); err != nil {
+ t.Fatal(err)
+ }
+
+ cgrEvWithIDs.Event[utils.CostDetails] = cdMap
+
+ cgrEvBytes, err := json.Marshal(cgrEvWithIDs)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var rcvCGREv CGREventWithEeIDs
+ if err = json.Unmarshal(cgrEvBytes, &rcvCGREv); err != nil {
+ t.Fatal(err)
+ }
+
+ expectedType := "*engine.EventCost"
+ if cdType := fmt.Sprintf("%T", rcvCGREv.Event[utils.CostDetails]); cdType != expectedType {
+ t.Fatalf("expected type to be %v, received %v", expectedType, cdType)
+ }
+
+ cgrEvWithIDs.Event[utils.CostDetails] = testEC
+ if !reflect.DeepEqual(rcvCGREv, cgrEvWithIDs) {
+ t.Errorf("expected: %v,\nreceived: %v",
+ utils.ToJSON(cgrEvWithIDs), utils.ToJSON(rcvCGREv))
+ }
+ })
+ t.Run("UnmarshalFromString", func(t *testing.T) {
+ cdStringBytes, err := json.Marshal(string(cdBytes))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var cdString string
+ if err = json.Unmarshal(cdStringBytes, &cdString); err != nil {
+ t.Fatal(err)
+ }
+
+ cgrEvWithIDs.Event[utils.CostDetails] = cdString
+
+ cgrEvBytes, err := json.Marshal(cgrEvWithIDs)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var rcvCGREv CGREventWithEeIDs
+ if err = json.Unmarshal(cgrEvBytes, &rcvCGREv); err != nil {
+ t.Fatal(err)
+ }
+
+ expectedType := "*engine.EventCost"
+ if cdType := fmt.Sprintf("%T", rcvCGREv.Event[utils.CostDetails]); cdType != expectedType {
+ t.Fatalf("expected type to be %v, received %v", expectedType, cdType)
+ }
+
+ cgrEvWithIDs.Event[utils.CostDetails] = testEC
+ if !reflect.DeepEqual(rcvCGREv, cgrEvWithIDs) {
+ t.Errorf("expected: %v,\nreceived: %v",
+ utils.ToJSON(cgrEvWithIDs), utils.ToJSON(rcvCGREv))
+ }
+ })
+}
diff --git a/engine/dynamicdp.go b/engine/dynamicdp.go
index 2ce3947c6..293a2ddb5 100644
--- a/engine/dynamicdp.go
+++ b/engine/dynamicdp.go
@@ -19,6 +19,7 @@ along with this program. If not, see
package engine
import (
+ "encoding/json"
"fmt"
"github.com/nyaruka/phonenumbers"
@@ -71,6 +72,45 @@ func (dDP *dynamicDP) FieldAsInterface(fldPath []string) (val any, err error) {
if len(fldPath) == 0 {
return nil, utils.ErrNotFound
}
+
+ // Parsing deeper than the first level in *req.CostDetails requires it to be
+ // of type *EventCost. If not, we serialize and deserialize into an *EventCost.
+ if len(fldPath) > 3 &&
+ fldPath[0] == utils.MetaReq && fldPath[1] == utils.CostDetails {
+ if mp, canCast := dDP.initialDP.(utils.MapStorage); canCast {
+ if event, canCast := mp[utils.MetaReq].(map[string]any); canCast {
+ if cd, has := event[utils.CostDetails]; has {
+ var cdBytes []byte
+ switch cd := cd.(type) {
+ case *EventCost:
+ // Directly proceed if already *EventCost.
+ return cd.FieldAsInterface(fldPath[2:])
+ case string:
+ // Convert string to bytes for unmarshalling
+ // if it's a serialized *EventCost.
+ cdBytes = []byte(cd)
+ default:
+ // Marshal non-string types to JSON bytes
+ // for unmarshalling into *EventCost.
+ cdBytes, err = json.Marshal(cd)
+ if err != nil {
+ return nil, err
+ }
+ }
+ var ec EventCost
+ if err = json.Unmarshal(cdBytes, &ec); err != nil {
+ return nil, err
+ }
+
+ // Update CostDetails with the unmarshalled *EventCost
+ // to avoid repetitive serialization.
+ event[utils.CostDetails] = &ec
+ return ec.FieldAsInterface(fldPath[2:])
+ }
+ }
+ }
+ }
+
if initialDPPrefixes.Has(fldPath[0]) {
return dDP.initialDP.FieldAsInterface(fldPath)
}
diff --git a/general_tests/ees_it_test.go b/general_tests/ees_it_test.go
index e84a07875..764e3c41e 100644
--- a/general_tests/ees_it_test.go
+++ b/general_tests/ees_it_test.go
@@ -51,6 +51,10 @@ func TestEEsExportEventChanges(t *testing.T) {
content := `{
+"general": {
+ "log_level": 7
+},
+
"data_db": {
"db_type": "*internal"
},
@@ -69,7 +73,7 @@ func TestEEsExportEventChanges(t *testing.T) {
"ees": {
"enabled": true,
- "attributes_conns":["*internal"],
+ "attributes_conns":["*localhost"],
"exporters": [
{
"id": "exporter1",
@@ -82,6 +86,9 @@ func TestEEsExportEventChanges(t *testing.T) {
"fields":[
{"tag": "CGRID", "path": "*uch.CGRID1", "type": "*variable", "value": "~*req.CGRID"},
{"tag": "RequestType", "path": "*uch.RequestType1", "type": "*variable", "value": "~*req.RequestType"},
+ {"tag": "BalanceID", "path": "*uch.BalanceID", "type": "*variable", "value": "~*req.CostDetails.Charges[0].Increments[0].Accounting.Balance.ID"},
+ {"tag": "BalanceType", "path": "*uch.BalanceType", "type": "*variable", "value": "~*req.CostDetails.Charges[0].Increments[0].Accounting.Balance.Type"},
+ {"tag": "BalanceFound", "path": "*uch.BalanceFound", "type": "*variable", "value": "~*req.BalanceFound"},
{"tag": "ExporterID", "path": "*uch.ExporterID1", "type": "*variable", "value": "~*opts.*exporterID"}
],
},
@@ -136,6 +143,19 @@ func TestEEsExportEventChanges(t *testing.T) {
},
},
},
+ {
+ FilterIDs: []string{
+ "*string:~*req.CostDetails.Charges[0].Increments[0].Accounting.Balance.ID:BALANCE_TEST",
+ "*string:~*req.CostDetails.Charges[0].Increments[0].Accounting.Balance.Type:*voice",
+ },
+ Path: "*req.BalanceFound",
+ Type: utils.MetaVariable,
+ Value: config.RSRParsers{
+ &config.RSRParser{
+ Rules: utils.TrueStr,
+ },
+ },
+ },
},
Blocker: false,
Weight: 10,
@@ -157,6 +177,31 @@ func TestEEsExportEventChanges(t *testing.T) {
Event: map[string]any{
utils.CGRID: "TEST",
utils.RequestType: utils.MetaRated,
+ utils.CostDetails: &engine.EventCost{
+ Charges: []*engine.ChargingInterval{
+ {
+ Increments: []*engine.ChargingIncrement{
+ {
+ AccountingID: "ACCOUNTING_TEST",
+ },
+ },
+ },
+ },
+ Accounting: engine.Accounting{
+ "ACCOUNTING_TEST": &engine.BalanceCharge{
+ BalanceUUID: "123456",
+ },
+ },
+ AccountSummary: &engine.AccountSummary{
+ BalanceSummaries: engine.BalanceSummaries{
+ {
+ ID: "BALANCE_TEST",
+ Type: utils.MetaVoice,
+ UUID: "123456",
+ },
+ },
+ },
+ },
},
},
}
@@ -220,4 +265,43 @@ func TestEEsExportEventChanges(t *testing.T) {
t.Errorf("expected %v, received %v", "exporter2", exporterID2)
}
})
+
+ t.Run("CheckAttributesAlteredFields", func(t *testing.T) {
+ var balanceID any
+ if err = client.Call(context.Background(), utils.CacheSv1GetItem, &utils.ArgsGetCacheItemWithAPIOpts{
+ Tenant: "cgrates.org",
+ ArgsGetCacheItem: utils.ArgsGetCacheItem{
+ CacheID: utils.CacheUCH,
+ ItemID: "BalanceID",
+ },
+ }, &balanceID); err != nil {
+ t.Error(err)
+ } else if balanceID != "BALANCE_TEST" {
+ t.Errorf("expected %v, received %v", "BALANCE_TEST", balanceID)
+ }
+ var balanceType any
+ if err = client.Call(context.Background(), utils.CacheSv1GetItem, &utils.ArgsGetCacheItemWithAPIOpts{
+ Tenant: "cgrates.org",
+ ArgsGetCacheItem: utils.ArgsGetCacheItem{
+ CacheID: utils.CacheUCH,
+ ItemID: "BalanceType",
+ },
+ }, &balanceType); err != nil {
+ t.Error(err)
+ } else if balanceType != utils.MetaVoice {
+ t.Errorf("expected %v, received %v", "BALANCE_TEST", balanceType)
+ }
+ var balanceFound any
+ if err = client.Call(context.Background(), utils.CacheSv1GetItem, &utils.ArgsGetCacheItemWithAPIOpts{
+ Tenant: "cgrates.org",
+ ArgsGetCacheItem: utils.ArgsGetCacheItem{
+ CacheID: utils.CacheUCH,
+ ItemID: "BalanceFound",
+ },
+ }, &balanceFound); err != nil {
+ t.Error(err)
+ } else if balanceFound != "true" {
+ t.Errorf("expected %v, received %v", "true", balanceFound)
+ }
+ })
}