Files
cgrates/engine/eventcost.go
2017-05-26 20:02:51 +02:00

592 lines
19 KiB
Go

/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package engine
import (
"errors"
"time"
"github.com/cgrates/cgrates/utils"
)
func NewBareEventCost() *EventCost {
return &EventCost{
Rating: make(Rating),
Accounting: make(Accounting),
RatingFilters: make(RatingFilters),
Rates: make(ChargedRates),
Timings: make(ChargedTimings),
}
}
func NewEventCostFromCallCost(cc *CallCost, cgrID, runID string) (ec *EventCost) {
ec = NewBareEventCost()
ec.CGRID = cgrID
ec.RunID = runID
ec.AccountSummary = cc.AccountSummary
if len(cc.Timespans) != 0 {
ec.Charges = make([]*ChargingInterval, len(cc.Timespans))
ec.StartTime = cc.Timespans[0].TimeStart
}
for i, ts := range cc.Timespans {
cIl := &ChargingInterval{CompressFactor: ts.CompressFactor}
rf := RatingMatchedFilters{"Subject": ts.MatchedSubject, "DestinationPrefix": ts.MatchedPrefix,
"DestinationID": ts.MatchedDestId, "RatingPlanID": ts.RatingPlanId}
cIl.RatingUUID = ec.ratingUUIDForRateInterval(ts.RateInterval, rf)
if len(ts.Increments) != 0 {
cIl.Increments = make([]*ChargingIncrement, len(ts.Increments))
}
for j, incr := range ts.Increments {
cIt := &ChargingIncrement{
Usage: incr.Duration,
Cost: incr.Cost,
CompressFactor: incr.CompressFactor}
if incr.BalanceInfo == nil {
continue
}
//BalanceChargeUUID
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.GetUUIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Monetary.UUID,
Units: incr.Cost,
RatingUUID: ec.ratingUUIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf),
}); uuid != "" {
ecUUID = uuid
}
}
cIt.BalanceChargeUUID = ec.Accounting.GetUUIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Unit.UUID,
Units: incr.BalanceInfo.Unit.Consumed,
RatingUUID: ec.ratingUUIDForRateInterval(incr.BalanceInfo.Unit.RateInterval, rf),
ExtraChargeUUID: ecUUID})
} else if incr.BalanceInfo.Monetary != nil { // Only monetary
cIt.BalanceChargeUUID = ec.Accounting.GetUUIDWithSet(
&BalanceCharge{
AccountID: incr.BalanceInfo.AccountID,
BalanceUUID: incr.BalanceInfo.Monetary.UUID,
Units: incr.Cost,
RatingUUID: ec.ratingUUIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf)})
}
cIl.Increments[j] = cIt
}
ec.Charges[i] = cIl
}
return
}
// EventCost stores cost for an Event
type EventCost struct {
CGRID string
RunID string
StartTime time.Time
Usage *time.Duration
Cost *float64 // pointer so we can nil it when dirty
Charges []*ChargingInterval
AccountSummary *AccountSummary // Account summary at the end of the event calculation
Rating Rating
Accounting Accounting
RatingFilters RatingFilters
Rates ChargedRates
Timings ChargedTimings
}
func (ec *EventCost) ratingUUIDForRateInterval(ri *RateInterval, rf RatingMatchedFilters) string {
if ri == nil || ri.Rating == nil {
return ""
}
var rfUUID string
if rf != nil {
rfUUID = ec.RatingFilters.GetUUIDWithSet(rf)
}
var tmID string
if ri.Timing != nil {
tmID = ec.Timings.GetUUIDWithSet(
&ChargedTiming{
Years: ri.Timing.Years,
Months: ri.Timing.Months,
MonthDays: ri.Timing.MonthDays,
WeekDays: ri.Timing.WeekDays,
StartTime: ri.Timing.StartTime})
}
var rtUUID string
if len(ri.Rating.Rates) != 0 {
rtUUID = ec.Rates.GetUUIDWithSet(ri.Rating.Rates)
}
return ec.Rating.GetUUIDWithSet(
&RatingUnit{
ConnectFee: ri.Rating.ConnectFee,
RoundingMethod: ri.Rating.RoundingMethod,
RoundingDecimals: ri.Rating.RoundingDecimals,
MaxCost: ri.Rating.MaxCost,
MaxCostStrategy: ri.Rating.MaxCostStrategy,
TimingUUID: tmID,
RatesUUID: rtUUID,
RatingFiltersUUID: rfUUID})
}
func (ec *EventCost) rateIntervalForRatingUUID(ratingUUID string) (ri *RateInterval) {
if ratingUUID == "" {
return
}
cIlRU := ec.Rating[ratingUUID]
ri = new(RateInterval)
ri.Rating = &RIRate{ConnectFee: cIlRU.ConnectFee,
RoundingMethod: cIlRU.RoundingMethod,
RoundingDecimals: cIlRU.RoundingDecimals,
MaxCost: cIlRU.MaxCost, MaxCostStrategy: cIlRU.MaxCostStrategy}
if cIlRU.RatesUUID != "" {
ri.Rating.Rates = ec.Rates[cIlRU.RatesUUID]
}
if cIlRU.TimingUUID != "" {
cIlTm := ec.Timings[cIlRU.TimingUUID]
ri.Timing = &RITiming{Years: cIlTm.Years, Months: cIlTm.Months, MonthDays: cIlTm.MonthDays,
WeekDays: cIlTm.WeekDays, StartTime: cIlTm.StartTime}
}
return
}
func (ec *EventCost) Clone() (cln *EventCost) {
cln = new(EventCost)
cln.CGRID = ec.CGRID
cln.RunID = ec.RunID
cln.StartTime = ec.StartTime
if ec.Usage != nil {
cln.Usage = utils.DurationPointer(*ec.Usage)
}
if ec.Cost != nil {
cln.Cost = utils.Float64Pointer(*ec.Cost)
}
if ec.Charges != nil {
cln.Charges = make([]*ChargingInterval, len(ec.Charges))
for i, cIl := range ec.Charges {
cln.Charges[i] = cIl.Clone()
}
}
if ec.AccountSummary != nil {
cln.AccountSummary = ec.AccountSummary.Clone()
}
cln.Rating = ec.Rating.Clone()
cln.Accounting = ec.Accounting.Clone()
cln.RatingFilters = ec.RatingFilters.Clone()
cln.Rates = ec.Rates.Clone()
cln.Timings = ec.Timings.Clone()
return
}
// Compute aggregates all the compute methods on EventCost
func (ec *EventCost) Compute() {
ec.ComputeUsage()
ec.ComputeEventCostUsageIndexes()
ec.ComputeCost()
}
// ResetCounters will reset all the computed cached values
func (ec *EventCost) ResetCounters() {
ec.Cost = nil
ec.Usage = nil
for _, cIl := range ec.Charges {
cIl.cost = nil
cIl.usage = nil
cIl.ecUsageIdx = nil
}
}
// ComputeCost iterates through Charges, computing EventCost.Cost
func (ec *EventCost) ComputeCost() float64 {
if ec.Cost == nil {
var cost float64
for _, ci := range ec.Charges {
cost += ci.Cost() * float64(ci.CompressFactor)
}
cost = utils.Round(cost, globalRoundingDecimals, utils.ROUNDING_MIDDLE)
ec.Cost = &cost
}
return *ec.Cost
}
// ComputeUsage iterates through Charges, computing EventCost.Usage
func (ec *EventCost) ComputeUsage() time.Duration {
if ec.Usage == nil {
var usage time.Duration
for _, ci := range ec.Charges {
usage += time.Duration(ci.Usage().Nanoseconds() * int64(ci.CompressFactor))
}
ec.Usage = &usage
}
return *ec.Usage
}
// ComputeEventCostUsageIndexes will iterate through Chargers and populate their ecUsageIdx
func (ec *EventCost) ComputeEventCostUsageIndexes() {
var totalUsage time.Duration
for _, cIl := range ec.Charges {
if cIl.ecUsageIdx == nil {
cIl.ecUsageIdx = utils.DurationPointer(totalUsage)
}
totalUsage += time.Duration(cIl.Usage().Nanoseconds() * int64(cIl.CompressFactor))
}
}
func (ec *EventCost) AsCallCost() *CallCost {
cc := &CallCost{
Cost: ec.ComputeCost(), RatedUsage: ec.ComputeUsage().Seconds(),
AccountSummary: ec.AccountSummary}
cc.Timespans = make(TimeSpans, len(ec.Charges))
for i, cIl := range ec.Charges {
ts := &TimeSpan{Cost: cIl.Cost(),
DurationIndex: *cIl.Usage(), 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.RatingUUID != "" {
if ec.Rating[cIl.RatingUUID].RatingFiltersUUID != "" {
rfs := ec.RatingFilters[ec.Rating[cIl.RatingUUID].RatingFiltersUUID]
ts.MatchedSubject = rfs["Subject"].(string)
ts.MatchedPrefix = rfs["DestinationPrefix"].(string)
ts.MatchedDestId = rfs["DestinationID"].(string)
ts.RatingPlanId = rfs["RatingPlanID"].(string)
}
}
ts.RateInterval = ec.rateIntervalForRatingUUID(cIl.RatingUUID)
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}
if cInc.BalanceChargeUUID != "" {
cBC := ec.Accounting[cInc.BalanceChargeUUID]
incr.BalanceInfo = &DebitInfo{AccountID: cBC.AccountID}
if cBC.ExtraChargeUUID != "" { // 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.rateIntervalForRatingUUID(cBC.RatingUUID)
if cBC.ExtraChargeUUID != utils.META_NONE {
cBC = ec.Accounting[cBC.ExtraChargeUUID] // overwrite original balance so we can process it in one place
}
}
if cBC.ExtraChargeUUID != utils.META_NONE {
incr.BalanceInfo.Monetary = &MonetaryInfo{UUID: cBC.BalanceUUID}
incr.BalanceInfo.Monetary.RateInterval = ec.rateIntervalForRatingUUID(cBC.RatingUUID)
}
}
ts.Increments[j] = incr
}
cc.Timespans[i] = ts
}
return cc
}
// ratingGetUUIDFomEventCost retrieves UUID based on data from another EventCost
func (ec *EventCost) ratingGetUUIDFomEventCost(oEC *EventCost, oRatingUUID string) string {
if oRatingUUID == "" {
return ""
}
oCIlRating := oEC.Rating[oRatingUUID].Clone() // clone so we don't influence the original data
oCIlRating.TimingUUID = ec.Timings.GetUUIDWithSet(oEC.Timings[oCIlRating.TimingUUID])
oCIlRating.RatingFiltersUUID = ec.RatingFilters.GetUUIDWithSet(oEC.RatingFilters[oCIlRating.RatingFiltersUUID])
oCIlRating.RatesUUID = ec.Rates.GetUUIDWithSet(oEC.Rates[oCIlRating.RatesUUID])
return ec.Rating.GetUUIDWithSet(oCIlRating)
}
// accountingGetUUIDFromEventCost retrieves UUID based on data from another EventCost
func (ec *EventCost) accountingGetUUIDFromEventCost(oEC *EventCost, oBalanceChargeUUID string) string {
if oBalanceChargeUUID == "" || oBalanceChargeUUID == utils.META_NONE {
return ""
}
oBC := oEC.Accounting[oBalanceChargeUUID].Clone()
oBC.RatingUUID = ec.ratingGetUUIDFomEventCost(oEC, oBC.RatingUUID)
oBC.ExtraChargeUUID = ec.accountingGetUUIDFromEventCost(oEC, oBC.ExtraChargeUUID)
return ec.Accounting.GetUUIDWithSet(oBC)
}
// appendCIl appends a ChargingInterval to existing chargers, no compression done
func (ec *EventCost) appendCIlFromEC(oEC *EventCost, cIlIdx int) {
cIl := oEC.Charges[cIlIdx]
cIl.RatingUUID = ec.ratingGetUUIDFomEventCost(oEC, cIl.RatingUUID)
for _, cIt := range cIl.Increments {
cIt.BalanceChargeUUID = ec.accountingGetUUIDFromEventCost(oEC, cIt.BalanceChargeUUID)
}
ec.Charges = append(ec.Charges, cIl)
}
// AppendChargingInterval appends or compresses a &ChargingInterval to existing ec.Chargers
func (ec *EventCost) AppendChargingIntervalFromEventCost(oEC *EventCost, cIlIdx int) {
lenChargers := len(ec.Charges)
if lenChargers != 0 && ec.Charges[lenChargers-1].PartiallyEquals(oEC.Charges[cIlIdx]) {
ec.Charges[lenChargers-1].CompressFactor += 1
} else {
ec.appendCIlFromEC(oEC, cIlIdx)
}
}
// Merge will merge a list of EventCosts into this one
func (ec *EventCost) Merge(ecs ...*EventCost) {
for _, newEC := range ecs {
ec.AccountSummary = newEC.AccountSummary // updated AccountSummary information
for cIlIdx := range newEC.Charges {
ec.AppendChargingIntervalFromEventCost(newEC, cIlIdx)
}
}
ec.ResetCounters()
}
// RemoveStaleReferences iterates through cached data and makes sure it is still referenced from Charging
func (ec *EventCost) RemoveStaleReferences() {
// RatingUUIDs
for key := range ec.Rating {
var keyUsed bool
for _, cIl := range ec.Charges {
if cIl.RatingUUID == key {
keyUsed = true
break
}
}
if !keyUsed { // look also in accounting for references
for _, bc := range ec.Accounting {
if bc.RatingUUID == key {
keyUsed = true
break
}
}
}
if !keyUsed { // not used, remove it
delete(ec.Rating, key)
}
}
for key := range ec.Accounting {
var keyUsed bool
for _, cIl := range ec.Charges {
for _, cIt := range cIl.Increments {
if cIt.BalanceChargeUUID == key {
keyUsed = true
break
}
}
}
if !keyUsed {
delete(ec.Accounting, key)
}
}
for key := range ec.RatingFilters {
var keyUsed bool
for _, ru := range ec.Rating {
if ru.RatingFiltersUUID == key {
keyUsed = true
break
}
}
if !keyUsed {
delete(ec.RatingFilters, key)
}
}
for key := range ec.Rates {
var keyUsed bool
for _, ru := range ec.Rating {
if ru.RatesUUID == key {
keyUsed = true
break
}
}
if !keyUsed {
delete(ec.Rates, key)
}
}
for key := range ec.Timings {
var keyUsed bool
for _, ru := range ec.Rating {
if ru.TimingUUID == key {
keyUsed = true
break
}
}
if !keyUsed {
delete(ec.Rates, key)
}
}
}
// Trim will cut the EventCost at specific duration
// returns the srplusEC as separate EventCost
func (ec *EventCost) Trim(atUsage time.Duration) (srplusEC *EventCost, err error) {
if ec.Usage == nil {
ec.ComputeUsage()
}
origECUsage := ec.ComputeUsage()
if atUsage >= *ec.Usage {
return // no trim
}
if atUsage == 0 {
srplusEC = ec
ec = NewBareEventCost()
ec.CGRID = srplusEC.CGRID
ec.RunID = srplusEC.RunID
ec.StartTime = srplusEC.StartTime
ec.AccountSummary = srplusEC.AccountSummary.Clone()
return // trim all, fresh EC with 0 usage
}
srplusEC = NewBareEventCost()
srplusEC.CGRID = ec.CGRID
srplusEC.RunID = ec.RunID
srplusEC.StartTime = ec.StartTime
srplusEC.AccountSummary = ec.AccountSummary.Clone()
var lastActiveCIlIdx *int // marks last index which should stay with ec
for i, cIl := range ec.Charges {
if cIl.ecUsageIdx == nil {
ec.ComputeEventCostUsageIndexes()
}
if cIl.usage == nil {
ec.ComputeUsage()
}
if *cIl.ecUsageIdx+*cIl.TotalUsage() >= atUsage {
lastActiveCIlIdx = utils.IntPointer(i)
break
}
}
if lastActiveCIlIdx == nil {
return nil, errors.New("cannot find last active ChargingInterval")
}
lastActiveCIl := ec.Charges[*lastActiveCIlIdx]
if *lastActiveCIl.ecUsageIdx >= atUsage {
return nil, errors.New("failed detecting last active ChargingInterval")
} else if lastActiveCIl.CompressFactor == 0 {
return nil, errors.New("ChargingInterval with 0 compressFactor")
}
srplusEC.Charges = ec.Charges[*lastActiveCIlIdx+1:]
ec.Charges = ec.Charges[:*lastActiveCIlIdx+1]
if lastActiveCIl.CompressFactor != 1 { // Split based on compress factor if needed
var laCF int
for ciCnt := 1; ciCnt <= lastActiveCIl.CompressFactor; ciCnt++ {
if *lastActiveCIl.ecUsageIdx+
time.Duration(lastActiveCIl.usage.Nanoseconds()*int64(ciCnt)) > atUsage {
laCF = ciCnt
break
}
}
if laCF == 0 {
return nil, errors.New("cannot detect last active CompressFactor in ChargingInterval")
}
if laCF != lastActiveCIl.CompressFactor {
srplsCIl := lastActiveCIl.Clone()
srplsCIl.CompressFactor = lastActiveCIl.CompressFactor - laCF
srplusEC.Charges = append([]*ChargingInterval{srplsCIl}, srplusEC.Charges...) // prepend surplus CIl
lastActiveCIl.CompressFactor = laCF // correct compress factor
}
}
atUsage = atUsage - time.Duration(lastActiveCIl.ecUsageIdx.Nanoseconds()*int64(lastActiveCIl.CompressFactor)) // remaining duration to cover in increments
// find out last increment covering duration
var lastActiveCItIdx *int
var incrementsUsage time.Duration
for i, cIt := range lastActiveCIl.Increments {
incrementsUsage += cIt.TotalUsage()
if incrementsUsage >= atUsage {
lastActiveCItIdx = utils.IntPointer(i)
break
}
}
if lastActiveCItIdx == nil { // bug in increments
return nil, errors.New("no active increment found")
}
lastActiveCIts := lastActiveCIl.Increments // so we can modify the reference in case we have surplus
lastIncrement := lastActiveCIts[*lastActiveCItIdx]
if lastIncrement.CompressFactor == 0 {
return nil, errors.New("empty compress factor in increment")
}
var srplsIncrements []*ChargingIncrement
if *lastActiveCItIdx < len(lastActiveCIl.Increments)-1 { // less that complete increments, have surplus
srplsIncrements = lastActiveCIts[*lastActiveCItIdx+1:]
lastActiveCIts = lastActiveCIts[:*lastActiveCItIdx+1]
}
var laItCF int
if lastIncrement.CompressFactor != 1 { // detect the increment covering the last part of usage
incrementsUsage -= lastIncrement.TotalUsage()
for cnt := 1; cnt <= lastIncrement.CompressFactor; cnt++ {
incrementsUsage += lastIncrement.Usage
if incrementsUsage >= atUsage {
laItCF = cnt
break
}
}
if laItCF == 0 {
return nil, errors.New("cannot detect last active CompressFactor in ChargingIncrement")
}
if laItCF != lastIncrement.CompressFactor {
srplsIncrement := lastIncrement.Clone()
srplsIncrement.CompressFactor = srplsIncrement.CompressFactor - laItCF
srplsIncrements = append([]*ChargingIncrement{srplsIncrement}, srplsIncrements...) // prepend the surplus out of compress
//lastIncrement.CompressFactor = laItCF
}
}
if len(srplsIncrements) != 0 { // partially covering, need trim
if lastActiveCIl.CompressFactor > 1 { // ChargingInterval not covering in full, need to split it
lastActiveCIl.CompressFactor -= 1
ec.Charges = append(ec.Charges, lastActiveCIl.Clone())
lastActiveCIl = ec.Charges[len(ec.Charges)-1]
lastActiveCIl.CompressFactor = 1
}
srplsCIl := lastActiveCIl.Clone()
srplsCIl.Increments = srplsIncrements
srplusEC.Charges = append([]*ChargingInterval{srplsCIl}, srplusEC.Charges...)
lastActiveCIl.Increments = make([]*ChargingIncrement, len(lastActiveCIts))
for i, incr := range lastActiveCIts {
lastActiveCIl.Increments[i] = incr.Clone() // avoid pointer references to the other interval
}
if laItCF != 0 {
lastActiveCIl.Increments[len(lastActiveCIl.Increments)-1].CompressFactor = laItCF // correct the compressFactor for the last increment
}
}
ec.ResetCounters()
if usage := ec.ComputeUsage(); usage < atUsage {
return nil, errors.New("usage of EventCost smaller than requested")
}
srplusEC.ResetCounters()
srplusEC.StartTime = ec.StartTime.Add(ec.ComputeUsage())
if srplsUsage := srplusEC.ComputeUsage(); srplsUsage > origECUsage-atUsage {
return nil, errors.New("surplus EventCost too big")
}
// close surplus with missing cache
for _, cIl := range srplusEC.Charges {
cIl.RatingUUID = srplusEC.ratingGetUUIDFomEventCost(ec, cIl.RatingUUID)
for _, incr := range cIl.Increments {
incr.BalanceChargeUUID = srplusEC.accountingGetUUIDFromEventCost(ec, incr.BalanceChargeUUID)
}
}
ec.RemoveStaleReferences() // data should be transfered by now, can clean the old one
return
}