You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
326 lines
11 KiB
326 lines
11 KiB
package cdr
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
DEBUG = false
|
|
)
|
|
|
|
func SetDebug(d bool) {
|
|
DEBUG = d
|
|
}
|
|
|
|
func CombineTextCDRs(cdrLines []Line, prices []Price) ([]Text, error) {
|
|
var err []string
|
|
|
|
numTextCDRs := 0
|
|
for _, l := range cdrLines {
|
|
if l.Kind != TextLine {
|
|
continue
|
|
}
|
|
if l.Reason != ORIG {
|
|
err = append(err, fmt.Sprintf("text CDR did not match reason ORIG: %+v", l))
|
|
continue
|
|
}
|
|
numTextCDRs++
|
|
}
|
|
|
|
// Now convert each text CDR to a priced Text
|
|
ret := make([]Text, numTextCDRs)
|
|
i := 0
|
|
for _, l := range cdrLines {
|
|
if l.Kind != TextLine || l.Reason != ORIG {
|
|
continue
|
|
}
|
|
ret[i] = Text{
|
|
PricedLine: PricedLine{
|
|
Line: l,
|
|
Cost: TEXT_OUTGOING_BUY,
|
|
Price: TEXT_OUTGOING_SELL,
|
|
},
|
|
Account: l.Account,
|
|
}
|
|
i++
|
|
}
|
|
|
|
if len(err) > 0 {
|
|
return nil, fmt.Errorf("error pricing text CDRs: %v", strings.Join(err, ", "))
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func CombineDataCDRs(cdrLines []Line, prices []Price) ([]Data, error) {
|
|
var err []string
|
|
numDataCDRs := 0
|
|
errSet := false
|
|
for _, l := range cdrLines {
|
|
errSet = false
|
|
if l.Kind != DataLine {
|
|
continue
|
|
}
|
|
if l.Reason != ORIG {
|
|
err = append(err, fmt.Sprintf("data CDR did not match reason ORIG: %+v", l))
|
|
errSet = true
|
|
}
|
|
if l.Destination != DATA_EXPECTED_DESTINATION {
|
|
err = append(err, fmt.Sprintf("data CDR did not match expected destination %q: %+v", DATA_EXPECTED_DESTINATION, l))
|
|
errSet = true
|
|
}
|
|
if errSet {
|
|
continue
|
|
}
|
|
numDataCDRs++
|
|
}
|
|
|
|
ret := make([]Data, numDataCDRs)
|
|
i := 0
|
|
for _, l := range cdrLines {
|
|
if l.Kind != DataLine || l.Reason != ORIG || l.Destination != DATA_EXPECTED_DESTINATION {
|
|
continue
|
|
}
|
|
|
|
// Initially set cost and price to national buy/sell values
|
|
dataCost := DATA_OUTGOING_NATIONAL_BUY
|
|
dataPrice := DATA_OUTGOING_NATIONAL_SELL
|
|
if l.Source != DATA_NATIONAL_SOURCE {
|
|
// Roaming CDR
|
|
// TODO: distinguish between Europe "Roam-like-at-home" and worldwide
|
|
dataCost = DATA_OUTGOING_ROAMING_BUY
|
|
dataPrice = DATA_OUTGOING_ROAMING_SELL
|
|
}
|
|
dataBytes := l.RawCount
|
|
dataCost *= dataBytes
|
|
dataPrice *= dataBytes
|
|
|
|
ret[i] = Data{
|
|
PricedLine: PricedLine{
|
|
Line: l,
|
|
Cost: int(dataCost / 1000.0), // compensate for cost specified per MB
|
|
Price: int(dataPrice / 1000.0), // compensate for price specified per MB
|
|
},
|
|
Account: l.Account,
|
|
Kilobytes: l.RawCount,
|
|
}
|
|
i++
|
|
}
|
|
|
|
if len(err) > 0 {
|
|
return nil, fmt.Errorf("error pricing data CDRs: %v", strings.Join(err, ", "))
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func CombineCallCDRs(cdrLines []Line, prices []Price) ([]Call, error) {
|
|
// Preprocess the price list into a map of "destination" -> Price, filter out the Voice destinations
|
|
destinationToPrice := make(map[string]Price)
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_FIXED.Destination] = CALL_NATIONAL_DESTINATION_FIXED
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_AIRTIME_TO_HANDSET.Destination] = CALL_NATIONAL_DESTINATION_AIRTIME_TO_HANDSET
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_AIRTIME_FROM_HANDSET.Destination] = CALL_NATIONAL_DESTINATION_AIRTIME_FROM_HANDSET
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_MOBILE.Destination] = CALL_NATIONAL_DESTINATION_MOBILE
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_MOBILE_SU.Destination] = CALL_NATIONAL_DESTINATION_MOBILE_SU
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_FREEPHONE.Destination] = CALL_NATIONAL_DESTINATION_FREEPHONE
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_PREMIUM_BIBA.Destination] = CALL_NATIONAL_DESTINATION_PREMIUM_BIBA
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_PREMIUM_FREE_NOSETUP.Destination] = CALL_NATIONAL_DESTINATION_PREMIUM_FREE_NOSETUP
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_PREMIUM_10ct.Destination] = CALL_NATIONAL_DESTINATION_PREMIUM_10ct
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_PREMIUM_45ct.Destination] = CALL_NATIONAL_DESTINATION_PREMIUM_45ct
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_PREMIUM_50ct_NOSETUP.Destination] = CALL_NATIONAL_DESTINATION_PREMIUM_50ct_NOSETUP
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_PREMIUM_80ct_NOSETUP.Destination] = CALL_NATIONAL_DESTINATION_PREMIUM_80ct_NOSETUP
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_PREMIUM_50ct_CALL.Destination] = CALL_NATIONAL_DESTINATION_PREMIUM_50ct_CALL
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_PREMIUM_80ct_CALL.Destination] = CALL_NATIONAL_DESTINATION_PREMIUM_80ct_CALL
|
|
destinationToPrice[CALL_NATIONAL_DESTINATION_VOICEMAIL.Destination] = CALL_NATIONAL_DESTINATION_VOICEMAIL
|
|
destinationToPrice[CALL_INTERNAL.Destination] = CALL_INTERNAL
|
|
destinationToPrice[CALL_UK_DESTINATION_NON_GEO.Destination] = CALL_UK_DESTINATION_NON_GEO
|
|
|
|
nonMatchedDestinations := make(map[string]struct{})
|
|
for _, l := range cdrLines {
|
|
if l.Kind != VoiceLine {
|
|
continue
|
|
}
|
|
// Cache destination prices
|
|
_, knownNonPriced := nonMatchedDestinations[l.Destination]
|
|
if _, ok := destinationToPrice[l.Destination]; !ok && !knownNonPriced {
|
|
matched := false
|
|
priceLoop:
|
|
for _, p := range prices {
|
|
if p.Type != PriceCall {
|
|
fmt.Printf("[warning] skipping this price: %+v\n", p)
|
|
continue priceLoop
|
|
}
|
|
if p.Destination == l.Destination {
|
|
destinationToPrice[l.Destination] = p
|
|
matched = true
|
|
break priceLoop
|
|
}
|
|
|
|
}
|
|
if !matched {
|
|
fmt.Printf("[warning] non-matched price for destination %q (from line: %+v)\n", l.Destination, l)
|
|
nonMatchedDestinations[l.Destination] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
for k := range nonMatchedDestinations {
|
|
fmt.Printf("[warning] no price for destination %v\n", k)
|
|
}
|
|
|
|
// a map from "%s_%s_%s_%s": l.Time.Format(DateTimeFormat), l.CLI, l.From, l.Account
|
|
uniqueCalls := make(map[string][]Line)
|
|
for _, l := range cdrLines {
|
|
if l.Kind != VoiceLine {
|
|
continue
|
|
}
|
|
idx := fmt.Sprintf("%s_%s_%s_%s", l.Time.Format(DateTimeFormat), l.CLI, l.From, l.Account)
|
|
if _, ok := uniqueCalls[idx]; !ok {
|
|
uniqueCalls[idx] = make([]Line, 0)
|
|
}
|
|
uniqueCalls[idx] = append(uniqueCalls[idx], l)
|
|
}
|
|
|
|
ret := make([]Call, len(uniqueCalls))
|
|
i := 0
|
|
for _, v := range uniqueCalls {
|
|
/*
|
|
fmt.Printf("[%s] %d\n", k, len(v))
|
|
for i, c := range v {
|
|
fmt.Printf("\t%d (%d) [%25s]: (%12s): %12s->%12s (%s)\n", i, c.Leg, c.Account, c.CLI, c.From, c.To, c.Reason)
|
|
}
|
|
fmt.Println()
|
|
*/
|
|
callCost := 0.0
|
|
callPrice := 0.0
|
|
rawCost := 0
|
|
pricedLegs := make([]PricedLine, len(v))
|
|
for i, leg := range v {
|
|
legPrice := Price{}
|
|
pricedLeg := PricedLine{}
|
|
// TODO: observation: first check the number of legs, if we have multiple, assume it's a HS->PBX->Destination (outgoing) call.
|
|
// Source->PBX->Handset ends up as one leg
|
|
if leg.CLI == leg.From {
|
|
switch {
|
|
case leg.Source == CALL_FROM_HANDSET_SOURCE && leg.Destination == CALL_FROM_HANDSET_DESTINATION:
|
|
// This leg consists of the airtime (outgoing call from the HS perspective) (HS->PBX, non-roaming)
|
|
legPrice = CALL_NATIONAL_DESTINATION_AIRTIME_FROM_HANDSET
|
|
|
|
case leg.From != leg.To && leg.Source == CALL_FROM_HANDSET_SOURCE: // Case 2, 3
|
|
// This leg consists of routing a call (from a PBX or Handset) through our platform. The costs are defined by the call's destination.
|
|
// Case 1: PBX->destination
|
|
// Case 2: HS->destination, non roaming
|
|
// Case 3: HS->destination, roaming
|
|
// Case 4: HS->PBX, roaming
|
|
fallthrough
|
|
case leg.Source != CALL_FROM_HANDSET_SOURCE && leg.Reason == ORIG: // Case 1
|
|
fallthrough
|
|
case leg.From != leg.To && leg.Source == CALL_FROM_PBX_SOURCE: // Case 4
|
|
legPrice = destinationToPrice[leg.Destination]
|
|
}
|
|
}
|
|
if leg.CLI != leg.From {
|
|
switch {
|
|
case leg.From == leg.To && leg.Source == CALL_TO_HANDSET_SOURCE && leg.Destination == CALL_TO_HANDSET_DESTINATION:
|
|
// This leg consists of the airtime (PBX->HS, roaming + non-roaming)
|
|
legPrice = CALL_NATIONAL_DESTINATION_AIRTIME_TO_HANDSET
|
|
case leg.Destination == CALL_TO_VOICEMAIL && (leg.Reason == CFNA || leg.Reason == CFBS || leg.Reason == CFIM):
|
|
legPrice = CALL_NATIONAL_DESTINATION_VOICEMAIL
|
|
// source -> HS (roaming)
|
|
case leg.Source == CALL_TO_HANDSET_SOURCE && leg.Reason == ROAM:
|
|
// We only know the destination country
|
|
fallthrough
|
|
case leg.CLI == CALL_CLI_ANONYMOUS:
|
|
// Incoming call from a blocked CLI
|
|
legPrice = destinationToPrice[leg.Destination]
|
|
}
|
|
}
|
|
if legPrice == (Price{}) {
|
|
// error, weird category
|
|
_, mapOk := destinationToPrice[leg.Destination]
|
|
fmt.Printf("[warning] non-matched CDR leg (cid: %q, dt: %v, CLI: %s, %s->%s, src: %s, dest: %s (%s) %v\n",
|
|
leg.Id, leg.Time.Format(DateTimeFormat), leg.CLI, leg.From, leg.To, leg.Source, leg.Destination, leg.Reason, mapOk)
|
|
} else {
|
|
// RawCount is in seconds. Buy/SellUnit is in cents*100, per minute
|
|
thisLegCost := float64(legPrice.BuyEach) + float64(leg.RawCount)*(float64(legPrice.BuyUnit)/60.0)
|
|
thisLegPrice := float64(legPrice.SellEach) + float64(leg.RawCount)*(float64(legPrice.SellUnit)/60.0)
|
|
|
|
pricedLeg.Line = leg
|
|
pricedLeg.Cost = int(math.Round(thisLegCost))
|
|
pricedLeg.Price = int(math.Round(thisLegPrice))
|
|
if legPrice.OurRef != "" {
|
|
pricedLeg.OurRef = legPrice.OurRef
|
|
}
|
|
pricedLegs[i] = pricedLeg
|
|
|
|
callCost += thisLegCost
|
|
callPrice += thisLegPrice
|
|
rawCost += leg.RawCosts
|
|
if DEBUG {
|
|
fmt.Printf("(%12s( %12s)->%12s) leg %d, callCost: %.4f, callPrice: %.4f (dst: %q)\n", leg.CLI, leg.From, leg.To, leg.Leg, callCost/10000.0, callPrice/10000.0, legPrice.OurRef)
|
|
}
|
|
}
|
|
// Source -> HS (non-roaming) (i.e. incoming calls) does not exist in the CSV
|
|
//fmt.Printf("(%12s->%12s) leg %d, price: %+v\n", leg.CLI, leg.To, leg.Leg, legPrice)
|
|
}
|
|
callDuration, _ := time.ParseDuration(fmt.Sprintf("%ds", v[0].RawCount))
|
|
var callFrom, callTo, callDescription string
|
|
|
|
ret[i] = Call{
|
|
Duration: callDuration,
|
|
Time: v[0].Time,
|
|
RawCost: rawCost,
|
|
Cost: int(math.Round(callCost)),
|
|
Price: int(math.Round(callPrice)),
|
|
Legs: pricedLegs,
|
|
}
|
|
if len(v) == 1 {
|
|
// CLI == From, then this leg's CLI is our "From" field, and this leg's "To" likewise
|
|
callFrom = v[0].CLI
|
|
callTo = v[0].To
|
|
if pricedLegs[0].OurRef == "" {
|
|
strippedDestination := strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(pricedLegs[0].Line.Destination, " - Proper"), " - Mobile"))
|
|
callDescription = strippedDestination
|
|
} else {
|
|
callDescription = pricedLegs[0].OurRef
|
|
}
|
|
} else {
|
|
// We capture the "To" from the leg that has no source, we capture the "From" from the leg that has both source and destination
|
|
for _, leg := range v {
|
|
if leg.Source == "" && leg.Destination != "" {
|
|
callTo = leg.To
|
|
strippedDestination := strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(leg.Destination, " - Proper"), " - Mobile"))
|
|
switch strippedDestination {
|
|
case "Netherlands - Mobile":
|
|
callDescription = "Nederland (mobiel)"
|
|
case "Netherlands - Fixed":
|
|
callDescription = "Nederland (vast)"
|
|
default:
|
|
callDescription = strippedDestination
|
|
}
|
|
} else if leg.Source != "" && leg.Destination != "" {
|
|
callFrom = leg.From
|
|
} else {
|
|
// We should not reach this
|
|
return nil, fmt.Errorf("failed to combine these legs into one call: %+v", v)
|
|
}
|
|
}
|
|
}
|
|
ret[i].From = callFrom
|
|
ret[i].To = callTo
|
|
ret[i].Account = v[0].Account
|
|
ret[i].Description = callDescription
|
|
|
|
//fmt.Printf(" ==> %+v\n\n", ret[i])
|
|
//ret[i].Legs = make([]PricedLine, len(v))
|
|
//for j, leg := range v {
|
|
//ret[i].Legs[j] = leg
|
|
//}
|
|
i++
|
|
}
|
|
|
|
//fmt.Printf("destinationPrice: %+v\n", destinationToPrice)
|
|
return ret, nil
|
|
}
|
|
|