Added RoundIncrement to EventCost

This commit is contained in:
Trial97
2020-10-07 18:16:59 +03:00
committed by Dan Christian Bogos
parent 830103a200
commit b1b9a81fc1
7 changed files with 234 additions and 126 deletions

View File

@@ -203,8 +203,9 @@ func (cc *CallCost) Round() {
//log.Print(cost, roundedCost, correctionCost)
if correctionCost != 0 {
ts.RoundIncrement = &Increment{
Cost: correctionCost,
BalanceInfo: inc.BalanceInfo,
Cost: correctionCost,
BalanceInfo: inc.BalanceInfo,
CompressFactor: 1,
}
totalCorrectionCost += correctionCost
ts.Cost += correctionCost

View File

@@ -174,7 +174,7 @@ func (cdrS *CDRServer) rateCDR(cdr *CDRWithArgDispatcher) ([]*CDR, error) {
cdrClone.OriginID = smCost.OriginID
if cdr.Usage == 0 {
cdrClone.Usage = smCost.Usage
} else if smCost.CostDetails.GetUsage() != cdr.Usage {
} else if smCost.Usage != cdr.Usage {
if err = cdrS.refundEventCost(smCost.CostDetails,
cdrClone.RequestType, cdrClone.ToR); err != nil {
return nil, err
@@ -832,7 +832,7 @@ func (cdrS *CDRServer) V2StoreSessionCost(args *ArgsV2CDRSStoreSMCost, reply *st
OriginID: args.Cost.OriginID,
CostSource: args.Cost.CostSource,
Usage: args.Cost.Usage,
CostDetails: args.Cost.CostDetails},
CostDetails: NewEventCostFromCallCost(cc, args.Cost.CGRID, args.Cost.RunID)},
args.CheckDuplicate); err != nil {
err = utils.NewErrServerError(err)
return

View File

@@ -56,53 +56,77 @@ func NewEventCostFromCallCost(cc *CallCost, cgrID, runID string) (ec *EventCost)
"DestinationID": ts.MatchedDestId, "RatingPlanID": ts.RatingPlanId}
cIl.RatingID = ec.ratingIDForRateInterval(ts.RateInterval, rf)
if len(ts.Increments) != 0 {
cIl.Increments = make([]*ChargingIncrement, len(ts.Increments))
cIl.Increments = make([]*ChargingIncrement, 0, len(ts.Increments)+1)
}
for j, incr := range ts.Increments {
cIt := &ChargingIncrement{
Usage: incr.Duration,
Cost: incr.Cost,
CompressFactor: incr.CompressFactor}
if incr.BalanceInfo == nil {
continue
}
//AccountingID
if incr.BalanceInfo.Unit != nil {
// 2 balances work-around
ecUUID := utils.META_NONE // populate no matter what due to Unit not nil
if incr.BalanceInfo.Monetary != nil {
if uuid := ec.Accounting.GetIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Monetary.UUID,
Units: incr.Cost,
RatingID: ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf),
}); uuid != "" {
ecUUID = uuid
}
}
cIt.AccountingID = ec.Accounting.GetIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Unit.UUID,
Units: incr.BalanceInfo.Unit.Consumed,
RatingID: ec.ratingIDForRateInterval(incr.BalanceInfo.Unit.RateInterval, rf),
ExtraChargeID: ecUUID})
} else if incr.BalanceInfo.Monetary != nil { // Only monetary
cIt.AccountingID = ec.Accounting.GetIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Monetary.UUID,
Units: incr.Cost,
RatingID: ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf)})
}
cIl.Increments[j] = cIt
for _, incr := range ts.Increments {
cIl.Increments = append(cIl.Increments, ec.newChargingIncrement(incr, rf, false))
}
if ts.RoundIncrement != nil {
rIncr := ec.newChargingIncrement(ts.RoundIncrement, rf, true)
rIncr.Cost = -rIncr.Cost
cIl.Increments = append(cIl.Increments, rIncr)
}
ec.Charges[i] = cIl
}
return
}
// newChargingIncrement creates ChargingIncrement from a Increment
// special case if is the roundIncrement the rateID is *rounding
func (ec *EventCost) newChargingIncrement(incr *Increment, rf RatingMatchedFilters, roundedIncrement bool) (cIt *ChargingIncrement) {
cIt = &ChargingIncrement{
Usage: incr.Duration,
Cost: incr.Cost,
CompressFactor: incr.CompressFactor,
}
if incr.BalanceInfo == nil {
return
}
rateID := utils.MetaRounding
//AccountingID
if incr.BalanceInfo.Unit != nil {
// 2 balances work-around
ecUUID := utils.META_NONE // populate no matter what due to Unit not nil
if incr.BalanceInfo.Monetary != nil {
if !roundedIncrement {
rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf)
}
if uuid := ec.Accounting.GetIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Monetary.UUID,
Units: incr.Cost,
RatingID: rateID,
}); uuid != "" {
ecUUID = uuid
}
}
if !roundedIncrement {
rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Unit.RateInterval, rf)
}
cIt.AccountingID = ec.Accounting.GetIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Unit.UUID,
Units: incr.BalanceInfo.Unit.Consumed,
RatingID: rateID,
ExtraChargeID: ecUUID,
})
} else if incr.BalanceInfo.Monetary != nil { // Only monetary
if !roundedIncrement {
rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf)
}
cIt.AccountingID = ec.Accounting.GetIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Monetary.UUID,
Units: incr.Cost,
RatingID: rateID,
})
}
return
}
// EventCost stores cost for an Event
type EventCost struct {
CGRID string
@@ -343,71 +367,94 @@ func (ec *EventCost) AsCallCost(tor string) *CallCost {
ToR: utils.FirstNonEmpty(tor, utils.VOICE),
Cost: ec.GetCost(),
RatedUsage: float64(ec.GetUsage().Nanoseconds()),
AccountSummary: ec.AccountSummary}
AccountSummary: ec.AccountSummary,
}
cc.Timespans = make(TimeSpans, len(ec.Charges))
for i, cIl := range ec.Charges {
ts := &TimeSpan{Cost: cIl.Cost(),
ts := &TimeSpan{
Cost: cIl.Cost(),
DurationIndex: *cIl.Usage(),
CompressFactor: cIl.CompressFactor}
CompressFactor: cIl.CompressFactor,
}
if cIl.ecUsageIdx == nil { // index was not populated yet
ec.ComputeEventCostUsageIndexes()
}
ts.TimeStart = ec.StartTime.Add(*cIl.ecUsageIdx)
ts.TimeEnd = ts.TimeStart.Add(
time.Duration(cIl.Usage().Nanoseconds() * int64(cIl.CompressFactor)))
if cIl.RatingID != "" {
if ec.Rating[cIl.RatingID].RatingFiltersID != "" {
rfs := ec.RatingFilters[ec.Rating[cIl.RatingID].RatingFiltersID]
ts.MatchedSubject = rfs[utils.Subject].(string)
ts.MatchedPrefix = rfs[utils.DestinationPrefix].(string)
ts.MatchedDestId = rfs[utils.DestinationID].(string)
ts.RatingPlanId = rfs[utils.RatingPlanID].(string)
}
if cIl.RatingID != "" &&
ec.Rating[cIl.RatingID].RatingFiltersID != "" {
rfs := ec.RatingFilters[ec.Rating[cIl.RatingID].RatingFiltersID]
ts.MatchedSubject = rfs[utils.Subject].(string)
ts.MatchedPrefix = rfs[utils.DestinationPrefix].(string)
ts.MatchedDestId = rfs[utils.DestinationID].(string)
ts.RatingPlanId = rfs[utils.RatingPlanID].(string)
}
ts.RateInterval = ec.rateIntervalForRatingID(cIl.RatingID)
if len(cIl.Increments) != 0 {
ts.Increments = make(Increments, len(cIl.Increments))
}
for j, cInc := range cIl.Increments {
incr := &Increment{Duration: cInc.Usage, Cost: cInc.Cost, CompressFactor: cInc.CompressFactor, BalanceInfo: new(DebitInfo)}
if cInc.AccountingID != "" {
cBC := ec.Accounting[cInc.AccountingID]
incr.BalanceInfo.AccountID = cBC.AccountID
var balanceType string
if cBC.BalanceUUID != "" {
if ec.AccountSummary != nil {
for _, b := range ec.AccountSummary.BalanceSummaries {
if b.UUID == cBC.BalanceUUID {
balanceType = b.Type
break
}
}
}
}
if utils.SliceHasMember([]string{utils.DATA, utils.VOICE}, balanceType) && cBC.ExtraChargeID == "" {
cBC.ExtraChargeID = utils.META_NONE // mark the balance to be exported as Unit type
}
if cBC.ExtraChargeID != "" { // have both monetary and data
// Work around, enforce logic with 2 balances for *voice/*monetary combination
// so we can stay compatible with CallCost
incr.BalanceInfo.Unit = &UnitInfo{UUID: cBC.BalanceUUID, Consumed: cBC.Units}
incr.BalanceInfo.Unit.RateInterval = ec.rateIntervalForRatingID(cBC.RatingID)
if cBC.ExtraChargeID != utils.META_NONE {
cBC = ec.Accounting[cBC.ExtraChargeID] // overwrite original balance so we can process it in one place
}
}
if cBC.ExtraChargeID != utils.META_NONE {
incr.BalanceInfo.Monetary = &MonetaryInfo{UUID: cBC.BalanceUUID}
incr.BalanceInfo.Monetary.RateInterval = ec.rateIntervalForRatingID(cBC.RatingID)
}
incrs := cIl.Increments
if l := len(cIl.Increments); l != 0 {
if ec.Accounting[cIl.Increments[l-1].AccountingID].RatingID == utils.MetaRounding {
// special case: if the last increment is has the ratingID equal to *roundig
// we consider it as the roundIncrement
l--
incrs = incrs[:l]
ts.RoundIncrement = ec.newIntervalFromCharge(cIl.Increments[l-1])
ts.RoundIncrement.Cost = -ts.RoundIncrement.Cost
}
ts.Increments[j] = incr
ts.Increments = make(Increments, l)
}
for j, cInc := range incrs {
ts.Increments[j] = ec.newIntervalFromCharge(cInc)
}
cc.Timespans[i] = ts
}
return cc
}
// newIntervalFromCharge creates Increment from a ChargingIncrement
func (ec *EventCost) newIntervalFromCharge(cInc *ChargingIncrement) (incr *Increment) {
incr = &Increment{
Duration: cInc.Usage,
Cost: cInc.Cost,
CompressFactor: cInc.CompressFactor,
BalanceInfo: new(DebitInfo),
}
if len(cInc.AccountingID) == 0 {
return
}
cBC := ec.Accounting[cInc.AccountingID]
incr.BalanceInfo.AccountID = cBC.AccountID
var balanceType string
if cBC.BalanceUUID != "" {
if ec.AccountSummary != nil {
for _, b := range ec.AccountSummary.BalanceSummaries {
if b.UUID == cBC.BalanceUUID {
balanceType = b.Type
break
}
}
}
}
if utils.SliceHasMember([]string{utils.DATA, utils.VOICE}, balanceType) && cBC.ExtraChargeID == "" {
cBC.ExtraChargeID = utils.META_NONE // mark the balance to be exported as Unit type
}
if cBC.ExtraChargeID != "" { // have both monetary and data
// Work around, enforce logic with 2 balances for *voice/*monetary combination
// so we can stay compatible with CallCost
incr.BalanceInfo.Unit = &UnitInfo{UUID: cBC.BalanceUUID, Consumed: cBC.Units}
incr.BalanceInfo.Unit.RateInterval = ec.rateIntervalForRatingID(cBC.RatingID)
if cBC.ExtraChargeID != utils.META_NONE {
cBC = ec.Accounting[cBC.ExtraChargeID] // overwrite original balance so we can process it in one place
}
}
if cBC.ExtraChargeID != utils.META_NONE {
incr.BalanceInfo.Monetary = &MonetaryInfo{UUID: cBC.BalanceUUID}
incr.BalanceInfo.Monetary.RateInterval = ec.rateIntervalForRatingID(cBC.RatingID)
}
return
}
// ratingGetIDFomEventCost retrieves UUID based on data from another EventCost
func (ec *EventCost) ratingGetIDFomEventCost(oEC *EventCost, oRatingID string) string {
if oRatingID == "" {

View File

@@ -323,13 +323,13 @@ func TestNewEventCostFromCallCost(t *testing.T) {
Account: "dan",
Destination: "+4986517174963",
ToR: utils.VOICE,
Cost: 0.85,
Cost: 0.75,
RatedUsage: 120.0,
Timespans: TimeSpans{
&TimeSpan{
TimeStart: time.Date(2017, 1, 9, 16, 18, 21, 0, time.UTC),
TimeEnd: time.Date(2017, 1, 9, 16, 19, 21, 0, time.UTC),
Cost: 0.25,
Cost: 0.15,
RateInterval: &RateInterval{ // standard rating
Timing: &RITiming{
StartTime: "00:00:00",
@@ -354,6 +354,16 @@ func TestNewEventCostFromCallCost(t *testing.T) {
MatchedDestId: "GERMANY",
RatingPlanId: "RPL_RETAIL1",
CompressFactor: 1,
RoundIncrement: &Increment{
Cost: 0.1,
BalanceInfo: &DebitInfo{
Monetary: &MonetaryInfo{UUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
ID: utils.MetaDefault,
Value: 9.9},
AccountID: "cgrates.org:dan",
},
CompressFactor: 1,
},
Increments: Increments{
&Increment{ // ConnectFee
Cost: 0.1,
@@ -500,10 +510,15 @@ func TestNewEventCostFromCallCost(t *testing.T) {
AccountingID: "906bfd0f-035c-40a3-93a8-46f71627983e",
CompressFactor: 30,
},
{
Cost: -0.1,
AccountingID: "44e97dec-8a7e-43d0-8b0a-e34a152",
CompressFactor: 1,
},
},
CompressFactor: 1,
usage: utils.DurationPointer(time.Duration(60 * time.Second)),
cost: utils.Float64Pointer(0.25),
cost: utils.Float64Pointer(0.15),
ecUsageIdx: utils.DurationPointer(time.Duration(0)),
},
&ChargingInterval{
@@ -568,6 +583,13 @@ func TestNewEventCostFromCallCost(t *testing.T) {
Units: 1,
ExtraChargeID: "*none",
},
"44e97dec-8a7e-43d0-8b0a-e34a152": &BalanceCharge{
AccountID: "cgrates.org:dan",
BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
RatingID: "*rounding",
Units: 0.1,
ExtraChargeID: "",
},
},
RatingFilters: RatingFilters{
"7e73a00d-be53-4083-a1ee-8ee0b546c62a": RatingMatchedFilters{
@@ -676,6 +698,21 @@ func TestNewEventCostFromCallCost(t *testing.T) {
utils.ToJSON(eEC.Rates[eEC.Rating[eEC.Accounting[eEC.Charges[0].Increments[2].AccountingID].RatingID].RatesID]),
utils.ToJSON(ec.Rates[ec.Rating[ec.Accounting[ec.Charges[0].Increments[2].AccountingID].RatingID].RatesID]))
}
// Compare to expected EC
if !reflect.DeepEqual(eEC.Accounting[eEC.Charges[0].Increments[3].AccountingID],
ec.Accounting[ec.Charges[0].Increments[3].AccountingID]) {
t.Errorf("Expecting: %s, received: %s",
utils.ToJSON(eEC.Accounting[eEC.Charges[0].Increments[3].AccountingID]),
utils.ToJSON(ec.Accounting[ec.Charges[0].Increments[3].AccountingID]))
}
ec.Charges[0].Increments[3].AccountingID = eEC.Charges[0].Increments[3].AccountingID
if !reflect.DeepEqual(eEC.Charges[0].Increments[3],
ec.Charges[0].Increments[3]) {
t.Errorf("Expecting: %s, received: %s",
utils.ToJSON(eEC.Charges[0].Increments[3]),
utils.ToJSON(ec.Charges[0].Increments[3]))
}
if len(ec.Accounting) != len(eEC.Accounting) {
t.Errorf("Expecting: %+v, received: %+v", eEC, ec)
}

View File

@@ -446,9 +446,8 @@ func (ts *TimeSpan) CalculateCost() float64 {
return 0
}
return ts.RateInterval.GetCost(ts.GetDuration(), ts.GetGroupStart())
} else {
return ts.Increments.GetTotalCost() * float64(ts.GetCompressFactor())
}
return ts.Increments.GetTotalCost() * float64(ts.GetCompressFactor())
}
func (ts *TimeSpan) setRatingInfo(rp *RatingInfo) {

View File

@@ -598,14 +598,8 @@ func (sS *SessionS) refundSession(s *Session, sRunIdx int, rUsage time.Duration)
// storeSCost will post the session cost to CDRs
// not thread safe, need to be handled in a layer above
func (sS *SessionS) storeSCost(s *Session, sRunIdx int) (err error) {
if sRunIdx >= len(s.SRuns) {
return errors.New("sRunIdx out of range")
}
sr := s.SRuns[sRunIdx]
if sr.EventCost == nil {
return // no costs to save, ignore the operation
}
smCost := &engine.V2SMCost{
smCost := &engine.SMCost{
CGRID: s.CGRID,
CostSource: utils.MetaSessionS,
RunID: sr.Event.GetStringIgnoreErrors(utils.RunID),
@@ -614,7 +608,7 @@ func (sS *SessionS) storeSCost(s *Session, sRunIdx int) (err error) {
Usage: sr.TotalUsage,
CostDetails: sr.EventCost,
}
argSmCost := &engine.ArgsV2CDRSStoreSMCost{
argSmCost := &engine.AttrCDRSStoreSMCost{
Cost: smCost,
CheckDuplicate: true,
ArgDispatcher: s.ArgDispatcher,
@@ -623,23 +617,46 @@ func (sS *SessionS) storeSCost(s *Session, sRunIdx int) (err error) {
},
}
var reply string
if err := sS.connMgr.Call(sS.cgrCfg.SessionSCfg().CDRsConns, nil, utils.CDRsV2StoreSessionCost,
argSmCost, &reply); err != nil {
if err == utils.ErrExists {
// use the v1 because it doesn't do rounding refund
if err := sS.connMgr.Call(sS.cgrCfg.SessionSCfg().CDRsConns, nil, utils.CDRsV1StoreSessionCost,
argSmCost, &reply); err != nil && err == utils.ErrExists {
utils.Logger.Warning(
fmt.Sprintf("<%s> refunding session: <%s> error: <%s>",
utils.SessionS, s.CGRID, err.Error()))
if err = sS.refundSession(s, sRunIdx, sr.CD.GetDuration()); err != nil { // refund entire duration
utils.Logger.Warning(
fmt.Sprintf("<%s> refunding session: <%s> error: <%s>",
utils.SessionS, s.CGRID, err.Error()))
if err = sS.refundSession(s, sRunIdx, sr.CD.GetDuration()); err != nil { // refund entire duration
utils.Logger.Warning(
fmt.Sprintf(
"<%s> failed refunding session: <%s>, srIdx: <%d>, error: <%s>",
utils.SessionS, s.CGRID, sRunIdx, err.Error()))
}
} else {
return err
fmt.Sprintf(
"<%s> failed refunding session: <%s>, srIdx: <%d>, error: <%s>",
utils.SessionS, s.CGRID, sRunIdx, err.Error()))
}
err = nil
}
return err
}
// roundCost will round the EventCost and will refund the extra debited increments
// should be called only at the endSession
// not thread safe, need to be handled in a layer above
func (sS *SessionS) roundCost(s *Session, sRunIdx int) (err error) {
sr := s.SRuns[sRunIdx]
runID := sr.Event.GetStringIgnoreErrors(utils.RunID)
cc := sr.EventCost.AsCallCost(utils.EmptyString)
cc.Round()
if roundIncrements := cc.GetRoundIncrements(); len(roundIncrements) != 0 {
cd := cc.CreateCallDescriptor()
cd.CgrID = s.CGRID
cd.RunID = runID
cd.Increments = roundIncrements
var response float64
if err = sS.connMgr.Call(sS.cgrCfg.SessionSCfg().ResSConns, nil,
utils.ResponderRefundRounding,
&engine.CallDescriptorWithArgDispatcher{CallDescriptor: cd},
&response); err != nil {
return
}
}
return nil
sr.EventCost = engine.NewEventCostFromCallCost(cc, s.CGRID, runID)
return
}
// disconnectSession will send disconnect from SessionS to clients
@@ -1478,6 +1495,20 @@ func (sS *SessionS) endSession(s *Session, tUsage, lastUsage *time.Duration,
utils.SessionS, s.CGRID, sRunIdx, err.Error()))
}
}
if err := sS.roundCost(s, sRunIdx); err != nil { // will round the cost and refund the extra increment
utils.Logger.Warning(
fmt.Sprintf("<%s> failed rounding session cost for <%s>, srIdx: <%d>, error: <%s>",
utils.SessionS, s.CGRID, sRunIdx, err.Error()))
}
if sS.cgrCfg.SessionSCfg().StoreSCosts {
if err := sS.storeSCost(s, sRunIdx); err != nil {
utils.Logger.Warning(
fmt.Sprintf("<%s> failed storing session cost for <%s>, srIdx: <%d>, error: <%s>",
utils.SessionS, s.CGRID, sRunIdx, err.Error()))
}
}
// set cost fields
sr.Event[utils.Cost] = sr.EventCost.GetCost()
sr.Event[utils.CostDetails] = utils.ToJSON(sr.EventCost) // avoid map[string]interface{} when decoding
@@ -1491,14 +1522,6 @@ func (sS *SessionS) endSession(s *Session, tUsage, lastUsage *time.Duration,
if aTime != nil {
sr.Event[utils.AnswerTime] = *aTime
}
if sS.cgrCfg.SessionSCfg().StoreSCosts {
if err := sS.storeSCost(s, sRunIdx); err != nil {
utils.Logger.Warning(
fmt.Sprintf("<%s> failed storing session cost for <%s>, srIdx: <%d>, error: <%s>",
utils.SessionS, s.CGRID, sRunIdx, err.Error()))
}
}
}
engine.Cache.Set(utils.CacheClosedSessions, s.CGRID, s,
nil, true, utils.NonTransactional)

View File

@@ -662,6 +662,7 @@ const (
FileName = "FileName"
MetaBusy = "*busy"
MetaQueue = "*queue"
MetaRounding = "*rounding"
)
// Migrator Action