Skip to main content

CDR Mediation Service — Jira-Ready Epics & User Stories

Status: populated Owner: Commerce + Regulator Liaison Last updated: 2026-04-20 Service prefix: CDR Scope: Canonical CDR generation, hash-chained append-only storage, TAP 3.12 / RAP regulator exports, signed file drops to ATRA, and adjustment records (corrections, voids, re-rates) without mutating originals.


Epic Summary

Epic IDTitleStoriesPoints
EP-CDR-01Canonical CDR Schema, Hourly Roll-up, Hash-Chained Append-Only StorageUS-CDR-001 – US-CDR-00639
EP-CDR-02TAP 3.12 / RAP Export Pipelines (per regulator schema)US-CDR-007 – US-CDR-01134
EP-CDR-03Daily Signed-File Drop to ATRA SFTP/APIUS-CDR-012 – US-CDR-01524
EP-CDR-04Adjustment Records (corrections, voids) without mutating originalsUS-CDR-016 – US-CDR-01818

Totals: 4 epics · 18 stories · 115 points


EP-CDR-01 · Canonical CDR Schema, Hourly Roll-up, Hash-Chained Append-Only Storage

Context: Foundation epic. Every billable event becomes a canonical CDR; hourly buckets are sealed with Merkle roots; chain hashes link buckets forward; sealed buckets are mirrored to S3 Object-Lock storage; daily anchors are submitted to a transparency log.

US-CDR-001 · Canonical CDR Projection from DLR

Type: Feature | Points: 8

Description: As the CDR mediation pipeline, I need to project every final-state DLR received on sms.dlr.inbound into a canonical CDR row so that every billable telephony event has a regulator-grade record.

Acceptance Criteria:

  • Final-state DLR (DELIVERED|FAILED|EXPIRED) on sms.dlr.inbound produces exactly one CDR row within P95 ≤ 200 ms
  • Canonical fields populated: callingPartyAddress, calledPartyAddress, recordingEntity, serviceCenterAddress, messageReference, eventTimeStamp, localTimeStamp (Asia/Kabul), segmentCount, bucketHour
  • NATS redelivery for the same messageId is rejected by the unique index on (messageId, adjustmentOf NULL)
  • Missing pricing → CDR written with chargeAmount=NULL and follow-up cdr.record.repriced within 24h once pricing arrives
  • cdr.record.appended published within 1 s with {cdrId, messageId, bucketHour, rowHash}
  • Zero loss on consumer restart (JetStream AckExplicit + PG transaction)

US-CDR-002 · Per-Row Hash Computation

Type: Feature | Points: 5

Description: As the CDR mediation pipeline, I need a deterministic SHA-256 row hash for every CDR forward-linked to the previous row so that any tampering invalidates the chain.

Acceptance Criteria:

  • rowHash = sha256(canonicalJson(row)) per RFC 8785 JCS
  • chainHashPrev = previous row's rowHash within the same partition
  • First row of a partition: chainHashPrev = bucketRoot[H-1]
  • Mutating a stored row (test-only) makes verifyRow(cdrId) return MISMATCH
  • POST /v1/cdr/verify returns {rowHash, chainHashPrev, computedHash, status: VALID|MISMATCH}
  • decimal(18,6) fields rendered with fixed-point precision in canonical JSON

US-CDR-003 · Hourly Bucket Seal & Merkle Root

Type: Feature | Points: 8

Description: As the CDR scheduler, I need to close each hour's bucket and compute its Merkle root within 5 minutes of hour-close so that a tamper-evident seal exists for every operating hour.

Acceptance Criteria:

  • Leader replica triggers seal job at HH:00 for HH-1; completes within 5 minutes (P95)
  • bucketRoot = SHA-256 Merkle root of ordered rowHash list (binary tree, padded with zero leaves)
  • chainHash[HH-1] = sha256(chainHash[HH-2] ‖ bucketRoot[HH-1]) INSERTed into cdr.cdr_chain
  • Empty hours record sentinel bucketRoot = sha256("EMPTY:" + bucketHour)
  • cdr.bucket.sealed{bucketHour, chainHash, recordCount, sealedAt} published
  • SEV1 cdr_bucket_seal_overdue if not sealed by HH+10m
  • Leader-only via cdr.export.leader advisory lock

US-CDR-004 · Append-Only Object Storage Mirror

Type: Feature | Points: 8

Description: As a long-term audit consumer, I need every sealed bucket mirrored to object storage with S3 Object Lock in Compliance mode so that even root credentials cannot delete sealed CDRs within the 10-year retention window.

Acceptance Criteria:

  • Files written to s3://ghasi-cdr-archive/v1/year=YYYY/month=MM/day=DD/hour=HH/<sequence>.cdr.json.zst (1000 rows/file)
  • x-amz-object-lock-mode: COMPLIANCE, retain-until = sealedAt + 10y
  • _MANIFEST.json per hour-bucket: {bucketHour, recordCount, bucketRoot, chainHash, files:[{name,sha256,recordCount}], signerKeyId}
  • Mirror failure: exponential backoff 24h, then SEV1
  • Delete attempt on locked object → AccessDenied + audit log entry
  • Auditor verifies chain integrity from manifest + public anchors alone

US-CDR-005 · Daily Transparency Log Anchor

Type: Feature | Points: 5

Description: As an external auditor, I need each day's terminal chainHash published to a Trillian-style transparency log so that I can verify chain integrity without trusting Ghasi-operated infrastructure.

Acceptance Criteria:

  • Day-rollover (00:00 Asia/Kabul): chainHash for hour 23 of prior day submitted within 30 minutes
  • Signed Log Entry stored in cdr.transparency_anchors {day, chainHash, sleSignature, leafIndex, logRootHash}
  • Wrong chain hash returned by log → SEV1 + retry with fresh inclusion proof
  • GET /v1/cdr/transparency/{day} returns SLE + inclusion proof
  • Nightly 7-day chain integrity check job walks all chain hashes vs row hashes

US-CDR-006 · Chain Verification API

Type: Feature | Points: 5

Description: As a regulator-portal user, I need to call POST /v1/cdr/chain/verify with a bucketHour and receive cryptographic proof of integrity so that I can certify to ATRA that no CDR was lost, mutated, or backdated.

Acceptance Criteria:

  • Returns {bucketRoot, chainHash, prevChainHash, recordCount, sealedAt, signerKeyId, verified: true} within P95 ≤ 1.5 s
  • Unsealed bucket → 409 BUCKET_NOT_SEALED
  • Recomputed Merkle root mismatch → verified: false + SEV1
  • proofForCdrId parameter returns Merkle inclusion proof
  • Missing cdr:read scope → 403

EP-CDR-02 · TAP 3.12 / RAP Export Pipelines

Context: Daily encoder jobs convert canonical CDRs into ATRA-mandated TAP 3.12 (settlement) and RAP (corrections) files. Schema variants are configurable to absorb regulator schema drift without code change.

US-CDR-007 · TAP 3.12 Daily Encoder Job

Type: Feature | Points: 13

Description: As the regulator export pipeline, I need to encode all sealed CDRs from the prior calendar day into ATRA-compliant TAP 3.12 files per operator so that ATRA receives the lawful settlement record by the 02:00 Asia/Kabul deadline.

Acceptance Criteria:

  • Job completes file generation for all (operatorId, settlementDay=D-1) tuples by 02:00 Asia/Kabul
  • Zero-record operator-day produces a NIL-filler TAP file (absence is auditable)
  • Output: BER-encoded ASN.1 TAP 3.12 with mandatory fields recordingEntity, originalSubscriberId, callingPartyAddress, calledPartyAddress, serviceCenterAddress, messageReference, eventTimeStamp, chargeInformation, totalCallEventDuration=0
  • Schema variant configurable (atra-tap-v1, atra-tap-v2, tap-3-12-standard)
  • Single-record encode failure → quarantine table; job continues
  • SHA-256 stored alongside file path in cdr.export_files

US-CDR-008 · RAP Adjustment File Encoder

Type: Feature | Points: 8

Description: As the regulator export pipeline, I need to encode all adjustment records issued since the last RAP cut-off into a daily RAP file so that corrections, voids, and re-rates are reconciled with ATRA.

Acceptance Criteria:

  • Adjustment rows with rapBatchedAt IS NULL batched at 02:30 Asia/Kabul into RAP file per (operatorId, settlementDay)
  • RAP 1.5 conformance with returnDetail linking each adjustment to the original messageReference
  • VOID: zero chargeInformation + configured void code in returnReason
  • RE_RATE: new chargeInformation + original referenced
  • All included adjustments updated atomically with rapBatchedAt = now() and rapFileId
  • No adjustments → no RAP file (logged)

US-CDR-009 · Configurable Regulator Schema Variants

Type: Feature | Points: 5

Description: As a platform operator, I need to switch between ATRA TAP schema variants via configuration without redeploying code so that regulator schema changes can be rolled out within hours.

Acceptance Criteria:

  • cdr.export.tap.schemaVariant config key supports atra-tap-v1, atra-tap-v2, tap-3-12-standard
  • ASN.1 modules load from config/tap-schemas/ at runtime
  • Variant change emits cdr.config.changed audit event with old/new + operator identity
  • Variant configured but file missing → service refuses to start
  • Variant pinned at file-generation time, not CDR-creation time

US-CDR-010 · TAP File Sequence Numbering

Type: Feature | Points: 3

Description: As the ATRA file-receipt system, I need each TAP file from a given recording entity to carry a strictly monotonic fileSequenceNumber so that I can detect missing files and reject out-of-order submissions.

Acceptance Criteria:

  • fileSequenceNumber = previous + 1 per recordingEntity
  • Counter stored in cdr.tap_sequence (recordingEntity, lastSequence), updated atomically with file generation
  • Generation failure → sequence increment rolled back via transaction
  • Filename TAP_{recordingEntity}_{senderRoamingPartner}_{seq:08d}.{YYYYMMDD}.tap
  • GET /v1/cdr/exports?recordingEntity=... lists in sequence order, gaps flagged

US-CDR-011 · Quarantine and Resubmission for Encoder Failures

Type: Feature | Points: 5

Description: As the on-call engineer, I need records that fail TAP encoding to land in a quarantine table that I can fix and resubmit so that a single bad record cannot block an entire daily export.

Acceptance Criteria:

  • Encoder failure → row moved to cdr.export_quarantine {cdrId, encoderError, encoderVariant, quarantinedAt}
  • Daily TAP file still generated for the rest with quarantined: N reported
  • POST /v1/cdr/quarantine/{id}/resubmit re-encodes; success → included in next-day supplement
  • >100 quarantined in 24h → SEV2 cdr_quarantine_high
  • cdr.quarantine.resubmitted event emitted

EP-CDR-03 · Daily Signed-File Drop to ATRA SFTP/API

Context: Generated TAP/RAP files are signed Ed25519 (HSM-resident key), atomically uploaded to ATRA SFTP, with REST API fallback when SFTP is down. Delivery receipts are tracked end-to-end.

US-CDR-012 · Ed25519 Signing of Export Files

Type: Feature | Points: 8

Description: As the ATRA file-receipt system, I need every TAP and RAP file signed with an Ed25519 signature whose public key I have on file so that I can verify the file originated from Ghasi and was not tampered with.

Acceptance Criteria:

  • Signing key id cdr-export-signer-v1 used via PKCS#11 against the HSM
  • Sidecar <filename>.sig written: KeyId:, Algorithm: Ed25519, Signature:, FileSha256:
  • Key rotation: both old + new keys valid for verification 30 days; active key id recorded per file
  • HSM unreachable → exponential backoff 1h, then SEV1
  • Code-side audit confirms no key material logged or written to disk
  • Post-upload ATRA verification status captured in cdr.export_deliveries.atraVerificationStatus

US-CDR-013 · SFTP File Drop with Atomic Rename

Type: Feature | Points: 5

Description: As the ATRA SFTP drop zone, I need files to arrive atomically (uploaded to .tmp then renamed) so that downstream batch jobs do not pick up half-written files.

Acceptance Criteria:

  • Upload writes <filename>.tmp, verifies size + SHA-256 via STAT, then RENAME to final
  • SFTP host fingerprint mismatch → connection refused + SEV1
  • SFTP key rotation: 7-day overlap, both keys tried in order
  • Permission error → fail fast + page on-call (no retry)
  • Signature sidecar uploaded immediately after with same atomic pattern
  • Existing (recordingEntity, fileSequenceNumber) on SFTP → DUPLICATE_FILE abort + page on-call

US-CDR-014 · ATRA REST API Fallback

Type: Feature | Points: 8

Description: As the CDR export pipeline, I need to fall back to the ATRA REST upload API when SFTP is unreachable so that a single-channel outage does not delay the daily file drop.

Acceptance Criteria:

  • Trigger: 3 SFTP failures in 15-min window
  • POST https://atra.gov.af/api/v1/cdr/upload (multipart, mTLS) with file + signature sidecar
  • 201 response with {receiptId, sha256} stored in cdr.export_deliveries (status=DELIVERED_API)
  • Hash mismatch → SEV1 + INTEGRITY_MISMATCH
  • mTLS handshake failure (cert expired) → immediate SEV1 + page
  • 5xx → exponential backoff with jitter, 6 retries over 1h
  • Both channels failing >2h → cdr.export.delivery.failed + auto P0 incident

US-CDR-015 · Delivery Receipt Tracking

Type: Feature | Points: 3

Description: As a compliance auditor, I need to query the delivery status of every TAP/RAP file ever sent to ATRA so that I can answer "did file X reach ATRA on date Y?" without log diving.

Acceptance Criteria:

  • GET /v1/cdr/exports?from=&to= (paginated, cdr:read) returns {filename, recordingEntity, fileSequenceNumber, sha256, signedAt, deliveryChannel, deliveryStatus, atraReceiptId, deliveredAt, atraVerificationStatus}
  • FAILED status includes latest error reason + retry count
  • POST /v1/cdr/exports/{id}/redrop enqueues fresh attempt with original signature reused
  • Audit log records every redrop with operator identity from JWT

EP-CDR-04 · Adjustment Records (Corrections, Voids) without Mutating Originals

Context: Corrections, voids, and retroactive re-rates are issued as new immutable rows that reference the original cdrId. Originals are never updated. Adjustments themselves are chained, hashed, and exported via RAP.

US-CDR-016 · Issue Adjustment Record (CORRECTION)

Type: Feature | Points: 5

Description: As a settlement operations engineer, I need to issue a CORRECTION adjustment for a CDR with a wrong field value, without mutating the original, so that audit integrity is preserved while the regulator receives corrected data.

Acceptance Criteria:

  • POST /v1/cdr/{cdrId}/adjustments with {type: "CORRECTION", correctedFields, reason, ticketId} INSERTs new row adjustmentOf=cdrId, adjustmentType=CORRECTION
  • Original CDR row remains byte-for-byte unchanged
  • cdr.adjustment.issued{adjustmentId, originalCdrId, type, reason} published
  • Correction-of-correction supported via adjustmentOf chain
  • Adjustment sealed in the hour it was issued, not original CDR's hour
  • Missing cdr:write scope → 403
  • Missing ticketId → 422

US-CDR-017 · Issue Adjustment Record (VOID)

Type: Feature | Points: 5

Description: As the fraud investigations team, I need to void a CDR that was incorrectly produced (e.g. duplicate DLR projection or fraud reversal) so that the record is functionally removed without violating immutability.

Acceptance Criteria:

  • POST /v1/cdr/{cdrId}/adjustments with {type: "VOID", voidReason, ticketId} INSERTs new row adjustmentType=VOID, chargeAmount=0
  • Net-effect-by-messageId consumers see void cancelling original
  • Already-voided CDR → 409 ALREADY_VOIDED
  • cdr.adjustment.issued{type: VOID} published; included in next RAP batch
  • Fraud-reversal flag from operator-management auto-issues void with voidReason: "FRAUD_REVERSAL" + linked fraud ticketId

US-CDR-018 · Issue Adjustment Record (RE_RATE)

Type: Feature | Points: 8

Description: As the commercial pricing team, I need to issue RE_RATE adjustments when a retroactive tariff change applies so that ATRA receives corrected charge amounts via the next RAP file.

Acceptance Criteria:

  • Bulk POST /v1/cdr/adjustments/rerate with {filter, newPricingTableId, ticketId, reason} INSERTs one RE_RATE row per matched CDR
  • New pricing snapshot fetched from billing-service at adjustment-creation time; chargeAmount + tapTariffClass reflect new pricing
  • >100,000 CDR impact requires approverJwt header (four-eyes principle); else 403
  • GET /v1/cdr/adjustments/jobs/{jobId} returns progress + final counts
  • Single cdr.adjustment.batch.issued{jobId, type: RE_RATE, count, sumDelta} event on completion
  • All re-rate adjustments included in next RAP file with returnReason: TARIFF_CHANGE

End of report — 4 epics, 18 stories, 115 points