consent-ledger-service — Jira-Ready Epics & User Stories
Status: populated Owner: Trust & Safety Last updated: 2026-04-21 Service prefix: CONS Scope: Authoritative consent ledger; National DND sync; STOP-keyword processing; consent audit (≥ 7 y); CheckConsent/RecordConsent/RevokeConsent API. Per ADR-0004 §3 and
07-epics-and-user-stories.md§6.12.
Epic Summary
| Epic ID | Title | Stories | Points |
|---|---|---|---|
| EP-CONS-01 | National DND Registry Sync + Per-Tenant Consent Records | US-CONS-001 – US-CONS-006 | 29 |
| EP-CONS-02 | STOP-Keyword Handling (inbound MO → consent revocation propagation) | US-CONS-007 – US-CONS-011 | 17 |
| EP-CONS-03 | Consent Audit Log (regulator-defensible, append-only, ≥ 7 years) | US-CONS-012 – US-CONS-015 | 16 |
| EP-CONS-04 | Consent API for Tenants (CheckConsent / RecordConsent / RevokeConsent) | US-CONS-016 – US-CONS-019 | 15 |
| Total | 19 stories | 77 |
EP-CONS-01 · National DND Registry Sync + Per-Tenant Consent Records
Context: Foundation epic. Ledger schema, hot-cache design, ATRA DND sync, double-opt-in flow, citizen self-service.
US-CONS-001 · National DND registry sync from ATRA
Type: Feature | Points: 5
Description: As a Trust & Safety engineer, I need to pull and sync the National DND registry from ATRA daily so that the platform respects the national do-not-disturb list.
Acceptance Criteria:
- Cron daily 03:00 Asia/Kabul fetches DND list from ATRA endpoint (SFTP / HTTPS)
- Diff applied to
consent.dnd_registry; new entries added, removed entries un-flagged -
dnd.registry.synced.v1published withadded,removed,total - Sync stale > 24 h → alert
ConsentDndStale - Manual override:
POST /v1/admin/consent/dnd/resync
US-CONS-002 · Per-tenant per-msisdn consent record
Type: Feature | Points: 3
Description: As a tenant developer, I need to record a consent state for a recipient via REST so that I can attest to my legal opt-in.
Acceptance Criteria:
-
POST /v1/consent/recordsaccepts{ msisdn, scope, source: { type, ref, capturedAt }} - Record persisted in
consent.records; previous record (if any) archived -
consent.granted.v1published - RLS enforced (tenant-scope only)
US-CONS-003 · Tenant double-opt-in flow
Type: Feature | Points: 5
Description: As a tenant developer, I need a double-opt-in flow where Ghasi sends a confirmation SMS containing an opt-in link so that consent is verifiable.
Acceptance Criteria:
-
POST /v1/consent/double-opt-in/initiatereturnsoptinId+ sends opt-in SMS via channel-router -
GET /v1/consent/double-opt-in/confirm?token=records consent - State machine:
PENDING → CONFIRMED | EXPIRED (24 h) - Confirmation publishes
consent.granted.v1withsource.type: "DOUBLE_OPT_IN"
US-CONS-004 · CheckConsent gRPC API (sub-5 ms)
Type: Feature | Points: 8
Description:
As compliance-engine / routing-engine / sms-firewall-service, I need to call CheckConsent(tenantId, msisdn, scope) and receive an answer in ≤ 5 ms P95 so pre-routing checks don't slow the pipeline.
Acceptance Criteria:
- gRPC method returns
{ allowed: bool, reason: enum, cachedAt } - Hot path: Redis hit on
consent:state:{tenant}:{msisdn}:{scope}; P95 ≤ 5 ms - Cache miss → Postgres read + cache fill (TTL 300 s)
- Cache + DB unavailable →
allowed: false, reason: "CONSENT_UNKNOWN"(fail-closed) - National DND match →
allowed: false, reason: "NATIONAL_DND"regardless of tenant record - Load test: 5000 RPS sustained P99 ≤ 20 ms
US-CONS-005 · Scope-aware consent (TRANSACTIONAL/MARKETING/OTP/EMERGENCY)
Type: Feature | Points: 3
Description: As a Trust & Safety engineer, I need consent recorded per scope so opt-out from MARKETING does not block OTP.
Acceptance Criteria:
-
consent.recordsPK is(tenantId, msisdn, scope) -
CheckConsentmatches exact scope - Default scope when caller omits is
TRANSACTIONAL
US-CONS-006 · Citizen self-service consent inspection
Type: Feature | Points: 5
Description: As a citizen, I want to see which tenants have my consent and revoke any I no longer want.
Acceptance Criteria:
- Citizen-portal page
/consent/{msisdn}after MSISDN verification (OTP via channel-router) - Lists consent records: tenantName, scope, grantedAt, source, status
- One-click revoke per record;
consent.revoked.v1immediate - Audit row written for every view
EP-CONS-02 · STOP-Keyword Handling (inbound MO → consent revocation propagation)
US-CONS-007 · STOP-keyword detection (multi-language)
Type: Feature | Points: 5
Description: As a T&S engineer, I need STOP/UNSUBSCRIBE keywords detected in English, Pashto, Dari, and Arabic on inbound MO so subscriber-typed opt-outs are honoured immediately.
Acceptance Criteria:
- NATS consumer for
sms.mo.inbound - Body trimmed/case-folded; matched against
consent.stop_keywordsper language - Default catalog per SERVICE_OVERVIEW §6.4; tenants may add (not remove)
- Conformance test: 200 MO samples (50 per language) including legitimate non-STOP words
US-CONS-008 · Consent revocation propagation + ack-back SMS
Type: Feature | Points: 3
Description: As a subscriber, I want a localised ack-back SMS confirming opt-out so I know it was honoured.
Acceptance Criteria:
- On STOP detection:
consent.revoked.v1published;consent.recordsupdated - Localised ack-back via channel-router (matching language); lane=P2 transactional
- Ack-back is one-shot (no loop)
- Default scope of revocation: per-tenant (configurable per US-CONS-010)
US-CONS-009 · STOP keyword catalog admin
Type: Feature | Points: 3
Description: As a T&S admin, I need to maintain the STOP-keyword catalog per language so new variants are recognised.
Acceptance Criteria:
- CRUD on
consent.stop_keywords; defaults cannot be removed (only added to) - Changes audit-logged
- Hot reload (no service restart)
US-CONS-010 · Cross-tenant STOP propagation policy
Type: Feature | Points: 3
Description: As a compliance officer, I need to configure whether STOP propagates only to one tenant or all tenants so we balance subscriber rights vs. tenant business needs.
Acceptance Criteria:
- Platform-wide
consent.policy.stop_scope∈{PER_TENANT, GLOBAL}; defaultPER_TENANT - Citizen-portal "STOP ALL" button forces GLOBAL revocation regardless of policy
- Policy change is dual-control + audit-logged
US-CONS-011 · STOP-keyword false-positive reporting
Type: Feature | Points: 3
Description: As a tenant, I want to report a STOP-keyword false positive so the keyword catalog improves.
Acceptance Criteria:
-
POST /v1/consent/feedback/false-positiveaccepts{ msisdn, mo, recordedTime } - T&S triages via admin dashboard; can re-grant consent on review
- Pattern aggregation suggests new keyword exclusions
EP-CONS-03 · Consent Audit Log (regulator-defensible, append-only, ≥ 7 years)
US-CONS-012 · Hash-chained immutable audit log
Type: Feature | Points: 5
Description: As a regulator auditor, I need the consent audit log to be tamper-evident via a hash chain so consent claims are defensible.
Acceptance Criteria:
- Each
consent.auditrow carriesprev_hashandrecord_hash = sha256(payload || prev_hash) - Cron daily verifies last 24 h chain integrity; alert on break
-
GET /v1/admin/consent/audit/verify?from=&to=returns chain integrity report - Postgres trigger rejects UPDATE/DELETE on
consent.audit
US-CONS-013 · 13-month hot retention + 7-y cold archive
Type: Feature | Points: 3
Description: As an SRE, I need hot Postgres to hold 13 months and older partitions archived to S3 with 7-y retention so hot DB stays performant.
Acceptance Criteria:
- Monthly partitions on
consent.audit - Cron archives partitions > 13 months old to S3 (immutable bucket policy)
- Restore script for regulator queries; SLA ≤ 1 h
US-CONS-014 · Audit-log query endpoint (regulator)
Type: Feature | Points: 3
Description: As a regulator-portal user, I need to query the audit log by msisdn, tenant, date range so I can answer subscriber complaints.
Acceptance Criteria:
-
GET /v1/admin/consent/audit?msisdn=&tenantId=&from=&to=(mTLS, regulator role) - Cursor-paged; max 100 rows/page
- P95 ≤ 2 s for 13-m hot window
- Older windows return job ID + S3-restore link
US-CONS-015 · Personal data erasure (GDPR right to erasure)
Type: Feature | Points: 5
Description: As a citizen, I want to request erasure of my consent records so my GDPR-equivalent rights are honoured.
Acceptance Criteria:
-
POST /v1/consent/erasure?msisdn=after MSISDN verification - MSISDN replaced by deterministic-hash token in
consent.recordsandconsent.audit; original purged - National-DND row retained (regulator override)
- Audit row written
consent.erasure.completed.v1 - SLA: completed within 30 days
EP-CONS-04 · Consent API for Tenants (CheckConsent / RecordConsent / RevokeConsent)
US-CONS-016 · RecordConsent gRPC for tenants
Type: Feature | Points: 3
Description:
As a tenant developer, I want to call RecordConsent via gRPC mTLS instead of REST for high-volume opt-in capture.
Acceptance Criteria:
- gRPC
RecordConsentwith mTLS + tenant scope - Same audit + Redis cache fill as REST
- Bulk variant:
RecordConsentBatchaccepts up to 1000 entries
US-CONS-017 · RevokeConsent gRPC for tenants
Type: Feature | Points: 2
Description: As a tenant developer, I want to revoke consent programmatically so my customer-data-deletion workflows propagate to Ghasi.
Acceptance Criteria:
- gRPC
RevokeConsent(tenantId, msisdn, scope, reason)— tenant-scope enforced -
consent.revoked.v1published withsource.type: "TENANT_API" - Cache invalidated immediately
US-CONS-018 · Bulk-import opt-in records (legacy migration)
Type: Feature | Points: 5
Description: As a tenant migrating from another platform, I want to bulk-upload opt-in records via CSV so I can move to Ghasi without losing consent provenance.
Acceptance Criteria:
-
POST /v1/consent/bulk-importaccepts CSV withmsisdn, scope, source_ref, captured_atcolumns - Validation per row; report of accepted/rejected
- Accepted rows audit-tagged
source.type: "BULK_IMPORT"with original CSV hash
US-CONS-019 · Consent SDK in dev portal
Type: Feature | Points: 5
Description: As a developer, I want a Ghasi SDK (Node, Python, Java) for consent operations so integration is fast and idiomatic.
Acceptance Criteria:
- SDK methods:
recordConsent,revokeConsent,checkConsent,bulkImport - Auth via API key per developer-portal pattern
- Examples + docs in dev portal