mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-12 18:46:24 +05:00
Removed cdr struct
This commit is contained in:
committed by
Dan Christian Bogos
parent
df412e55fb
commit
7e80fe008f
370
engine/cdr.go
370
engine/cdr.go
@@ -1,370 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
func NewCDRFromExternalCDR(extCdr *ExternalCDR, timezone string) (*CDR, error) {
|
||||
var err error
|
||||
cdr := &CDR{RunID: extCdr.RunID, OrderID: extCdr.OrderID, ToR: extCdr.ToR,
|
||||
OriginID: extCdr.OriginID, OriginHost: extCdr.OriginHost,
|
||||
Source: extCdr.Source, RequestType: extCdr.RequestType, Tenant: extCdr.Tenant, Category: extCdr.Category,
|
||||
Account: extCdr.Account, Subject: extCdr.Subject, Destination: extCdr.Destination,
|
||||
CostSource: extCdr.CostSource, Cost: extCdr.Cost, PreRated: extCdr.PreRated}
|
||||
if extCdr.SetupTime != "" {
|
||||
if cdr.SetupTime, err = utils.ParseTimeDetectLayout(extCdr.SetupTime, timezone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if extCdr.AnswerTime != "" {
|
||||
if cdr.AnswerTime, err = utils.ParseTimeDetectLayout(extCdr.AnswerTime, timezone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if extCdr.Usage != "" {
|
||||
if cdr.Usage, err = utils.ParseDurationWithNanosecs(extCdr.Usage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if extCdr.ExtraFields != nil {
|
||||
cdr.ExtraFields = make(map[string]string)
|
||||
}
|
||||
for k, v := range extCdr.ExtraFields {
|
||||
cdr.ExtraFields[k] = v
|
||||
}
|
||||
return cdr, nil
|
||||
}
|
||||
|
||||
type CDR struct {
|
||||
RunID string
|
||||
OrderID int64 // Stor order id used as export order id
|
||||
OriginHost string // represents the IP address of the host generating the CDR (automatically populated by the server)
|
||||
Source string // formally identifies the source of the CDR (free form field)
|
||||
OriginID string // represents the unique accounting id given by the telecom switch generating the CDR
|
||||
ToR string // type of record, meta-field, should map to one of the TORs hardcoded inside the server <*voice|*data|*sms|*generic>
|
||||
RequestType string // matching the supported request types by the **CGRateS**, accepted values are hardcoded in the server <prepaid|postpaid|pseudoprepaid|rated>.
|
||||
Tenant string // tenant whom this record belongs
|
||||
Category string // free-form filter for this record, matching the category defined in rating profiles.
|
||||
Account string // account id (accounting subsystem) the record should be attached to
|
||||
Subject string // rating subject (rating subsystem) this record should be attached to
|
||||
Destination string // destination to be charged
|
||||
SetupTime time.Time // set-up time of the event. Supported formats: datetime RFC3339 compatible, SQL datetime (eg: MySQL), unix timestamp.
|
||||
AnswerTime time.Time // answer time of the event. Supported formats: datetime RFC3339 compatible, SQL datetime (eg: MySQL), unix timestamp.
|
||||
Usage time.Duration // event usage information (eg: in case of tor=*voice this will represent the total duration of a call)
|
||||
ExtraFields map[string]string // Extra fields to be stored in CDR
|
||||
ExtraInfo string // Container for extra information related to this CDR, eg: populated with error reason in case of error on calculation
|
||||
Partial bool // Used for partial record processing by ERs
|
||||
PreRated bool // Mark the CDR as rated so we do not process it during rating
|
||||
CostSource string // The source of this cost
|
||||
Cost float64 //
|
||||
}
|
||||
|
||||
// AddDefaults will add missing information based on other fields
|
||||
func (cdr *CDR) AddDefaults(cfg *config.CGRConfig) {
|
||||
|
||||
if cdr.RunID == utils.EmptyString {
|
||||
cdr.RunID = utils.MetaDefault
|
||||
}
|
||||
if cdr.ToR == utils.EmptyString {
|
||||
cdr.ToR = utils.MetaVoice
|
||||
}
|
||||
if cdr.RequestType == utils.EmptyString {
|
||||
cdr.RequestType = cfg.GeneralCfg().DefaultReqType
|
||||
}
|
||||
if cdr.Tenant == utils.EmptyString {
|
||||
cdr.Tenant = cfg.GeneralCfg().DefaultTenant
|
||||
}
|
||||
if cdr.Category == utils.EmptyString {
|
||||
cdr.Category = cfg.GeneralCfg().DefaultCategory
|
||||
}
|
||||
if cdr.Subject == utils.EmptyString {
|
||||
cdr.Subject = cdr.Account
|
||||
}
|
||||
}
|
||||
|
||||
// FormatCost formats the cost as string on export
|
||||
func (cdr *CDR) FormatCost(shiftDecimals, roundDecimals int) string {
|
||||
cost := cdr.Cost
|
||||
if shiftDecimals != 0 {
|
||||
cost = cost * math.Pow10(shiftDecimals)
|
||||
}
|
||||
return strconv.FormatFloat(cost, 'f', roundDecimals, 64)
|
||||
}
|
||||
|
||||
// FieldAsString is used to retrieve fields as string, primary fields are const labeled
|
||||
func (cdr *CDR) FieldAsString(rsrPrs *config.RSRParser) (parsed string, err error) {
|
||||
parsed, err = rsrPrs.ParseDataProviderWithInterfaces(
|
||||
cdr.AsMapStorage())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FieldsAsString concatenates values of multiple fields defined in template, used eg in CDR templates
|
||||
func (cdr *CDR) FieldsAsString(rsrFlds config.RSRParsers) string {
|
||||
outVal, err := rsrFlds.ParseDataProvider(
|
||||
cdr.AsMapStorage())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return outVal
|
||||
}
|
||||
|
||||
func (cdr *CDR) Clone() *CDR {
|
||||
if cdr == nil {
|
||||
return nil
|
||||
}
|
||||
cln := &CDR{
|
||||
RunID: cdr.RunID,
|
||||
OrderID: cdr.OrderID,
|
||||
OriginHost: cdr.OriginHost,
|
||||
Source: cdr.Source,
|
||||
OriginID: cdr.OriginID,
|
||||
ToR: cdr.ToR,
|
||||
RequestType: cdr.RequestType,
|
||||
Tenant: cdr.Tenant,
|
||||
Category: cdr.Category,
|
||||
Account: cdr.Account,
|
||||
Subject: cdr.Subject,
|
||||
Destination: cdr.Destination,
|
||||
SetupTime: cdr.SetupTime,
|
||||
AnswerTime: cdr.AnswerTime,
|
||||
Usage: cdr.Usage,
|
||||
ExtraFields: cdr.ExtraFields,
|
||||
ExtraInfo: cdr.ExtraInfo,
|
||||
Partial: cdr.Partial,
|
||||
PreRated: cdr.PreRated,
|
||||
CostSource: cdr.CostSource,
|
||||
Cost: cdr.Cost,
|
||||
}
|
||||
if cdr.ExtraFields != nil {
|
||||
cln.ExtraFields = make(map[string]string, len(cdr.ExtraFields))
|
||||
for key, val := range cdr.ExtraFields {
|
||||
cln.ExtraFields[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
return cln
|
||||
}
|
||||
|
||||
func (cdr *CDR) AsMapStorage() (mp utils.MapStorage) {
|
||||
mp = utils.MapStorage{
|
||||
utils.MetaReq: cdr.AsMapStringIface(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cdr *CDR) AsMapStringIface() (mp map[string]interface{}) {
|
||||
mp = make(map[string]interface{})
|
||||
for fld, val := range cdr.ExtraFields {
|
||||
mp[fld] = val
|
||||
}
|
||||
mp[utils.RunID] = cdr.RunID
|
||||
mp[utils.OrderID] = cdr.OrderID
|
||||
mp[utils.OriginHost] = cdr.OriginHost
|
||||
mp[utils.Source] = cdr.Source
|
||||
mp[utils.OriginID] = cdr.OriginID
|
||||
mp[utils.ToR] = cdr.ToR
|
||||
mp[utils.RequestType] = cdr.RequestType
|
||||
mp[utils.Tenant] = cdr.Tenant
|
||||
mp[utils.Category] = cdr.Category
|
||||
mp[utils.AccountField] = cdr.Account
|
||||
mp[utils.Subject] = cdr.Subject
|
||||
mp[utils.Destination] = cdr.Destination
|
||||
mp[utils.SetupTime] = cdr.SetupTime
|
||||
mp[utils.AnswerTime] = cdr.AnswerTime
|
||||
mp[utils.Usage] = cdr.Usage
|
||||
mp[utils.ExtraInfo] = cdr.ExtraInfo
|
||||
mp[utils.Partial] = cdr.Partial
|
||||
mp[utils.PreRated] = cdr.PreRated
|
||||
mp[utils.CostSource] = cdr.CostSource
|
||||
mp[utils.Cost] = cdr.Cost
|
||||
return
|
||||
}
|
||||
|
||||
func (cdr *CDR) AsExternalCDR() *ExternalCDR {
|
||||
var usageStr string
|
||||
switch cdr.ToR {
|
||||
case utils.MetaVoice: // usage as time
|
||||
usageStr = cdr.Usage.String()
|
||||
default: // usage as units
|
||||
usageStr = strconv.FormatInt(cdr.Usage.Nanoseconds(), 10)
|
||||
}
|
||||
return &ExternalCDR{
|
||||
RunID: cdr.RunID,
|
||||
OrderID: cdr.OrderID,
|
||||
OriginHost: cdr.OriginHost,
|
||||
Source: cdr.Source,
|
||||
OriginID: cdr.OriginID,
|
||||
ToR: cdr.ToR,
|
||||
RequestType: cdr.RequestType,
|
||||
Tenant: cdr.Tenant,
|
||||
Category: cdr.Category,
|
||||
Account: cdr.Account,
|
||||
Subject: cdr.Subject,
|
||||
Destination: cdr.Destination,
|
||||
SetupTime: cdr.SetupTime.Format(time.RFC3339),
|
||||
AnswerTime: cdr.AnswerTime.Format(time.RFC3339),
|
||||
Usage: usageStr,
|
||||
ExtraFields: cdr.ExtraFields,
|
||||
CostSource: cdr.CostSource,
|
||||
Cost: cdr.Cost,
|
||||
ExtraInfo: cdr.ExtraInfo,
|
||||
PreRated: cdr.PreRated,
|
||||
}
|
||||
}
|
||||
|
||||
func (cdr *CDR) String() string {
|
||||
mrsh, _ := json.Marshal(cdr)
|
||||
return string(mrsh)
|
||||
}
|
||||
|
||||
// AsCDRsql converts the CDR into the format used for SQL storage
|
||||
func (cdr *CDR) AsCDRsql() (cdrSQL *CDRsql) {
|
||||
cdrSQL = new(CDRsql)
|
||||
cdrSQL.RunID = cdr.RunID
|
||||
cdrSQL.OriginHost = cdr.OriginHost
|
||||
cdrSQL.Source = cdr.Source
|
||||
cdrSQL.OriginID = cdr.OriginID
|
||||
cdrSQL.TOR = cdr.ToR
|
||||
cdrSQL.RequestType = cdr.RequestType
|
||||
cdrSQL.Tenant = cdr.Tenant
|
||||
cdrSQL.Category = cdr.Category
|
||||
cdrSQL.Account = cdr.Account
|
||||
cdrSQL.Subject = cdr.Subject
|
||||
cdrSQL.Destination = cdr.Destination
|
||||
cdrSQL.SetupTime = cdr.SetupTime
|
||||
if !cdr.AnswerTime.IsZero() {
|
||||
cdrSQL.AnswerTime = utils.TimePointer(cdr.AnswerTime)
|
||||
}
|
||||
cdrSQL.Usage = cdr.Usage.Nanoseconds()
|
||||
cdrSQL.ExtraFields = utils.ToJSON(cdr.ExtraFields)
|
||||
cdrSQL.CostSource = cdr.CostSource
|
||||
cdrSQL.Cost = cdr.Cost
|
||||
cdrSQL.ExtraInfo = cdr.ExtraInfo
|
||||
cdrSQL.CreatedAt = time.Now()
|
||||
return
|
||||
}
|
||||
|
||||
func (cdr *CDR) AsCGREvent() *utils.CGREvent {
|
||||
return &utils.CGREvent{
|
||||
Tenant: cdr.Tenant,
|
||||
ID: utils.UUIDSha1Prefix(),
|
||||
Event: cdr.AsMapStringIface(),
|
||||
APIOpts: map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewCDRFromSQL converts the CDRsql into CDR
|
||||
func NewCDRFromSQL(cdrSQL *CDRsql) (cdr *CDR, err error) {
|
||||
cdr = new(CDR)
|
||||
cdr.RunID = cdrSQL.RunID
|
||||
cdr.OriginHost = cdrSQL.OriginHost
|
||||
cdr.Source = cdrSQL.Source
|
||||
cdr.OriginID = cdrSQL.OriginID
|
||||
cdr.OrderID = cdrSQL.ID
|
||||
cdr.ToR = cdrSQL.TOR
|
||||
cdr.RequestType = cdrSQL.RequestType
|
||||
cdr.Tenant = cdrSQL.Tenant
|
||||
cdr.Category = cdrSQL.Category
|
||||
cdr.Account = cdrSQL.Account
|
||||
cdr.Subject = cdrSQL.Subject
|
||||
cdr.Destination = cdrSQL.Destination
|
||||
cdr.SetupTime = cdrSQL.SetupTime
|
||||
if cdrSQL.AnswerTime != nil {
|
||||
cdr.AnswerTime = *cdrSQL.AnswerTime
|
||||
}
|
||||
cdr.Usage = time.Duration(cdrSQL.Usage)
|
||||
cdr.CostSource = cdrSQL.CostSource
|
||||
cdr.Cost = cdrSQL.Cost
|
||||
cdr.ExtraInfo = cdrSQL.ExtraInfo
|
||||
if cdrSQL.ExtraFields != "" {
|
||||
if err = json.Unmarshal([]byte(cdrSQL.ExtraFields), &cdr.ExtraFields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ExternalCDR struct {
|
||||
RunID string
|
||||
OrderID int64
|
||||
OriginHost string
|
||||
Source string
|
||||
OriginID string
|
||||
ToR string
|
||||
RequestType string
|
||||
Tenant string
|
||||
Category string
|
||||
Account string
|
||||
Subject string
|
||||
Destination string
|
||||
SetupTime string
|
||||
AnswerTime string
|
||||
Usage string
|
||||
ExtraFields map[string]string
|
||||
CostSource string
|
||||
Cost float64
|
||||
CostDetails string
|
||||
ExtraInfo string
|
||||
PreRated bool // Mark the CDR as rated so we do not process it during mediation
|
||||
}
|
||||
|
||||
// UsageRecord is used when authorizing requests from outside, eg APIerSv1.GetMaxUsage
|
||||
type UsageRecord struct {
|
||||
ToR string
|
||||
RequestType string
|
||||
Tenant string
|
||||
Category string
|
||||
Account string
|
||||
Subject string
|
||||
Destination string
|
||||
SetupTime string
|
||||
AnswerTime string
|
||||
Usage string
|
||||
ExtraFields map[string]string
|
||||
}
|
||||
|
||||
func (uR *UsageRecord) GetID() string {
|
||||
return utils.Sha1(uR.ToR, uR.RequestType, uR.Tenant, uR.Category, uR.Account, uR.Subject, uR.Destination, uR.SetupTime, uR.AnswerTime, uR.Usage)
|
||||
}
|
||||
|
||||
type ExternalCDRWithAPIOpts struct {
|
||||
*ExternalCDR
|
||||
APIOpts map[string]interface{}
|
||||
}
|
||||
|
||||
type UsageRecordWithAPIOpts struct {
|
||||
*UsageRecord
|
||||
APIOpts map[string]interface{}
|
||||
}
|
||||
|
||||
type CDRWithAPIOpts struct {
|
||||
*CDR
|
||||
APIOpts map[string]interface{}
|
||||
}
|
||||
@@ -1,475 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
func TestNewCDRFromExternalCDR(t *testing.T) {
|
||||
extCdr := &ExternalCDR{
|
||||
// CGRID: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()),
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002",
|
||||
SetupTime: "2013-11-07T08:42:20Z", AnswerTime: "2013-11-07T08:42:26Z", RunID: utils.MetaDefault,
|
||||
Usage: "10", Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
eStorCdr := &CDR{
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated, RunID: utils.MetaDefault,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001",
|
||||
Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
Usage: 10, Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
if CDR, err := NewCDRFromExternalCDR(extCdr, ""); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(eStorCdr, CDR) {
|
||||
t.Errorf("Expected: %+v, received: %+v", eStorCdr, CDR)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDRClone(t *testing.T) {
|
||||
storCdr := &CDR{
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated, Tenant: "cgrates.org",
|
||||
Category: "call", Account: "1001", Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault, Usage: 10,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
Cost: 1.01, PreRated: true,
|
||||
}
|
||||
if clnStorCdr := storCdr.Clone(); !reflect.DeepEqual(storCdr, clnStorCdr) {
|
||||
t.Errorf("Expecting: %+v, received: %+v", storCdr, clnStorCdr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsAsString(t *testing.T) {
|
||||
cdr := CDR{
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1", Source: "test",
|
||||
RequestType: utils.MetaRated, Tenant: "cgrates.org",
|
||||
Category: "call", Account: "1001", Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault, Usage: 10 * time.Second,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01,
|
||||
}
|
||||
eVal := "call_from_1001"
|
||||
if val := cdr.FieldsAsString(
|
||||
config.NewRSRParsersMustCompile("~*req.Category;_from_;~*req.Account", utils.InfieldSep)); val != eVal {
|
||||
t.Errorf("Expecting : %s, received: %q", eVal, val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatCost(t *testing.T) {
|
||||
cdr := CDR{Cost: 1.01}
|
||||
if cdr.FormatCost(0, 4) != "1.0100" {
|
||||
t.Error("Unexpected format of the cost: ", cdr.FormatCost(0, 4))
|
||||
}
|
||||
cdr = CDR{Cost: 1.01001}
|
||||
if cdr.FormatCost(0, 4) != "1.0100" {
|
||||
t.Error("Unexpected format of the cost: ", cdr.FormatCost(0, 4))
|
||||
}
|
||||
if cdr.FormatCost(2, 0) != "101" {
|
||||
t.Error("Unexpected format of the cost: ", cdr.FormatCost(2, 0))
|
||||
}
|
||||
if cdr.FormatCost(1, 0) != "10" {
|
||||
t.Error("Unexpected format of the cost: ", cdr.FormatCost(1, 0))
|
||||
}
|
||||
if cdr.FormatCost(2, 3) != "101.001" {
|
||||
t.Error("Unexpected format of the cost: ", cdr.FormatCost(2, 3))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDRAsMapStringIface(t *testing.T) {
|
||||
cdr := &CDR{
|
||||
OrderID: 123,
|
||||
ToR: utils.MetaVoice,
|
||||
OriginID: "dsafdsaf",
|
||||
OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest,
|
||||
RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org",
|
||||
Category: "call",
|
||||
Account: "1002",
|
||||
Subject: "1001",
|
||||
Destination: "+4986517174963",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault,
|
||||
Usage: 10 * time.Second,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
Cost: 1.01,
|
||||
}
|
||||
|
||||
mp := map[string]interface{}{
|
||||
"field_extr1": "val_extr1",
|
||||
"fieldextr2": "valextr2",
|
||||
utils.RunID: utils.MetaDefault,
|
||||
utils.OrderID: cdr.OrderID,
|
||||
utils.OriginHost: "192.168.1.1",
|
||||
utils.Source: utils.UnitTest,
|
||||
utils.OriginID: "dsafdsaf",
|
||||
utils.ToR: utils.MetaVoice,
|
||||
utils.RequestType: utils.MetaRated,
|
||||
utils.Tenant: "cgrates.org",
|
||||
utils.Category: "call",
|
||||
utils.AccountField: "1002",
|
||||
utils.Subject: "1001",
|
||||
utils.Destination: "+4986517174963",
|
||||
utils.SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
utils.AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
utils.Usage: 10 * time.Second,
|
||||
utils.CostSource: cdr.CostSource,
|
||||
utils.Cost: 1.01,
|
||||
utils.PreRated: false,
|
||||
utils.Partial: false,
|
||||
utils.ExtraInfo: cdr.ExtraInfo,
|
||||
}
|
||||
if cdrMp := cdr.AsMapStringIface(); !reflect.DeepEqual(mp, cdrMp) {
|
||||
t.Errorf("Expecting: %+v, received: %+v", mp, cdrMp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDRNewCDRFromSQL(t *testing.T) {
|
||||
extraFields := map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}
|
||||
cdrSQL := &CDRsql{
|
||||
ID: 123,
|
||||
// Cgrid: "abecd993d06672714c4218a6dcf8278e0589a171",
|
||||
RunID: utils.MetaDefault,
|
||||
OriginID: "dsafdsaf",
|
||||
TOR: utils.MetaVoice,
|
||||
Source: utils.UnitTest,
|
||||
Tenant: "cgrates.org",
|
||||
Category: "call",
|
||||
Account: "1001",
|
||||
Subject: "1001",
|
||||
Destination: "+4986517174963",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: utils.TimePointer(time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC)),
|
||||
Usage: 10000000000,
|
||||
Cost: 1.01,
|
||||
RequestType: utils.MetaRated,
|
||||
OriginHost: "192.168.1.1",
|
||||
ExtraFields: utils.ToJSON(extraFields),
|
||||
}
|
||||
|
||||
cdr := &CDR{
|
||||
OrderID: 123,
|
||||
ToR: utils.MetaVoice,
|
||||
OriginID: "dsafdsaf",
|
||||
OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest,
|
||||
RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org",
|
||||
Category: "call",
|
||||
Account: "1001",
|
||||
Subject: "1001",
|
||||
Destination: "+4986517174963",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault,
|
||||
Usage: 10 * time.Second,
|
||||
Cost: 1.01,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
|
||||
if eCDR, err := NewCDRFromSQL(cdrSQL); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(cdr, eCDR) {
|
||||
t.Errorf("Expecting: %+v, received: %+v", cdr, eCDR)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCDRAsCGREvent(t *testing.T) {
|
||||
cdr := &CDR{
|
||||
OrderID: 123,
|
||||
ToR: utils.MetaVoice,
|
||||
OriginID: "dsafdsaf",
|
||||
OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest,
|
||||
RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org",
|
||||
Category: "call",
|
||||
Account: "1001",
|
||||
Subject: "1001",
|
||||
Destination: "+4986517174963",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault,
|
||||
Usage: 10 * time.Second,
|
||||
Cost: 1.01,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
eCGREvent := utils.CGREvent{
|
||||
Tenant: "cgrates.org",
|
||||
ID: "GenePreRated",
|
||||
Event: map[string]interface{}{
|
||||
"Account": "1001",
|
||||
"AnswerTime": time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
"Category": "call",
|
||||
"Cost": 1.01,
|
||||
"CostSource": "",
|
||||
"Destination": "+4986517174963",
|
||||
"ExtraInfo": "",
|
||||
"OrderID": int64(123),
|
||||
"OriginHost": "192.168.1.1",
|
||||
"OriginID": "dsafdsaf",
|
||||
"Partial": false,
|
||||
"RequestType": utils.MetaRated,
|
||||
"RunID": utils.MetaDefault,
|
||||
"SetupTime": time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
"Source": "UNIT_TEST",
|
||||
"Subject": "1001",
|
||||
"Tenant": "cgrates.org",
|
||||
"ToR": "*voice",
|
||||
"Usage": 10 * time.Second,
|
||||
"field_extr1": "val_extr1",
|
||||
"fieldextr2": "valextr2",
|
||||
"PreRated": false,
|
||||
},
|
||||
}
|
||||
cgrEvent := cdr.AsCGREvent()
|
||||
if !reflect.DeepEqual(eCGREvent.Tenant, cgrEvent.Tenant) {
|
||||
t.Errorf("Expecting: %+v, received: %+v", eCGREvent.Tenant, cgrEvent.Tenant)
|
||||
}
|
||||
for fldName, fldVal := range eCGREvent.Event {
|
||||
if _, has := cgrEvent.Event[fldName]; !has {
|
||||
t.Errorf("Expecting: %+v, received: %+v", fldName, nil)
|
||||
} else if fldVal != cgrEvent.Event[fldName] {
|
||||
t.Errorf("Expecting: %s:%+v, received: %s:%+v",
|
||||
fldName, eCGREvent.Event[fldName], fldName, cgrEvent.Event[fldName])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDRAddDefaults(t *testing.T) {
|
||||
cdr := &CDR{
|
||||
OriginID: "dsafdsaf",
|
||||
OriginHost: "192.168.1.2",
|
||||
Account: "1001",
|
||||
}
|
||||
cfg := config.NewDefaultCGRConfig()
|
||||
|
||||
eCDR := &CDR{
|
||||
ToR: utils.MetaVoice,
|
||||
RunID: utils.MetaDefault,
|
||||
Subject: "1001",
|
||||
RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org",
|
||||
Category: utils.Call,
|
||||
OriginID: "dsafdsaf",
|
||||
OriginHost: "192.168.1.2",
|
||||
Account: "1001",
|
||||
}
|
||||
cdr.AddDefaults(cfg)
|
||||
if !reflect.DeepEqual(cdr, eCDR) {
|
||||
t.Errorf("Expecting: %+v, received: %+v", eCDR, cdr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCDRFromExternalCDRSetupTimeError(t *testing.T) {
|
||||
extCdr := &ExternalCDR{
|
||||
// CGRID: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()),
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002",
|
||||
SetupTime: "*testTime", AnswerTime: "2013-11-07T08:42:26Z", RunID: utils.MetaDefault,
|
||||
Usage: "10", Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
_, err := NewCDRFromExternalCDR(extCdr, "")
|
||||
if err == nil || err.Error() != "Unsupported time format" {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCDRFromExternalCDRAnswerTimeError(t *testing.T) {
|
||||
extCdr := &ExternalCDR{
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", AnswerTime: "*testTime", RunID: utils.MetaDefault,
|
||||
Usage: "10", Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
_, err := NewCDRFromExternalCDR(extCdr, "")
|
||||
if err == nil || err.Error() != "Unsupported time format" {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCDRFromExternalCDRUsageError(t *testing.T) {
|
||||
extCdr := &ExternalCDR{
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", RunID: utils.MetaDefault,
|
||||
Usage: "testUsage", Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
_, err := NewCDRFromExternalCDR(extCdr, "")
|
||||
if err == nil || err.Error() != `time: invalid duration "testUsage"` {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDRCloneNilCDR(t *testing.T) {
|
||||
var storCdr *CDR
|
||||
if clnStorCdr := storCdr.Clone(); !reflect.DeepEqual(storCdr, clnStorCdr) {
|
||||
t.Errorf("\nExpecting: %+v, \nreceived: %+v", storCdr, clnStorCdr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsExternalCDR(t *testing.T) {
|
||||
extCdr := &ExternalCDR{
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002",
|
||||
SetupTime: "2013-11-07T08:42:20Z", AnswerTime: "2013-11-07T08:42:26Z", RunID: utils.MetaDefault,
|
||||
Usage: "10ns", Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
eStorCdr := &CDR{
|
||||
OrderID: 123, ToR: utils.MetaVoice, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated, RunID: utils.MetaDefault,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001",
|
||||
Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
Usage: 10, Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
if eCDR := eStorCdr.AsExternalCDR(); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(extCdr, eCDR) {
|
||||
t.Errorf("\nExpected: %+v, \nreceived: %+v", utils.ToJSON(extCdr), utils.ToJSON(eCDR))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsExternalCDRDefaultTOR(t *testing.T) {
|
||||
extCdr := &ExternalCDR{
|
||||
OrderID: 123, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002",
|
||||
SetupTime: "2013-11-07T08:42:20Z", AnswerTime: "2013-11-07T08:42:26Z", RunID: utils.MetaDefault,
|
||||
Usage: "10", Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
eStorCdr := &CDR{
|
||||
OrderID: 123, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated, RunID: utils.MetaDefault,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001",
|
||||
Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
Usage: 10, Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
if eCDR := eStorCdr.AsExternalCDR(); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(extCdr, eCDR) {
|
||||
t.Errorf("\nExpected: %+v, \nreceived: %+v", utils.ToJSON(extCdr), utils.ToJSON(eCDR))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDRString(t *testing.T) {
|
||||
testCdr := &CDR{
|
||||
OrderID: 123, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated, RunID: utils.MetaDefault,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001",
|
||||
Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
Usage: 10, Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
mrsh, _ := json.Marshal(testCdr)
|
||||
expected := string(mrsh)
|
||||
result := testCdr.String()
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("\nExpected <%+v>, \nreceived <%+v>", expected, result)
|
||||
}
|
||||
}
|
||||
func TestCDRAsCDRsql(t *testing.T) {
|
||||
cdr := &CDR{
|
||||
OrderID: 123, OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated, RunID: utils.MetaDefault,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001",
|
||||
Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
Usage: 10, Cost: 1.01, PreRated: true,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
|
||||
cdrSQL := new(CDRsql)
|
||||
cdrSQL.RunID = cdr.RunID
|
||||
cdrSQL.OriginHost = cdr.OriginHost
|
||||
cdrSQL.Source = cdr.Source
|
||||
cdrSQL.OriginID = cdr.OriginID
|
||||
cdrSQL.TOR = cdr.ToR
|
||||
cdrSQL.RequestType = cdr.RequestType
|
||||
cdrSQL.Tenant = cdr.Tenant
|
||||
cdrSQL.Category = cdr.Category
|
||||
cdrSQL.Account = cdr.Account
|
||||
cdrSQL.Subject = cdr.Subject
|
||||
cdrSQL.Destination = cdr.Destination
|
||||
cdrSQL.SetupTime = cdr.SetupTime
|
||||
if !cdr.AnswerTime.IsZero() {
|
||||
cdrSQL.AnswerTime = utils.TimePointer(cdr.AnswerTime)
|
||||
}
|
||||
cdrSQL.Usage = cdr.Usage.Nanoseconds()
|
||||
cdrSQL.ExtraFields = utils.ToJSON(cdr.ExtraFields)
|
||||
cdrSQL.CostSource = cdr.CostSource
|
||||
cdrSQL.Cost = cdr.Cost
|
||||
cdrSQL.ExtraInfo = cdr.ExtraInfo
|
||||
|
||||
result := cdr.AsCDRsql()
|
||||
cdrSQL.CreatedAt = result.CreatedAt
|
||||
if !reflect.DeepEqual(result, cdrSQL) {
|
||||
t.Errorf("\nExpected <%+v>, \nreceived <%+v>", utils.ToJSON(cdrSQL), utils.ToJSON(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDRGetID(t *testing.T) {
|
||||
uR := &UsageRecord{
|
||||
RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org",
|
||||
Category: "call",
|
||||
Account: "1001",
|
||||
Subject: "1001",
|
||||
Destination: "1002",
|
||||
Usage: "10",
|
||||
}
|
||||
result := uR.GetID()
|
||||
expected := utils.Sha1(uR.ToR, uR.RequestType, uR.Tenant, uR.Category, uR.Account, uR.Subject, uR.Destination, uR.SetupTime, uR.AnswerTime, uR.Usage)
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("\nExpected <%+v>, \nreceived <%+v>", utils.ToJSON(expected), utils.ToJSON(result))
|
||||
}
|
||||
}
|
||||
179
engine/fscdr.go
179
engine/fscdr.go
@@ -1,179 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// Freswitch event property names
|
||||
fsCDRMap = "variables"
|
||||
fsUUID = "uuid" // -Unique ID for this call leg
|
||||
fsCallDestNR = "dialed_extension"
|
||||
fsParkTime = "start_epoch"
|
||||
fsSetupTime = "start_epoch"
|
||||
fsAnswerTime = "answer_epoch"
|
||||
fsHangupTime = "end_epoch"
|
||||
fsDuration = "billsec"
|
||||
fsUsernameVar = "user_name"
|
||||
fsCDRSource = "freeswitch_json"
|
||||
fsSIPReqUser = "sip_req_user" // Apps like FusionPBX do not set dialed_extension, alternative being destination_number but that comes in customer profile, not in vars
|
||||
fsProgressMediamsec = "progress_mediamsec"
|
||||
fsProgressMS = "progressmsec"
|
||||
fsUsername = "username"
|
||||
fsIPv4 = "FreeSWITCH-IPv4"
|
||||
)
|
||||
|
||||
func NewFSCdr(body io.Reader, cgrCfg *config.CGRConfig) (*FSCdr, error) {
|
||||
fsCdr := &FSCdr{cgrCfg: cgrCfg, vars: make(map[string]string)}
|
||||
var err error
|
||||
if err = json.NewDecoder(body).Decode(&fsCdr.body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if variables, ok := fsCdr.body[fsCDRMap]; ok {
|
||||
if variables, ok := variables.(map[string]interface{}); ok {
|
||||
for k, v := range variables {
|
||||
fsCdr.vars[k] = v.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fsCdr, nil
|
||||
}
|
||||
|
||||
type FSCdr struct {
|
||||
cgrCfg *config.CGRConfig
|
||||
vars map[string]string
|
||||
body map[string]interface{} // keeps the loaded body for extra field search
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) getOriginID() string {
|
||||
return utils.Sha1(fsCdr.vars[fsUUID],
|
||||
utils.FirstNonEmpty(fsCdr.vars[utils.CGROriginHost], fsCdr.vars[fsIPv4]))
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) getExtraFields() map[string]string {
|
||||
extraFields := make(map[string]string, len(fsCdr.cgrCfg.CdrsCfg().ExtraFields))
|
||||
const dynprefix string = utils.MetaDynReq + utils.NestingSep
|
||||
for _, field := range fsCdr.cgrCfg.CdrsCfg().ExtraFields {
|
||||
if !strings.HasPrefix(field.Rules, dynprefix) {
|
||||
continue
|
||||
}
|
||||
attrName := field.AttrName()[5:]
|
||||
origFieldVal, foundInVars := fsCdr.vars[attrName]
|
||||
if !foundInVars {
|
||||
origFieldVal = fsCdr.searchExtraField(attrName, fsCdr.body)
|
||||
}
|
||||
if parsed, err := field.ParseValue(origFieldVal); err == nil {
|
||||
extraFields[attrName] = parsed
|
||||
}
|
||||
}
|
||||
return extraFields
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) searchExtraField(field string, body map[string]interface{}) (result string) {
|
||||
for key, value := range body {
|
||||
if key == field {
|
||||
return utils.IfaceAsString(value)
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case map[string]interface{}:
|
||||
if result = fsCdr.searchExtraField(field, v); len(result) != 0 {
|
||||
return
|
||||
}
|
||||
case []interface{}:
|
||||
for _, item := range v {
|
||||
if otherMap, ok := item.(map[string]interface{}); ok {
|
||||
if result = fsCdr.searchExtraField(field, otherMap); len(result) != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// firstDefined will return first defined or search for dfltFld
|
||||
func (fsCdr FSCdr) firstDefined(fldNames []string, dfltFld string) (val string) {
|
||||
var has bool
|
||||
for _, fldName := range fldNames {
|
||||
if val, has = fsCdr.vars[fldName]; has {
|
||||
return
|
||||
}
|
||||
}
|
||||
return fsCdr.searchExtraField(dfltFld, fsCdr.body)
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) AsCDR(timezone string) (storCdr *CDR, err error) {
|
||||
storCdr = &CDR{
|
||||
RunID: fsCdr.vars["cgr_runid"],
|
||||
OriginHost: utils.FirstNonEmpty(fsCdr.vars[utils.CGROriginHost], fsCdr.vars[fsIPv4]),
|
||||
Source: fsCDRSource,
|
||||
OriginID: fsCdr.vars[fsUUID],
|
||||
ToR: utils.MetaVoice,
|
||||
RequestType: utils.FirstNonEmpty(fsCdr.vars[utils.CGRReqType], fsCdr.cgrCfg.GeneralCfg().DefaultReqType),
|
||||
Tenant: utils.FirstNonEmpty(fsCdr.vars[utils.CGRTenant], fsCdr.cgrCfg.GeneralCfg().DefaultTenant),
|
||||
Category: utils.FirstNonEmpty(fsCdr.vars[utils.CGRCategory], fsCdr.cgrCfg.GeneralCfg().DefaultCategory),
|
||||
Account: fsCdr.firstDefined([]string{utils.CGRAccount, fsUsernameVar}, fsUsername),
|
||||
Subject: fsCdr.firstDefined([]string{utils.CGRSubject, utils.CGRAccount, fsUsernameVar}, fsUsername),
|
||||
Destination: utils.FirstNonEmpty(fsCdr.vars[utils.CGRDestination], fsCdr.vars[fsCallDestNR], fsCdr.vars[fsSIPReqUser]),
|
||||
ExtraFields: fsCdr.getExtraFields(),
|
||||
ExtraInfo: fsCdr.vars["cgr_extrainfo"],
|
||||
CostSource: fsCdr.vars["cgr_costsource"],
|
||||
Cost: -1,
|
||||
}
|
||||
if orderID, hasIt := fsCdr.vars["cgr_orderid"]; hasIt {
|
||||
if storCdr.OrderID, err = strconv.ParseInt(orderID, 10, 64); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if setupTime, hasIt := fsCdr.vars[fsSetupTime]; hasIt {
|
||||
if storCdr.SetupTime, err = utils.ParseTimeDetectLayout(setupTime, timezone); err != nil {
|
||||
return nil, err
|
||||
} // Not interested to process errors, should do them if necessary in a previous step
|
||||
}
|
||||
if answerTime, hasIt := fsCdr.vars[fsAnswerTime]; hasIt {
|
||||
if storCdr.AnswerTime, err = utils.ParseTimeDetectLayout(answerTime, timezone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if usage, hasIt := fsCdr.vars[fsDuration]; hasIt {
|
||||
if storCdr.Usage, err = utils.ParseDurationWithSecs(usage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if partial, hasIt := fsCdr.vars["cgr_partial"]; hasIt {
|
||||
if storCdr.Partial, err = strconv.ParseBool(partial); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if preRated, hasIt := fsCdr.vars["cgr_prerated"]; hasIt {
|
||||
if storCdr.PreRated, err = strconv.ParseBool(preRated); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,787 +0,0 @@
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
var body = []byte(`{
|
||||
"core-uuid": "eb8bcdd2-d9eb-4f8f-80c3-1a4042aabe31",
|
||||
"switchname": "FSDev1",
|
||||
"channel_data": {
|
||||
"state": "CS_REPORTING",
|
||||
"state_number": "11",
|
||||
"flags": "0=1;1=1;3=1;20=1;37=1;38=1;40=1;43=1;48=1;53=1;75=1;98=1;112=1;113=1;122=1;134=1",
|
||||
"caps": "1=1;2=1;3=1;4=1;5=1;6=1"
|
||||
},
|
||||
"callStats": {
|
||||
"audio": {
|
||||
"inbound": {
|
||||
"raw_bytes": 572588,
|
||||
"media_bytes": 572588,
|
||||
"packet_count": 3329,
|
||||
"media_packet_count": 3329,
|
||||
"skip_packet_count": 10,
|
||||
"jitter_packet_count": 0,
|
||||
"dtmf_packet_count": 0,
|
||||
"cng_packet_count": 0,
|
||||
"flush_packet_count": 0,
|
||||
"largest_jb_size": 0,
|
||||
"jitter_min_variance": 0,
|
||||
"jitter_max_variance": 0,
|
||||
"jitter_loss_rate": 0,
|
||||
"jitter_burst_rate": 0,
|
||||
"mean_interval": 0,
|
||||
"flaw_total": 0,
|
||||
"quality_percentage": 100,
|
||||
"mos": 4.500000
|
||||
},
|
||||
"outbound": {
|
||||
"raw_bytes": 0,
|
||||
"media_bytes": 0,
|
||||
"packet_count": 0,
|
||||
"media_packet_count": 0,
|
||||
"skip_packet_count": 0,
|
||||
"dtmf_packet_count": 0,
|
||||
"cng_packet_count": 0,
|
||||
"rtcp_packet_count": 0,
|
||||
"rtcp_octet_count": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"uuid": "3da8bf84-c133-4959-9e24-e72875cb33a1",
|
||||
"session_id": "7",
|
||||
"sip_from_user": "1001",
|
||||
"sip_from_uri": "1001@10.10.10.204",
|
||||
"sip_from_host": "10.10.10.204",
|
||||
"channel_name": "sofia/internal/1001@10.10.10.204",
|
||||
"ep_codec_string": "CORE_PCM_MODULE.PCMU@8000h@20i@64000b,CORE_PCM_MODULE.PCMA@8000h@20i@64000b",
|
||||
"sip_local_network_addr": "10.10.10.204",
|
||||
"sip_network_ip": "10.10.10.100",
|
||||
"sip_network_port": "5060",
|
||||
"sip_invite_stamp": "1515666344534355",
|
||||
"sip_received_ip": "10.10.10.100",
|
||||
"sip_received_port": "5060",
|
||||
"sip_via_protocol": "udp",
|
||||
"sip_from_user_stripped": "1001",
|
||||
"sofia_profile_name": "internal",
|
||||
"recovery_profile_name": "internal",
|
||||
"sip_req_user": "1002",
|
||||
"sip_req_uri": "1002@10.10.10.204",
|
||||
"sip_req_host": "10.10.10.204",
|
||||
"sip_to_user": "1002",
|
||||
"sip_to_uri": "1002@10.10.10.204",
|
||||
"sip_to_host": "10.10.10.204",
|
||||
"sip_contact_params": "transport=udp;registering_acc=10_10_10_204",
|
||||
"sip_contact_user": "1001",
|
||||
"sip_contact_port": "5060",
|
||||
"sip_contact_uri": "1001@10.10.10.100:5060",
|
||||
"sip_contact_host": "10.10.10.100",
|
||||
"sip_user_agent": "Jitsi2.10.5550Linux",
|
||||
"sip_via_host": "10.10.10.100",
|
||||
"sip_via_port": "5060",
|
||||
"presence_id": "1001@10.10.10.204",
|
||||
"cgr_notify": "AUTH_OK",
|
||||
"max_forwards": "69",
|
||||
"transfer_history": "1515666344:b4300942-e809-4393-99cb-d39a1bc3c219:bl_xfer:1002/default/XML",
|
||||
"transfer_source": "1515666344:b4300942-e809-4393-99cb-d39a1bc3c219:bl_xfer:1002/default/XML",
|
||||
"DP_MATCH": "ARRAY::1002|:1002",
|
||||
"call_uuid": "3da8bf84-c133-4959-9e24-e72875cb33a1",
|
||||
"call_timeout": "30",
|
||||
"current_application_data": "user/1002@10.10.10.204",
|
||||
"current_application": "bridge",
|
||||
"dialed_user": "1002",
|
||||
"dialed_domain": "10.10.10.204",
|
||||
"originated_legs": "ARRAY::f52c26f1-b018-4963-bf6d-a3111d1a0320;Outbound Call;1002|:f52c26f1-b018-4963-bf6d-a3111d1a0320;Outbound Call;1002",
|
||||
"switch_m_sdp": "v=0\r\no=1002-jitsi.org 0 0 IN IP4 10.10.10.100\r\ns=-\r\nc=IN IP4 10.10.10.100\r\nt=0 0\r\nm=audio 5022 RTP/AVP 0 8 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\n",
|
||||
"rtp_use_codec_name": "PCMU",
|
||||
"rtp_use_codec_rate": "8000",
|
||||
"rtp_use_codec_ptime": "20",
|
||||
"rtp_use_codec_channels": "1",
|
||||
"rtp_last_audio_codec_string": "PCMU@8000h@20i@1c",
|
||||
"read_codec": "PCMU",
|
||||
"original_read_codec": "PCMU",
|
||||
"read_rate": "8000",
|
||||
"original_read_rate": "8000",
|
||||
"write_codec": "PCMU",
|
||||
"write_rate": "8000",
|
||||
"video_possible": "true",
|
||||
"video_media_flow": "sendonly",
|
||||
"local_media_ip": "10.10.10.204",
|
||||
"local_media_port": "21566",
|
||||
"advertised_media_ip": "10.10.10.204",
|
||||
"rtp_use_timer_name": "soft",
|
||||
"rtp_use_pt": "0",
|
||||
"rtp_use_ssrc": "1448966920",
|
||||
"endpoint_disposition": "ANSWER",
|
||||
"originate_causes": "ARRAY::f52c26f1-b018-4963-bf6d-a3111d1a0320;NONE|:f52c26f1-b018-4963-bf6d-a3111d1a0320;NONE",
|
||||
"originate_disposition": "SUCCESS",
|
||||
"DIALSTATUS": "SUCCESS",
|
||||
"last_bridge_to": "f52c26f1-b018-4963-bf6d-a3111d1a0320",
|
||||
"bridge_channel": "sofia/internal/1002@10.10.10.100:5060",
|
||||
"bridge_uuid": "f52c26f1-b018-4963-bf6d-a3111d1a0320",
|
||||
"signal_bond": "f52c26f1-b018-4963-bf6d-a3111d1a0320",
|
||||
"last_sent_callee_id_name": "Outbound Call",
|
||||
"last_sent_callee_id_number": "1002",
|
||||
"switch_r_sdp": "v=0\r\no=1001-jitsi.org 0 1 IN IP4 10.10.10.100\r\ns=-\r\nc=IN IP4 10.10.10.100\r\nt=0 0\r\nm=audio 5018 RTP/AVP 96 97 98 9 100 102 0 8 103 3 104 101\r\na=rtpmap:96 opus/48000/2\r\na=fmtp:96 usedtx=1\r\na=rtpmap:97 SILK/24000\r\na=rtpmap:98 SILK/16000\r\na=rtpmap:9 G722/8000\r\na=rtpmap:100 speex/32000\r\na=rtpmap:102 speex/16000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:103 iLBC/8000\r\na=rtpmap:3 GSM/8000\r\na=rtpmap:104 speex/8000\r\na=rtpmap:101 telephone-event/8000\r\na=sendonly\r\na=ptime:20\r\na=extmap:1 urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=rtcp-xr:voip-metrics\r\nm=video 0 RTP/AVP 105 99\r\n",
|
||||
"rtp_use_codec_string": "PCMU,PCMA",
|
||||
"audio_media_flow": "recvonly",
|
||||
"remote_media_ip": "10.10.10.100",
|
||||
"remote_media_port": "5018",
|
||||
"rtp_audio_recv_pt": "0",
|
||||
"dtmf_type": "rfc2833",
|
||||
"rtp_2833_send_payload": "101",
|
||||
"rtp_2833_recv_payload": "101",
|
||||
"rtp_local_sdp_str": "v=0\r\no=FreeSWITCH 1515644781 1515644783 IN IP4 10.10.10.204\r\ns=FreeSWITCH\r\nc=IN IP4 10.10.10.204\r\nt=0 0\r\nm=audio 21566 RTP/AVP 0 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-16\r\na=ptime:20\r\na=recvonly\r\n",
|
||||
"sip_to_tag": "m3g4NZ4rXFX3p",
|
||||
"sip_from_tag": "f25afe20",
|
||||
"sip_cseq": "2",
|
||||
"sip_call_id": "818e26f805701988c1a330175d7d2629@0:0:0:0:0:0:0:0",
|
||||
"sip_full_via": "SIP/2.0/UDP 10.10.10.100:5060;branch=z9hG4bK-313838-2ee350643dea826b4a74f8049852f307",
|
||||
"sip_from_display": "1001",
|
||||
"sip_full_from": "\"1001\" <sip:1001@10.10.10.204>;tag=f25afe20",
|
||||
"sip_full_to": "<sip:1002@10.10.10.204>;tag=m3g4NZ4rXFX3p",
|
||||
"sip_hangup_phrase": "OK",
|
||||
"last_bridge_hangup_cause": "NORMAL_CLEARING",
|
||||
"last_bridge_proto_specific_hangup_cause": "sip:200",
|
||||
"bridge_hangup_cause": "NORMAL_CLEARING",
|
||||
"hangup_cause": "NORMAL_CLEARING",
|
||||
"hangup_cause_q850": "16",
|
||||
"digits_dialed": "none",
|
||||
"start_stamp": "2018-01-11 11:25:44",
|
||||
"profile_start_stamp": "2018-01-11 11:25:44",
|
||||
"answer_stamp": "2018-01-11 11:25:47",
|
||||
"bridge_stamp": "2018-01-11 11:25:47",
|
||||
"hold_stamp": "2018-01-11 11:25:48",
|
||||
"progress_stamp": "2018-01-11 11:25:44",
|
||||
"progress_media_stamp": "2018-01-11 11:25:47",
|
||||
"hold_events": "{{1515666348363496,1515666415502648}}",
|
||||
"end_stamp": "2018-01-11 11:26:55",
|
||||
"start_epoch": "1515666344",
|
||||
"start_uepoch": "1515666344534355",
|
||||
"profile_start_epoch": "1515666344",
|
||||
"profile_start_uepoch": "1515666344534355",
|
||||
"answer_epoch": "1515666347",
|
||||
"answer_uepoch": "1515666347954373",
|
||||
"bridge_epoch": "1515666347",
|
||||
"bridge_uepoch": "1515666347954373",
|
||||
"last_hold_epoch": "1515666348",
|
||||
"last_hold_uepoch": "1515666348363496",
|
||||
"hold_accum_seconds": "67",
|
||||
"hold_accum_usec": "67139151",
|
||||
"hold_accum_ms": "67139",
|
||||
"resurrect_epoch": "0",
|
||||
"resurrect_uepoch": "0",
|
||||
"progress_epoch": "1515666344",
|
||||
"progress_uepoch": "1515666344594267",
|
||||
"progress_media_epoch": "1515666347",
|
||||
"progress_media_uepoch": "1515666347954373",
|
||||
"end_epoch": "1515666415",
|
||||
"end_uepoch": "1515666415494269",
|
||||
"last_app": "bridge",
|
||||
"last_arg": "user/1002@10.10.10.204",
|
||||
"caller_id": "\"1001\" <1001>",
|
||||
"duration": "71",
|
||||
"billsec": "68",
|
||||
"progresssec": "0",
|
||||
"answersec": "3",
|
||||
"waitsec": "3",
|
||||
"progress_mediasec": "3",
|
||||
"flow_billsec": "71",
|
||||
"mduration": "70960",
|
||||
"billmsec": "67540",
|
||||
"progressmsec": "60",
|
||||
"answermsec": "3420",
|
||||
"waitmsec": "3420",
|
||||
"progress_mediamsec": "3420",
|
||||
"flow_billmsec": "70960",
|
||||
"uduration": "70959914",
|
||||
"billusec": "67539896",
|
||||
"progressusec": "59912",
|
||||
"answerusec": "3420018",
|
||||
"waitusec": "3420018",
|
||||
"progress_mediausec": "3420018",
|
||||
"flow_billusec": "70959914",
|
||||
"sip_hangup_disposition": "send_bye",
|
||||
"rtp_audio_in_raw_bytes": "572588",
|
||||
"rtp_audio_in_media_bytes": "572588",
|
||||
"rtp_audio_in_packet_count": "3329",
|
||||
"rtp_audio_in_media_packet_count": "3329",
|
||||
"rtp_audio_in_skip_packet_count": "10",
|
||||
"rtp_audio_in_jitter_packet_count": "0",
|
||||
"rtp_audio_in_dtmf_packet_count": "0",
|
||||
"rtp_audio_in_cng_packet_count": "0",
|
||||
"rtp_audio_in_flush_packet_count": "0",
|
||||
"rtp_audio_in_largest_jb_size": "0",
|
||||
"rtp_audio_in_jitter_min_variance": "0.00",
|
||||
"rtp_audio_in_jitter_max_variance": "0.00",
|
||||
"rtp_audio_in_jitter_loss_rate": "0.00",
|
||||
"rtp_audio_in_jitter_burst_rate": "0.00",
|
||||
"rtp_audio_in_mean_interval": "0.00",
|
||||
"rtp_audio_in_flaw_total": "0",
|
||||
"rtp_audio_in_quality_percentage": "100.00",
|
||||
"rtp_audio_in_mos": "4.50",
|
||||
"rtp_audio_out_raw_bytes": "0",
|
||||
"rtp_audio_out_media_bytes": "0",
|
||||
"rtp_audio_out_packet_count": "0",
|
||||
"rtp_audio_out_media_packet_count": "0",
|
||||
"rtp_audio_out_skip_packet_count": "0",
|
||||
"rtp_audio_out_dtmf_packet_count": "0",
|
||||
"rtp_audio_out_cng_packet_count": "0",
|
||||
"rtp_audio_rtcp_packet_count": "0",
|
||||
"rtp_audio_rtcp_octet_count": "0"
|
||||
},
|
||||
"app_log": {
|
||||
"applications": [{
|
||||
"app_name": "park",
|
||||
"app_data": "",
|
||||
"app_stamp": "1515666344548466"
|
||||
}, {
|
||||
"app_name": "set",
|
||||
"app_data": "ringback=",
|
||||
"app_stamp": "1515666344575066"
|
||||
}, {
|
||||
"app_name": "set",
|
||||
"app_data": "call_timeout=30",
|
||||
"app_stamp": "1515666344576009"
|
||||
}, {
|
||||
"app_name": "bridge",
|
||||
"app_data": "user/1002@10.10.10.204",
|
||||
"app_stamp": "1515666344576703"
|
||||
}]
|
||||
},
|
||||
"callflow": [{
|
||||
"dialplan": "XML",
|
||||
"profile_index": "2",
|
||||
"extension": {
|
||||
"name": "Local_Extension",
|
||||
"number": "1002",
|
||||
"applications": [{
|
||||
"app_name": "set",
|
||||
"app_data": "ringback=${us-ring}"
|
||||
}, {
|
||||
"app_name": "set",
|
||||
"app_data": "call_timeout=30"
|
||||
}, {
|
||||
"app_name": "bridge",
|
||||
"app_data": "user/${destination_number}@${domain_name}"
|
||||
}]
|
||||
},
|
||||
"caller_profile": {
|
||||
"username": "1001",
|
||||
"dialplan": "XML",
|
||||
"caller_id_name": "1001",
|
||||
"ani": "1001",
|
||||
"aniii": "",
|
||||
"caller_id_number": "1001",
|
||||
"network_addr": "10.10.10.100",
|
||||
"rdnis": "1002",
|
||||
"destination_number": "1002",
|
||||
"uuid": "3da8bf84-c133-4959-9e24-e72875cb33a1",
|
||||
"source": "mod_sofia",
|
||||
"context": "default",
|
||||
"chan_name": "sofia/internal/1001@10.10.10.204",
|
||||
"originatee": {
|
||||
"originatee_caller_profiles": [{
|
||||
"username": "1001",
|
||||
"dialplan": "XML",
|
||||
"caller_id_name": "1001",
|
||||
"ani": "1001",
|
||||
"aniii": "",
|
||||
"caller_id_number": "1001",
|
||||
"network_addr": "10.10.10.100",
|
||||
"rdnis": "1002",
|
||||
"destination_number": "1002",
|
||||
"uuid": "f52c26f1-b018-4963-bf6d-a3111d1a0320",
|
||||
"source": "mod_sofia",
|
||||
"context": "default",
|
||||
"chan_name": "sofia/internal/1002@10.10.10.100:5060"
|
||||
}, {
|
||||
"username": "1001",
|
||||
"dialplan": "XML",
|
||||
"caller_id_name": "1001",
|
||||
"ani": "1001",
|
||||
"aniii": "",
|
||||
"caller_id_number": "1001",
|
||||
"network_addr": "10.10.10.100",
|
||||
"rdnis": "1002",
|
||||
"destination_number": "1002",
|
||||
"uuid": "f52c26f1-b018-4963-bf6d-a3111d1a0320",
|
||||
"source": "mod_sofia",
|
||||
"context": "default",
|
||||
"chan_name": "sofia/internal/1002@10.10.10.100:5060"
|
||||
}]
|
||||
}
|
||||
},
|
||||
"times": {
|
||||
"created_time": "1515666344534355",
|
||||
"profile_created_time": "1515666344534355",
|
||||
"progress_time": "1515666344594267",
|
||||
"progress_media_time": "1515666347954373",
|
||||
"answered_time": "1515666347954373",
|
||||
"bridged_time": "1515666347954373",
|
||||
"last_hold_time": "1515666348363496",
|
||||
"hold_accum_time": "67139151",
|
||||
"hangup_time": "1515666415494269",
|
||||
"resurrect_time": "0",
|
||||
"transfer_time": "0"
|
||||
}
|
||||
}, {
|
||||
"dialplan": "XML",
|
||||
"profile_index": "1",
|
||||
"extension": {
|
||||
"name": "CGRateS_Auth",
|
||||
"number": "1002",
|
||||
"applications": [{
|
||||
"app_name": "park",
|
||||
"app_data": ""
|
||||
}]
|
||||
},
|
||||
"caller_profile": {
|
||||
"username": "1001",
|
||||
"dialplan": "XML",
|
||||
"caller_id_name": "1001",
|
||||
"ani": "1001",
|
||||
"aniii": "",
|
||||
"caller_id_number": "1001",
|
||||
"network_addr": "10.10.10.100",
|
||||
"rdnis": "",
|
||||
"destination_number": "1002",
|
||||
"uuid": "3da8bf84-c133-4959-9e24-e72875cb33a1",
|
||||
"source": "mod_sofia",
|
||||
"context": "default",
|
||||
"chan_name": "sofia/internal/1001@10.10.10.204"
|
||||
},
|
||||
"times": {
|
||||
"created_time": "1515666344534355",
|
||||
"profile_created_time": "1515666344534355",
|
||||
"progress_time": "0",
|
||||
"progress_media_time": "0",
|
||||
"answered_time": "0",
|
||||
"bridged_time": "0",
|
||||
"last_hold_time": "0",
|
||||
"hold_accum_time": "0",
|
||||
"hangup_time": "0",
|
||||
"resurrect_time": "0",
|
||||
"transfer_time": "1515666344534355"
|
||||
}
|
||||
}]
|
||||
}`)
|
||||
|
||||
var fsCdrCfg *config.CGRConfig
|
||||
|
||||
func TestFsCdrFirstNonEmpty(t *testing.T) {
|
||||
fsCdrCfg = config.NewDefaultCGRConfig()
|
||||
reader := bytes.NewReader(body)
|
||||
fsCdr, err := NewFSCdr(reader, fsCdrCfg)
|
||||
if err != nil {
|
||||
t.Errorf("Error loading cdr: %v", err)
|
||||
}
|
||||
//fsc := fsCdr.(FSCdr)
|
||||
if _, ok := fsCdr.vars["cgr_notify"]; !ok {
|
||||
t.Error("Error parsing cdr: ", fsCdr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCdrCDRFields(t *testing.T) {
|
||||
fsCdrCfg.CdrsCfg().ExtraFields = config.NewRSRParsersMustCompile("~*req.sip_user_agent", utils.FieldsSep)
|
||||
reader := bytes.NewReader(body)
|
||||
fsCdr, err := NewFSCdr(reader, fsCdrCfg)
|
||||
if err != nil {
|
||||
t.Errorf("Error loading cdr: %v", err)
|
||||
}
|
||||
setupTime, _ := utils.ParseTimeDetectLayout("1515666344", "")
|
||||
answerTime, _ := utils.ParseTimeDetectLayout("1515666347", "")
|
||||
expctCDR := &CDR{
|
||||
ToR: utils.MetaVoice,
|
||||
OriginID: "3da8bf84-c133-4959-9e24-e72875cb33a1",
|
||||
OriginHost: "",
|
||||
Source: "freeswitch_json",
|
||||
Category: "call",
|
||||
RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org",
|
||||
Account: "1001",
|
||||
Subject: "1001",
|
||||
Destination: "1002",
|
||||
SetupTime: setupTime,
|
||||
AnswerTime: answerTime,
|
||||
Usage: 68 * time.Second,
|
||||
Cost: -1,
|
||||
ExtraFields: map[string]string{"sip_user_agent": "Jitsi2.10.5550Linux"},
|
||||
}
|
||||
if CDR, err := fsCdr.AsCDR(""); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(expctCDR, CDR) {
|
||||
t.Errorf("Expecting: %+v, received: %+v", expctCDR, CDR)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCdrSearchExtraFieldLast(t *testing.T) {
|
||||
newReader := bytes.NewReader(body)
|
||||
fsCdr, err := NewFSCdr(newReader, fsCdrCfg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
value := fsCdr.searchExtraField("progress_media_time", fsCdr.body)
|
||||
if value != "1515666347954373" {
|
||||
t.Error("Error finding extra field: ", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCdrSearchExtraField(t *testing.T) {
|
||||
newReader := bytes.NewReader(body)
|
||||
fsCdr, err := NewFSCdr(newReader, fsCdrCfg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fsCdrCfg.CdrsCfg().ExtraFields, err = config.NewRSRParsersFromSlice([]string{"~*req.caller_id_name"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extraFields := fsCdr.getExtraFields()
|
||||
if len(extraFields) != 1 || extraFields["caller_id_name"] != "1001" {
|
||||
t.Error("Error parsing extra fields: ", utils.ToJSON(extraFields))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFsCdrSearchExtraFieldInSlice(t *testing.T) {
|
||||
newReader := bytes.NewReader(body)
|
||||
if fsCdr, err := NewFSCdr(newReader, fsCdrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else if value := fsCdr.searchExtraField("floatfld1", map[string]interface{}{"floatfld1": 6.4}); value != "6.4" {
|
||||
t.Errorf("Expecting: 6.4, received: %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCdrSearchReplaceInExtraFields(t *testing.T) {
|
||||
fsCdrCfg.CdrsCfg().ExtraFields = config.NewRSRParsersMustCompile(`~*req.read_codec;~*req.sip_user_agent:s/([A-Za-z]*).+/$1/;~*req.write_codec`, utils.InfieldSep)
|
||||
newReader := bytes.NewReader(body)
|
||||
fsCdr, err := NewFSCdr(newReader, fsCdrCfg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
extraFields := fsCdr.getExtraFields()
|
||||
if len(extraFields) != 3 {
|
||||
t.Error("Error parsing extra fields: ", extraFields)
|
||||
}
|
||||
if extraFields["sip_user_agent"] != "Jitsi" {
|
||||
t.Error("Error parsing extra fields: ", utils.ToJSON(extraFields))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCdrDDazRSRExtraFields(t *testing.T) {
|
||||
eFieldsCfg := `{"cdrs": {
|
||||
"extra_fields": ["~*req.effective_caller_id_number:s/(\\d+)/+$1/"],
|
||||
},}`
|
||||
simpleJSONCdr := []byte(`{
|
||||
"core-uuid": "feef0b51-7fdf-4c4a-878e-aff233752de2",
|
||||
"channel_data": {
|
||||
"state": "CS_REPORTING",
|
||||
"state_number": "11",
|
||||
"flags": "0=1;1=1;3=1;36=1;37=1;39=1;42=1;47=1;52=1;73=1;75=1;94=1",
|
||||
"caps": "1=1;2=1;3=1;4=1;5=1;6=1"
|
||||
},
|
||||
"variables": {
|
||||
"uuid": "86cfd6e2-dbda-45a3-b59d-f683ec368e8b",
|
||||
"session_id": "5",
|
||||
"accountcode": "1001",
|
||||
"user_context": "default",
|
||||
"effective_caller_id_name": "Extension 1001",
|
||||
"effective_caller_id_number": "4986517174963",
|
||||
"outbound_caller_id_name": "FreeSWITCH",
|
||||
"outbound_caller_id_number": "0000000000"
|
||||
},
|
||||
"times": {
|
||||
"created_time": "1396984221278217",
|
||||
"profile_created_time": "1396984221278217",
|
||||
"progress_time": "0",
|
||||
"progress_media_time": "0",
|
||||
"answered_time": "0",
|
||||
"hangup_time": "0",
|
||||
"resurrect_time": "0",
|
||||
"transfer_time": "1396984221377035"
|
||||
}
|
||||
}`)
|
||||
var err error
|
||||
fsCdrCfg, err = config.NewCGRConfigFromJSONStringWithDefaults(eFieldsCfg)
|
||||
expCdrExtra := config.NewRSRParsersMustCompile(`~*req.effective_caller_id_number:s/(\d+)/+$1/`, utils.InfieldSep)
|
||||
if err != nil {
|
||||
t.Error("Could not parse the config", err.Error())
|
||||
} else if !reflect.DeepEqual(expCdrExtra[0], fsCdrCfg.CdrsCfg().ExtraFields[0]) { // Kinda deepEqual bug since without index does not match
|
||||
t.Errorf("Expecting: %+v, received: %+v", utils.ToJSON(expCdrExtra), utils.ToJSON(fsCdrCfg.CdrsCfg().ExtraFields))
|
||||
}
|
||||
newReader := bytes.NewReader(simpleJSONCdr)
|
||||
fsCdr, err := NewFSCdr(newReader, fsCdrCfg)
|
||||
if err != nil {
|
||||
t.Error("Could not parse cdr", err.Error())
|
||||
}
|
||||
extraFields := fsCdr.getExtraFields()
|
||||
if extraFields["effective_caller_id_number"] != "+4986517174963" {
|
||||
t.Errorf("Unexpected effective_caller_id_number received: %+v", extraFields["effective_caller_id_number"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCdrFirstDefined(t *testing.T) {
|
||||
newReader := bytes.NewReader(body)
|
||||
fsCdr, _ := NewFSCdr(newReader, fsCdrCfg)
|
||||
value := fsCdr.firstDefined([]string{utils.CGRSubject, utils.CGRAccount, fsUsernameVar}, fsUsername)
|
||||
if value != "1001" {
|
||||
t.Errorf("Expecting: 1001, received: %s", value)
|
||||
}
|
||||
value = fsCdr.firstDefined([]string{utils.CGRAccount, fsUsernameVar}, fsUsername)
|
||||
if value != "1001" {
|
||||
t.Errorf("Expecting: 1001, received: %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFscdrAsCDR(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
cgrCfg.CdrsCfg().ExtraFields, err = config.NewRSRParsersFromSlice([]string{"~*req.PayPalAccount"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fsCdrByte := []byte(` {
|
||||
"variables": {
|
||||
"cgr_orderid": "123",
|
||||
"cgr_partial": "true",
|
||||
"cgr_prerated": "false"
|
||||
}
|
||||
}`)
|
||||
expectedCdr := &CDR{
|
||||
OrderID: 123,
|
||||
ToR: utils.MetaVoice,
|
||||
Source: fsCDRSource, Category: cgrCfg.GeneralCfg().DefaultCategory,
|
||||
Tenant: cgrCfg.GeneralCfg().DefaultTenant,
|
||||
RequestType: cgrCfg.GeneralCfg().DefaultReqType,
|
||||
Partial: true,
|
||||
PreRated: false,
|
||||
ExtraFields: map[string]string{
|
||||
"PayPalAccount": "",
|
||||
},
|
||||
Cost: -1,
|
||||
}
|
||||
newReader := bytes.NewReader(fsCdrByte)
|
||||
if fsCdr, err := NewFSCdr(newReader, cgrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
if cdr, err := fsCdr.AsCDR(""); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(expectedCdr, cdr) {
|
||||
t.Errorf("Expected %+v \n, received %+v", utils.ToJSON(expectedCdr), utils.ToJSON(cdr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFscdrAsCdrOrderId(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
fsCdrByte := []byte(` {
|
||||
"variables": {
|
||||
"cgr_orderid": "123s"
|
||||
}
|
||||
}`)
|
||||
expectedErr := "strconv.ParseInt: parsing \"123s\": invalid syntax"
|
||||
newReader := bytes.NewReader(fsCdrByte)
|
||||
if fsCdr, err := NewFSCdr(newReader, cgrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else if _, err := fsCdr.AsCDR(""); err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Expected %+v \n, received %+v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFscdrAsCdrSetupTime(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
fsCdrByte := []byte(` {
|
||||
"variables": {
|
||||
"start_epoch": "123ss"
|
||||
}
|
||||
}`)
|
||||
expectedErr := "Unsupported time format"
|
||||
newReader := bytes.NewReader(fsCdrByte)
|
||||
if fsCdr, err := NewFSCdr(newReader, cgrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else if _, err := fsCdr.AsCDR(""); err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Expected %+v \n, received %+v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFscdrAsCdrAnswerTime(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
fsCdrByte := []byte(` {
|
||||
"variables": {
|
||||
"answer_epoch": "123ss"
|
||||
}
|
||||
}`)
|
||||
expectedErr := "Unsupported time format"
|
||||
newReader := bytes.NewReader(fsCdrByte)
|
||||
if fsCdr, err := NewFSCdr(newReader, cgrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else if _, err := fsCdr.AsCDR(""); err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Expected %+v \n, received %+v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFscdrAsCdrUsage(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
fsCdrByte := []byte(` {
|
||||
"variables": {
|
||||
"billsec": "1ss"
|
||||
}
|
||||
}`)
|
||||
expectedErr := "time: unknown unit \"ss\" in duration \"1ss\""
|
||||
newReader := bytes.NewReader(fsCdrByte)
|
||||
if fsCdr, err := NewFSCdr(newReader, cgrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else if _, err := fsCdr.AsCDR(""); err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Expected %+v \n, received %+v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFscdrAsCdrPartial(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
fsCdrByte := []byte(` {
|
||||
"variables": {
|
||||
"cgr_partial": "InvalidBoolFormat"
|
||||
}
|
||||
}`)
|
||||
expectedErr := "strconv.ParseBool: parsing \"InvalidBoolFormat\": invalid syntax"
|
||||
newReader := bytes.NewReader(fsCdrByte)
|
||||
if fsCdr, err := NewFSCdr(newReader, cgrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else if _, err := fsCdr.AsCDR(""); err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Expected %+v \n, received %+v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFscdrAsCdrPreRated(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
fsCdrByte := []byte(` {
|
||||
"variables": {
|
||||
"cgr_prerated": "InvalidBoolFormat"
|
||||
}
|
||||
}`)
|
||||
expectedErr := "strconv.ParseBool: parsing \"InvalidBoolFormat\": invalid syntax"
|
||||
newReader := bytes.NewReader(fsCdrByte)
|
||||
if fsCdr, err := NewFSCdr(newReader, cgrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else if _, err := fsCdr.AsCDR(""); err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Expected %+v \n, received %+v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFscdrAsCdrFirstDefined(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
fsCdrByte := []byte(` {
|
||||
"variables": {
|
||||
"cgr_account": "randomAccount"
|
||||
}
|
||||
}`)
|
||||
expectedCdr := &CDR{
|
||||
ToR: utils.MetaVoice,
|
||||
Source: fsCDRSource, Category: cgrCfg.GeneralCfg().DefaultCategory,
|
||||
Tenant: cgrCfg.GeneralCfg().DefaultTenant,
|
||||
RequestType: cgrCfg.GeneralCfg().DefaultReqType,
|
||||
Account: "randomAccount",
|
||||
Subject: "randomAccount",
|
||||
ExtraFields: map[string]string{},
|
||||
Cost: -1,
|
||||
}
|
||||
newReader := bytes.NewReader(fsCdrByte)
|
||||
if fsCdr, err := NewFSCdr(newReader, cgrCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
if cdr, err := fsCdr.AsCDR(""); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(expectedCdr, cdr) {
|
||||
t.Errorf("Expected %+v \n, redceived %+v", utils.ToJSON(expectedCdr), utils.ToJSON(cdr))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestNewFSCdrDecodeError(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
expectedErr := "EOF"
|
||||
newReader := bytes.NewReader(nil)
|
||||
if _, err := NewFSCdr(newReader, cgrCfg); err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Expected %+v, received %+v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchExtraFieldDefaultType(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
newMap := map[string]interface{}{
|
||||
"variables": map[string]string{
|
||||
"cgr_orderid": "123",
|
||||
},
|
||||
}
|
||||
fsCdr := FSCdr{
|
||||
cgrCfg: cgrCfg,
|
||||
body: newMap,
|
||||
}
|
||||
fsCdr.searchExtraField(utils.EmptyString, newMap)
|
||||
}
|
||||
|
||||
func TestSearchExtraFieldInterface(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
newMap := map[string]interface{}{ //There is a slice with no maps
|
||||
"variables": []interface{}{
|
||||
2,
|
||||
"randomValue",
|
||||
true,
|
||||
},
|
||||
}
|
||||
fsCdr := FSCdr{
|
||||
cgrCfg: cgrCfg,
|
||||
body: newMap,
|
||||
}
|
||||
fsCdr.searchExtraField(utils.EmptyString, newMap)
|
||||
}
|
||||
|
||||
func TestGetExtraFields(t *testing.T) {
|
||||
cgrCfg := config.NewDefaultCGRConfig()
|
||||
|
||||
cgrCfg.CdrsCfg().ExtraFields, err = config.NewRSRParsersFromSlice([]string{"PayPalAccount"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fsCdr := FSCdr{
|
||||
cgrCfg: cgrCfg,
|
||||
}
|
||||
expected := map[string]string{}
|
||||
if reply := fsCdr.getExtraFields(); !reflect.DeepEqual(reply, expected) {
|
||||
t.Errorf("Expected %+v, received %+v", utils.ToJSON(expected), utils.ToJSON(reply))
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
@@ -195,80 +193,6 @@ func (me MapEvent) AsMapString(ignoredFlds utils.StringSet) (mp map[string]strin
|
||||
return
|
||||
}
|
||||
|
||||
// AsCDR exports the MapEvent as CDR
|
||||
func (me MapEvent) AsCDR(cfg *config.CGRConfig, tnt, tmz string) (cdr *CDR, err error) {
|
||||
cdr = &CDR{Tenant: tnt, Cost: -1.0, ExtraFields: make(map[string]string)}
|
||||
for k, v := range me {
|
||||
if !utils.MainCDRFields.Has(k) { // not primary field, populate extra ones
|
||||
cdr.ExtraFields[k] = utils.IfaceAsString(v)
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
default:
|
||||
// for the momment this return can not be reached because we implemented a case for every MainCDRField
|
||||
return nil, fmt.Errorf("unimplemented CDR field: <%s>", k)
|
||||
case utils.RunID:
|
||||
cdr.RunID = utils.IfaceAsString(v)
|
||||
case utils.OriginHost:
|
||||
cdr.OriginHost = utils.IfaceAsString(v)
|
||||
case utils.Source:
|
||||
cdr.Source = utils.IfaceAsString(v)
|
||||
case utils.OriginID:
|
||||
cdr.OriginID = utils.IfaceAsString(v)
|
||||
case utils.ToR:
|
||||
cdr.ToR = utils.IfaceAsString(v)
|
||||
case utils.RequestType:
|
||||
cdr.RequestType = utils.IfaceAsString(v)
|
||||
case utils.Tenant:
|
||||
cdr.Tenant = utils.IfaceAsString(v)
|
||||
case utils.Category:
|
||||
cdr.Category = utils.IfaceAsString(v)
|
||||
case utils.AccountField:
|
||||
cdr.Account = utils.IfaceAsString(v)
|
||||
case utils.Subject:
|
||||
cdr.Subject = utils.IfaceAsString(v)
|
||||
case utils.Destination:
|
||||
cdr.Destination = utils.IfaceAsString(v)
|
||||
case utils.SetupTime:
|
||||
if cdr.SetupTime, err = utils.IfaceAsTime(v, tmz); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case utils.AnswerTime:
|
||||
if cdr.AnswerTime, err = utils.IfaceAsTime(v, tmz); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case utils.Usage:
|
||||
if cdr.Usage, err = utils.IfaceAsDuration(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case utils.Partial:
|
||||
if cdr.Partial, err = utils.IfaceAsBool(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case utils.PreRated:
|
||||
if cdr.PreRated, err = utils.IfaceAsBool(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case utils.CostSource:
|
||||
cdr.CostSource = utils.IfaceAsString(v)
|
||||
case utils.Cost:
|
||||
if cdr.Cost, err = utils.IfaceAsFloat64(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case utils.ExtraInfo:
|
||||
cdr.ExtraInfo = utils.IfaceAsString(v)
|
||||
case utils.OrderID:
|
||||
if cdr.OrderID, err = utils.IfaceAsTInt64(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if cfg != nil {
|
||||
cdr.AddDefaults(cfg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Data returns the MapEvent as a map[string]interface{}
|
||||
func (me MapEvent) Data() map[string]interface{} {
|
||||
return me
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
var sureTaxClient *http.Client // Cache the client here if in use
|
||||
|
||||
// Init a new request to be sent out to SureTax
|
||||
func NewSureTaxRequest(cdr *CDR, stCfg *config.SureTaxCfg) (*SureTaxRequest, error) {
|
||||
if stCfg == nil {
|
||||
return nil, errors.New("invalid SureTax config")
|
||||
}
|
||||
aTimeLoc := cdr.AnswerTime.In(stCfg.Timezone)
|
||||
revenue := utils.Round(cdr.Cost, 4, utils.MetaRoundingMiddle)
|
||||
unts, err := strconv.ParseInt(cdr.FieldsAsString(stCfg.Units), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
taxExempt := []string{}
|
||||
definedTaxExtempt := cdr.FieldsAsString(stCfg.TaxExemptionCodeList)
|
||||
if len(definedTaxExtempt) != 0 {
|
||||
taxExempt = strings.Split(cdr.FieldsAsString(stCfg.TaxExemptionCodeList), ",")
|
||||
}
|
||||
stReq := new(STRequest)
|
||||
stReq.ClientNumber = stCfg.ClientNumber
|
||||
stReq.BusinessUnit = stCfg.BusinessUnit
|
||||
stReq.ValidationKey = stCfg.ValidationKey
|
||||
stReq.DataYear = strconv.Itoa(aTimeLoc.Year())
|
||||
stReq.DataMonth = strconv.Itoa(int(aTimeLoc.Month()))
|
||||
stReq.TotalRevenue = revenue
|
||||
stReq.ReturnFileCode = stCfg.ReturnFileCode
|
||||
stReq.ClientTracking = cdr.FieldsAsString(stCfg.ClientTracking)
|
||||
stReq.ResponseGroup = stCfg.ResponseGroup
|
||||
stReq.ResponseType = stCfg.ResponseType
|
||||
stReq.ItemList = []*STRequestItem{
|
||||
{
|
||||
CustomerNumber: cdr.FieldsAsString(stCfg.CustomerNumber),
|
||||
OrigNumber: cdr.FieldsAsString(stCfg.OrigNumber),
|
||||
TermNumber: cdr.FieldsAsString(stCfg.TermNumber),
|
||||
BillToNumber: cdr.FieldsAsString(stCfg.BillToNumber),
|
||||
Zipcode: cdr.FieldsAsString(stCfg.Zipcode),
|
||||
Plus4: cdr.FieldsAsString(stCfg.Plus4),
|
||||
P2PZipcode: cdr.FieldsAsString(stCfg.P2PZipcode),
|
||||
P2PPlus4: cdr.FieldsAsString(stCfg.P2PPlus4),
|
||||
TransDate: aTimeLoc.Format("2006-01-02T15:04:05"),
|
||||
Revenue: revenue,
|
||||
Units: unts,
|
||||
UnitType: cdr.FieldsAsString(stCfg.UnitType),
|
||||
Seconds: int64(cdr.Usage.Seconds()),
|
||||
TaxIncludedCode: cdr.FieldsAsString(stCfg.TaxIncluded),
|
||||
TaxSitusRule: cdr.FieldsAsString(stCfg.TaxSitusRule),
|
||||
TransTypeCode: cdr.FieldsAsString(stCfg.TransTypeCode),
|
||||
SalesTypeCode: cdr.FieldsAsString(stCfg.SalesTypeCode),
|
||||
RegulatoryCode: stCfg.RegulatoryCode,
|
||||
TaxExemptionCodeList: taxExempt,
|
||||
},
|
||||
}
|
||||
jsnContent, err := json.Marshal(stReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SureTaxRequest{Request: string(jsnContent)}, nil
|
||||
}
|
||||
|
||||
// SureTax JSON Request
|
||||
type SureTaxRequest struct {
|
||||
Request string `json:"request"` // SureTax Requires us to encapsulate the content into a request element
|
||||
}
|
||||
|
||||
// SureTax JSON Response
|
||||
type SureTaxResponse struct {
|
||||
D string // SureTax requires encapsulating reply into a D object
|
||||
}
|
||||
|
||||
// SureTax Request type
|
||||
type STRequest struct {
|
||||
ClientNumber string // Client ID Number – provided by SureTax. Required. Max Len: 10
|
||||
BusinessUnit string // Client’s Business Unit. Value for this field is not required. Max Len: 20
|
||||
ValidationKey string // Validation Key provided by SureTax. Required for client access to API function. Max Len: 36
|
||||
DataYear string // Required. YYYY – Year to use for tax calculation purposes
|
||||
DataMonth string // Required. MM – Month to use for tax calculation purposes. Leading zero is preferred.
|
||||
TotalRevenue float64 // Required. Format: $$$$$$$$$.CCCC. For Negative charges, the first position should have a minus ‘-‘ indicator.
|
||||
ReturnFileCode string // Required. 0 – Default.Q – Quote purposes – taxes are computed and returned in the response message for generating quotes.
|
||||
ClientTracking string // Field for client transaction tracking. This value will be provided in the response data. Value for this field is not required, but preferred. Max Len: 100
|
||||
IndustryExemption string // Reserved for future use.
|
||||
ResponseGroup string // Required. Determines how taxes are grouped for the response.
|
||||
ResponseType string // Required. Determines the granularity of taxes and (optionally) the decimal precision for the tax calculations and amounts in the response.
|
||||
ItemList []*STRequestItem // List of Item records
|
||||
}
|
||||
|
||||
// Part of SureTax Request
|
||||
type STRequestItem struct {
|
||||
LineNumber string // Used to identify an item within the request. If no value is provided, requests are numbered sequentially. Max Len: 40
|
||||
InvoiceNumber string // Used for tax aggregation by Invoice. Must be alphanumeric. Max Len: 40
|
||||
CustomerNumber string // Used for tax aggregation by Customer. Must be alphanumeric. Max Len: 40
|
||||
OrigNumber string // Required when using Tax Situs Rule 01 or 03. Format: NPANXXNNNN
|
||||
TermNumber string // Required when using Tax Situs Rule 01. Format: NPANXXNNNN
|
||||
BillToNumber string // Required when using Tax Situs Rule 01 or 02. Format: NPANXXNNNN
|
||||
Zipcode string // Required when using Tax Situs Rule 04, 05, or 14.
|
||||
Plus4 string // Zip code extension in format: 9999 (not applicable for Tax Situs Rule 14)
|
||||
P2PZipcode string // Secondary zip code in format: 99999 (US or US territory) or X9X9X9 (Canadian)
|
||||
P2PPlus4 string // Secondary zip code extension in format: 99999 (US or US territory) or X9X9X9 (Canadian)
|
||||
TransDate string // Required. Date of transaction. Valid date formats include: MM/DD/YYYY, MM-DD-YYYY, YYYY-MM-DDTHH:MM:SS
|
||||
Revenue float64 // Required. Format: $$$$$$$$$.CCCC. For Negative charges, the first position should have a minus ‘-‘indicator.
|
||||
Units int64 // Required. Units representing number of “lines” or unique charges contained within the revenue. This value is essentially a multiplier on unit-based fees (e.g. E911 fees). Format: 99999. Default should be 1 (one unit).
|
||||
UnitType string // Required. 00 – Default / Number of unique access lines.
|
||||
Seconds int64 // Required. Duration of call in seconds. Format 99999. Default should be 1.
|
||||
TaxIncludedCode string // Required. Values: 0 – Default (No Tax Included) 1 – Tax Included in Revenue
|
||||
TaxSitusRule string // Required.
|
||||
TransTypeCode string // Required. Transaction Type Indicator.
|
||||
SalesTypeCode string // Required. Values: R – Residential customer (default) B – Business customer I – Industrial customer L – Lifeline customer
|
||||
RegulatoryCode string // Required. Provider Type.
|
||||
TaxExemptionCodeList []string // Required. Tax Exemption to be applied to this item only.
|
||||
}
|
||||
|
||||
type STResponse struct {
|
||||
Successful string // Response will be either ‘Y' or ‘N' : Y = Success / Success with Item error N = Failure
|
||||
ResponseCode string // ResponseCode: 9999 – Request was successful. 1101-1400 – Range of values for a failed request (no processing occurred) 9001 – Request was successful, but items within the request have errors. The specific items with errors are provided in the ItemMessages field.
|
||||
HeaderMessage string // Response message: For ResponseCode 9999 – “Success”For ResponseCode 9001 – “Success with Item errors”. For ResponseCode 1100-1400 – Unsuccessful / declined web request.
|
||||
ItemMessages []*STItemMessage // This field contains a list of items that were not able to be processed due to bad or invalid data (see Response Code of “9001”).
|
||||
ClientTracking string // Client transaction tracking provided in web request.
|
||||
TotalTax string // Total Tax – a total of all taxes included in the TaxList
|
||||
TransId int // Transaction ID – provided by SureTax
|
||||
GroupList []*STGroup // contains one-to-many Groups
|
||||
}
|
||||
|
||||
// Part of the SureTax Response
|
||||
type STItemMessage struct {
|
||||
LineNumber string // value corresponding to the line number in the web request
|
||||
ResponseCode string // a value in the range 9100-9400
|
||||
Message string // the error message corresponding to the ResponseCode
|
||||
}
|
||||
|
||||
// Part of the SureTax Response
|
||||
type STGroup struct {
|
||||
StateCode string // Tax State
|
||||
InvoiceNumber string // Invoice Number
|
||||
CustomerNumber string // Customer number
|
||||
TaxList []*STTaxItem // contains one-to-many Tax Items
|
||||
}
|
||||
|
||||
// Part of the SureTax Response
|
||||
type STTaxItem struct {
|
||||
TaxTypeCode string // Tax Type Code
|
||||
TaxTypeDesc string // Tax Type Description
|
||||
TaxAmount string // Tax Amount
|
||||
}
|
||||
|
||||
func SureTaxProcessCdr(cdr *CDR) error {
|
||||
stCfg := config.CgrConfig().SureTaxCfg()
|
||||
if stCfg == nil {
|
||||
return errors.New("Invalid SureTax configuration")
|
||||
}
|
||||
if sureTaxClient == nil { // First time used, init the client here
|
||||
sureTaxClient = &http.Client{
|
||||
Transport: httpPstrTransport,
|
||||
}
|
||||
}
|
||||
req, err := NewSureTaxRequest(cdr, stCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jsnContent, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := sureTaxClient.Post(stCfg.URL, "application/json", bytes.NewBuffer(jsnContent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode > 299 {
|
||||
return fmt.Errorf("Unexpected status code received: %d", resp.StatusCode)
|
||||
}
|
||||
var respFull SureTaxResponse
|
||||
if err := json.Unmarshal(respBody, &respFull); err != nil {
|
||||
return err
|
||||
}
|
||||
var stResp STResponse
|
||||
if err := json.Unmarshal([]byte(respFull.D), &stResp); err != nil {
|
||||
return err
|
||||
}
|
||||
if stResp.ResponseCode != "9999" {
|
||||
cdr.ExtraInfo = stResp.HeaderMessage
|
||||
return nil // No error because the request was processed by SureTax, error will be in the ExtraInfo
|
||||
}
|
||||
// Write cost to CDR
|
||||
totalTax, err := strconv.ParseFloat(stResp.TotalTax, 64)
|
||||
if err != nil {
|
||||
cdr.ExtraInfo = err.Error()
|
||||
}
|
||||
if !stCfg.IncludeLocalCost {
|
||||
cdr.Cost = utils.Round(totalTax,
|
||||
config.CgrConfig().GeneralCfg().RoundingDecimals,
|
||||
utils.MetaRoundingMiddle)
|
||||
} else {
|
||||
cdr.Cost = utils.Round(cdr.Cost+totalTax,
|
||||
config.CgrConfig().GeneralCfg().RoundingDecimals,
|
||||
utils.MetaRoundingMiddle)
|
||||
}
|
||||
// Add response into extra fields to be available for later review
|
||||
cdr.ExtraFields[utils.MetaSureTax] = respFull.D
|
||||
return nil
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
func TestNewSureTaxRequest(t *testing.T) {
|
||||
cdr := &CDR{OrderID: 123, ToR: utils.MetaVoice,
|
||||
OriginID: "dsafdsaf", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001",
|
||||
Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault,
|
||||
Usage: 12 * time.Second,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
Cost: 1.01, PreRated: true,
|
||||
}
|
||||
cfg := config.NewDefaultCGRConfig()
|
||||
stCfg := cfg.SureTaxCfg()
|
||||
stCfg.ClientNumber = "000000000"
|
||||
stCfg.ValidationKey = "19491161-F004-4F44-BDB3-E976D6739A64"
|
||||
stCfg.Timezone = time.UTC
|
||||
eSTRequest := &STRequest{
|
||||
ClientNumber: "000000000",
|
||||
ValidationKey: "19491161-F004-4F44-BDB3-E976D6739A64",
|
||||
DataYear: "2013",
|
||||
DataMonth: "11",
|
||||
TotalRevenue: 1.01,
|
||||
ReturnFileCode: "0",
|
||||
ResponseGroup: "03",
|
||||
ResponseType: "D4",
|
||||
ItemList: []*STRequestItem{
|
||||
{
|
||||
CustomerNumber: "1001",
|
||||
OrigNumber: "1001",
|
||||
TermNumber: "1002",
|
||||
BillToNumber: "",
|
||||
TransDate: "2013-11-07T08:42:26",
|
||||
Revenue: 1.01,
|
||||
Units: 1,
|
||||
UnitType: "00",
|
||||
Seconds: 12,
|
||||
TaxIncludedCode: "0",
|
||||
TaxSitusRule: "04",
|
||||
TransTypeCode: "010101",
|
||||
SalesTypeCode: "R",
|
||||
RegulatoryCode: "03",
|
||||
TaxExemptionCodeList: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
jsnReq, _ := json.Marshal(eSTRequest)
|
||||
eSureTaxRequest := &SureTaxRequest{Request: string(jsnReq)}
|
||||
if stReq, err := NewSureTaxRequest(cdr, stCfg); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(eSureTaxRequest, stReq) {
|
||||
t.Errorf("Expecting:\n%s\nReceived:\n%s", string(eSureTaxRequest.Request), string(stReq.Request))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuretaxNewSureTaxRequestNilCfg(t *testing.T) {
|
||||
|
||||
cdr := &CDR{
|
||||
OrderID: 123,
|
||||
ToR: utils.MetaVoice,
|
||||
OriginID: "testOriginID",
|
||||
OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest,
|
||||
RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org",
|
||||
Category: "call",
|
||||
Account: "1001",
|
||||
Subject: "1001",
|
||||
Destination: "1002",
|
||||
SetupTime: time.Date(2021, 1, 1, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2021, 1, 1, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault,
|
||||
Usage: 12 * time.Second,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
Cost: 1.01, PreRated: true,
|
||||
}
|
||||
var stCfg *config.SureTaxCfg
|
||||
|
||||
experr := "invalid SureTax config"
|
||||
stReq, err := NewSureTaxRequest(cdr, stCfg)
|
||||
|
||||
if err == nil || err.Error() != experr {
|
||||
t.Fatalf("\nExpected: %q, \nReceived: %q", experr, err)
|
||||
}
|
||||
|
||||
if stReq != nil {
|
||||
t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>",
|
||||
nil, stReq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuretaxNewSureTaxRequestInvalidUnits(t *testing.T) {
|
||||
cdr := &CDR{OrderID: 123, ToR: utils.MetaVoice,
|
||||
OriginID: "testOriginID", OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest, RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org", Category: "call", Account: "1001",
|
||||
Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2021, 1, 1, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2021, 1, 1, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault,
|
||||
Usage: 12 * time.Second,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
Cost: 1.01, PreRated: true,
|
||||
}
|
||||
cfg := config.NewDefaultCGRConfig()
|
||||
stCfg := cfg.SureTaxCfg()
|
||||
stCfg.Units = nil
|
||||
stCfg.ClientNumber = "000000000"
|
||||
stCfg.ValidationKey = "19491161-F004-4F44-BDB3-E976D6739A64"
|
||||
stCfg.Timezone = time.UTC
|
||||
experr := "strconv.ParseInt: parsing \"\": invalid syntax"
|
||||
stReq, err := NewSureTaxRequest(cdr, stCfg)
|
||||
|
||||
if err == nil || err.Error() != experr {
|
||||
t.Fatalf("\nExpected: %q, \nReceived: %q", experr, err)
|
||||
}
|
||||
|
||||
if stReq != nil {
|
||||
t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>",
|
||||
nil, stReq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuretaxSureTaxProcessCdrPostErr(t *testing.T) {
|
||||
cdr := &CDR{
|
||||
|
||||
OrderID: 123,
|
||||
ToR: utils.MetaVoice,
|
||||
OriginID: "testOriginID",
|
||||
OriginHost: "192.168.1.1",
|
||||
Source: utils.UnitTest,
|
||||
RequestType: utils.MetaRated,
|
||||
Tenant: "cgrates.org",
|
||||
Category: "call",
|
||||
Account: "1001",
|
||||
Subject: "1001",
|
||||
Destination: "1002",
|
||||
SetupTime: time.Date(2021, 1, 1, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2021, 1, 1, 8, 42, 26, 0, time.UTC),
|
||||
RunID: utils.MetaDefault,
|
||||
Usage: 12 * time.Second,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
Cost: 1.01, PreRated: true,
|
||||
}
|
||||
|
||||
experr := `Post "": unsupported protocol scheme ""`
|
||||
err := SureTaxProcessCdr(cdr)
|
||||
|
||||
if err == nil || err.Error() != experr {
|
||||
t.Errorf("\nExpected: %q, \nReceived: %q", experr, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user