Skip to main content

CDR Mediation Service (cdr-mediation-service) — Service Overview

Status: populated Owner: Commerce + Regulator Liaison Last updated: 2026-04-20 Companion: DOMAIN_MODEL · API_CONTRACTS · EVENT_SCHEMAS · DATA_MODEL

1. Purpose — The Regulator-Facing System of Record for Traffic

The CDR Mediation Service generates, stores, signs, and exports the canonical Charging Data Records (CDRs) for every billable SMS event traversing the Ghasi-SMS-Gateway national backbone. Although the platform separately runs billing-service for tenant invoicing, CDRs are an independent, regulator-grade artifact: they are the lawful record of who sent what to whom, when, through which operator, and at what tariff. ATRA (Afghanistan Telecom Regulatory Authority), MNO settlement teams, and external auditors consume CDRs — not billing events.

Two design tenets shape the service:

  1. CDRs are immutable, hash-chained, and append-only. Once an hour-bucket of CDRs is sealed, its SHA-256 root is computed, recorded in the chain table, and the prior bucket's root is folded into the new one. Corrections never mutate originals — they are issued as adjustment records that reference the original cdrId.
  2. CDRs and billing events are intentionally distinct. A billing event is "platform charged tenant X for message Y at price Z." A CDR is "operator-recordable telephony event with calling/called party, timestamps, segment count, sender ID, recording entity, and tariff class." They share input data (DLR + routing + pricing) but diverge in schema, retention, and consumer.

The service implements four capabilities:

CapabilityDescription
Canonical CDR generationSubscribe to sms.dlr.inbound, project DLR + message metadata into a canonical CDR row, append to the hash-chained store
TAP 3.12 / RAP exportsDaily roll-up jobs translate canonical CDRs into TAP 3.12 batches (and RAP correction batches) per ATRA-prescribed schema variants
Signed file dropsDaily TAP/RAP files are signed (Ed25519 over SHA-256) and pushed to ATRA SFTP / API endpoints with delivery receipts and replay protection
Adjustment recordsCorrections, voids, and tariff re-rates are issued as new immutable adjustment rows, never as in-place edits to original CDRs

2. Bounded Context

DimensionValue
DomainCommerce / Regulatory Settlement
Owner squadCommerce Engineering + Regulator Liaison
Deployment unitKubernetes Deploymentcdr-mediation-service (3 replicas; one elected leader for export jobs)
Communication styleInbound: NATS JetStream (sms.dlr.inbound, sms.events.status) · gRPC (admin/query) · HTTP REST (regulator-portal-service callable)
StoragePostgreSQL schema cdr (hot, last 90 days) · Object storage s3://ghasi-cdr-archive/ (cold, ≥ 10 years, append-only)
Failure modeFail-loud, never lose — CDR write must be durable before NATS ACK; export jobs retry indefinitely; missing hours raise SEV1

3. Position in the Platform


4. Responsibilities

#Responsibility
R1Consume sms.dlr.inbound (final-state DLRs) and sms.events.status (terminal status transitions) and project each into exactly one canonical CDR row
R2Persist CDRs to PostgreSQL cdr.cdr_records partitioned hourly with monotonically increasing cdrSequence per partition
R3Mirror every sealed hour-bucket to object storage at s3://ghasi-cdr-archive/v1/year=YYYY/month=MM/day=DD/hour=HH/<sequence>.cdr.json.zst
R4Maintain the SHA-256 hash chain in cdr.cdr_chain such that chainHash[N] = sha256(chainHash[N-1] ‖ bucketRoot[N]), allowing tamper-evident verification
R5Run hourly roll-up job that closes the previous hour's bucket, computes its Merkle root, writes the chain entry, and emits cdr.bucket.sealed
R6Run daily TAP 3.12 encoder job emitting one TAP file per (operatorId, settlement-day) tuple per regulator schema variant declared in config
R7Run daily RAP encoder job for any adjustment records issued since the last RAP cut-off
R8Sign every TAP and RAP file with Ed25519 keys held in the HSM (key id cdr-export-signer-v1); embed Signature: <base64> and KeyId: headers per ATRA spec
R9Push signed files to ATRA SFTP (primary, port 2222) or ATRA REST API (POST /atra/v1/cdr/upload, mTLS) and record delivery receipts
R10Issue adjustment records (`adjustmentType: CORRECTION
R11Serve regulator-portal-service queries over gRPC: GetCDRById, ListCDRsByMSISDN, VerifyChain(bucketHour)
R12Emit lifecycle events: cdr.record.appended, cdr.bucket.sealed, cdr.export.tap.delivered, cdr.export.rap.delivered, cdr.adjustment.issued

5. Non-Responsibilities

  • Does not invoice tenants — billing-service owns customer billing
  • Does not compute tenant pricing — pulls a pricing snapshot from billing-service at CDR-creation time
  • Does not route or transmit messages — strictly downstream of the dispatch pipeline
  • Does not house the regulator portal UI — regulator-portal-service consumes CDR APIs
  • Does not decide retention class — retention is set by data classification policy in docs/security/data-classification.md (CDRs = Class IV, ≥ 10 years)

6. Upstream / Downstream Dependencies

DirectionServiceProtocolPurpose
Inbound eventdlr-processorNATS JetStream sms.dlr.inboundFinal delivery state with operator timestamps
Inbound eventsms-orchestratorNATS JetStream sms.events.statusTerminal status (DELIVERED, FAILED, EXPIRED)
Inbound (sync)billing-servicegRPC (mTLS)GetPricingSnapshot(messageId) for tariff class + customerPrice + operatorCost at charge time
Inbound (sync)operator-management-servicegRPC (mTLS)GetRecordingEntity(operatorId) — returns the TAP recordingEntity (3-digit MCC + 2-digit MNC + sequence) per operator
Inbound (sync)regulator-portal-servicegRPC (mTLS)CDR query, chain verification, file-drop status
Outbound writePostgreSQL cdr schemaTCPHot CDRs, chain table, export jobs, delivery receipts
Outbound writeObject storage (S3-compatible, regional)HTTPSCold archive, append-only bucket policy + Object Lock (Compliance mode)
OutboundHSM (PKCS#11 / KMIP)TLSEd25519 signing of TAP/RAP files
OutboundATRA SFTP (primary)SSH/SFTP port 2222Daily signed file drop
OutboundATRA REST (fallback)HTTPS + mTLSAPI-mode upload when SFTP unreachable
Outbound eventsNATS JetStreamTCPLifecycle events to regulator-portal-service, analytics-service

7. Canonical CDR Schema (extract)

The canonical row mirrors TAP 3.12 fields without TAP encoding so downstream encoders are pure transforms.

FieldTypeNotes
cdrIdUUIDv7Identity
cdrSequencebigintMonotonic per (yearMonthDayHour) partition
tenantIdUUIDv4Submitter
messageIdUUIDv4Source message reference
callingPartyAddressE.164 / alpha-idSender ID (alphanumeric or MSISDN)
calledPartyAddressE.164Recipient MSISDN
originalServedSubscriberIdE.164For MNP cases — original IMSI/MSISDN before port
recordingEntitystring(15)TAP 3.12 recording entity (operator code)
serviceCenterAddressE.164SMSC identifier
messageReferencestring(20)Operator-side message reference
eventTimeStampUTC ts(ms)DLR final-state timestamp
localTimeStamplocal ts(ms)Asia/Kabul localised
chargeAmountdecimal(18,6)From billing snapshot
chargeTypeenumMO, MT, BROADCAST, INTERNATIONAL_MT
tapTariffClassstring(4)ATRA tariff class code
segmentCountsmallintPer GSM 03.40
bucketHourtimestamptzTruncated to hour, partition key
chainHashPrevbytea(32)Previous CDR's row hash for forward-link
rowHashbytea(32)sha256 of canonical-form JSON
adjustmentOfUUIDv7?Set on adjustment records
adjustmentTypeenum?CORRECTION, VOID, RE_RATE

8. Hash Chain & Bucket Sealing

hour H-1 closes → enumerate rows ordered by cdrSequence
→ compute Merkle root of rowHashes (binary tree, sha256)
→ bucketRoot[H-1] = merkle_root
→ chainHash[H-1] = sha256(chainHash[H-2] ‖ bucketRoot[H-1])
→ INSERT cdr.cdr_chain (bucketHour, bucketRoot, chainHash, sealedAt, signerKeyId)
→ write s3://ghasi-cdr-archive/.../hour=H-1/_MANIFEST.json
→ publish cdr.bucket.sealed{bucketHour, chainHash, count}

A second-tier daily anchor is published to a public transparency log (Trillian-style) on day close so external auditors can verify retroactive integrity without trusting Ghasi infra.


9. TAP 3.12 / RAP Export Pipeline

File naming convention (per ATRA spec, configurable):

  • TAP: TAP_{recordingEntity}_{senderRoamingPartner}_{fileSequence}.{YYYYMMDD}.tap
  • RAP: RAP_{recordingEntity}_{senderRoamingPartner}_{fileSequence}.{YYYYMMDD}.rap
  • Signature sidecar: <filename>.sig containing KeyId:, Algorithm: Ed25519, Signature: <base64>

10. Adjustment Record Semantics

OperationBehaviour
CORRECTIONIssued when a CDR field needs amending (e.g. wrong tariff class). New row with same messageId, adjustmentOf=originalCdrId, full corrected payload. Original row is never updated. Net effect computed by consumers as latest-by-messageId.
VOIDIssued when a CDR was incorrectly produced (duplicate DLR projection, fraud reversal). New row with adjustmentType=VOID, zero chargeAmount, voidReason populated.
RE_RATEIssued when retroactive pricing change applies. New row carries the new chargeAmount and tapTariffClass. RAP file batches these for regulator reconciliation.

All adjustments are themselves chained — a correction-of-a-correction is permitted and traceable.


11. Key Design Decisions

DecisionRationale
CDRs separated from billing eventsDifferent schema, retention class, consumer set, and audit obligations. Coupling them would force lowest-common-denominator schema and make ATRA changes leak into invoice code.
Hash-chained + Merkle bucket sealingTamper-evidence at row, hour, and day granularity. ATRA can request VerifyChain(2026-04-20T13:00Z) and receive cryptographic proof.
Object storage with S3 Object Lock (Compliance mode)Append-only at the storage layer — even root credentials cannot delete sealed buckets within the retention window.
Fail-loud on missing hourBetter to page on-call at 03:05 for a missing 02:00 bucket than to ship a TAP file with silent gaps.
Ed25519 over RSA for export signaturesFaster, smaller signatures, and aligns with the platform PKI defined in ADR-0004.
HSM-resident signing keyCompromise of the cdr-mediation-service host must not expose the signing key. Key never leaves HSM.
Leader-election for export jobsDaily file drop must run exactly once per (operator, day). Replicas race for cdr.export.leader advisory lock; loser replicas only do CDR ingestion.
Pricing snapshot at CDR-creation, not at exportTariff changes mid-day must not retro-rewrite morning CDRs; instead, RAP batches retro re-rates as adjustment records.
Adjustments by append, never by mutationRequired by ETSI / TAP rules and by the ADR-0004 immutability principle.
Daily transparency-log anchorExternal cryptographic anchoring lets any auditor verify chain integrity without trusting Ghasi-operated infrastructure.

12. Operational SLOs

SLOTarget
CDR row append latency from DLR receiptP95 ≤ 200 ms
Hour-bucket seal completionwithin 5 minutes of hour-close
Daily TAP file generationcomplete by 02:00 Asia/Kabul for prior calendar day
ATRA SFTP delivery success rate≥ 99.9 % monthly
Chain verification APIP95 ≤ 1.5 s for any 24h range
Zero data loss objectiveRPO 0 (synchronous PG commit before NATS ACK); RTO 15 min

13. Cross-References


End of SERVICE_OVERVIEW.md