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{ Line: l, Cost: TEXT_OUTGOING_BUY, Price: TEXT_OUTGOING_SELL, }, } 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 }, 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 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 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)) pricedLegs[i] = pricedLeg callCost += thisLegCost callPrice += thisLegPrice 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 string ret[i] = Call{ Duration: callDuration, Time: v[0].Time, 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 } 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 } 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 //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 }