diff --git a/agents/dmtagent.go b/agents/dmtagent.go index 162689fd4..23314e69c 100644 --- a/agents/dmtagent.go +++ b/agents/dmtagent.go @@ -20,6 +20,7 @@ package agents import ( "fmt" + "strconv" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" @@ -82,14 +83,22 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro cca := NewBareCCAFromCCR(ccr, self.cgrCfg.DiameterAgentCfg().OriginHost, self.cgrCfg.DiameterAgentCfg().OriginRealm) smgEv, err := ccr.AsSMGenericEvent(reqProcessor.CCRFields) if err != nil { - cca.ResultCode = DiameterRatingFailed + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA reply-code, error: %s", ccr.diamMessage, err)) + return nil + } utils.Logger.Err(fmt.Sprintf(" Processing message: %+v AsSMGenericEvent, error: %s", ccr.diamMessage, err)) return cca } var maxUsage float64 if reqProcessor.DryRun { // DryRun does not send over network utils.Logger.Info(fmt.Sprintf(" SMGenericEvent: %+v", smgEv)) - cca.ResultCode = diam.LimitedSuccess + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.LimitedSuccess), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } } else { // Find out maxUsage over APIs switch ccr.CCRequestType { case 1: @@ -101,7 +110,7 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro if ccr.CCRequestType == 3 { err = self.smg.Call("SMGenericV1.SessionEnd", smgEv, &rpl) } else if ccr.CCRequestType == 4 { - err = self.smg.Call("SMGenericV1.ChargeEvent", smgEv, &rpl) + err = self.smg.Call("SMGenericV1.ChargeEvent", smgEv, &maxUsage) } if self.cgrCfg.DiameterAgentCfg().CreateCDR { if errCdr := self.smg.Call("SMGenericV1.ProcessCdr", smgEv, &rpl); errCdr != nil { @@ -110,19 +119,41 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro } } if err != nil { - cca.ResultCode = DiameterRatingFailed + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } utils.Logger.Err(fmt.Sprintf(" Processing message: %+v, API error: %s", ccr.diamMessage, err)) return cca } - if ccr.CCRequestType != 3 && maxUsage == 0 { // Not enough balance, RFC demands 4012 - cca.ResultCode = 4012 - } else { - cca.ResultCode = diam.Success + var unauthorizedResultCode bool + if ccr.CCRequestType != 3 && ccr.CCRequestType != 4 && maxUsage == 0 { // Not enough balance, RFC demands 4012 + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, "4012", + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } + unauthorizedResultCode = true + } else if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(diam.Success), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } + if ccr.CCRequestType != 3 && ccr.CCRequestType != 4 && !unauthorizedResultCode { // For terminate or previously marked unauthorized, we don't add granted-service-unit AVP + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Granted-Service-Unit", "CC-Time"}, strconv.FormatFloat(maxUsage, 'f', 0, 64), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Granted-Service-Unit, error: %s", ccr.diamMessage, err)) + return nil + } } - cca.GrantedServiceUnit.CCTime = int(maxUsage) } if err := cca.SetProcessorAVPs(reqProcessor, maxUsage); err != nil { - cca.ResultCode = DiameterRatingFailed + if err := messageSetAVPsWithPath(cca.diamMessage, []interface{}{"Result-Code"}, strconv.Itoa(DiameterRatingFailed), + false, self.cgrCfg.DiameterAgentCfg().Timezone); err != nil { + utils.Logger.Err(fmt.Sprintf(" Processing message: %+v set CCA Reply-Code, error: %s", ccr.diamMessage, err)) + return nil + } utils.Logger.Err(fmt.Sprintf(" CCA SetProcessorAVPs for message: %+v, error: %s", ccr.diamMessage, err)) return cca } diff --git a/agents/dmtagent_it_test.go b/agents/dmtagent_it_test.go index 9a449b0b0..3ec68ef73 100644 --- a/agents/dmtagent_it_test.go +++ b/agents/dmtagent_it_test.go @@ -213,6 +213,11 @@ func TestDmtAgentSendCCRInit(t *testing.T) { } else if strCCTime := avpValAsString(avps[0]); strCCTime != "300" { t.Errorf("Expecting 300, received: %s", strCCTime) } + if result, err := msg.FindAVP("Result-Code", dict.UndefinedVendorID); err != nil { + t.Error(err) + } else if resultStr := avpValAsString(result); resultStr != "2001" { + t.Errorf("Expecting 2001, received: %s", resultStr) + } var acnt *engine.Account attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} eAcntVal := 9.484 @@ -434,7 +439,7 @@ func TestDmtAgentCdrs(t *testing.T) { if cdrs[0].Usage != "610" { t.Errorf("Unexpected CDR Usage received, cdr: %+v ", cdrs[0]) } - if cdrs[0].Cost != 0.795 { + if cdrs[0].Cost != 0.5349 { t.Errorf("Unexpected CDR Cost received, cdr: %+v ", cdrs[0]) } } diff --git a/apier/v1/apier.go b/apier/v1/apier.go index fcd11bd4d..23ae308ec 100644 --- a/apier/v1/apier.go +++ b/apier/v1/apier.go @@ -510,6 +510,7 @@ func (self *ApierV1) SetActions(attrs utils.AttrSetActions, reply *string) error Weight: apiAct.Weight, ExpirationString: apiAct.ExpiryTime, ExtraParameters: apiAct.ExtraParameters, + Filter: apiAct.Filter, Balance: &engine.Balance{ Uuid: utils.GenUUID(), Id: apiAct.BalanceId, @@ -546,6 +547,7 @@ func (self *ApierV1) GetActions(actsId string, reply *[]*utils.TPAction) error { BalanceType: engAct.BalanceType, ExpiryTime: engAct.ExpirationString, ExtraParameters: engAct.ExtraParameters, + Filter: engAct.Filter, Weight: engAct.Weight, } if engAct.Balance != nil { @@ -739,7 +741,7 @@ type AttrResetTriggeredAction struct { func (self *ApierV1) ResetTriggeredActions(attr AttrResetTriggeredAction, reply *string) error { var a *engine.Action if attr.Id != "" { - // we can identify the trigge by the id + // we can identify the trigger by the id a = &engine.Action{Id: attr.Id} } else { extraParameters, err := json.Marshal(struct { diff --git a/apier/v1/cdre.go b/apier/v1/cdre.go index fe77a03d1..224058cfc 100644 --- a/apier/v1/cdre.go +++ b/apier/v1/cdre.go @@ -144,6 +144,10 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E if attr.SmsUsageMultiplyFactor != nil && *attr.SmsUsageMultiplyFactor != 0.0 { smsUsageMultiplyFactor = *attr.SmsUsageMultiplyFactor } + mmsUsageMultiplyFactor := exportTemplate.MMSUsageMultiplyFactor + if attr.MmsUsageMultiplyFactor != nil && *attr.MmsUsageMultiplyFactor != 0.0 { + mmsUsageMultiplyFactor = *attr.MmsUsageMultiplyFactor + } genericUsageMultiplyFactor := exportTemplate.GenericUsageMultiplyFactor if attr.GenericUsageMultiplyFactor != nil && *attr.GenericUsageMultiplyFactor != 0.0 { genericUsageMultiplyFactor = *attr.GenericUsageMultiplyFactor @@ -179,8 +183,7 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E *reply = utils.ExportedFileCdrs{ExportedFilePath: ""} return nil } - cdrexp, err := cdre.NewCdrExporter(cdrs, self.CdrDb, exportTemplate, cdrFormat, fieldSep, exportId, dataUsageMultiplyFactor, smsUsageMultiplyFactor, genericUsageMultiplyFactor, - costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify, self.Config.DefaultTimezone) + cdrexp, err := cdre.NewCdrExporter(cdrs, self.CdrDb, exportTemplate, cdrFormat, fieldSep, exportId, dataUsageMultiplyFactor, smsUsageMultiplyFactor, mmsUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify, self.Config.DefaultTimezone) if err != nil { return utils.NewErrServerError(err) } diff --git a/apier/v1/smgenericbirpcv1.go b/apier/v1/smgenericbirpcv1.go index b80bcf960..035d55b86 100644 --- a/apier/v1/smgenericbirpcv1.go +++ b/apier/v1/smgenericbirpcv1.go @@ -100,11 +100,12 @@ func (self *SMGenericBiRpcV1) SessionEnd(clnt *rpc2.Client, ev sessionmanager.SM } // Called on individual Events (eg SMS) -func (self *SMGenericBiRpcV1) ChargeEvent(clnt *rpc2.Client, ev sessionmanager.SMGenericEvent, reply *string) error { - if err := self.sm.ChargeEvent(ev, clnt); err != nil { +func (self *SMGenericBiRpcV1) ChargeEvent(clnt *rpc2.Client, ev sessionmanager.SMGenericEvent, maxUsage *float64) error { + if minMaxUsage, err := self.sm.ChargeEvent(ev, clnt); err != nil { return utils.NewErrServerError(err) + } else { + *maxUsage = minMaxUsage.Seconds() } - *reply = utils.OK return nil } diff --git a/apier/v1/smgenericv1.go b/apier/v1/smgenericv1.go index 834ebbe9f..d56c1d431 100644 --- a/apier/v1/smgenericv1.go +++ b/apier/v1/smgenericv1.go @@ -71,11 +71,12 @@ func (self *SMGenericV1) SessionEnd(ev sessionmanager.SMGenericEvent, reply *str } // Called on individual Events (eg SMS) -func (self *SMGenericV1) ChargeEvent(ev sessionmanager.SMGenericEvent, reply *string) error { - if err := self.sm.ChargeEvent(ev, nil); err != nil { +func (self *SMGenericV1) ChargeEvent(ev sessionmanager.SMGenericEvent, maxUsage *float64) error { + if minMaxUsage, err := self.sm.ChargeEvent(ev, nil); err != nil { return utils.NewErrServerError(err) + } else { + *maxUsage = minMaxUsage.Seconds() } - *reply = utils.OK return nil } @@ -146,7 +147,7 @@ func (self *SMGenericV1) Call(serviceMethod string, args interface{}, reply inte if !canConvert { return rpcclient.ErrWrongArgsType } - replyConverted, canConvert := reply.(*string) + replyConverted, canConvert := reply.(*float64) if !canConvert { return rpcclient.ErrWrongReplyType } diff --git a/apier/v2/accounts.go b/apier/v2/accounts.go index 73a8f9fe7..d1c49b5fd 100644 --- a/apier/v2/accounts.go +++ b/apier/v2/accounts.go @@ -77,3 +77,101 @@ func (self *ApierV2) GetAccount(attr *utils.AttrGetAccount, reply *engine.Accoun *reply = *account return nil } + +type AttrSetAccount struct { + Tenant string + Account string + ActionPlanId string + ActionTriggersId string + AllowNegative *bool + Disabled *bool + ReloadScheduler bool +} + +func (self *ApierV2) SetAccount(attr AttrSetAccount, reply *string) error { + if missing := utils.MissingStructFields(&attr, []string{"Tenant", "Account"}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + var schedulerReloadNeeded = false + accID := utils.AccountKey(attr.Tenant, attr.Account) + var ub *engine.Account + _, err := engine.Guardian.Guard(func() (interface{}, error) { + if bal, _ := self.AccountDb.GetAccount(accID); bal != nil { + ub = bal + } else { // Not found in db, create it here + ub = &engine.Account{ + Id: accID, + } + } + if len(attr.ActionPlanId) != 0 { + _, err := engine.Guardian.Guard(func() (interface{}, error) { + var ap *engine.ActionPlan + var err error + ap, err = self.RatingDb.GetActionPlan(attr.ActionPlanId, false) + if err != nil { + return 0, err + } + if _, exists := ap.AccountIDs[accID]; !exists { + if ap.AccountIDs == nil { + ap.AccountIDs = make(map[string]struct{}) + } + ap.AccountIDs[accID] = struct{}{} + schedulerReloadNeeded = true + // create tasks + for _, at := range ap.ActionTimings { + if at.IsASAP() { + t := &engine.Task{ + Uuid: utils.GenUUID(), + AccountID: accID, + ActionsID: at.ActionsID, + } + if err = self.RatingDb.PushTask(t); err != nil { + return 0, err + } + } + } + if err := self.RatingDb.SetActionPlan(attr.ActionPlanId, ap); err != nil { + return 0, err + } + // update cache + self.RatingDb.CacheRatingPrefixValues(map[string][]string{utils.ACTION_PLAN_PREFIX: []string{utils.ACTION_PLAN_PREFIX + attr.ActionPlanId}}) + } + return 0, nil + }, 0, utils.ACTION_PLAN_PREFIX) + if err != nil { + return 0, err + } + } + + if len(attr.ActionTriggersId) != 0 { + atrs, err := self.RatingDb.GetActionTriggers(attr.ActionTriggersId) + if err != nil { + return 0, err + } + ub.ActionTriggers = atrs + ub.InitCounters() + } + if attr.AllowNegative != nil { + ub.AllowNegative = *attr.AllowNegative + } + if attr.Disabled != nil { + ub.Disabled = *attr.Disabled + } + // All prepared, save account + if err := self.AccountDb.SetAccount(ub); err != nil { + return 0, err + } + return 0, nil + }, 0, accID) + if err != nil { + return utils.NewErrServerError(err) + } + if attr.ReloadScheduler && schedulerReloadNeeded { + // reload scheduler + if self.Sched != nil { + self.Sched.Reload(true) + } + } + *reply = utils.OK // This will mark saving of the account, error still can show up in actionTimingsId + return nil +} diff --git a/apier/v2/cdre.go b/apier/v2/cdre.go index d4e0d81bb..31ea8f43e 100644 --- a/apier/v2/cdre.go +++ b/apier/v2/cdre.go @@ -80,6 +80,10 @@ func (self *ApierV2) ExportCdrsToFile(attr utils.AttrExportCdrsToFile, reply *ut if attr.SMSUsageMultiplyFactor != nil && *attr.SMSUsageMultiplyFactor != 0.0 { SMSUsageMultiplyFactor = *attr.SMSUsageMultiplyFactor } + MMSUsageMultiplyFactor := exportTemplate.MMSUsageMultiplyFactor + if attr.MMSUsageMultiplyFactor != nil && *attr.MMSUsageMultiplyFactor != 0.0 { + MMSUsageMultiplyFactor = *attr.MMSUsageMultiplyFactor + } genericUsageMultiplyFactor := exportTemplate.GenericUsageMultiplyFactor if attr.GenericUsageMultiplyFactor != nil && *attr.GenericUsageMultiplyFactor != 0.0 { genericUsageMultiplyFactor = *attr.GenericUsageMultiplyFactor @@ -115,8 +119,7 @@ func (self *ApierV2) ExportCdrsToFile(attr utils.AttrExportCdrsToFile, reply *ut *reply = utils.ExportedFileCdrs{ExportedFilePath: ""} return nil } - cdrexp, err := cdre.NewCdrExporter(cdrs, self.CdrDb, exportTemplate, cdrFormat, fieldSep, ExportID, dataUsageMultiplyFactor, SMSUsageMultiplyFactor, genericUsageMultiplyFactor, - costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify, self.Config.DefaultTimezone) + cdrexp, err := cdre.NewCdrExporter(cdrs, self.CdrDb, exportTemplate, cdrFormat, fieldSep, ExportID, dataUsageMultiplyFactor, SMSUsageMultiplyFactor, MMSUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify, self.Config.DefaultTimezone) if err != nil { return utils.NewErrServerError(err) } diff --git a/cdre/cdrexporter.go b/cdre/cdrexporter.go index 00fbe67d9..a98a26c95 100644 --- a/cdre/cdrexporter.go +++ b/cdre/cdrexporter.go @@ -43,6 +43,7 @@ const ( META_NRCDRS = "*cdrs_number" META_DURCDRS = "*cdrs_duration" META_SMSUSAGE = "*sms_usage" + META_MMSUSAGE = "*mms_usage" META_GENERICUSAGE = "*generic_usage" META_DATAUSAGE = "*data_usage" META_COSTCDRS = "*cdrs_cost" @@ -53,7 +54,7 @@ const ( var err error func NewCdrExporter(cdrs []*engine.CDR, cdrDb engine.CdrStorage, exportTpl *config.CdreConfig, cdrFormat string, fieldSeparator rune, exportId string, - dataUsageMultiplyFactor, smsUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor float64, + dataUsageMultiplyFactor, smsUsageMultiplyFactor, mmsUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor float64, costShiftDigits, roundDecimals, cgrPrecision int, maskDestId string, maskLen int, httpSkipTlsCheck bool, timezone string) (*CdrExporter, error) { if len(cdrs) == 0 { // Nothing to export return nil, nil @@ -66,7 +67,7 @@ func NewCdrExporter(cdrs []*engine.CDR, cdrDb engine.CdrStorage, exportTpl *conf fieldSeparator: fieldSeparator, exportId: exportId, dataUsageMultiplyFactor: dataUsageMultiplyFactor, - smsUsageMultiplyFactor: smsUsageMultiplyFactor, + mmsUsageMultiplyFactor: mmsUsageMultiplyFactor, costMultiplyFactor: costMultiplyFactor, costShiftDigits: costShiftDigits, roundDecimals: roundDecimals, @@ -92,18 +93,19 @@ type CdrExporter struct { exportId string // Unique identifier or this export dataUsageMultiplyFactor, smsUsageMultiplyFactor, // Multiply the SMS usage (eg: some billing systems billing them as minutes) + mmsUsageMultiplyFactor, genericUsageMultiplyFactor, costMultiplyFactor float64 - costShiftDigits, roundDecimals, cgrPrecision int - maskDestId string - maskLen int - httpSkipTlsCheck bool - timezone string - header, trailer []string // Header and Trailer fields - content [][]string // Rows of cdr fields - firstCdrATime, lastCdrATime time.Time - numberOfRecords int - totalDuration, totalDataUsage, totalSmsUsage, totalGenericUsage time.Duration + costShiftDigits, roundDecimals, cgrPrecision int + maskDestId string + maskLen int + httpSkipTlsCheck bool + timezone string + header, trailer []string // Header and Trailer fields + content [][]string // Rows of cdr fields + firstCdrATime, lastCdrATime time.Time + numberOfRecords int + totalDuration, totalDataUsage, totalSmsUsage, totalMmsUsage, totalGenericUsage time.Duration totalCost float64 firstExpOrderId, lastExpOrderId int64 @@ -234,6 +236,9 @@ func (cdre *CdrExporter) metaHandler(tag, arg string) (string, error) { case META_SMSUSAGE: emulatedCdr := &engine.CDR{ToR: utils.SMS, Usage: cdre.totalSmsUsage} return emulatedCdr.FormatUsage(arg), nil + case META_MMSUSAGE: + emulatedCdr := &engine.CDR{ToR: utils.MMS, Usage: cdre.totalMmsUsage} + return emulatedCdr.FormatUsage(arg), nil case META_GENERICUSAGE: emulatedCdr := &engine.CDR{ToR: utils.GENERIC, Usage: cdre.totalGenericUsage} return emulatedCdr.FormatUsage(arg), nil @@ -322,6 +327,8 @@ func (cdre *CdrExporter) processCdr(cdr *engine.CDR) error { cdr.UsageMultiply(cdre.dataUsageMultiplyFactor, cdre.cgrPrecision) } else if cdre.smsUsageMultiplyFactor != 0 && cdr.ToR == utils.SMS { cdr.UsageMultiply(cdre.smsUsageMultiplyFactor, cdre.cgrPrecision) + } else if cdre.mmsUsageMultiplyFactor != 0 && cdr.ToR == utils.MMS { + cdr.UsageMultiply(cdre.mmsUsageMultiplyFactor, cdre.cgrPrecision) } else if cdre.genericUsageMultiplyFactor != 0 && cdr.ToR == utils.GENERIC { cdr.UsageMultiply(cdre.genericUsageMultiplyFactor, cdre.cgrPrecision) } @@ -392,6 +399,9 @@ func (cdre *CdrExporter) processCdr(cdr *engine.CDR) error { if cdr.ToR == utils.SMS { // Count usage for SMS cdre.totalSmsUsage += cdr.Usage } + if cdr.ToR == utils.MMS { // Count usage for MMS + cdre.totalMmsUsage += cdr.Usage + } if cdr.ToR == utils.GENERIC { // Count usage for GENERIC cdre.totalGenericUsage += cdr.Usage } diff --git a/cdre/cdrexporter_test.go b/cdre/cdrexporter_test.go index 0960c8b8f..045fc0e4b 100644 --- a/cdre/cdrexporter_test.go +++ b/cdre/cdrexporter_test.go @@ -52,7 +52,7 @@ func TestCdreGetCombimedCdrFieldVal(t *testing.T) { Usage: time.Duration(10) * time.Second, RunID: "RETAIL1", Cost: 5.01}, } cdre, err := NewCdrExporter(cdrs, nil, cfg.CdreProfiles["*default"], cfg.CdreProfiles["*default"].CdrFormat, cfg.CdreProfiles["*default"].FieldSeparator, - "firstexport", 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "") + "firstexport", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "") if err != nil { t.Error("Unexpected error received: ", err) } diff --git a/cdre/csv_test.go b/cdre/csv_test.go index 0990c7a80..c69c140b9 100644 --- a/cdre/csv_test.go +++ b/cdre/csv_test.go @@ -40,7 +40,7 @@ func TestCsvCdrWriter(t *testing.T) { Usage: time.Duration(10) * time.Second, RunID: utils.DEFAULT_RUNID, ExtraFields: map[string]string{"extra1": "val_extra1", "extra2": "val_extra2", "extra3": "val_extra3"}, Cost: 1.01, } - cdre, err := NewCdrExporter([]*engine.CDR{storedCdr1}, nil, cfg.CdreProfiles["*default"], utils.CSV, ',', "firstexport", 0.0, 0.0, 0.0, 0.0, 0, 4, + cdre, err := NewCdrExporter([]*engine.CDR{storedCdr1}, nil, cfg.CdreProfiles["*default"], utils.CSV, ',', "firstexport", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "") if err != nil { t.Error("Unexpected error received: ", err) @@ -69,7 +69,7 @@ func TestAlternativeFieldSeparator(t *testing.T) { ExtraFields: map[string]string{"extra1": "val_extra1", "extra2": "val_extra2", "extra3": "val_extra3"}, Cost: 1.01, } cdre, err := NewCdrExporter([]*engine.CDR{storedCdr1}, nil, cfg.CdreProfiles["*default"], utils.CSV, '|', - "firstexport", 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "") + "firstexport", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify, "") if err != nil { t.Error("Unexpected error received: ", err) } diff --git a/cdre/fixedwidth_test.go b/cdre/fixedwidth_test.go index ffdd1f747..ef5f5137e 100644 --- a/cdre/fixedwidth_test.go +++ b/cdre/fixedwidth_test.go @@ -127,7 +127,7 @@ func TestWriteCdr(t *testing.T) { Usage: time.Duration(10) * time.Second, RunID: utils.DEFAULT_RUNID, Cost: 2.34567, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, } - cdre, err := NewCdrExporter([]*engine.CDR{cdr}, nil, cdreCfg, utils.CDRE_FIXED_WIDTH, ',', "fwv_1", 0.0, 0.0, 0.0, 0.0, 0, 4, + cdre, err := NewCdrExporter([]*engine.CDR{cdr}, nil, cdreCfg, utils.CDRE_FIXED_WIDTH, ',', "fwv_1", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify, "") if err != nil { t.Error(err) @@ -203,7 +203,7 @@ func TestWriteCdrs(t *testing.T) { } cfg, _ := config.NewDefaultCGRConfig() cdre, err := NewCdrExporter([]*engine.CDR{cdr1, cdr2, cdr3, cdr4}, nil, cdreCfg, utils.CDRE_FIXED_WIDTH, ',', - "fwv_1", 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify, "") + "fwv_1", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify, "") if err != nil { t.Error(err) } diff --git a/config/cdreconfig.go b/config/cdreconfig.go index 55a98c3b5..f55bad28e 100644 --- a/config/cdreconfig.go +++ b/config/cdreconfig.go @@ -24,6 +24,7 @@ type CdreConfig struct { FieldSeparator rune DataUsageMultiplyFactor float64 SMSUsageMultiplyFactor float64 + MMSUsageMultiplyFactor float64 GenericUsageMultiplyFactor float64 CostMultiplyFactor float64 CostRoundingDecimals int @@ -54,6 +55,9 @@ func (self *CdreConfig) loadFromJsonCfg(jsnCfg *CdreJsonCfg) error { if jsnCfg.Sms_usage_multiply_factor != nil { self.SMSUsageMultiplyFactor = *jsnCfg.Sms_usage_multiply_factor } + if jsnCfg.Mms_usage_multiply_factor != nil { + self.MMSUsageMultiplyFactor = *jsnCfg.Mms_usage_multiply_factor + } if jsnCfg.Generic_usage_multiply_factor != nil { self.GenericUsageMultiplyFactor = *jsnCfg.Generic_usage_multiply_factor } @@ -100,6 +104,7 @@ func (self *CdreConfig) Clone() *CdreConfig { clnCdre.FieldSeparator = self.FieldSeparator clnCdre.DataUsageMultiplyFactor = self.DataUsageMultiplyFactor clnCdre.SMSUsageMultiplyFactor = self.SMSUsageMultiplyFactor + clnCdre.MMSUsageMultiplyFactor = self.MMSUsageMultiplyFactor clnCdre.GenericUsageMultiplyFactor = self.GenericUsageMultiplyFactor clnCdre.CostMultiplyFactor = self.CostMultiplyFactor clnCdre.CostRoundingDecimals = self.CostRoundingDecimals diff --git a/config/cfg_data.json b/config/cfg_data.json index 77f20a7da..f5eb4a811 100644 --- a/config/cfg_data.json +++ b/config/cfg_data.json @@ -33,7 +33,7 @@ "run_delay": 1, "cdr_source_id": "csv2", // free form field, tag identifying the source of the CDRs within CDRS database "content_fields":[ // import template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value - {"field_id": "ToR", "value": "~7:s/^(voice|data|sms|generic)$/*$1/"}, + {"field_id": "ToR", "value": "~7:s/^(voice|data|sms|mms|generic)$/*$1/"}, {"field_id": "AnswerTime", "value": "1"}, {"field_id": "Usage", "value": "~9:s/^(\\d+)$/${1}s/"}, ], diff --git a/config/config_defaults.go b/config/config_defaults.go index 8040aec98..cc66ebe52 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -134,6 +134,7 @@ const CGRATES_CFG_JSON = ` "field_separator": ",", "data_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from KBytes to Bytes) "sms_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from SMS unit to call duration in some billing systems) + "mms_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from MMS unit to call duration in some billing systems) "generic_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from GENERIC unit to call duration in some billing systems) "cost_multiply_factor": 1, // multiply cost before export, eg: add VAT "cost_rounding_decimals": -1, // rounding decimals for Cost values. -1 to disable rounding diff --git a/config/config_json_test.go b/config/config_json_test.go index 30f6f6fbc..36a779fff 100644 --- a/config/config_json_test.go +++ b/config/config_json_test.go @@ -252,6 +252,7 @@ func TestDfCdreJsonCfgs(t *testing.T) { Field_separator: utils.StringPointer(","), Data_usage_multiply_factor: utils.Float64Pointer(1.0), Sms_usage_multiply_factor: utils.Float64Pointer(1.0), + Mms_usage_multiply_factor: utils.Float64Pointer(1.0), Generic_usage_multiply_factor: utils.Float64Pointer(1.0), Cost_multiply_factor: utils.Float64Pointer(1.0), Cost_rounding_decimals: utils.IntPointer(-1), @@ -621,7 +622,7 @@ func TestNewCgrJsonCfgFromFile(t *testing.T) { t.Error("Received: ", gCfg) } cdrFields := []*CdrFieldJsonCfg{ - &CdrFieldJsonCfg{Field_id: utils.StringPointer(utils.TOR), Value: utils.StringPointer("~7:s/^(voice|data|sms|generic)$/*$1/")}, + &CdrFieldJsonCfg{Field_id: utils.StringPointer(utils.TOR), Value: utils.StringPointer("~7:s/^(voice|data|sms|mms|generic)$/*$1/")}, &CdrFieldJsonCfg{Field_id: utils.StringPointer(utils.ANSWER_TIME), Value: utils.StringPointer("1")}, &CdrFieldJsonCfg{Field_id: utils.StringPointer(utils.USAGE), Value: utils.StringPointer(`~9:s/^(\d+)$/${1}s/`)}, } @@ -645,7 +646,7 @@ func TestNewCgrJsonCfgFromFile(t *testing.T) { if cfg, err := cgrJsonCfg.CdrcJsonCfg(); err != nil { t.Error(err) } else if !reflect.DeepEqual(eCfgCdrc, cfg) { - t.Error("Received: ", cfg["CDRC-CSV2"]) + t.Error("Received: ", utils.ToIJSON(cfg["CDRC-CSV2"])) } eCfgSmFs := &SmFsJsonCfg{ Enabled: utils.BoolPointer(true), diff --git a/config/libconfig_json.go b/config/libconfig_json.go index 24195518c..f751cf83c 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -127,6 +127,7 @@ type CdreJsonCfg struct { Field_separator *string Data_usage_multiply_factor *float64 Sms_usage_multiply_factor *float64 + Mms_usage_multiply_factor *float64 Generic_usage_multiply_factor *float64 Cost_multiply_factor *float64 Cost_rounding_decimals *int diff --git a/data/storage/mysql/create_tariffplan_tables.sql b/data/storage/mysql/create_tariffplan_tables.sql index 14feeb608..15783c927 100644 --- a/data/storage/mysql/create_tariffplan_tables.sql +++ b/data/storage/mysql/create_tariffplan_tables.sql @@ -160,8 +160,10 @@ CREATE TABLE `tp_actions` ( `categories` varchar(32) NOT NULL, `shared_groups` varchar(64) NOT NULL, `balance_weight` DECIMAL(8,2) NOT NULL, + `balance_blocker` BOOLEAN NOT NULL, `balance_disabled` BOOLEAN NOT NULL, `extra_parameters` varchar(256) NOT NULL, + `filter` varchar(256) NOT NULL, `weight` DECIMAL(8,2) NOT NULL, `created_at` TIMESTAMP, PRIMARY KEY (`id`), @@ -211,6 +213,7 @@ CREATE TABLE `tp_action_triggers` ( `balance_expiry_time` varchar(24) NOT NULL, `balance_timing_tags` varchar(128) NOT NULL, `balance_weight` DECIMAL(8,2) NOT NULL, + `balance_blocker` BOOL NOT NULL, `balance_disabled` BOOL NOT NULL, `min_queued_items` int(11) NOT NULL, `actions_tag` varchar(64) NOT NULL, diff --git a/data/storage/postgres/create_tariffplan_tables.sql b/data/storage/postgres/create_tariffplan_tables.sql index 8ecdffc73..857fb956f 100644 --- a/data/storage/postgres/create_tariffplan_tables.sql +++ b/data/storage/postgres/create_tariffplan_tables.sql @@ -155,8 +155,10 @@ CREATE TABLE tp_actions ( categories VARCHAR(32) NOT NULL, shared_groups VARCHAR(64) NOT NULL, balance_weight NUMERIC(8,2) NOT NULL, + balance_blocker BOOLEAN NOT NULL, balance_disabled BOOLEAN NOT NULL, extra_parameters VARCHAR(256) NOT NULL, + filter VARCHAR(256) NOT NULL, weight NUMERIC(8,2) NOT NULL, created_at TIMESTAMP, UNIQUE (tpid, tag, action, balance_tag, balance_type, directions, expiry_time, timing_tags, destination_tags, shared_groups, balance_weight, weight) @@ -206,6 +208,7 @@ CREATE TABLE tp_action_triggers ( balance_expiry_time VARCHAR(24) NOT NULL, balance_timing_tags VARCHAR(128) NOT NULL, balance_weight NUMERIC(8,2) NOT NULL, + balance_blocker BOOL NOT NULL, balance_disabled BOOL NOT NULL, min_queued_items INTEGER NOT NULL, actions_tag VARCHAR(64) NOT NULL, diff --git a/data/tariffplans/cdrstats/ActionTriggers.csv b/data/tariffplans/cdrstats/ActionTriggers.csv index 061f0d531..6081b9d67 100644 --- a/data/tariffplans/cdrstats/ActionTriggers.csv +++ b/data/tariffplans/cdrstats/ActionTriggers.csv @@ -1,6 +1,6 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceDisabled[16],StatsMinQueuedItems[17],ActionsTag[18],Weight[19] -CDRST3_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,3,CDRST_LOG,10 -CDRST3_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST3_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST4_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,5,CDRST_LOG,10 -CDRST4_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,2,CDRST_LOG,10 +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsTag[19],Weight[20] +CDRST3_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_LOG,10 +CDRST3_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST3_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST4_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_LOG,10 +CDRST4_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,2,CDRST_LOG,10 diff --git a/data/tariffplans/cdrstats/Actions.csv b/data/tariffplans/cdrstats/Actions.csv index 09e0c5a14..a87f15e9b 100644 --- a/data/tariffplans/cdrstats/Actions.csv +++ b/data/tariffplans/cdrstats/Actions.csv @@ -1,2 +1 @@ -#ActionsTag,Action,BalanceTag,BalanceType,Directions,Units,ExpiryTime,TimingTags,DestinationTags,RatingSubject,Categories,BalanceWeight,SharedGroup,ExtraParameters,Weight -CDRST_LOG,*log,,,,,,,,,,,,,false,10 +CDRST_LOG,*log,,,,,,,,,,,,,,false,false,10 diff --git a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv index 1c8e2803b..c50ff9a7c 100644 --- a/data/tariffplans/prepaid1centpsec/ActionTriggers.csv +++ b/data/tariffplans/prepaid1centpsec/ActionTriggers.csv @@ -1,9 +1,9 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceDisabled[16],StatsMinQueuedItems[17],ActionsTag[18],Weight[19] -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,LOG_BALANCE,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,LOG_BALANCE,10 -STANDARD_TRIGGERS,,*max_event_counter,15,false,0,,*monetary,*out,,FS_USERS,,,,,,,,LOG_BALANCE,10 -CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,2,CDRST_LOG,10 +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationTags[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingTags[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsTag[19],Weight[20] +STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_BALANCE,10 +STANDARD_TRIGGERS,,*max_event_counter,15,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_BALANCE,10 +CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,2,CDRST_LOG,10 diff --git a/data/tariffplans/prepaid1centpsec/Actions.csv b/data/tariffplans/prepaid1centpsec/Actions.csv index 6680eccaa..2729b8c77 100644 --- a/data/tariffplans/prepaid1centpsec/Actions.csv +++ b/data/tariffplans/prepaid1centpsec/Actions.csv @@ -1,6 +1,6 @@ -#ActionsTag[0],Action[1],ActionExtraParameters[2],BalanceTag[3],BalanceType[4],Directions[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingTags[11],Units[12],BalanceWeight[13],BalanceDisabled[14],Weight[15] -PREPAID_10,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,10 -BONUS_1,*topup,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,10 -LOG_BALANCE,*log,,,,,,,,,,,,,false,10 -CDRST_WARN_HTTP,*call_url,http://localhost:8080,,,,,,,,,,,,false,10 -CDRST_LOG,*log,,,,,,,,,,,,,false,10 +#ActionsTag[0],Action[1],ActionExtraParameters[2],Filter[3],BalanceTag[4],BalanceType[5],Directions[6],Categories[7],DestinationIds[8],RatingSubject[9],SharedGroup[10],ExpiryTime[11],TimingTags[12],Units[13],BalanceWeight[14],BalanceBlocker[15],BalanceDisabled[16],Weight[17] +PREPAID_10,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +BONUS_1,*topup,,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 +LOG_BALANCE,*log,,,,,,,,,,,,,,false,false,10 +CDRST_WARN_HTTP,*call_url,http://localhost:8080,,,,,,,,,,,,,false,false,10 +CDRST_LOG,*log,,,,,,,,,,,,,,false,false,10 diff --git a/data/tariffplans/tutorial/ActionTriggers.csv b/data/tariffplans/tutorial/ActionTriggers.csv index 15209486f..e68fbdf34 100644 --- a/data/tariffplans/tutorial/ActionTriggers.csv +++ b/data/tariffplans/tutorial/ActionTriggers.csv @@ -1,12 +1,12 @@ -#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationIds[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingIds[14],BalanceWeight[15],BalanceDisabled[16],StatsMinQueuedItems[17],ActionsId[18],Weight[19] -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,100,false,0,,*monetary,*out,,,,,,,,,,DISABLE_AND_LOG,10 -CDRST1_WARN,,*min_asr,45,true,1m,,,,,,,,,,,,3,LOG_WARNING,10 -CDRST1_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1_WARN,,*max_acc,10,true,1m,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1001_WARN,,*min_asr,65,true,1m,,,,,,,,,,,,3,LOG_WARNING,10 -CDRST1001_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST1001_WARN,,*max_acc,5,true,1m,,,,,,,,,,,,5,LOG_WARNING,10 -CDRST3_WARN,,*min_acd,60,false,1m,,,,,,,,,,,,5,LOG_WARNING,10 +#Tag[0],UniqueId[1],ThresholdType[2],ThresholdValue[3],Recurrent[4],MinSleep[5],BalanceTag[6],BalanceType[7],BalanceDirections[8],BalanceCategories[9],BalanceDestinationIds[10],BalanceRatingSubject[11],BalanceSharedGroup[12],BalanceExpiryTime[13],BalanceTimingIds[14],BalanceWeight[15],BalanceBlocker[16],BalanceDisabled[17],StatsMinQueuedItems[18],ActionsId[19],Weight[20] +STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,100,false,0,,*monetary,*out,,,,,,,,,,,DISABLE_AND_LOG,10 +CDRST1_WARN,,*min_asr,45,true,1m,,,,,,,,,,,,,3,LOG_WARNING,10 +CDRST1_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1_WARN,,*max_acc,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1001_WARN,,*min_asr,65,true,1m,,,,,,,,,,,,,3,LOG_WARNING,10 +CDRST1001_WARN,,*min_acd,10,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST1001_WARN,,*max_acc,5,true,1m,,,,,,,,,,,,,5,LOG_WARNING,10 +CDRST3_WARN,,*min_acd,60,false,1m,,,,,,,,,,,,,5,LOG_WARNING,10 diff --git a/data/tariffplans/tutorial/Actions.csv b/data/tariffplans/tutorial/Actions.csv index ede6ee591..93a3faa1f 100644 --- a/data/tariffplans/tutorial/Actions.csv +++ b/data/tariffplans/tutorial/Actions.csv @@ -1,10 +1,10 @@ -#ActionsId[0],Action[1],ExtraParameters[2],BalanceId[3],BalanceType[4],Directions[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingIds[11],Units[12],BalanceWeight[13],BalanceDisabled[14],Weight[15] -TOPUP_RST_10,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,10 -TOPUP_RST_5,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,5,20,false,10 -TOPUP_RST_5,*topup_reset,,,*voice,*out,,DST_1002,SPECIAL_1002,,*unlimited,,90,20,false,10 -TOPUP_120_DST1003,*topup_reset,,,*voice,*out,,DST_1003,,,*unlimited,,120,20,false,10 -TOPUP_RST_SHARED_5,*topup,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,5,10,false,10 -SHARED_A_0,*topup_reset,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,0,10,false,10 -LOG_WARNING,*log,,,,,,,,,,,,,false,10 -DISABLE_AND_LOG,*log,,,,,,,,,,,,,false,10 -DISABLE_AND_LOG,*disable_account,,,,,,,,,,,,,false,10 +#ActionsId[0],Action[1],ExtraParameters[2],Filter[3],BalanceId[4],BalanceType[5],Directions[6],Categories[7],DestinationIds[8],RatingSubject[9],SharedGroup[10],ExpiryTime[11],TimingIds[12],Units[13],BalanceWeight[14],BalanceBlocker[15],BalanceDisabled[16],Weight[17] +TOPUP_RST_10,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +TOPUP_RST_5,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,5,20,false,false,10 +TOPUP_RST_5,*topup_reset,,,,*voice,*out,,DST_1002,SPECIAL_1002,,*unlimited,,90,20,false,false,10 +TOPUP_120_DST1003,*topup_reset,,,,*voice,*out,,DST_1003,,,*unlimited,,120,20,false,false,10 +TOPUP_RST_SHARED_5,*topup,,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,5,10,false,false,10 +SHARED_A_0,*topup_reset,,,,*monetary,*out,,*any,,SHARED_A,*unlimited,,0,10,false,false,10 +LOG_WARNING,*log,,,,,,,,,,,,,,false,false,10 +DISABLE_AND_LOG,*log,,,,,,,,,,,,,,false,false,10 +DISABLE_AND_LOG,*disable_account,,,,,,,,,,,,,,false,false,10 diff --git a/docs/cdrserver.rst b/docs/cdrserver.rst index 686ffb510..4d6c9133c 100644 --- a/docs/cdrserver.rst +++ b/docs/cdrserver.rst @@ -11,7 +11,7 @@ CDR-CGR Available as handler within http server. -To feed CDRs in via this interface, one must use url of the form: . +To feed CDRs in via this interface, one must use url of the form: . The CDR fields are received via http form (although for simplicity we support inserting them within query parameters as well) and are expected to be urlencoded in order to transport special characters reliably. All fields are expected by CGRateS as string, particular conversions being done on processing each CDR. The fields received are split into two different categories based on CGRateS interest in them: @@ -38,7 +38,7 @@ Extra fields: any field coming in via the http request and not a member of prima Example of sample CDR generated simply using curl: :: - curl --data "curl --data "tor=*voice&accid=iiaasbfdsaf&cdrhost=192.168.1.1&cdrsource=curl_cdr&reqtype=rated&direction=*out&tenant=192.168.56.66&category=call&account=dan&subject=dan&destination=%2B4986517174963&answer_time=1383813746&usage=1&sip_user=Jitsi&subject2=1003" http://127.0.0.1:2080/cgr + curl --data "curl --data "tor=*voice&accid=iiaasbfdsaf&cdrhost=192.168.1.1&cdrsource=curl_cdr&reqtype=rated&direction=*out&tenant=192.168.56.66&category=call&account=dan&subject=dan&destination=%2B4986517174963&answer_time=1383813746&usage=1&sip_user=Jitsi&subject2=1003" http://127.0.0.1:2080/cdr_http CDR-FS_JSON diff --git a/engine/account.go b/engine/account.go index b3dcb777b..7ae6b8083 100644 --- a/engine/account.go +++ b/engine/account.go @@ -26,6 +26,7 @@ import ( "github.com/cgrates/cgrates/cache2go" "github.com/cgrates/cgrates/utils" + "github.com/cgrates/structmatcher" "strings" ) @@ -176,9 +177,11 @@ func (ub *Account) enableDisableBalanceAction(a *Action) error { } found := false id := a.BalanceType + disabled := a.Balance.Disabled + a.Balance.Disabled = !disabled // match for the opposite for _, b := range ub.BalanceMap[id] { if b.MatchFilter(a.Balance, false) { - b.Disabled = a.Balance.Disabled + b.Disabled = disabled b.dirty = true found = true } @@ -316,9 +319,13 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo return } } + // check for blocker + if dryRun && balance.Blocker { + //log.Print("BLOCKER!") + return // don't go to next balances + } } } - // debit money moneyBalanceChecker := true for moneyBalanceChecker { @@ -332,14 +339,13 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo return nil, debitErr } //utils.Logger.Info(fmt.Sprintf("CD AFTER MONEY: %+v", cd)) - //log.Printf("partCC: %+v", partCC) if partCC != nil { cc.Timespans = append(cc.Timespans, partCC.Timespans...) cc.negativeConnectFee = partCC.negativeConnectFee - //for i, ts := range cc.Timespans { - //log.Printf("cc.times[an[%d]: %+v\n", i, ts) - //} + /*for i, ts := range cc.Timespans { + log.Printf("cc.times[an[%d]: %+v\n", i, ts) + }*/ cd.TimeStart = cc.GetEndTime() //log.Printf("CD: %+v", cd) //log.Printf("CD: %+v - %+v", cd.TimeStart, cd.TimeEnd) @@ -354,6 +360,11 @@ func (ub *Account) debitCreditBalance(cd *CallDescriptor, count bool, dryRun boo return } } + // check for blocker + if dryRun && balance.Blocker { + //log.Print("BLOCKER!") + return // don't go to next balances + } } } //log.Printf("END CD: %+v", cd) @@ -687,6 +698,31 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance } } +func (acc *Account) matchActionFilter(condition string) (bool, error) { + sm, err := structmatcher.NewStructMatcher(condition) + if err != nil { + return false, err + } + for balanceType, balanceChain := range acc.BalanceMap { + for _, b := range balanceChain { + check, err := sm.Match(&struct { + Type string + *Balance + }{ + Type: balanceType, + Balance: b, + }) + if err != nil { + return false, err + } + if check { + return true, nil + } + } + } + return false, nil +} + // used in some api for transition func (acc *Account) AsOldStructure() interface{} { type Balance struct { diff --git a/engine/action.go b/engine/action.go index 734b86c64..147b176a7 100644 --- a/engine/action.go +++ b/engine/action.go @@ -42,7 +42,8 @@ type Action struct { ActionType string BalanceType string ExtraParameters string - ExpirationString string + Filter string + ExpirationString string // must stay as string because it can have relative values like 1month Weight float64 Balance *Balance } @@ -131,9 +132,9 @@ func getActionFunc(typ string) (actionTypeFunc, bool) { case SET_DDESTINATIONS: return setddestinations, true case REMOVE_ACCOUNT: - return removeAccount, true + return removeAccountAction, true case REMOVE_BALANCE: - return removeBalance, true + return removeBalanceAction, true case TRANSFER_MONETARY_DEFAULT: return transferMonetaryDefault, true } @@ -522,7 +523,7 @@ func setddestinations(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actio return nil } -func removeAccount(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { +func removeAccountAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { var accID string if ub != nil { accID = ub.Id @@ -568,7 +569,7 @@ func removeAccount(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) return nil } -func removeBalance(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { +func removeBalanceAction(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { if _, exists := ub.BalanceMap[a.BalanceType]; !exists { return utils.ErrNotFound } @@ -592,6 +593,10 @@ func removeBalance(ub *Account, sq *StatsQueueTriggered, a *Action, acs Actions) } func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { + if acc == nil { + utils.Logger.Err("*transfer_monetary_default called without account") + return utils.ErrAccountNotFound + } if _, exists := acc.BalanceMap[utils.MONETARY]; !exists { return utils.ErrNotFound } @@ -599,7 +604,8 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a bChain := acc.BalanceMap[utils.MONETARY] for _, balance := range bChain { if balance.Uuid != defaultBalance.Uuid && - balance.Id != defaultBalance.Id { // extra caution + balance.Id != defaultBalance.Id && // extra caution + balance.MatchFilter(a.Balance, false) { if balance.Value > 0 { defaultBalance.Value += balance.Value balance.Value = 0 diff --git a/engine/action_plan.go b/engine/action_plan.go index 1acea3887..163ca4595 100644 --- a/engine/action_plan.go +++ b/engine/action_plan.go @@ -291,6 +291,16 @@ func (at *ActionTiming) Execute() (err error) { transactionFailed := false removeAccountActionFound := false for _, a := range aac { + // check action filter + if len(a.Filter) > 0 { + matched, err := ub.matchActionFilter(a.Filter) + if err != nil { + return 0, err + } + if !matched { + continue + } + } if ub.Disabled && a.ActionType != ENABLE_ACCOUNT { continue // disabled acocunts are not removed from action plan //return 0, fmt.Errorf("Account %s is disabled", accID) diff --git a/engine/action_trigger.go b/engine/action_trigger.go index 286736c81..9fa9ace1d 100644 --- a/engine/action_trigger.go +++ b/engine/action_trigger.go @@ -46,7 +46,8 @@ type ActionTrigger struct { BalanceRatingSubject string // filter for balance BalanceCategories utils.StringMap // filter for balance BalanceSharedGroups utils.StringMap // filter for balance - BalanceDisabled bool // filter for balance + BalanceBlocker bool + BalanceDisabled bool // filter for balance Weight float64 ActionsId string MinQueuedItems int // Trigger actions only if this number is hit (stats only) @@ -75,6 +76,17 @@ func (at *ActionTrigger) Execute(ub *Account, sq *StatsQueueTriggered) (err erro transactionFailed := false removeAccountActionFound := false for _, a := range aac { + // check action filter + if len(a.Filter) > 0 { + matched, err := ub.matchActionFilter(a.Filter) + if err != nil { + return err + } + if !matched { + continue + } + } + if a.Balance == nil { a.Balance = &Balance{} } @@ -117,7 +129,7 @@ func (at *ActionTrigger) Match(a *Action) bool { return match } id := a.BalanceType == "" || at.BalanceType == a.BalanceType - thresholdType, thresholdValue, direction, destinationId, weight, ratingSubject, categories, sharedGroup, timings, disabled := true, true, true, true, true, true, true, true, true, true + thresholdType, thresholdValue, direction, destinationId, weight, ratingSubject, categories, sharedGroup, timings, blocker, disabled := true, true, true, true, true, true, true, true, true, true, true if a.ExtraParameters != "" { t := struct { ThresholdType string @@ -129,6 +141,7 @@ func (at *ActionTrigger) Match(a *Action) bool { BalanceCategories string BalanceSharedGroups string BalanceTimingTags string + BalanceBlocker bool BalanceDisabled bool }{} json.Unmarshal([]byte(a.ExtraParameters), &t) @@ -141,9 +154,10 @@ func (at *ActionTrigger) Match(a *Action) bool { sharedGroup = len(t.BalanceSharedGroups) == 0 || at.BalanceSharedGroups.Equal(utils.ParseStringMap(t.BalanceSharedGroups)) weight = t.BalanceWeight == 0 || at.BalanceWeight == t.BalanceWeight ratingSubject = t.BalanceRatingSubject == "" || at.BalanceRatingSubject == t.BalanceRatingSubject + blocker = at.BalanceBlocker == t.BalanceBlocker disabled = at.BalanceDisabled == t.BalanceDisabled } - return id && direction && thresholdType && thresholdValue && destinationId && weight && ratingSubject && categories && sharedGroup && timings && disabled + return id && direction && thresholdType && thresholdValue && destinationId && weight && ratingSubject && categories && sharedGroup && timings && blocker && disabled } // makes a shallow copy of the receiver @@ -163,6 +177,7 @@ func (at *ActionTrigger) CreateBalance() *Balance { Categories: at.BalanceCategories, SharedGroups: at.BalanceSharedGroups, TimingIDs: at.BalanceTimingTags, + Blocker: at.BalanceBlocker, Disabled: at.BalanceDisabled, Weight: at.BalanceWeight, } diff --git a/engine/actions_test.go b/engine/actions_test.go index bccf168c6..604b85447 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -1507,6 +1507,258 @@ func TestActionTransferMonetaryDefault(t *testing.T) { } } +func TestActionTransferMonetaryDefaultFilter(t *testing.T) { + err := accountingStorage.SetAccount( + &Account{ + Id: "cgrates.org:trans", + BalanceMap: map[string]BalanceChain{ + utils.MONETARY: BalanceChain{ + &Balance{ + Uuid: utils.GenUUID(), + Id: utils.META_DEFAULT, + Value: 10, + Weight: 20, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 3, + Weight: 20, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 1, + Weight: 10, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 6, + Weight: 20, + }, + }, + }, + }) + if err != nil { + t.Errorf("error setting account: %v", err) + } + + a := &Action{ + ActionType: TRANSFER_MONETARY_DEFAULT, + Balance: &Balance{Weight: 20}, + } + + at := &ActionTiming{ + accountIDs: map[string]struct{}{"cgrates.org:trans": struct{}{}}, + actions: Actions{a}, + } + at.Execute() + + afterUb, err := accountingStorage.GetAccount("cgrates.org:trans") + if err != nil { + t.Error("account not found: ", err, afterUb) + } + if afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 20 || + afterUb.BalanceMap[utils.MONETARY][0].Value != 19 || + afterUb.BalanceMap[utils.MONETARY][1].Value != 0 || + afterUb.BalanceMap[utils.MONETARY][2].Value != 1 || + afterUb.BalanceMap[utils.MONETARY][3].Value != 0 { + for _, b := range afterUb.BalanceMap[utils.MONETARY] { + t.Logf("B: %+v", b) + } + t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue()) + } +} + +func TestActionConditionalTopup(t *testing.T) { + err := accountingStorage.SetAccount( + &Account{ + Id: "cgrates.org:cond", + BalanceMap: map[string]BalanceChain{ + utils.MONETARY: BalanceChain{ + &Balance{ + Uuid: utils.GenUUID(), + Id: utils.META_DEFAULT, + Value: 10, + Weight: 20, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 3, + Weight: 20, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 1, + Weight: 10, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 6, + Weight: 20, + }, + }, + }, + }) + if err != nil { + t.Errorf("error setting account: %v", err) + } + + a := &Action{ + ActionType: TOPUP, + BalanceType: utils.MONETARY, + Filter: `{"Type":"*monetary","Value":1,"Weight":10}`, + Balance: &Balance{ + Value: 11, + Weight: 30, + }, + } + + at := &ActionTiming{ + accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}}, + actions: Actions{a}, + } + at.Execute() + + afterUb, err := accountingStorage.GetAccount("cgrates.org:cond") + if err != nil { + t.Error("account not found: ", err, afterUb) + } + if len(afterUb.BalanceMap[utils.MONETARY]) != 5 || + afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 31 || + afterUb.BalanceMap[utils.MONETARY][4].Value != 11 { + for _, b := range afterUb.BalanceMap[utils.MONETARY] { + t.Logf("B: %+v", b) + } + t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue()) + } +} + +func TestActionConditionalTopupNoMatch(t *testing.T) { + err := accountingStorage.SetAccount( + &Account{ + Id: "cgrates.org:cond", + BalanceMap: map[string]BalanceChain{ + utils.MONETARY: BalanceChain{ + &Balance{ + Uuid: utils.GenUUID(), + Id: utils.META_DEFAULT, + Value: 10, + Weight: 20, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 3, + Weight: 20, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 1, + Weight: 10, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 6, + Weight: 20, + }, + }, + }, + }) + if err != nil { + t.Errorf("error setting account: %v", err) + } + + a := &Action{ + ActionType: TOPUP, + BalanceType: utils.MONETARY, + Filter: `{"Type":"*monetary","Value":2,"Weight":10}`, + Balance: &Balance{ + Value: 11, + Weight: 30, + }, + } + + at := &ActionTiming{ + accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}}, + actions: Actions{a}, + } + at.Execute() + + afterUb, err := accountingStorage.GetAccount("cgrates.org:cond") + if err != nil { + t.Error("account not found: ", err, afterUb) + } + if len(afterUb.BalanceMap[utils.MONETARY]) != 4 || + afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 20 { + for _, b := range afterUb.BalanceMap[utils.MONETARY] { + t.Logf("B: %+v", b) + } + t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue()) + } +} + +func TestActionConditionalTopupExistingBalance(t *testing.T) { + err := accountingStorage.SetAccount( + &Account{ + Id: "cgrates.org:cond", + BalanceMap: map[string]BalanceChain{ + utils.MONETARY: BalanceChain{ + &Balance{ + Uuid: utils.GenUUID(), + Value: 1, + Weight: 10, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 6, + Weight: 20, + }, + }, + utils.VOICE: BalanceChain{ + &Balance{ + Uuid: utils.GenUUID(), + Value: 10, + Weight: 10, + }, + &Balance{ + Uuid: utils.GenUUID(), + Value: 100, + Weight: 20, + }, + }, + }, + }) + if err != nil { + t.Errorf("error setting account: %v", err) + } + + a := &Action{ + ActionType: TOPUP, + BalanceType: utils.MONETARY, + Filter: `{"Type":"*voice","Value":{"*gte":100}}`, + Balance: &Balance{ + Value: 11, + Weight: 10, + }, + } + + at := &ActionTiming{ + accountIDs: map[string]struct{}{"cgrates.org:cond": struct{}{}}, + actions: Actions{a}, + } + at.Execute() + + afterUb, err := accountingStorage.GetAccount("cgrates.org:cond") + if err != nil { + t.Error("account not found: ", err, afterUb) + } + if len(afterUb.BalanceMap[utils.MONETARY]) != 2 || + afterUb.BalanceMap[utils.MONETARY].GetTotalValue() != 18 { + for _, b := range afterUb.BalanceMap[utils.MONETARY] { + t.Logf("B: %+v", b) + } + t.Error("ransfer balance value: ", afterUb.BalanceMap[utils.MONETARY].GetTotalValue()) + } +} + /**************** Benchmarks ********************************/ func BenchmarkUUID(b *testing.B) { diff --git a/engine/balances.go b/engine/balances.go index d667eaf76..bc0ad33f8 100644 --- a/engine/balances.go +++ b/engine/balances.go @@ -46,6 +46,7 @@ type Balance struct { TimingIDs utils.StringMap Disabled bool Factor ValueFactor + Blocker bool precision int account *Account // used to store ub reference for shared balances dirty bool @@ -67,10 +68,14 @@ func (b *Balance) Equal(o *Balance) bool { b.RatingSubject == o.RatingSubject && b.Categories.Equal(o.Categories) && b.SharedGroups.Equal(o.SharedGroups) && - b.Disabled == o.Disabled + b.Disabled == o.Disabled && + b.Blocker == o.Blocker } func (b *Balance) MatchFilter(o *Balance, skipIds bool) bool { + if o == nil { + return true + } if !skipIds && o.Uuid != "" { return b.Uuid == o.Uuid } @@ -85,6 +90,8 @@ func (b *Balance) MatchFilter(o *Balance, skipIds bool) bool { } return (o.ExpirationDate.IsZero() || b.ExpirationDate.Equal(o.ExpirationDate)) && (o.Weight == 0 || b.Weight == o.Weight) && + (b.Blocker == o.Blocker) && + (b.Disabled == o.Disabled) && (len(o.DestinationIds) == 0 || b.DestinationIds.Includes(o.DestinationIds)) && (len(o.Directions) == 0 || b.Directions.Includes(o.Directions)) && (len(o.Categories) == 0 || b.Categories.Includes(o.Categories)) && @@ -231,6 +238,7 @@ func (b *Balance) Clone() *Balance { SharedGroups: b.SharedGroups, TimingIDs: b.TimingIDs, Timings: b.Timings, // should not be a problem with aliasing + Blocker: b.Blocker, Disabled: b.Disabled, dirty: b.dirty, } diff --git a/engine/callcost.go b/engine/callcost.go index 767efc8f1..e786318a7 100644 --- a/engine/callcost.go +++ b/engine/callcost.go @@ -29,6 +29,7 @@ type CallCost struct { Direction, Category, Tenant, Subject, Account, Destination, TOR string Cost float64 Timespans TimeSpans + RatedUsage float64 deductConnectFee bool negativeConnectFee bool // the connect fee went negative on default balance maxCostDisconect bool @@ -61,6 +62,15 @@ func (cc *CallCost) GetDuration() (td time.Duration) { return } +func (cc *CallCost) UpdateRatedUsage() time.Duration { + if cc == nil { + return 0 + } + totalDuration := cc.GetDuration() + cc.RatedUsage = totalDuration.Seconds() + return totalDuration +} + func (cc *CallCost) GetConnectFee() float64 { if len(cc.Timespans) == 0 || cc.Timespans[0].RateInterval == nil || diff --git a/engine/callcost_test.go b/engine/callcost_test.go index c9ce24f01..55b0eced8 100644 --- a/engine/callcost_test.go +++ b/engine/callcost_test.go @@ -50,6 +50,10 @@ func TestSingleResultMerge(t *testing.T) { if cc1.Cost != 122 { t.Errorf("Exdpected 120 was %v", cc1.Cost) } + d := cc1.UpdateRatedUsage() + if d != 2*time.Minute || cc1.RatedUsage != 120.0 { + t.Errorf("error updating rating usage: %v, %v", d, cc1.RatedUsage) + } } func TestMultipleResultMerge(t *testing.T) { diff --git a/engine/calldesc.go b/engine/calldesc.go index 00d7c801e..abbb684bc 100644 --- a/engine/calldesc.go +++ b/engine/calldesc.go @@ -195,7 +195,11 @@ func (cd *CallDescriptor) LoadRatingPlans() (err error) { err, _ = cd.getRatingPlansForPrefix(cd.GetKey(FALLBACK_SUBJECT), 1) } //load the rating plans - if err != nil || !cd.continousRatingInfos() { + if err != nil { + utils.Logger.Err(fmt.Sprintf("Rating plan not found for destination %s and account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject))) + err = utils.ErrRatingPlanNotFound + } + if !cd.continousRatingInfos() { utils.Logger.Err(fmt.Sprintf("Destination %s not authorized for account: %s, subject: %s", cd.Destination, cd.GetAccountKey(), cd.GetKey(cd.Subject))) err = utils.ErrUnauthorizedDestination } @@ -522,6 +526,7 @@ func (cd *CallDescriptor) getCost() (*CallCost, error) { cc.Cost = utils.Round(cc.Cost, roundingDecimals, roundingMethod) //utils.Logger.Info(fmt.Sprintf(" Get Cost: %s => %v", cd.GetKey(), cc)) cc.Timespans.Compress() + cc.UpdateRatedUsage() return cc, err } @@ -555,7 +560,6 @@ func (origCD *CallDescriptor) getMaxSessionDuration(origAcc *Account) (time.Dura //utils.Logger.Debug("ACCOUNT: " + utils.ToJSON(account)) //utils.Logger.Debug("DEFAULT_BALANCE: " + utils.ToJSON(defaultBalance)) - // cc, err := cd.debit(account, true, false) //utils.Logger.Debug("CC: " + utils.ToJSON(cc)) //log.Print("CC: ", utils.ToIJSON(cc)) @@ -665,6 +669,7 @@ func (cd *CallDescriptor) debit(account *Account, dryRun bool, goNegative bool) return nil, err } cc.updateCost() + cc.UpdateRatedUsage() cc.Timespans.Compress() //log.Printf("OUT CC: ", cc) return diff --git a/engine/calldesc_test.go b/engine/calldesc_test.go index b33807f38..f470910ae 100644 --- a/engine/calldesc_test.go +++ b/engine/calldesc_test.go @@ -537,6 +537,57 @@ func TestMaxSessionTimeWithMaxCost(t *testing.T) { } } +func TestGetMaxSessiontWithBlocker(t *testing.T) { + ap, _ := ratingStorage.GetActionPlan("BLOCK_AT", false) + for _, at := range ap.ActionTimings { + at.accountIDs = ap.AccountIDs + at.Execute() + } + acc, err := accountingStorage.GetAccount("cgrates.org:block") + if err != nil { + t.Error("error getting account: ", err) + } + if len(acc.BalanceMap[utils.MONETARY]) != 2 || + acc.BalanceMap[utils.MONETARY][0].Blocker != true { + for _, b := range acc.BalanceMap[utils.MONETARY] { + t.Logf("B: %+v", b) + } + t.Error("Error executing action plan on account: ", acc.BalanceMap[utils.MONETARY]) + } + cd := &CallDescriptor{ + Direction: "*out", + Category: "call", + Tenant: "cgrates.org", + Subject: "block", + Account: "block", + Destination: "0723", + TimeStart: time.Date(2016, 1, 13, 14, 0, 0, 0, time.UTC), + TimeEnd: time.Date(2016, 1, 13, 14, 30, 0, 0, time.UTC), + MaxCostSoFar: 0, + } + result, err := cd.GetMaxSessionDuration() + expected := 985 * time.Second + if result != expected || err != nil { + t.Errorf("Expected %v was %v (%v)", expected, result, err) + } + cd = &CallDescriptor{ + Direction: "*out", + Category: "call", + Tenant: "cgrates.org", + Subject: "block", + Account: "block", + Destination: "444", + TimeStart: time.Date(2016, 1, 13, 14, 0, 0, 0, time.UTC), + TimeEnd: time.Date(2016, 1, 13, 14, 30, 0, 0, time.UTC), + MaxCostSoFar: 0, + } + result, err = cd.GetMaxSessionDuration() + expected = 30 * time.Minute + if result != expected || err != nil { + t.Errorf("Expected %v was %v (%v)", expected, result, err) + } +} + func TestGetCostWithMaxCost(t *testing.T) { ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false) for _, at := range ap.ActionTimings { @@ -560,6 +611,7 @@ func TestGetCostWithMaxCost(t *testing.T) { t.Errorf("Expected %v was %v", expected, cc.Cost) } } + func TestGetCostRoundingIssue(t *testing.T) { ap, _ := ratingStorage.GetActionPlan("TOPUP10_AT", false) for _, at := range ap.ActionTimings { diff --git a/engine/cdr.go b/engine/cdr.go index 599de20de..24164959f 100644 --- a/engine/cdr.go +++ b/engine/cdr.go @@ -126,7 +126,7 @@ func (cdr *CDR) FormatCost(shiftDecimals, roundDecimals int) string { // Formats usage on export func (cdr *CDR) FormatUsage(layout string) string { - if utils.IsSliceMember([]string{utils.DATA, utils.SMS, utils.GENERIC}, cdr.ToR) { + if utils.IsSliceMember([]string{utils.DATA, utils.SMS, utils.MMS, utils.GENERIC}, cdr.ToR) { return strconv.FormatFloat(utils.Round(cdr.Usage.Seconds(), 0, utils.ROUNDING_MIDDLE), 'f', -1, 64) } switch layout { diff --git a/engine/cdrs.go b/engine/cdrs.go index 6966240e8..d9dd39870 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -153,6 +153,7 @@ func (self *CdrServer) LogCallCost(ccl *CallCostLog, reply *string) error { // RPC method, used to log callcosts to db func (self *CdrServer) LocalLogCallCost(ccl *CallCostLog) error { + ccl.CallCost.UpdateRatedUsage() // make sure rated usage is updated if ccl.CheckDuplicate { _, err := self.guard.Guard(func() (interface{}, error) { cc, err := self.cdrDb.GetCallCostLog(ccl.CgrId, ccl.RunId) @@ -220,6 +221,9 @@ func (self *CdrServer) processCdr(cdr *CDR) (err error) { cdr.RunID = utils.MetaRaw } if self.cgrCfg.CDRSStoreCdrs { // Store RawCDRs, this we do sync so we can reply with the status + if cdr.CostDetails != nil { + cdr.CostDetails.UpdateRatedUsage() + } if err := self.cdrDb.SetCDR(cdr, false); err != nil { // Only original CDR stored in primary table, no derived utils.Logger.Err(fmt.Sprintf(" Storing primary CDR %+v, got error: %s", cdr, err.Error())) return err // Error is propagated back and we don't continue processing the CDR if we cannot store it @@ -276,6 +280,9 @@ func (self *CdrServer) rateStoreStatsReplicate(cdr *CDR, sendToStats bool) error } if self.cgrCfg.CDRSStoreCdrs { // Store CDRs // Store RatedCDR + if cdr.CostDetails != nil { + cdr.CostDetails.UpdateRatedUsage() + } if err := self.cdrDb.SetCDR(cdr, true); err != nil { utils.Logger.Err(fmt.Sprintf(" Storing rated CDR %+v, got error: %s", cdr, err.Error())) } diff --git a/engine/loader_csv_test.go b/engine/loader_csv_test.go index fc8d985dc..c13bac58d 100644 --- a/engine/loader_csv_test.go +++ b/engine/loader_csv_test.go @@ -147,6 +147,7 @@ DY_PLAN,RT_DY,*any,10 *in,cgrates.org,LCR_STANDARD,max,2013-03-23T00:00:00Z,RP_MX,, *out,cgrates.org,call,money,2015-02-28T00:00:00Z,EVENING,, *out,cgrates.org,call,dy,2015-02-28T00:00:00Z,DY_PLAN,, +*out,cgrates.org,call,block,2015-02-28T00:00:00Z,DY_PLAN,, ` sharedGroups = ` SG1,*any,*lowest, @@ -159,18 +160,20 @@ SG3,*any,*lowest, *in,cgrates.org,call,*any,*any,*any,LCR_STANDARD,*lowest_cost,,2012-01-01T00:00:00Z,20 ` actions = ` -MINI,*topup_reset,,,*monetary,*out,,,,,*unlimited,,10,10,false,10 -MINI,*topup,,,*voice,*out,,NAT,test,,*unlimited,,100,10,false,10 -SHARED,*topup,,,*monetary,*out,,,,SG1,*unlimited,,100,10,false,10 -TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10 -SE0,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,0,10,false,10 -SE10,*topup_reset,,,*monetary,*out,,,,SG2,*unlimited,,10,5,false,10 -SE10,*topup,,,*monetary,*out,,,,,*unlimited,,10,10,false,10 -EE0,*topup_reset,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,10 -EE0,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,10 -DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,false,10 -NEG,*allow_negative,,,*monetary,*out,,,,,*unlimited,,0,10,false,10 +MINI,*topup_reset,,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10 +MINI,*topup,,,,*voice,*out,,NAT,test,,*unlimited,,100,10,false,false,10 +SHARED,*topup,,,,*monetary,*out,,,,SG1,*unlimited,,100,10,false,false,10 +TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,1,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10 +SE0,*topup_reset,,,,*monetary,*out,,,,SG2,*unlimited,,0,10,false,false,10 +SE10,*topup_reset,,,,*monetary,*out,,,,SG2,*unlimited,,10,5,false,false,10 +SE10,*topup,,,,*monetary,*out,,,,,*unlimited,,10,10,false,false,10 +EE0,*topup_reset,,,,*monetary,*out,,,,SG3,*unlimited,,0,10,false,false,10 +EE0,*allow_negative,,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 +DEFEE,*cdrlog,"{""Category"":""^ddi"",""MediationRunId"":""^did_run""}",,,,,,,,,,,,,false,false,10 +NEG,*allow_negative,,,,*monetary,*out,,,,,*unlimited,,0,10,false,false,10 +BLOCK,*topup,,,bblocker,*monetary,*out,,NAT,,,*unlimited,,10,20,true,false,20 +BLOCK,*topup,,,bfree,*monetary,*out,,,,,*unlimited,,20,10,false,false,10 ` actionPlans = ` MORE_MINUTES,MINI,ONE_TIME_RUN,10 @@ -181,19 +184,20 @@ TOPUP_SHARED0_AT,SE0,*asap,10 TOPUP_SHARED10_AT,SE10,*asap,10 TOPUP_EMPTY_AT,EE0,*asap,10 POST_AT,NEG,*asap,10 +BLOCK_AT,BLOCK,*asap,10 ` actionTriggers = ` -STANDARD_TRIGGER,st0,*min_event_counter,10,false,0,,*voice,*out,,GERMANY_O2,,,,,,,,SOME_1,10 -STANDARD_TRIGGER,st1,*max_balance,200,false,0,,*voice,*out,,GERMANY,,,,,,,,SOME_2,10 -STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,LOG_WARNING,10 -STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,LOG_WARNING,10 -CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 -CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +STANDARD_TRIGGER,st0,*min_event_counter,10,false,0,,*voice,*out,,GERMANY_O2,,,,,,,,,SOME_1,10 +STANDARD_TRIGGER,st1,*max_balance,200,false,0,,*voice,*out,,GERMANY,,,,,,,,,SOME_2,10 +STANDARD_TRIGGERS,,*min_balance,2,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_balance,20,false,0,,*monetary,*out,,,,,,,,,,,LOG_WARNING,10 +STANDARD_TRIGGERS,,*max_event_counter,5,false,0,,*monetary,*out,,FS_USERS,,,,,,,,,LOG_WARNING,10 +CDRST1_WARN_ASR,,*min_asr,45,true,1h,,,,,,,,,,,,,3,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACD,,*min_acd,10,true,1h,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST1_WARN_ACC,,*max_acc,10,true,10m,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ASR,,*min_asr,30,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 +CDRST2_WARN_ACD,,*min_acd,3,true,0,,,,,,,,,,,,,5,CDRST_WARN_HTTP,10 ` accountActions = ` vdf,minitsboy,MORE_MINUTES,STANDARD_TRIGGER,, @@ -207,6 +211,7 @@ vdf,emptyX,TOPUP_EMPTY_AT,,, vdf,emptyY,TOPUP_EMPTY_AT,,, vdf,post,POST_AT,,, cgrates.org,alodis,TOPUP_EMPTY_AT,,true,true +cgrates.org,block,BLOCK_AT,,false,false ` derivedCharges = ` @@ -790,7 +795,7 @@ func TestLoadRatingPlans(t *testing.T) { } func TestLoadRatingProfiles(t *testing.T) { - if len(csvr.ratingProfiles) != 22 { + if len(csvr.ratingProfiles) != 23 { t.Error("Failed to load rating profiles: ", len(csvr.ratingProfiles), csvr.ratingProfiles) } rp := csvr.ratingProfiles["*out:test:0:trp"] @@ -809,7 +814,7 @@ func TestLoadRatingProfiles(t *testing.T) { } func TestLoadActions(t *testing.T) { - if len(csvr.actions) != 9 { + if len(csvr.actions) != 10 { t.Error("Failed to load actions: ", len(csvr.actions)) } as1 := csvr.actions["MINI"] @@ -822,12 +827,14 @@ func TestLoadActions(t *testing.T) { ExtraParameters: "", Weight: 10, Balance: &Balance{ - Uuid: as1[0].Balance.Uuid, - Directions: utils.NewStringMap(utils.OUT), - Value: 10, - Weight: 10, - TimingIDs: utils.StringMap{}, - SharedGroups: utils.StringMap{}, + Uuid: as1[0].Balance.Uuid, + Directions: utils.NewStringMap(utils.OUT), + Value: 10, + Weight: 10, + DestinationIds: utils.StringMap{}, + TimingIDs: utils.StringMap{}, + SharedGroups: utils.StringMap{}, + Categories: utils.StringMap{}, }, }, &Action{ @@ -846,10 +853,11 @@ func TestLoadActions(t *testing.T) { DestinationIds: utils.NewStringMap("NAT"), TimingIDs: utils.StringMap{}, SharedGroups: utils.StringMap{}, + Categories: utils.StringMap{}, }, }, } - if !reflect.DeepEqual(as1[1], expected[1]) { + if !reflect.DeepEqual(as1, expected) { t.Errorf("Error loading action1: %+v", as1[0].Balance) } as2 := csvr.actions["SHARED"] @@ -868,10 +876,11 @@ func TestLoadActions(t *testing.T) { Weight: 10, SharedGroups: utils.NewStringMap("SG1"), TimingIDs: utils.StringMap{}, + Categories: utils.StringMap{}, }, }, } - if !reflect.DeepEqual(as2[0], expected[0]) { + if !reflect.DeepEqual(as2, expected) { t.Errorf("Error loading action: %+v", as2[0].Balance) } as3 := csvr.actions["DEFEE"] @@ -886,7 +895,9 @@ func TestLoadActions(t *testing.T) { Directions: utils.StringMap{}, DestinationIds: utils.StringMap{}, TimingIDs: utils.StringMap{}, + Categories: utils.StringMap{}, SharedGroups: utils.StringMap{}, + Blocker: false, }, }, } @@ -982,7 +993,7 @@ func TestLoadLCRs(t *testing.T) { } func TestLoadActionTimings(t *testing.T) { - if len(csvr.actionPlans) != 6 { + if len(csvr.actionPlans) != 7 { t.Error("Failed to load action timings: ", len(csvr.actionPlans)) } atm := csvr.actionPlans["MORE_MINUTES"] @@ -1070,7 +1081,7 @@ func TestLoadActionTriggers(t *testing.T) { } func TestLoadAccountActions(t *testing.T) { - if len(csvr.accountActions) != 11 { + if len(csvr.accountActions) != 12 { t.Error("Failed to load account actions: ", len(csvr.accountActions)) } aa := csvr.accountActions["vdf:minitsboy"] diff --git a/engine/model_converters.go b/engine/model_converters.go index 058ea73f8..d3035772e 100644 --- a/engine/model_converters.go +++ b/engine/model_converters.go @@ -173,12 +173,15 @@ func APItoModelAction(as *utils.TPActions) (result []TpAction) { Directions: a.Directions, Units: a.Units, ExpiryTime: a.ExpiryTime, + Filter: a.Filter, TimingTags: a.TimingTags, DestinationTags: a.DestinationIds, RatingSubject: a.RatingSubject, Categories: a.Categories, SharedGroups: a.SharedGroups, BalanceWeight: a.BalanceWeight, + BalanceBlocker: a.BalanceBlocker, + BalanceDisabled: a.BalanceDisabled, ExtraParameters: a.ExtraParameters, Weight: a.Weight, }) @@ -231,6 +234,7 @@ func APItoModelActionTrigger(ats *utils.TPActionTriggers) (result []TpActionTrig BalanceRatingSubject: at.BalanceRatingSubject, BalanceCategories: at.BalanceCategories, BalanceSharedGroups: at.BalanceSharedGroups, + BalanceBlocker: at.BalanceBlocker, BalanceDisabled: at.BalanceDisabled, MinQueuedItems: at.MinQueuedItems, ActionsTag: at.ActionsId, diff --git a/engine/model_helpers.go b/engine/model_helpers.go index 0d452e0e5..0d2d0a648 100644 --- a/engine/model_helpers.go +++ b/engine/model_helpers.go @@ -390,12 +390,15 @@ func (tps TpActions) GetActions() (map[string][]*utils.TPAction, error) { Directions: tpAc.Directions, Units: tpAc.Units, ExpiryTime: tpAc.ExpiryTime, + Filter: tpAc.Filter, TimingTags: tpAc.TimingTags, DestinationIds: tpAc.DestinationTags, RatingSubject: tpAc.RatingSubject, Categories: tpAc.Categories, SharedGroups: tpAc.SharedGroups, BalanceWeight: tpAc.BalanceWeight, + BalanceBlocker: tpAc.BalanceBlocker, + BalanceDisabled: tpAc.BalanceDisabled, ExtraParameters: tpAc.ExtraParameters, Weight: tpAc.Weight, } @@ -437,6 +440,7 @@ func (tps TpActionTriggers) GetActionTriggers() (map[string][]*utils.TPActionTri BalanceRatingSubject: tpAt.BalanceRatingSubject, BalanceCategories: tpAt.BalanceCategories, BalanceSharedGroups: tpAt.BalanceSharedGroups, + BalanceBlocker: tpAt.BalanceBlocker, BalanceDisabled: tpAt.BalanceDisabled, Weight: tpAt.Weight, ActionsId: tpAt.ActionsTag, diff --git a/engine/model_helpers_test.go b/engine/model_helpers_test.go index d8edf26cb..7d0413c9a 100644 --- a/engine/model_helpers_test.go +++ b/engine/model_helpers_test.go @@ -265,8 +265,8 @@ func TestTPActionsAsExportSlice(t *testing.T) { }, } expectedSlc := [][]string{ - []string{"TEST_ACTIONS", "*topup_reset", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "10"}, - []string{"TEST_ACTIONS", "*http_post", "http://localhost/¶m1=value1", "", "", "", "", "", "", "", "", "", "0", "0", "false", "20"}, + []string{"TEST_ACTIONS", "*topup_reset", "", "", "", "*monetary", utils.OUT, "call", "*any", "special1", "GROUP1", "*never", "", "5", "10", "false", "false", "10"}, + []string{"TEST_ACTIONS", "*http_post", "http://localhost/¶m1=value1", "", "", "", "", "", "", "", "", "", "", "0", "0", "false", "false", "20"}, } ms := APItoModelAction(tpActs) @@ -567,6 +567,7 @@ func TestTPActionPlanAsExportSlice(t *testing.T) { BalanceRatingSubject: "special1", BalanceCategories: "call", BalanceSharedGroups: "SHARED_1", + BalanceBlocker: false, BalanceDisabled: false, MinQueuedItems: 0, ActionsId: "LOG_WARNING", @@ -588,6 +589,7 @@ func TestTPActionPlanAsExportSlice(t *testing.T) { BalanceRatingSubject: "special1", BalanceCategories: "call", BalanceSharedGroups: "SHARED_1", + BalanceBlocker: false, BalanceDisabled: false, MinQueuedItems: 0, ActionsId: "LOG_WARNING", @@ -595,8 +597,8 @@ func TestTPActionPlanAsExportSlice(t *testing.T) { }, } expectedSlc := [][]string{ - []string{"STANDARD_TRIGGERS", "1", "*min_balance", "2", "false", "0", "b1", "*monetary", "*out", "call", "", "special1", "SHARED_1", "*never", "T1", "0", "false", "0", "LOG_WARNING", "10"}, - []string{"STANDARD_TRIGGERS", "2", "*max_event_counter", "5", "false", "0", "b2", "*monetary", "*out", "call", "FS_USERS", "special1", "SHARED_1", "*never", "T1", "0", "false", "0", "LOG_WARNING", "10"}, + []string{"STANDARD_TRIGGERS", "1", "*min_balance", "2", "false", "0", "b1", "*monetary", "*out", "call", "", "special1", "SHARED_1", "*never", "T1", "0", "false", "false", "0", "LOG_WARNING", "10"}, + []string{"STANDARD_TRIGGERS", "2", "*max_event_counter", "5", "false", "0", "b2", "*monetary", "*out", "call", "FS_USERS", "special1", "SHARED_1", "*never", "T1", "0", "false", "false", "0", "LOG_WARNING", "10"}, } ms := APItoModelActionTrigger(at) var slc [][]string diff --git a/engine/models.go b/engine/models.go index ead7492dc..2fb2ffc56 100644 --- a/engine/models.go +++ b/engine/models.go @@ -156,19 +156,21 @@ type TpAction struct { Tag string `index:"0" re:"\w+\s*"` Action string `index:"1" re:"\*\w+\s*"` ExtraParameters string `index:"2" re:"\S+\s*"` - BalanceTag string `index:"3" re:"\w+\s*"` - BalanceType string `index:"4" re:"\*\w+\s*"` - Directions string `index:"5" re:""` - Categories string `index:"6" re:""` - DestinationTags string `index:"7" re:"\*any|\w+\s*"` - RatingSubject string `index:"8" re:"\w+\s*"` - SharedGroups string `index:"9" re:"[0-9A-Za-z_;]*"` - ExpiryTime string `index:"10" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` - TimingTags string `index:"11" re:"[0-9A-Za-z_;]*|\*any"` - Units float64 `index:"12" re:"\d+\s*"` - BalanceWeight float64 `index:"13" re:"\d+\.?\d*\s*"` - BalanceDisabled bool `index:"14" re:""` - Weight float64 `index:"15" re:"\d+\.?\d*\s*"` + Filter string `index:"3" re:"\S+\s*"` + BalanceTag string `index:"4" re:"\w+\s*"` + BalanceType string `index:"5" re:"\*\w+\s*"` + Directions string `index:"6" re:""` + Categories string `index:"7" re:""` + DestinationTags string `index:"8" re:"\*any|\w+\s*"` + RatingSubject string `index:"9" re:"\w+\s*"` + SharedGroups string `index:"10" re:"[0-9A-Za-z_;]*"` + ExpiryTime string `index:"11" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` + TimingTags string `index:"12" re:"[0-9A-Za-z_;]*|\*any"` + Units float64 `index:"13" re:"\d+\s*"` + BalanceWeight float64 `index:"14" re:"\d+\.?\d*\s*"` + BalanceBlocker bool `index:"15" re:""` + BalanceDisabled bool `index:"16" re:""` + Weight float64 `index:"17" re:"\d+\.?\d*\s*"` CreatedAt time.Time } @@ -201,10 +203,11 @@ type TpActionTrigger struct { BalanceExpiryTime string `index:"13" re:"\*\w+\s*|\+\d+[smh]\s*|\d+\s*"` BalanceTimingTags string `index:"14" re:"[0-9A-Za-z_;]*|\*any"` BalanceWeight float64 `index:"15" re:"\d+\.?\d*"` - BalanceDisabled bool `index:"16" re:""` - MinQueuedItems int `index:"17" re:"\d+"` - ActionsTag string `index:"18" re:"\w+"` - Weight float64 `index:"19" re:"\d+\.?\d*"` + BalanceBlocker bool `index:"16" re:""` + BalanceDisabled bool `index:"17" re:""` + MinQueuedItems int `index:"18" re:"\d+"` + ActionsTag string `index:"19" re:"\w+"` + Weight float64 `index:"20" re:"\d+\.?\d*"` CreatedAt time.Time } diff --git a/engine/storage_test.go b/engine/storage_test.go index 375979fa9..98a312739 100644 --- a/engine/storage_test.go +++ b/engine/storage_test.go @@ -274,7 +274,7 @@ func TestDifferentUuid(t *testing.T) { func TestStorageTask(t *testing.T) { // clean previous unused tasks - for i := 0; i < 16; i++ { + for i := 0; i < 18; i++ { ratingStorage.PopTask() } diff --git a/engine/tp_reader.go b/engine/tp_reader.go index 5f22f0a2a..517606d95 100644 --- a/engine/tp_reader.go +++ b/engine/tp_reader.go @@ -513,6 +513,7 @@ func (tpr *TpReader) LoadActions() (err error) { Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, + Filter: tpact.Filter, Balance: &Balance{ Id: tpact.BalanceId, Value: tpact.Units, @@ -523,6 +524,8 @@ func (tpr *TpReader) LoadActions() (err error) { DestinationIds: utils.ParseStringMap(tpact.DestinationIds), SharedGroups: utils.ParseStringMap(tpact.SharedGroups), TimingIDs: utils.ParseStringMap(tpact.TimingTags), + Blocker: tpact.BalanceBlocker, + Disabled: tpact.BalanceDisabled, }, } // load action timings from tags @@ -640,6 +643,8 @@ func (tpr *TpReader) LoadActionTriggers() (err error) { BalanceRatingSubject: atr.BalanceRatingSubject, BalanceCategories: utils.ParseStringMap(atr.BalanceCategories), BalanceSharedGroups: utils.ParseStringMap(atr.BalanceSharedGroups), + BalanceBlocker: atr.BalanceBlocker, + BalanceDisabled: atr.BalanceDisabled, Weight: atr.Weight, ActionsId: atr.ActionsId, MinQueuedItems: atr.MinQueuedItems, @@ -788,9 +793,12 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error BalanceDestinationIds: utils.ParseStringMap(apiAtr.BalanceDestinationIds), BalanceWeight: apiAtr.BalanceWeight, BalanceExpirationDate: expTime, + BalanceTimingTags: utils.ParseStringMap(apiAtr.BalanceTimingTags), BalanceRatingSubject: apiAtr.BalanceRatingSubject, BalanceCategories: utils.ParseStringMap(apiAtr.BalanceCategories), BalanceSharedGroups: utils.ParseStringMap(apiAtr.BalanceSharedGroups), + BalanceBlocker: apiAtr.BalanceBlocker, + BalanceDisabled: apiAtr.BalanceDisabled, Weight: apiAtr.Weight, ActionsId: apiAtr.ActionsId, } @@ -830,14 +838,19 @@ func (tpr *TpReader) LoadAccountActionsFiltered(qriedAA *TpAccountAction) error Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, + Filter: tpact.Filter, Balance: &Balance{ + Id: tpact.BalanceId, Value: tpact.Units, Weight: tpact.BalanceWeight, RatingSubject: tpact.RatingSubject, + Categories: utils.ParseStringMap(tpact.Categories), Directions: utils.ParseStringMap(tpact.Directions), DestinationIds: utils.ParseStringMap(tpact.DestinationIds), SharedGroups: utils.ParseStringMap(tpact.SharedGroups), TimingIDs: utils.ParseStringMap(tpact.TimingTags), + Blocker: tpact.BalanceBlocker, + Disabled: tpact.BalanceDisabled, }, } } @@ -1055,14 +1068,19 @@ func (tpr *TpReader) LoadCdrStatsFiltered(tag string, save bool) (err error) { Weight: tpact.Weight, ExtraParameters: tpact.ExtraParameters, ExpirationString: tpact.ExpiryTime, + Filter: tpact.Filter, Balance: &Balance{ + Id: tpact.BalanceId, Value: tpact.Units, Weight: tpact.BalanceWeight, RatingSubject: tpact.RatingSubject, + Categories: utils.ParseStringMap(tpact.Categories), Directions: utils.ParseStringMap(tpact.Directions), DestinationIds: utils.ParseStringMap(tpact.DestinationIds), SharedGroups: utils.ParseStringMap(tpact.SharedGroups), TimingIDs: utils.ParseStringMap(tpact.TimingTags), + Blocker: tpact.BalanceBlocker, + Disabled: tpact.BalanceDisabled, }, } } diff --git a/general_tests/acntacts_test.go b/general_tests/acntacts_test.go index d61797377..572f60180 100644 --- a/general_tests/acntacts_test.go +++ b/general_tests/acntacts_test.go @@ -44,9 +44,9 @@ func TestAcntActsLoadCsv(t *testing.T) { ratingProfiles := `` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*voice,*out,,*any,,,*unlimited,,10,10,false,10 -DISABLE_ACNT,*disable_account,,,,,,,,,,,,,false,10 -ENABLE_ACNT,*enable_account,,,,,,,,,,,,,false,10` + actions := `TOPUP10_AC,*topup_reset,,,,*voice,*out,,*any,,,*unlimited,,10,10,false,false,10 +DISABLE_ACNT,*disable_account,,,,,,,,,,,,,,false,false,10 +ENABLE_ACNT,*enable_account,,,,,,,,,,,,,,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10` actionTriggers := `` accountActions := `cgrates.org,1,TOPUP10_AT,,,` diff --git a/general_tests/auth_test.go b/general_tests/auth_test.go index b9069716e..81a683305 100644 --- a/general_tests/auth_test.go +++ b/general_tests/auth_test.go @@ -56,7 +56,7 @@ RP_ANY,DR_ANY_1CNT,*any,10` *out,cgrates.org,call,*any,2013-01-06T00:00:00Z,RP_ANY,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,10` + actions := `TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,*asap,10` actionTriggers := `` accountActions := `cgrates.org,testauthpostpaid1,TOPUP10_AT,,,` diff --git a/general_tests/ddazmbl1_test.go b/general_tests/ddazmbl1_test.go index afc603bb8..1884d24eb 100644 --- a/general_tests/ddazmbl1_test.go +++ b/general_tests/ddazmbl1_test.go @@ -53,8 +53,8 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10` + actions := `TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,10,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10 TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` diff --git a/general_tests/ddazmbl2_test.go b/general_tests/ddazmbl2_test.go index cd371fe7a..18b7ba4de 100644 --- a/general_tests/ddazmbl2_test.go +++ b/general_tests/ddazmbl2_test.go @@ -53,8 +53,8 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC,*topup_reset,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,10 -TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10` + actions := `TOPUP10_AC,*topup_reset,,,,*monetary,*out,,*any,,,*unlimited,,0,10,false,false,10 +TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC,ASAP,10 TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` diff --git a/general_tests/ddazmbl3_test.go b/general_tests/ddazmbl3_test.go index 2df5c03e3..541584982 100644 --- a/general_tests/ddazmbl3_test.go +++ b/general_tests/ddazmbl3_test.go @@ -53,7 +53,7 @@ RP_UK,DR_UK_Mobile_BIG5,ALWAYS,10` *out,cgrates.org,call,discounted_minutes,2013-01-06T00:00:00Z,RP_UK_Mobile_BIG5_PKG,,` sharedGroups := `` lcrs := `` - actions := `TOPUP10_AC1,*topup_reset,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,10` + actions := `TOPUP10_AC1,*topup_reset,,,,*voice,*out,,DST_UK_Mobile_BIG5,discounted_minutes,,*unlimited,,40,10,false,false,10` actionPlans := `TOPUP10_AT,TOPUP10_AC1,ASAP,10` actionTriggers := `` accountActions := `cgrates.org,12346,TOPUP10_AT,,,` diff --git a/glide.lock b/glide.lock index 57fb59cd8..6a8bae15e 100644 --- a/glide.lock +++ b/glide.lock @@ -1,10 +1,5 @@ -<<<<<<< HEAD -hash: 0ca45753122a2e205a1b401e7f38b17e58ea22d3b105894604e504aeace503cb -updated: 2015-12-24T18:29:52.686317738+02:00 -======= -hash: 330fc999239d5766f033409be2335338cfd172d0dcf18ad752b8613f45e9f451 -updated: 2016-01-06T13:35:12.445693385+02:00 ->>>>>>> master +hash: 855bc23b0e58452edf1d31f228430476a2602b79d225397d33b805b252cebb83 +updated: 2016-01-21T12:30:17.296295607+02:00 imports: - name: github.com/cenkalti/hub version: 57d753b5f4856e77b3cf8ecce78c97215a7d324d @@ -18,6 +13,8 @@ imports: version: 3d6beed663452471dec3ca194137a30d379d9e8f - name: github.com/cgrates/rpcclient version: 79661b1e514823a9ac93b2b9e97e037ee190ba47 +- name: github.com/cgrates/structmatcher + version: 98feee0bab15ce165540fe5f0fa006db2e9f898c - name: github.com/DisposaBoy/JsonConfigReader version: 33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4 - name: github.com/fiorix/go-diameter @@ -36,7 +33,6 @@ imports: version: 2f7811c55f286c55cfc3a2aefb5c4049b9cd5214 - name: github.com/jinzhu/inflection version: 3272df6c21d04180007eb3349844c89a3856bc25 - repo: https://github.com/jinzhu/inflection - name: github.com/kr/pty version: f7ee69f31298ecbe5d2b349c711e2547a617d398 - name: github.com/lib/pq @@ -60,7 +56,6 @@ imports: - /websocket - name: golang.org/x/text version: cf4986612c83df6c55578ba198316d1684a9a287 - repo: https://golang.org/x/text - name: gopkg.in/fsnotify.v1 version: 508915b7500b6e42a87132e4afeb4729cebc7cbb - name: gopkg.in/mgo.v2 @@ -69,5 +64,4 @@ imports: - bson - name: gopkg.in/tomb.v2 version: 14b3d72120e8d10ea6e6b7f87f7175734b1faab8 - repo: https://gopkg.in/tomb.v2 -devImports: [] +devImports: [] \ No newline at end of file diff --git a/glide.yaml b/glide.yaml index d69b5ba46..d1d548281 100644 --- a/glide.yaml +++ b/glide.yaml @@ -6,6 +6,7 @@ import: - package: github.com/cgrates/kamevapi - package: github.com/cgrates/osipsdagram - package: github.com/cgrates/rpcclient +- package: github.com/cgrates/structmatcher - package: github.com/fiorix/go-diameter subpackages: - /diam diff --git a/sessionmanager/smg_session.go b/sessionmanager/smg_session.go index e2b5289e9..2bd906513 100644 --- a/sessionmanager/smg_session.go +++ b/sessionmanager/smg_session.go @@ -158,7 +158,7 @@ func (self *SMGSession) refund(refundDuration time.Duration) error { Increments: refundIncrements, } cd.Increments.Compress() - utils.Logger.Info(fmt.Sprintf("Refunding duration %v with cd: %+v", initialRefundDuration, utils.ToJSON(cd))) + utils.Logger.Info(fmt.Sprintf("Refunding duration %v with cd: %s", initialRefundDuration, utils.ToJSON(cd))) var response float64 err := self.rater.Call("Responder.RefundIncrements", cd, &response) if err != nil { diff --git a/sessionmanager/smgeneric.go b/sessionmanager/smgeneric.go index 81143d87d..3e77de53f 100644 --- a/sessionmanager/smgeneric.go +++ b/sessionmanager/smgeneric.go @@ -108,7 +108,7 @@ func (self *SMGeneric) sessionStart(evStart SMGenericEvent, connId string) error } // End a session from outside -func (self *SMGeneric) sessionEnd(sessionId string, endTime time.Time) error { +func (self *SMGeneric) sessionEnd(sessionId string, usage time.Duration) error { _, err := self.guard.Guard(func() (interface{}, error) { // Lock it on UUID level ss := self.getSession(sessionId) if len(ss) == 0 { // Not handled by us @@ -121,7 +121,12 @@ func (self *SMGeneric) sessionEnd(sessionId string, endTime time.Time) error { if idx == 0 && s.stopDebit != nil { close(s.stopDebit) // Stop automatic debits } - if err := s.close(endTime); err != nil { + aTime, err := s.eventStart.GetAnswerTime(utils.META_DEFAULT, self.cgrCfg.DefaultTimezone) + if err != nil || aTime.IsZero() { + utils.Logger.Err(fmt.Sprintf(" Could not retrieve answer time for session: %s, runId: %s, aTime: %+v, error: %s", + sessionId, s.runId, aTime, err.Error())) + } + if err := s.close(aTime.Add(usage)); err != nil { utils.Logger.Err(fmt.Sprintf(" Could not close session: %s, runId: %s, error: %s", sessionId, s.runId, err.Error())) } if err := s.saveOperations(); err != nil { @@ -192,32 +197,90 @@ func (self *SMGeneric) SessionStart(gev SMGenericEvent, clnt *rpc2.Client) (time // Called on session end, should stop debit loop func (self *SMGeneric) SessionEnd(gev SMGenericEvent, clnt *rpc2.Client) error { - endTime, err := gev.GetEndTime(utils.META_DEFAULT, self.timezone) + usage, err := gev.GetUsage(utils.META_DEFAULT) if err != nil { return err } - if err := self.sessionEnd(gev.GetUUID(), endTime); err != nil { + if err := self.sessionEnd(gev.GetUUID(), usage); err != nil { return err } return nil } // Processes one time events (eg: SMS) -func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) error { +func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) (maxDur time.Duration, err error) { var sessionRuns []*engine.SessionRun if err := self.rater.Call("Responder.GetSessionRuns", gev.AsStoredCdr(self.cgrCfg, self.timezone), &sessionRuns); err != nil { - return err + return nilDuration, err } else if len(sessionRuns) == 0 { - return nil + return nilDuration, nil + } + for _, sR := range sessionRuns { + cc := new(engine.CallCost) + if err := self.rater.Call("Responder.MaxDebit", sR.CallDescriptor, cc); err != nil { + withErrors = true + utils.Logger.Err(fmt.Sprintf(" Could not Debit CD: %+v, RunID: %s, error: %s", sR.CallDescriptor, sR.DerivedCharger.RunID, err.Error())) + break + } + sR.CallCosts = append(sR.CallCosts, cc) // Save it so we can revert on issues + if ccDur := cc.GetDuration(); ccDur == 0 { + err = errors.New("INSUFFICIENT_FUNDS") + break + } else if ccDur < maxDur { + maxDur = ccDur + } + } + if err != nil { // Refund the ones already taken since we have error on one of the debits + for _, sR := range sessionRuns { + if len(sR.CallCosts) == 0 { + continue + } + cc := sR.CallCosts[0] + if len(sR.CallCosts) > 1 { + for _, ccSR := range sR.CallCosts { + cc.Merge(ccSR) + } + } + // collect increments + var refundIncrements engine.Increments + cc.Timespans.Decompress() + for _, ts := range cc.Timespans { + refundIncrements = append(refundIncrements, ts.Increments...) + } + // refund cc + if len(refundIncrements) > 0 { + cd := &engine.CallDescriptor{ + Direction: cc.Direction, + Tenant: cc.Tenant, + Category: cc.Category, + Subject: cc.Subject, + Account: cc.Account, + Destination: cc.Destination, + TOR: cc.TOR, + Increments: refundIncrements, + } + cd.Increments.Compress() + utils.Logger.Info(fmt.Sprintf("Refunding session run callcost: %s", utils.ToJSON(cd))) + var response float64 + err := self.rater.RefundIncrements(cd, &response) + if err != nil { + return nilDuration, err + } + } + } + return nilDuration, err } var withErrors bool for _, sR := range sessionRuns { - cc := new(engine.CallCost) - if err := self.rater.Call("Responder.Debit", sR.CallDescriptor, cc); err != nil { - withErrors = true - utils.Logger.Err(fmt.Sprintf(" Could not Debit CD: %+v, RunID: %s, error: %s", sR.CallDescriptor, sR.DerivedCharger.RunID, err.Error())) + if len(sR.CallCosts) == 0 { continue } + cc := sR.CallCosts[0] + if len(sR.CallCosts) > 1 { + for _, ccSR := range sR.CallCosts[1:] { + cc.Merge(ccSR) + } + } var reply string if err := self.cdrsrv.Call("CdrServer.LogCallCost", &engine.CallCostLog{ CgrId: gev.GetCgrId(self.timezone), @@ -231,9 +294,9 @@ func (self *SMGeneric) ChargeEvent(gev SMGenericEvent, clnt *rpc2.Client) error } } if withErrors { - return ErrPartiallyExecuted + return nilDuration, ErrPartiallyExecuted } - return nil + return maxDur, nil } func (self *SMGeneric) ProcessCdr(gev SMGenericEvent) error { @@ -251,7 +314,7 @@ func (self *SMGeneric) Connect() error { // System shutdown func (self *SMGeneric) Shutdown() error { for ssId := range self.getSessions() { // Force sessions shutdown - self.sessionEnd(ssId, time.Now()) + self.sessionEnd(ssId, time.Duration(self.cgrCfg.MaxCallDuration)) } return nil } diff --git a/utils/apitpdata.go b/utils/apitpdata.go index f4246bd3a..f235c33ed 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -279,6 +279,7 @@ type TPAction struct { Directions string // Balance direction Units float64 // Number of units to add/deduct ExpiryTime string // Time when the units will expire + Filter string // The condition on balances that is checked before the action TimingTags string // Timing when balance is active DestinationIds string // Destination profile id RatingSubject string // Reference a rate subject defined in RatingProfiles @@ -286,6 +287,8 @@ type TPAction struct { SharedGroups string // Reference to a shared group BalanceWeight float64 // Balance weight ExtraParameters string + BalanceBlocker bool + BalanceDisabled bool Weight float64 // Action's weight } @@ -487,6 +490,7 @@ type TPActionTrigger struct { BalanceRatingSubject string // filter for balance BalanceCategories string // filter for balance BalanceSharedGroups string // filter for balance + BalanceBlocker bool // filter for balance BalanceDisabled bool // filter for balance MinQueuedItems int // Trigger actions only if this number is hit (stats only) ActionsId string // Actions which will execute on threshold reached @@ -594,6 +598,7 @@ type AttrExpFileCdrs struct { ExportTemplate *string // Exported fields template <""|fld1,fld2|*xml:instance_name> DataUsageMultiplyFactor *float64 // Multiply data usage before export (eg: convert from KBytes to Bytes) SmsUsageMultiplyFactor *float64 // Multiply sms usage before export (eg: convert from SMS unit to call duration for some billing systems) + MmsUsageMultiplyFactor *float64 // Multiply mms usage before export (eg: convert from MMS unit to call duration for some billing systems) GenericUsageMultiplyFactor *float64 // Multiply generic usage before export (eg: convert from GENERIC unit to call duration for some billing systems) CostMultiplyFactor *float64 // Multiply the cost before export, eg: apply VAT CostShiftDigits *int // If defined it will shift cost digits before applying rouding (eg: convert from Eur->cents), -1 to use general config ones @@ -1086,6 +1091,7 @@ type AttrExportCdrsToFile struct { ExportTemplate *string // Exported fields template <""|fld1,fld2|*xml:instance_name> DataUsageMultiplyFactor *float64 // Multiply data usage before export (eg: convert from KBytes to Bytes) SMSUsageMultiplyFactor *float64 // Multiply sms usage before export (eg: convert from SMS unit to call duration for some billing systems) + MMSUsageMultiplyFactor *float64 // Multiply mms usage before export (eg: convert from MMS unit to call duration for some billing systems) GenericUsageMultiplyFactor *float64 // Multiply generic usage before export (eg: convert from GENERIC unit to call duration for some billing systems) CostMultiplyFactor *float64 // Multiply the cost before export, eg: apply VAT CostShiftDigits *int // If defined it will shift cost digits before applying rouding (eg: convert from Eur->cents), -1 to use general config ones diff --git a/utils/consts.go b/utils/consts.go index a78f97e88..fc09a9194 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -26,7 +26,8 @@ var ( ErrInvalidPath = errors.New("INVALID_PATH") ErrInvalidKey = errors.New("INVALID_KEY") ErrUnauthorizedDestination = errors.New("UNAUTHORIZED_DESTINATION") - ErrAccountNotFound = errors.New("AccountNotFound") + ErrRatingPlanNotFound = errors.New("RATING_PLAN_NOT_FOUND") + ErrAccountNotFound = errors.New("ACCOUNT_NOT_FOUND") ) const ( @@ -147,6 +148,7 @@ const ( HDR_VAL_SEP = "/" MONETARY = "*monetary" SMS = "*sms" + MMS = "*mms" GENERIC = "*generic" DATA = "*data" VOICE = "*voice"