Number Intelligence Service — Jira-Ready Epics & User Stories
Status: populated Owner: Messaging Core Last updated: 2026-04-20 Service prefix: NI Scope: HLR/HSS lookup with layered cache, MNP registry and daily MNO reconciliation, EIR/CEIR cross-check, public tenant-callable Lookup API. Derived from
docs/07-epics-and-user-stories.md§6.2 and ADR-0004 §3.
Epic Summary
| Epic ID | Title | Stories | Points |
|---|---|---|---|
| EP-NI-01 | HLR/HSS Lookup with Cache (MSISDN → MNO, line-type, country) | US-NI-001 – US-NI-006 | 32 |
| EP-NI-02 | MNP (Mobile Number Portability) Registry & Daily Reconciliation with MNOs | US-NI-007 – US-NI-012 | 25 |
| EP-NI-03 | EIR/CEIR Cross-Check (IMEI, stolen-device exclusion) | US-NI-013 – US-NI-016 | 16 |
| EP-NI-04 | Public Lookup API (tenant-callable, billable, cached) | US-NI-017 – US-NI-021 | 19 |
| Total | 21 stories | 92 |
EP-NI-01 · HLR/HSS Lookup with Cache
Context: Authoritative resolution of
MSISDN → (MNO, line_type, country, is_ported, VLR)via a four-tier cascade (in-process LRU → Redis → Postgres → live MAP/SS7 or REST HLR). Consumed in the hot path byrouting-engine,sms-firewall-service,channel-router-service, andfraud-intel-service.
US-NI-001 · Resolve MSISDN via cache cascade
Type: Feature | Points: 8
Description:
As routing-engine, I need ResolveMsisdn(e164, opts) to return typed attribution in P95 ≤ 15 ms so I can pick the correct per-MNO SMPP connector without a paid SS7 lookup on every call.
Acceptance Criteria:
- LRU hit returns in P95 ≤ 1 ms with
source = "lru" - Redis hit returns in P95 ≤ 4 ms with
source = "redis", populates LRU - Postgres hit returns in P95 ≤ 15 ms with
source = "postgres", populates Redis + LRU - Full miss triggers live HLR through
ni-hlr-gateway; result written to all tiers;source = "live_hlr" - Live-HLR failure/timeout returns persisted answer with
confidence = "low"rather than erroring - Malformed E.164 (regex
^\+[1-9]\d{6,14}$) returns gRPCINVALID_ARGUMENT
US-NI-002 · Batch MSISDN resolution
Type: Feature | Points: 5
Description:
As sms-orchestrator's bulk-submit pipeline, I need ResolveBatch to resolve up to 1000 MSISDNs per call so I avoid 1000 round trips on bulk campaigns.
Acceptance Criteria:
- 500-entry warm-cache batch returns in P95 ≤ 80 ms via streaming gRPC response
- > 1000 entries →
RESOURCE_EXHAUSTED - Per-entry timeouts do not fail the whole batch
- Duplicates deduplicated against cascade exactly once
- Live-HLR fan-out bounded by per-MNO TPS governor
US-NI-003 · Per-MNO TPS governor for live HLR
Type: Feature | Points: 5
Description:
As platform SRE, I need Redis-backed token buckets per MNO so outbound MAP SendRoutingInfoForSM never exceeds contracted SS7 quota.
Acceptance Criteria:
- Redis bucket
ni:tps:hlr:{mno}with capacity and refill fromoperator-management-service - Denied calls either wait up to
tpsWaitMsor return stale-throttled answer - Bucket config refreshed on
operator.config.changed.v1 -
/metricsexposesni_hlr_tps_admitted_totalandni_hlr_tps_denied_totalper MNO - Alert
NIHLRThrottlingwhen deny rate > 5 % over 5 min
US-NI-004 · Sovereign LRU & Redis cache classes
Type: Feature | Points: 3
Description: As the cache implementer, I need TTLs that vary by data class so stable attributes survive days while volatile attributes refresh in minutes.
Acceptance Criteria:
- Redis namespace
ni:hlr:{e164}with hash fields and per-classEXPIREAT - Class TTLs: LINE_TYPE 30 d, MNO 24 h, MNP 24 h, VLR/IMSI 5 min, EIR 24 h
- LRU default 100 000 entries per pod, exact-LRU eviction
- Per-region Redis only — no cross-region replication
-
/metricscache-tier hit counters
US-NI-005 · HLR/HSS gateway with MAP and REST adapters
Type: Feature | Points: 8
Description:
As the ni-hlr-gateway operator, I need both a SIGTRAN MAP adapter and an HTTPS REST adapter per MNO so each operator can be reached on its preferred protocol.
Acceptance Criteria:
- SIGTRAN M3UA/SCTP association per MNO; MAP context
shortMsgGatewayContext-v3 - REST adapter:
POST /v1/hlr/lookupwith Bearer JWT; returns{ imsi, vlr, line_type, mno_id } - Internal RPC
LiveLookup(e164, mno_hint)with SPIFFEspiffe://ghasi.platform/ns/ni/sa/hlr-gateway - MAP timeout 1500 ms, REST timeout 800 ms →
DEADLINE_EXCEEDEDon breach - 0.1 % MAP pcap sampled to MinIO encrypted with
ni-pcap-kek
US-NI-006 · Authoritative attribution write-through
Type: Feature | Points: 3
Description:
As Postgres writer, I need every live HLR success UPSERTed into ni.msisdn_attribution so later callers benefit.
Acceptance Criteria:
-
INSERT … ON CONFLICT (e164) DO UPDATEupdates mno/line_type/vlr/imsi_prefix/last_seen - Write is non-blocking; caller never waits
- Write failure → retry via
ni.attribution_outboxwith exp back-off, max 6 attempts -
ni.attribution.changed.v1emitted only whenmnooris_portedactually changed - Monthly partitioning on
last_seen; detach partitions > 24 months toni_archive
EP-NI-02 · MNP Registry & Daily Reconciliation with MNOs
Context: Daily authoritative refresh of Mobile Number Portability state from each MNO's published MNP delta file. Governs the
ni.mnp_registrytable and theLookupPortingfast-path.
US-NI-007 · Daily MNP file ingest
Type: Feature | Points: 8
Description: As reconciliation operator, I need a nightly CronJob that pulls and loads each MNO's MNP delta file.
Acceptance Criteria:
- CronJob
mnp-reconat 02:30 Asia/Kabul inkblregion - SFTP pull of
{mno-sftp}/mnp/YYYY-MM-DD.csvper configured MNO - Files landed to MinIO
ni-mnp-raw/{mno}/{yyyy}/{mm}/{dd}.csvwith sha256 tag -
INSERT … ON CONFLICT (msisdn) DO UPDATEwhenport_date > existing.port_date - Per-MNO
ni.mnp.reconciled.v1event with counts - Retries hourly until 23:00 same day; escalates to P1 afterwards
US-NI-008 · On-demand MNP refresh
Type: Feature | Points: 3
Description: As platform operator, I need an admin endpoint to trigger MNP ingest outside the nightly window for emergency corrections.
Acceptance Criteria:
-
POST /v1/admin/mnp/reconrequires admin JWT withplatform-admin - Publishes
ni.recon.mnp.requested.v1, returns 202 withjobId -
GET /v1/admin/mnp/recon/{jobId}returns{ status, accepted, rejected, errors } - All admin operations emit
audit.admin.action.v1
US-NI-009 · LookupPorting fast-path RPC
Type: Feature | Points: 3
Description:
As routing-engine, I need LookupPorting(e164) to return { isPorted, currentMno, originalMno, portDate, source }.
Acceptance Criteria:
- Cascade Redis → Postgres → default-unported in P95 ≤ 8 ms
- MNP entry takes precedence over HLR observation
-
source∈{ mnp_registry, hlr_observation, default_unported } - Divergence between MNP and HLR emits
ni.attribution.divergence.v1
US-NI-010 · MNP discrepancy reconciliation
Type: Feature | Points: 5
Description: As trust & safety analyst, I need to see MSISDNs where live HLR and MNP file disagree so I can investigate possible SIM-swap fraud.
Acceptance Criteria:
- Nightly discrepancy job writes to
ni.mnp_discrepancies - Severity HIGH when
port_date < now() - '7 days' - Daily summary email to
trust-safety@ghasi.local -
ni.attribution.divergence.v1consumed byfraud-intel-service
US-NI-011 · MNP registry hash chain integrity
Type: Feature | Points: 3
Description: As compliance auditor, I need cryptographic evidence that the MNP registry has not been mutated outside the ingest job.
Acceptance Criteria:
-
ni.mnp_recon_logrows includeprev_chain_hash,this_chain_hash - Daily verification job recomputes chain end-to-end
- Alert
MNPChainBrokenon mismatch -
GET /v1/admin/mnp/chain/verifyreturns on-demand verification - Application role has no
UPDATE/DELETEonni.mnp_registry
US-NI-012 · MNP webhook to subscribed services
Type: Feature | Points: 3
Description:
As routing-engine and sms-firewall-service, I need ni.attribution.changed.v1 so I can proactively warm or invalidate caches.
Acceptance Criteria:
- Event payload
{ e164Hash, oldMno, newMno, isPorted, portDate, eventTime, source } - JetStream subject with
interestretention,MaxAge7 d - Durable consumer names
re-ni-warmerandfw-ni-warmer; P95 handle ≤ 5 s - Schema registered under
ni.attribution.changed.v1
EP-NI-03 · EIR/CEIR Cross-Check
Context: Daily refresh of blacklisted / greylisted IMEIs from ATRA and per-MNO CEIR feeds; opportunistic observation of MSISDN↔IMEI links through MAP responses; enrichment only (no blocking).
US-NI-013 · Daily EIR/CEIR file ingest
Type: Feature | Points: 5
Description:
As reconciliation operator, I need daily ATRA + per-MNO CEIR files pulled and loaded into ni.eir_status.
Acceptance Criteria:
- CronJob
eir-reconat 03:30 Asia/Kabul - SFTP pull from ATRA and per-MNO CEIR endpoints
- Luhn-validated IMEI, status enum
BLACKLIST | GREYLIST | WHITELIST - Most-restrictive status wins across feeds
-
ni.eir.flagged.v1emitted on transition into BLACKLIST - Raw files retained in MinIO
ni-eir-rawfor 24 months
US-NI-014 · LookupEir(imei) RPC
Type: Feature | Points: 3
Description:
As sms-firewall-service and fraud-intel-service, I need LookupEir(imei) for device-stolen enrichment.
Acceptance Criteria:
- Cascade Redis → Postgres in P95 ≤ 8 ms
- Response
{ status, reasonCode, reportedBy[], lastUpdated, source } - Unknown IMEI →
status = "UNKNOWN", not an error - Invalid Luhn →
INVALID_ARGUMENT
US-NI-015 · IMEI/MSISDN cross-link via VLR observation
Type: Feature | Points: 5
Description: As trust & safety analyst, I need an observational link between MSISDN and IMEI when MAP responses include IMEI.
Acceptance Criteria:
-
ni.msisdn_imei_observedupsert with(msisdn, imei, last_seen, observation_count) - No inference when MNO withholds IMEI
-
LookupMsisdnImei(msisdn)returns most-recent observation in P95 ≤ 8 ms - Annotated
confidence = "observed", never sole basis for block
US-NI-016 · EIR-driven MT block recommendation
Type: Feature | Points: 3
Description:
As sms-firewall-service, I need a recommendation when recipient MSISDN was observed on a blacklisted IMEI.
Acceptance Criteria:
-
ResolveMsisdnresponse carries optionaleir_observation: { imei, status, observed_at } -
sms-firewall-servicerule pipeline consumes the field -
audit.lookup.v1records the presence of the observation - NI never auto-blocks — only enriches
EP-NI-04 · Public Lookup API
Context: Tenant-callable, billable Lookup API exposed through Kong. Returns attribution + staleness with per-tenant quotas and per-call metering.
US-NI-017 · Public Lookup API single endpoint
Type: Feature | Points: 5
Description:
As tenant developer, I need GET /v1/lookup/{msisdn} so I can validate input and pre-route my own traffic.
Acceptance Criteria:
- Requires Bearer JWT and
X-Tenant-Id - Response
{ msisdn, country, mno, lineType, isPorted, originalMno, fetchedAt, stalenessSeconds, confidence } -
?maxStaleness=300triggers fresh-tier billing - Invalid E.164 → 400
INVALID_MSISDN; quota over → 429QUOTA_EXCEEDED - P95 ≤ 200 ms; P99 ≤ 500 ms
US-NI-018 · Public Lookup batch endpoint
Type: Feature | Points: 3
Description:
As tenant developer, I need POST /v1/lookup/batch for up to 100 MSISDNs in one call.
Acceptance Criteria:
- Max 100 entries, 413 on overflow
- Results preserve input order
- Each success metered as one billable lookup
- Warm-cache P95 ≤ 800 ms for 100-entry batch
US-NI-019 · Per-call billing metering event
Type: Feature | Points: 3
Description:
As billing-service, I need one billing.metering.recorded.v1 per chargeable lookup.
Acceptance Criteria:
- Payload
{ tenantId, sku, quantity, occurredAt, requestId, msisdnHash } -
Nats-Msg-Id = requestIdfor idempotent consumption - SKU
lookup.fresh.v1when live HLR was triggered;lookup.v1otherwise - Publish failure sets
X-Metering-Status: degraded; outbox retries - Internal callers are NOT metered
US-NI-020 · Per-tenant Lookup quota and rate limiting
Type: Feature | Points: 5
Description: As platform operator, I need per-tenant RPS and per-month quotas.
Acceptance Criteria:
- Redis token bucket
ni:tps:lookup:{tenantId}default 10/s - Month counter
ni:quota:lookup:{tenantId}:{yyyymm}resets 1st 00:00 Asia/Kabul - 429 body
{ code: "QUOTA_EXCEEDED", scope, retryAfter } -
audit.lookup.quota_exceeded.v1emitted on 429 -
billing.tenant.plan.changed.v1propagates new bucket within 60 s
US-NI-021 · Public Lookup audit log
Type: Feature | Points: 3
Description: As compliance officer, I need an immutable audit trail of every Public Lookup call.
Acceptance Criteria:
-
audit.lookup.v1event per call with{ tenantId, actorSub, msisdnHash, resultClass, resultMno, stalenessSeconds, ipAddress, userAgent, requestId, occurredAt } - Persisted by platform audit pipeline with WORM storage
- MSISDN never published in cleartext to audit channel; only
sha256(msisdn || tenantSalt) - Schema registered; breaking change →
v2 - Regulator query "all lookups by tenant T against +93... in last 90 d" runnable in ≤ 5 min