ADR-0017 - Billing rating engine: integration in cdr-worker
- Status: Accepted (2026-05-21)
- Deciders: Massimo Bagnoli, Claude
- Implementation tasks: TASK-190
- Supersedes: nessuno
- Superseded by: nessuno
Context
The rating engine computes the billed amount for each CDR and applies the corresponding balance debit. Four implementation locations were considered:
- A separate worker consuming a
cdr.processedsubject. - The existing
cdr-worker, immediately after CDR parsing and before acknowledgement. - A periodic background job using
arq. - A PostgreSQL trigger on CDR insert.
Billing must be near real-time so low-balance thresholds and auto-suspension can react quickly. It must also be idempotent under JetStream redelivery.
Decision
Extend cdr-worker with rating and balance debit logic.
For each parsed CDR, the worker performs the following steps:
- Resolve company and destination.
- Call
get_effective_rate, introduced by TASK-186. - Compute
billed_amountusing increment seconds, minimum duration and setup fee. - Run one atomic database transaction containing CDR insert, balance update and balance movement insert.
Failed calls have billed_amount=0 and do not create a balance debit.
Rationale
- A single transaction keeps the CDR row, balance debit and balance movement consistent.
- If the worker fails before commit, the transaction rolls back and JetStream redelivers the message.
- The
cdr.call_idunique constraint prevents double billing on retries. - The design avoids an extra NATS hop and avoids races between
cdr.insertedandbalance.updatedevents. - PostgreSQL triggers would hide business logic in the database and make the rating behavior harder to test.
- Periodic
arqjobs are too delayed for real-time threshold enforcement.
Consequences
Positive
- Rating is applied with low latency.
- Billing state is updated atomically with CDR persistence.
- JetStream redelivery remains the retry mechanism for failed processing.
Negative
cdr-workergrows beyond ingestion and now owns billing behavior too.- Each CDR insert performs rating lookup, balance locking and movement insert work.
- Slow tariff lookup can directly slow the CDR consumer.
Mitigations
- The pilot target of 50 cps accepts the added database work.
- If tariff lookup becomes a bottleneck, cache
get_effective_rateresults in Redis with a 5 minute TTL as a GA optimization. - Keep the transaction boundary explicit and covered by tests in TASK-190.
Alternatives considered
Separate worker on cdr.processed
Rejected. It adds an extra NATS hop and introduces ordering and race concerns between CDR persistence and balance mutation.
Periodic arq batch
Rejected. Batch latency breaks real-time balance threshold behavior.
PostgreSQL trigger
Rejected. It couples business logic to database triggers and makes rating logic harder to test and evolve.
References
- ADR-0016: CDR pipeline implementation.
- TASK-186:
get_effective_ratehelper. - TASK-188: balance movements.
- TASK-190:
cdr-workerrating extension.