Telecom billing
Migrating a fixed-line billing system without losing a single CDR
Every billing migration I have seen is planned as a data problem and lands as an evidence problem. The plan says: extract, transform, load, verify, cut over. What actually decides the project is whether you can stand in a room with the finance director and show that the new system and the old one agree on a month that has already been invoiced, to a tolerance you wrote down before you looked at the first difference.
I ran billing at PeterStar, a fixed-line operator in St Petersburg, from 1993 to 2004: around 100,000 subscribers, several million CDRs a day once transit and interconnect traffic is counted, which is most of it. In 1994 and 1995 I was project manager for the migration. We came off a legacy billing system delivered from the UK by GPT and onto a new one built by Peter-Service, the St Petersburg vendor that had written the billing for Delta Telecom in 1992 and is now called Nexign. I wrote the requirements the new system was built to, ran the cutover, and then ran development on it for the nine years after that.
Two things from those two years have stayed useful ever since. The requirements document is where a migration is decided, because everything I failed to make explicit in it arrived later as an argument about whose fault it was. And we kept three years of the old system’s data readable after the cutover, which I have never regretted once.
What follows is the shape of the problem, not a vendor’s method.
A CDR is not a row
Treat a call detail record as a row in a table and you have already lost the argument, because a row can be inserted twice and no one can tell. A CDR is a claim by a switch that something happened, and its identity comes from the switch: origin plus sequence, not a database key you generate on import.
That identity is what makes duplicate suppression possible. Re-running a batch has to be a no-op, and it can only be a no-op if the record carries something the switch controls.
The switch is also the only authority on volume. Not your import log, not the count of rows you loaded. Every batch reconciles against the switch’s own counters, and a mismatch stops the pipeline instead of appearing in a report that someone reads on Monday. In our system that gate is the reason a duplicate load surfaced before invoices went out rather than afterwards, from customers.
Loss has four shapes, and only the first one is loud:
- The record was never collected. A file did not arrive, a sequence gap opened.
- The record was collected twice. Revenue goes up, which is why nobody investigates.
- The record was collected and dropped in mediation, usually by a format quirk in one switch that the new parser handles more strictly than the old one did.
- The record was collected, rated, and rated under the wrong tariff version. Nothing is missing. The money is wrong.
The fourth one is the migration killer, and no row count will ever find it.
The parallel run is the deliverable
Pick a month that is closed and invoiced. Rate it again in the new system from the same raw CDRs, and diff the result against what the customer was actually billed, at the level of invoice lines rather than totals. Totals hide compensating errors, and compensating errors are exactly what two independently built rating engines produce.
Then the part people skip. Before you look at the first diff, write down what counts as agreement: the acceptable per-line difference, the acceptable share of lines allowed to differ at all, and what you will do about interconnect, where the counterparty gets a say. A threshold you set after seeing the output is not a threshold, it is a description of the output. I learned that lesson expensively in a different domain, testing trading strategies, and wrote it up separately; it transfers without modification.
Expect the first parallel run to fail. That is the run doing its job.
The differences you will actually find
They cluster, and almost none of them are in the rating arithmetic.
Rounding point. Per call or per invoice, and if the two systems chose differently, the discrepancy is small, systematic, and impossible to attribute to any single bill. In our own system, two code paths disagreed about this along with the order of discount and VAT. Finance could measure the gap in aggregate while nobody could point at a wrong invoice, which is the worst shape a money bug takes. The fix was one canonical order of operations and one rounding point, in a single routine every report had to call. In a migration that disagreement is the default state. Two systems written by different people a decade apart will not have made the same choice, and nothing in either specification records which choice was made.
Duration treatment. Minimum billable duration, pulse or per-second, how the first interval is charged, what happens to a two-second call. Every operator has local answers here and they are rarely written down anywhere except the old code.
Day and month boundaries. Which timezone closes a billing day, and what happens to a call that starts at 23:58 and ends after midnight, under two tariffs.
Free units and thresholds. Included minutes, package allowances and discounts that depend on aggregate usage make rating stateful, and state is what you cannot import as a table.
Interconnect. Settlement with other operators is the one place where your counterparty runs their own reconciliation, and the regulator can change the model retroactively, applying it to periods you have already invoiced. That happened to me once, and it is the reason the system survived eleven years without a rating rewrite: tariffs and settlement rules were versioned with validity intervals from the start, so a retroactive change meant a new version and a recalculation, not a rewrite.
You are migrating history, not balances
The instinct is to migrate open balances and start clean. Then in November someone disputes a bill from March, and the question is not what the balance was but why the number came out that way, with which tariff, which discount, which subscriber state.
So the thing you actually have to carry across is the ability to reproduce a past invoice: raw records, the tariff version in force at the time of the call, and the subscriber’s plan assignment as it was then. If rating is a pure function of those three inputs, reproduction is a query. If the old system stored tariffs as current state and edited them in place, faithful reproduction is impossible and no import will fix it. That is a business decision, and it needs to be made out loud rather than discovered: keep the old system’s data alive read-only for the dispute window, and say how long that window is before anyone asks.
At PeterStar we kept three years. That number came from how long a bill can realistically come back at you, and three years of read-only history is cheap next to one dispute you cannot answer. The temptation is always to shorten it, because the old system costs something to keep breathing and delivers nothing visible. It delivers exactly one thing: an answer, on the day somebody needs it.
The cutover boundary belongs to the switch
A cutover time lives in the record stream rather than on a calendar. Choose a boundary the switch agrees with, define which system owns records that straddle it, and make that rule mechanical rather than a judgement call at 02:00.
One system rates at a time. Mediation can fan out to both, and during a parallel run it should, because that is how you keep comparing. Rating and invoicing stay single-owner, because two systems that both believe they are authoritative will produce two truths and a reconciliation project nobody budgeted for.
Month end is the worst possible cutover date and it is the date that will be proposed, because it looks clean on a calendar. It is the point of heaviest load and lowest tolerance for anything unusual.
What to settle before the date goes in the plan
- CDR identity: which fields make a record unique, and does the new mediation layer preserve them
- Reconciliation: which switch counters you compare against, and does a mismatch stop the pipeline or write a warning
- Agreement tolerance for the parallel run, written down, dated, and shown to finance
- Rounding point and the order of discount, tax and adjustment, defined once and shared by rating and every report
- Tariff versioning in the target system, and whether it can express a rule that was in force in a period already invoiced
- Recalculation: can you re-rate an arbitrary period on demand, and diff the result before anything reaches a customer
- Interconnect: who reconciles with counterparties during the parallel run, and what happens if the regulator changes the model mid-project
- Dispute path for pre-migration invoices, with a retention period stated in years and a system that will still be running at the end of them
Half of these are questions about the target system’s data model, and the answers do not change once you have signed. Ask them during selection, not during migration.
The measure that matters
A migration is not finished when the new system issues its first invoice. It is finished when, a year later, finance asks for a bill from before the cutover, gets it, and gets the tariff version that produced it.
The system we cut over to in 1995 ran until 2004, through hundreds of tariff plans and a regulator-mandated replacement of the interconnect settlement model, and it never needed a rating rewrite. The reason is unglamorous: the schema only ever changed by addition. The code around it got rewritten repeatedly. Whoever migrates off a system like that inherits an obligation to the data model rather than to the code, so preserve what the model guaranteed and let the new vendor’s code look however they like. If you want the longer version of how that was built, the case study is here, and the invariant checklist I apply to anything that touches money is here.
If you are planning one of these and want a second pair of eyes on the reconciliation design before the dates are fixed, get in touch.