Number Intelligence Service — Domain Model
Version: 1.0 Status: Draft Owner: Messaging Core Last Updated: 2026-04-21 Companion: SERVICE_OVERVIEW · APPLICATION_LOGIC · DATA_MODEL · EVENT_SCHEMAS · API_CONTRACTS · SECURITY_MODEL
1. Bounded Context
Telecom Numbering and Subscriber Attribution. The number-intelligence-service (NI) owns every authoritative answer to the question "what carrier, line type, portability state, and device flag is associated with this MSISDN/IMEI today?" on the Ghasi-SMS-Gateway national backbone. It is a peer to routing-engine (dispatch), sms-firewall-service (inbound policing), and compliance-engine (regulatory gate); it is not an aspect of any of those services.
The context boundary is drawn such that:
- Inside the boundary: MSISDN attribution (MNO, line type, country), MNP (Mobile Number Portability) registry and transition history, EIR/CEIR status for IMEIs, MSISDN↔IMEI observational link, HLR probe adapters, reconciliation runs, MNP conflict resolutions, tenant-lookup audit, tenant lookup quotas and billing meters, cache-warming state, and the telemetry counters required to prove SLO attainment.
- Outside the boundary: SMS message storage (
sms-orchestrator), routing weights and TPS budgets (routing-engine+operator-management-service), firewall rules (sms-firewall-service), consent state (consent-ledger-service), sender-ID alphanumeric attribution (sender-id-registry-service), tenant identity (auth-service), payment settlement (billing-service), SMPP transport (smpp-connector).
All external numbering concepts follow ITU-T E.164 numbering plan, ETSI TS 123 003 for MSISDN/IMSI format, 3GPP TS 29.002 (MAP) for SendRoutingInfoForSM, and GSMA AA.13 for Mobile Number Portability operational conventions. IMEIs are 15-digit TAC+SNR+Luhn strings per 3GPP TS 23.003 §6.
2. Aggregates
NumberRecord
The authoritative attribution row for a single MSISDN. One record per E.164; history is held separately in PortabilityRecord.
| Field | Type | Notes |
|---|---|---|
e164 | string | Identity in canonical form (+[1-9]\d{6,14}) |
msisdnHash | bytes (32) | `sha256(e164 |
mnoId | MnoId | Current owning MNO (recipient MNO if ported) |
originalMnoId | MnoId | null | Populated when mnpStatus ∈ {PORTED_IN, PORTED_OUT} |
lineType | LineType | MOBILE · FIXED · VOIP · UNKNOWN |
country | ISO 3166-1 alpha-2 | Derived from E.164 CC; persisted to avoid repeat lookup |
mnpStatus | MnpStatus | NATIVE · PORTED_IN · PORTED_OUT · UNKNOWN |
vlr | string | null | Last-known VLR address from HLR probe (volatile — 5 min TTL) |
imsiPrefix | string | null | First 6 digits (MCC+MNC) of the IMSI, if observed |
source | AttributionSource | MNO_HLR_DUMP · LIVE_HLR_MAP · LIVE_HLR_REST · MNP_RECON · PREFIX_FALLBACK · ADMIN_OVERRIDE |
confidence | Confidence | HIGH · MEDIUM · LOW · UNKNOWN |
lastSeen | Instant | Most-recent observation (live or reconciliation) |
cachedAt | Instant | When the persisted row was last written |
lookupCount | bigint | Cumulative successful lookups (approximate; eventually-consistent) |
version | int | Monotonic per-row; bumped on every authoritative write |
Invariants
- Hash-on-write.
msisdnHashis computed from the NFKC-normalised E.164 with the platform-wide pepper (Vault KVsecret/ghasi/numint/msisdn_pepper) at write time and never recomputed from mutable inputs. Pepper rotation is envelope-style (see SECURITY_MODEL §3.2). - MNP precedes HLR. When
PortabilityRecordsays the number is ported to MNO-B and a live HLR probe claims MNO-A, the MNP registry wins; the divergence is recorded as aMnpHlrDivergencedomain event (fraud-intel-service consumer). - No mutation without version bump. Updates are conditional on
version = :expected; concurrent writers collide on an advisory lock keyed onmsisdnHash. mnpStatus = NATIVEimpliesoriginalMnoId IS NULL. DB-levelCHECKconstraint (see DATA_MODEL §1).- Public authoritative data, RLS-free. Unlike
consent-ledger-service.records, NI attribution is platform-wide authoritative fact, not tenant-scoped PII. Row-Level Security is intentionally not enabled; access control is at the API layer (authenticated callers and per-tenant rate-limits on the Public Lookup API).
PortabilityRecord
Append-only per-MSISDN MNP transition history. One row per observed port event. Keyed by (msisdnHash, portDate).
| Field | Type | Notes |
|---|---|---|
portId | ni_<ULID> | Identity |
msisdnHash | bytes (32) | Keys the chain |
donorMnoId | MnoId | Source MNO |
recipientMnoId | MnoId | Destination MNO |
portDate | date | Effective date (from MNO file) |
direction | PortDirection | IN · OUT (relative to the recipient MNO's export file) |
sourceFeed | string | MNP source identifier (afghan-wireless-mnp-2026-04-20.csv etc.) |
reconRunId | string | FK → ReconciliationRun |
prevChainHash | bytes (32) | Links into the per-MSISDN hash chain |
recordHash | bytes (32) | `sha256(canonicalPayload |
signingKeyId | string | Vault Transit key version at write time |
observedAt | Instant | When reconciliation inserted this row |
Invariants
- Audit-immortal.
portability_historyis append-only enforced by Postgres rules; no UPDATE or DELETE accepted. See DATA_MODEL §3.1. - Chain ordering. For any
msisdnHash, rows are lex-orderable byportDatethenseq. The chain root usesprevChainHash = 0x00…32. - Idempotent re-ingest. Same
(sourceFeed, msisdnHash, portDate, recipientMnoId)is a no-op. MNP files are often re-shipped by MNOs; duplicate rows would corrupt the chain. - No erasure. GDPR erasure does not apply; MSISDN never stored raw on this aggregate — only the hash. Carrier MNP is public telecom fact.
HlrProbe
Represents one live HLR query made via the ni-hlr-gateway. Used for audit, cost accounting, and SS7 PCAP correlation.
| Field | Type | Notes |
|---|---|---|
probeId | prb_<ULID> | Identity |
msisdnHash | bytes (32) | Subject |
mnoHint | MnoId | null | Prefix-derived hint passed to the gateway |
transport | ProbeTransport | MAP_SRI_SM · REST_ADAPTER |
gccInvokeId | int | null | MAP invoke ID for SS7 PCAP correlation (when transport = MAP) |
status | ProbeStatus | OK · TIMEOUT · MAP_ABORT · REST_5XX · THROTTLED · ADAPTER_DOWN |
durationMs | int | End-to-end |
resultSnapshot | JSONB | The live attribution returned (if OK) |
startedAt, endedAt | Instants |
Invariants
- TPS-governed. Probes are issued only through the Redis token bucket
numint:tps:hlr:{mno};status = THROTTLEDis recorded when the bucket denies. - Cost attribution. Every
OKprobe against a MAP adapter is a billable SS7 message; counters drive the per-MNOnumint_hlr_tps_admitted_total{mno}metric so finance and SRE share the same source of truth.
LookupAuditEntry
Append-only immutable audit row for every Public Lookup API call (internal gRPC calls are not written here — they are logged to Prometheus and structured logs only). Partitioned monthly.
| Field | Type | Notes |
|---|---|---|
auditId | nia_<ULID> | |
seq | bigint | Monotonic per-partition |
tenantId | UUID | |
actorSub | string | JWT subject |
msisdnHash | bytes (32) | Salted with per-tenant salt so cross-tenant re-derivation requires that salt |
resultClass | LookupResultClass | SUCCESS · INVALID_MSISDN · QUOTA_EXCEEDED · RATE_LIMITED · ERROR |
resultMno | MnoId | null | Populated when resultClass = SUCCESS |
stalenessSeconds | int | From response |
source | AttributionSource | |
tier | LookupLatencyClass | LRU · REDIS · PG · LIVE · FALLBACK |
ipAddress | inet | null | Tenant IP (if observable through Kong) |
userAgent | string | null | |
requestId | string | W3C traceparent |
prevHash, recordHash | bytes (32) | Hash-chained (RFC 8785 canonicalisation) |
signingKeyId | string | |
occurredAt | Instant |
Invariants
- Append-only (Postgres rule). Raw MSISDN never recorded — hash only.
- Hash-chained. Chain break is CRITICAL; verifier runs daily (see APPLICATION_LOGIC §UC-AuditChainVerify).
- Tenant-salted hash.
msisdnHash = sha256(e164 || tenantSalt)so two tenants looking up the same number produce different hashes — prevents cross-tenant correlation on the audit store.
MnoSnapshot
Per-MNO metadata (endpoints, TPS quotas, prefix ranges) maintained by operator-management-service and mirrored into NI for fast local access.
| Field | Type | Notes |
|---|---|---|
mnoId | MnoId | Primary key |
name | string | "Afghan Wireless", "MTN Afghanistan" etc. |
country | ISO 3166-1 alpha-2 | |
prefixes | string[] | Owned E.164 prefix ranges (MSISDN ranges assigned by ATRA) |
hlrEndpoint | HlrEndpoint VO | SIGTRAN (point code, SSN, GT) or REST (URL, auth profile) |
mnpSftpEndpoint | string | MNP file source |
tpsLimit | int | Contracted SS7 TPS |
restTimeoutMs, mapTimeoutMs | int | Gateway timeouts |
active | boolean | |
updatedAt | Instant |
ReconciliationRun
One row per MNP or EIR daily ingest.
| Field | Type | Notes |
|---|---|---|
runId | rcn_<ULID> | |
kind | ReconKind | MNP · EIR |
mnoId | MnoId | null | null for EIR ATRA-wide runs |
fileSha256 | string | Tamper-evidence of the source file |
totalRecords, accepted, rejected | int | |
conflictsCount | int | Count of rows entering ReconciliationConflict |
durationMs | int | |
prevChainHash, recordHash | bytes (32) | Hash-chained across runs per MNO |
startedAt, completedAt | Instants | |
status | ReconStatus | PENDING · RUNNING · COMPLETED · FAILED |
ReconciliationConflict
Two MNOs claim the same ported number on overlapping dates — genuine discrepancy requiring manual resolution.
| Field | Type | Notes |
|---|---|---|
conflictId | cfl_<ULID> | |
msisdnHash | bytes (32) | |
candidateA, candidateB | JSONB | { mnoId, portDate, sourceFeed } per candidate |
severity | ConflictSeverity | HIGH if port dates differ ≥ 7 days; MEDIUM otherwise |
resolution | ConflictResolution | null | A_WINS · B_WINS · KEEP_BOTH_PENDING_VENDOR_CONFIRM · DISCARDED |
resolvedBy | UUID | null | Platform admin |
resolvedAt | Instant | null | |
createdAt | Instant |
Invariants
- Unresolved conflicts hold the corresponding
PortabilityRecordinsert; the activeNumberRecord.mnpStatusremains at its previous value until resolution.
EirRecord
IMEI-keyed device status across ATRA + per-MNO CEIR feeds.
| Field | Type | Notes |
|---|---|---|
imei | string | 15-digit; Luhn-valid |
imeiHash | bytes (32) | `sha256(imei |
status | EirStatus | WHITELIST · GREYLIST · BLACKLIST · UNKNOWN |
reasonCode | string | null | STOLEN, LOST, COUNTERFEIT, GOVT_ACTION, etc. |
reportedBy | MnoId[] + "ATRA" | Multi-source aggregation |
effectiveStatus | EirStatus | Derived: most-restrictive across reporters (BLACKLIST > GREYLIST > WHITELIST) |
firstSeenAt, lastUpdatedAt | Instants |
Invariants
effectiveStatus = max_restriction(statuses reported by reporters in reportedBy).- Unknown IMEIs return
status = UNKNOWN, not an error.
MsisdnImeiObservation
Opportunistic link between an MSISDN and the IMEI observed in its VLR registration. Confidence is always OBSERVED — never authoritative.
| Field | Type | Notes |
|---|---|---|
msisdnHash | bytes (32) | |
imeiHash | bytes (32) | |
firstObservedAt, lastObservedAt | Instants | |
observationCount | int | Number of HLR responses including this IMEI |
source | AttributionSource | Only LIVE_HLR_MAP or LIVE_HLR_REST permitted |
TenantLookupQuota
Per-tenant rate & quota state; mirrored from billing-service.tenant_plans.
| Field | Notes |
|---|---|
tenantId | UUID |
rpsLimit | int — Redis token-bucket capacity |
monthlyQuota | int — per calendar month |
currentMonthUsed | int — incremented on each chargeable call |
freshLookupRpsLimit | int — lower cap for maxStaleness < 86400 (which forces live HLR) |
overageMode | OverageMode — REJECT · THROTTLE_TO_PLAN |
planVersion | int — for plan-change idempotency |
updatedAt | Instant |
AuditLog (service-wide administrative)
Append-only record of every administrative state change: MNO snapshot updates, conflict resolutions, quota overrides, manual admin overrides on NumberRecord. Distinct from LookupAuditEntry which is per-call.
3. Value Objects
| VO | Shape | Invariants |
|---|---|---|
Msisdn | string | Canonical E.164 (^\+[1-9]\d{6,14}$); NFKC-normalised; no whitespace; no separators |
MsisdnHash | bytes(32) | `sha256(msisdn |
MnoId | string slug | e.g. afghan-wireless, mtn-afghanistan, etisalat-af, roshan, salaam; registered in operator-management-service |
LineType | enum | MOBILE · FIXED · VOIP · UNKNOWN |
MnpStatus | enum | NATIVE · PORTED_IN · PORTED_OUT · UNKNOWN |
Confidence | enum | HIGH (live-HLR ≤ 5 min, or MNP ≤ 24 h) · MEDIUM (PG cache ≤ 24 h) · LOW (PG cache > 24 h, or throttled) · UNKNOWN (no data) |
LookupLatencyClass | enum | LRU · REDIS · PG · LIVE · FALLBACK — bound to the Prometheus tier label |
RiskFlag | enum | STOLEN_DEVICE · MNP_DIVERGENCE · ABNORMAL_MNP_CHURN · PREFIX_MISMATCH · UNUSUAL_VLR |
AttributionSource | enum | See NumberRecord.source values |
HlrEndpoint | discriminated union | { kind: "MAP", pc, ssn, gt, mapContext: "shortMsgGatewayContext-v3" } or { kind: "REST", url, authProfile } |
Imei | string | 15 digits; Luhn valid (3GPP TS 23.003 §6.2.1) |
PortDirection | enum | IN · OUT |
ConflictResolution | enum | See ReconciliationConflict |
4. Domain Events (produced)
Detailed schemas in EVENT_SCHEMAS.md.
| Event | Trigger |
|---|---|
numint.attribution.changed.v1 | NumberRecord.mnoId or mnpStatus changes vs previous row |
numint.mnp.changed.v1 | New PortabilityRecord inserted (per-MSISDN port transition) |
numint.mnp.divergence.v1 | LookupPorting or ResolveMsisdn detects MNP registry vs live HLR disagreement |
numint.cache.refreshed.v1 | Warm-on-deploy or manual warm completes (ops marker) |
numint.reconciliation.completed.v1 | MNP or EIR daily run finishes (success or fail) |
numint.reconciliation.conflict.v1 | New ReconciliationConflict row |
numint.hlr_probe.completed.v1 | Live HLR probe finishes — for fraud-intel correlation |
numint.eir.flagged.v1 | IMEI transitions to BLACKLIST from non-blacklist |
numint.lookup.billed.v1 | One chargeable Public Lookup API call; consumed by billing-service |
numint.audit.v1 | Administrative state change (mirrors AuditLog) |
5. MNP State Machine
Transitions are effected by PortabilityRecord inserts. The NumberRecord.mnpStatus field is the materialised view of the most-recent state.
6. Global Invariants
- Fail-degraded, not fail-closed. Unlike
compliance-engine, NI is an enrichment service. Returning a stale answer withconfidence: LOWis always safer than returning no answer — downstreamrouting-enginehas a prefix-table fallback. The one exception isLookupEirfor a BLACKLIST check, which must not false-negative; on total failure it returnsstatus = UNKNOWNso the caller's own fallback policy applies. - Authoritative source. No other service may directly query an MNO HLR/HSS, build a parallel MNP table, or ingest an MNO MNP file. All MSISDN attribution flows through this service.
- Hash-on-write, raw-in-PG. MSISDN is stored in the hash form in all cache layers, all events, and all audit rows. Raw MSISDN lives only in
numint.number_records.e164(platform-authoritative fact, not PII per regulator guidance for carrier attribution) and on the wire of the Public Lookup API response (tenant-submitted input echoed back). It never appears in NATS, logs, or audit. - Daily MNP reconciliation idempotent. Re-running the same
(mno, date)file produces zero new rows and zero new events. - Confidence floor. Callers that treat
confidence: UNKNOWNas actionable without a fallback are misusing the contract;routing-engine,sms-firewall-service,compliance-engineeach document their fallback policy (prefix table, allow-by-default, geo-default respectively). - Tenant-salted audit hash.
LookupAuditEntry.msisdnHashuses per-tenant salt; cross-tenant correlation requires that salt (held in Vault, per SECURITY_MODEL §3.2). - RLS intentionally absent on attribution tables. Attribution is platform-public fact (like an ATRA prefix table would be). Tenant isolation is enforced by the API layer (Kong + quota), not by DB-level row filtering. This is a deliberate departure from
consent-ledger-serviceDATA_MODEL §3.2 — documented so reviewers do not flag it as an omission.