Browse Source

Implement CDR lines to Call (WIP)

master
Gerdriaan Mulder 6 years ago
parent
commit
d1186d2b87
  1. 12
      cmd/cdrtool/main.go
  2. 136
      combine.go
  3. 35
      record.go

12
cmd/cdrtool/main.go

@ -13,7 +13,8 @@ var (
csvFile = flag.String("csv", "", "use this call detail record CSV file") csvFile = flag.String("csv", "", "use this call detail record CSV file")
pricesFile = flag.String("prices", "", "use this pricing CSV file") pricesFile = flag.String("prices", "", "use this pricing CSV file")
priceExec = flag.Bool("execute", false, "price each CDR according to the prices file") priceExec = flag.Bool("execute", false, "price each CDR according to the prices file")
debug = flag.Bool("debug", false, "show extra debugging") debugImport = flag.Bool("debugImport", false, "show extra debugging info for import")
debug = flag.Bool("debug", false, "show extra debugging info")
) )
func usage() { func usage() {
@ -48,7 +49,7 @@ func main() {
if err != nil && *priceExec { if err != nil && *priceExec {
log.Fatalf("importing prices CSV %q failed: %v", *pricesFile, err) log.Fatalf("importing prices CSV %q failed: %v", *pricesFile, err)
} }
if *debug { if *debugImport {
if err != nil { if err != nil {
log.Printf("[warning] importing prices CSV %q failed: %v", *pricesFile, err) log.Printf("[warning] importing prices CSV %q failed: %v", *pricesFile, err)
} else { } else {
@ -56,7 +57,7 @@ func main() {
} }
} }
if *debug { if *debugImport {
for _, i := range imported { for _, i := range imported {
fmt.Printf("l: %+v\n", i) fmt.Printf("l: %+v\n", i)
} }
@ -72,7 +73,6 @@ func main() {
log.Fatalf("combining text CDRs with prices failed: %v", err) log.Fatalf("combining text CDRs with prices failed: %v", err)
} }
/*
data, err := cdr.CombineDataCDRs(imported, importedPrices) data, err := cdr.CombineDataCDRs(imported, importedPrices)
if err != nil { if err != nil {
log.Fatalf("combining data CDRs with prices failed: %v", err) log.Fatalf("combining data CDRs with prices failed: %v", err)
@ -82,20 +82,18 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("combining call CDRs with prices failed: %v", err) log.Fatalf("combining call CDRs with prices failed: %v", err)
} }
*/
if *debug { if *debug {
for _, t := range texts { for _, t := range texts {
fmt.Printf("t: %+v\n", t) fmt.Printf("t: %+v\n", t)
} }
/*
for _, d := range data { for _, d := range data {
fmt.Printf("d: %+v\n", d) fmt.Printf("d: %+v\n", d)
} }
for _, c := range calls { for _, c := range calls {
fmt.Printf("c: %+v\n", c) fmt.Printf("c: %+v\n", c)
} }
*/
} }
} }
} }

136
combine.go

@ -2,14 +2,21 @@ package cdr
import ( import (
"fmt" "fmt"
"strings"
) )
func CombineTextCDRs(cdrLines []Line, prices []Price) ([]Text, error) { func CombineTextCDRs(cdrLines []Line, prices []Price) ([]Text, error) {
var err []string
numTextCDRs := 0 numTextCDRs := 0
for _, l := range cdrLines { for _, l := range cdrLines {
if l.Kind != TextLine { if l.Kind != TextLine {
continue continue
} }
if l.Reason != ORIG {
err = append(err, fmt.Sprintf("text CDR did not match reason ORIG: %+v", l))
continue
}
numTextCDRs++ numTextCDRs++
} }
@ -17,7 +24,7 @@ func CombineTextCDRs(cdrLines []Line, prices []Price) ([]Text, error) {
ret := make([]Text, numTextCDRs) ret := make([]Text, numTextCDRs)
i := 0 i := 0
for _, l := range cdrLines { for _, l := range cdrLines {
if l.Kind != TextLine { if l.Kind != TextLine || l.Reason != ORIG {
continue continue
} }
ret[i] = Text{ ret[i] = Text{
@ -30,41 +37,150 @@ func CombineTextCDRs(cdrLines []Line, prices []Price) ([]Text, error) {
i++ i++
} }
if len(err) > 0 {
return nil, fmt.Errorf("error pricing text CDRs: %v", strings.Join(err, ", "))
}
return ret, nil return ret, nil
} }
func CombineDataCDRs(cdrLines []Line, prices []Price) ([]Data, error) { func CombineDataCDRs(cdrLines []Line, prices []Price) ([]Data, error) {
var err []string
numDataCDRs := 0 numDataCDRs := 0
errSet := false
for _, l := range cdrLines { for _, l := range cdrLines {
errSet = false
if l.Kind != DataLine { if l.Kind != DataLine {
continue 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++ numDataCDRs++
} }
ret := make([]Data, numDataCDRs) ret := make([]Data, numDataCDRs)
return ret, fmt.Errorf("not yet implemented") 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
},
Bytes: 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) { func CombineCallCDRs(cdrLines []Line, prices []Price) ([]Call, error) {
// Preprocess the price list into a map of "destination" -> Price, filter out the Text destinations, and count how many Text CDRs we have // Preprocess the price list into a map of "destination" -> Price, filter out the Voice destinations
destinationToPriceLine := make(map[string]Price) destinationToPrice := make(map[string]Price)
numCallCDRs := 0 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
for _, l := range cdrLines { for _, l := range cdrLines {
if l.Kind != VoiceLine { if l.Kind != VoiceLine {
continue continue
} }
if _, ok := destinationToPriceLine[l.Destination]; !ok { // Cache destination prices
if _, ok := destinationToPrice[l.Destination]; !ok {
priceLoop:
for _, p := range prices { for _, p := range prices {
if p.Type != PriceCall { if p.Type != PriceCall {
continue continue priceLoop
} }
if p.Destination == l.Destination { if p.Destination == l.Destination {
destinationToPriceLine[l.Destination] = p destinationToPrice[l.Destination] = p
break break priceLoop
}
}
}
}
// a map from "%s_%s_%s": l.Time.Format(DateTimeFormat), l.CLI, l.From
uniqueCalls := make(map[string][]Line)
for _, l := range cdrLines {
if l.Kind != VoiceLine {
continue
}
idx := fmt.Sprintf("%s_%s_%s", l.Time.Format(DateTimeFormat), l.CLI, l.From)
if _, ok := uniqueCalls[idx]; !ok {
uniqueCalls[idx] = make([]Line, 0)
}
uniqueCalls[idx] = append(uniqueCalls[idx], l)
}
for k, 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()
} }
/*
ret := make([]Call, 0)
for _, l := range cdrLines {
if l.Kind != VoiceLine {
continue
}
switch {
// HS -> PBX (non-roaming)
case l.CLI == l.From && l.Source == CALL_FROM_HANDSET_SOURCE && l.Destination == CALL_FROM_HANDSET_DESTINATION:
// PBX -> HS (roaming+non-roaming)
case l.From == l.To && l.Source == CALL_TO_HANDSET_SOURCE && l.Destination == CALL_TO_HANDSET_DESTINATION:
// HS -> Regular (non-roaming)
case l.CLI == l.From && l.Source == CALL_FROM_HANDSET_SOURCE:
// HS -> Regular (roaming)
case l.CLI == l.From && l.Source != CALL_FROM_HANDSET_SOURCE && l.Reason == ORIG:
// Regular -> HS (roaming)
case l.CLI != l.From && l.Source == CALL_TO_HANDSET_SOURCE && l.Reason == ROAM:
// Regular -> HS (non-roaming) does not exist in the CSV
default:
} }
numCallCDRs++
} }
fmt.Printf("destinationPrice: %+v\n", destinationToPrice)
*/
return nil, fmt.Errorf("not yet implemented") return nil, fmt.Errorf("not yet implemented")
} }

35
record.go

@ -9,6 +9,8 @@ type (
LineKind int LineKind int
// ReasonKind is metadata about a certain Line // ReasonKind is metadata about a certain Line
ReasonKind int ReasonKind int
// CallKind defines if the call came through a user's PBX
CallKind int
) )
const ( const (
@ -48,6 +50,27 @@ const (
PBXOR PBXOR
) )
func (r ReasonKind) String() string {
switch r {
case ORIG:
return "ORIG"
case CFIM:
return "CFIM"
case CFNA:
return "CFNA"
case CFBS:
return "CFBS"
case CFOR:
return "CFOR"
case ROAM:
return "ROAM"
case PBXOR:
return "PBXOR"
default:
return "UNKN"
}
}
// ReasonKindMap translates string from CSV to our typed ReasonKind // ReasonKindMap translates string from CSV to our typed ReasonKind
var ReasonKindMap = map[string]ReasonKind{ var ReasonKindMap = map[string]ReasonKind{
"ORIG": ORIG, "ORIG": ORIG,
@ -59,6 +82,17 @@ var ReasonKindMap = map[string]ReasonKind{
"PBXOR": PBXOR, "PBXOR": PBXOR,
} }
const (
UnknownCall CallKind = iota
Regular
PBX
)
var CallKindMap = map[string]CallKind{
"Regular": Regular,
"PBX": PBX,
}
// Line contains the original metadata of a (call) detail record // Line contains the original metadata of a (call) detail record
type Line struct { type Line struct {
Id string Id string
@ -97,6 +131,7 @@ type Call struct {
Duration time.Duration Duration time.Duration
Cost int Cost int
Price int Price int
Kind CallKind
Legs []PricedLine Legs []PricedLine
} }

Loading…
Cancel
Save