From d1186d2b870649831c82255e425bec0d165fd0d0 Mon Sep 17 00:00:00 2001 From: Gerdriaan Mulder Date: Sun, 26 Jan 2020 19:04:24 +0100 Subject: [PATCH] Implement CDR lines to Call (WIP) --- cmd/cdrtool/main.go | 46 +++++++-------- combine.go | 136 ++++++++++++++++++++++++++++++++++++++++---- record.go | 35 ++++++++++++ 3 files changed, 183 insertions(+), 34 deletions(-) diff --git a/cmd/cdrtool/main.go b/cmd/cdrtool/main.go index d78bb82..3c3055e 100644 --- a/cmd/cdrtool/main.go +++ b/cmd/cdrtool/main.go @@ -10,10 +10,11 @@ import ( ) var ( - csvFile = flag.String("csv", "", "use this call detail record CSV file") - pricesFile = flag.String("prices", "", "use this pricing CSV file") - priceExec = flag.Bool("execute", false, "price each CDR according to the prices file") - debug = flag.Bool("debug", false, "show extra debugging") + csvFile = flag.String("csv", "", "use this call detail record CSV file") + pricesFile = flag.String("prices", "", "use this pricing CSV file") + priceExec = flag.Bool("execute", false, "price each CDR according to the prices file") + debugImport = flag.Bool("debugImport", false, "show extra debugging info for import") + debug = flag.Bool("debug", false, "show extra debugging info") ) func usage() { @@ -48,7 +49,7 @@ func main() { if err != nil && *priceExec { log.Fatalf("importing prices CSV %q failed: %v", *pricesFile, err) } - if *debug { + if *debugImport { if err != nil { log.Printf("[warning] importing prices CSV %q failed: %v", *pricesFile, err) } else { @@ -56,7 +57,7 @@ func main() { } } - if *debug { + if *debugImport { for _, i := range imported { fmt.Printf("l: %+v\n", i) } @@ -72,30 +73,27 @@ func main() { log.Fatalf("combining text CDRs with prices failed: %v", err) } - /* - data, err := cdr.CombineDataCDRs(imported, importedPrices) - if err != nil { - log.Fatalf("combining data CDRs with prices failed: %v", err) - } + data, err := cdr.CombineDataCDRs(imported, importedPrices) + if err != nil { + log.Fatalf("combining data CDRs with prices failed: %v", err) + } - calls, err := cdr.CombineCallCDRs(imported, importedPrices) - if err != nil { - log.Fatalf("combining call CDRs with prices failed: %v", err) - } - */ + calls, err := cdr.CombineCallCDRs(imported, importedPrices) + if err != nil { + log.Fatalf("combining call CDRs with prices failed: %v", err) + } if *debug { for _, t := range texts { fmt.Printf("t: %+v\n", t) } - /* - for _, d := range data { - fmt.Printf("d: %+v\n", d) - } - for _, c := range calls { - fmt.Printf("c: %+v\n", c) - } - */ + for _, d := range data { + fmt.Printf("d: %+v\n", d) + } + + for _, c := range calls { + fmt.Printf("c: %+v\n", c) + } } } } diff --git a/combine.go b/combine.go index a37e95a..4ff1043 100644 --- a/combine.go +++ b/combine.go @@ -2,14 +2,21 @@ package cdr import ( "fmt" + "strings" ) 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++ } @@ -17,7 +24,7 @@ func CombineTextCDRs(cdrLines []Line, prices []Price) ([]Text, error) { ret := make([]Text, numTextCDRs) i := 0 for _, l := range cdrLines { - if l.Kind != TextLine { + if l.Kind != TextLine || l.Reason != ORIG { continue } ret[i] = Text{ @@ -30,41 +37,150 @@ func CombineTextCDRs(cdrLines []Line, prices []Price) ([]Text, error) { 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) - 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) { - // Preprocess the price list into a map of "destination" -> Price, filter out the Text destinations, and count how many Text CDRs we have - destinationToPriceLine := make(map[string]Price) - numCallCDRs := 0 + // 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 + for _, l := range cdrLines { if l.Kind != VoiceLine { continue } - if _, ok := destinationToPriceLine[l.Destination]; !ok { + // Cache destination prices + if _, ok := destinationToPrice[l.Destination]; !ok { + priceLoop: for _, p := range prices { if p.Type != PriceCall { - continue + continue priceLoop } if p.Destination == l.Destination { - destinationToPriceLine[l.Destination] = p - break + destinationToPrice[l.Destination] = p + break priceLoop } } } - numCallCDRs++ } + + // 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: + } + } + + fmt.Printf("destinationPrice: %+v\n", destinationToPrice) + */ return nil, fmt.Errorf("not yet implemented") } diff --git a/record.go b/record.go index f68b611..0d275d6 100644 --- a/record.go +++ b/record.go @@ -9,6 +9,8 @@ type ( LineKind int // ReasonKind is metadata about a certain Line ReasonKind int + // CallKind defines if the call came through a user's PBX + CallKind int ) const ( @@ -48,6 +50,27 @@ const ( 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 var ReasonKindMap = map[string]ReasonKind{ "ORIG": ORIG, @@ -59,6 +82,17 @@ var ReasonKindMap = map[string]ReasonKind{ "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 type Line struct { Id string @@ -97,6 +131,7 @@ type Call struct { Duration time.Duration Cost int Price int + Kind CallKind Legs []PricedLine }