Skip to main content

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.

FieldTypeNotes
e164stringIdentity in canonical form (+[1-9]\d{6,14})
msisdnHashbytes (32)`sha256(e164
mnoIdMnoIdCurrent owning MNO (recipient MNO if ported)
originalMnoIdMnoId | nullPopulated when mnpStatus ∈ {PORTED_IN, PORTED_OUT}
lineTypeLineTypeMOBILE · FIXED · VOIP · UNKNOWN
countryISO 3166-1 alpha-2Derived from E.164 CC; persisted to avoid repeat lookup
mnpStatusMnpStatusNATIVE · PORTED_IN · PORTED_OUT · UNKNOWN
vlrstring | nullLast-known VLR address from HLR probe (volatile — 5 min TTL)
imsiPrefixstring | nullFirst 6 digits (MCC+MNC) of the IMSI, if observed
sourceAttributionSourceMNO_HLR_DUMP · LIVE_HLR_MAP · LIVE_HLR_REST · MNP_RECON · PREFIX_FALLBACK · ADMIN_OVERRIDE
confidenceConfidenceHIGH · MEDIUM · LOW · UNKNOWN
lastSeenInstantMost-recent observation (live or reconciliation)
cachedAtInstantWhen the persisted row was last written
lookupCountbigintCumulative successful lookups (approximate; eventually-consistent)
versionintMonotonic per-row; bumped on every authoritative write

Invariants

  • Hash-on-write. msisdnHash is computed from the NFKC-normalised E.164 with the platform-wide pepper (Vault KV secret/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 PortabilityRecord says the number is ported to MNO-B and a live HLR probe claims MNO-A, the MNP registry wins; the divergence is recorded as a MnpHlrDivergence domain event (fraud-intel-service consumer).
  • No mutation without version bump. Updates are conditional on version = :expected; concurrent writers collide on an advisory lock keyed on msisdnHash.
  • mnpStatus = NATIVE implies originalMnoId IS NULL. DB-level CHECK constraint (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).

FieldTypeNotes
portIdni_<ULID>Identity
msisdnHashbytes (32)Keys the chain
donorMnoIdMnoIdSource MNO
recipientMnoIdMnoIdDestination MNO
portDatedateEffective date (from MNO file)
directionPortDirectionIN · OUT (relative to the recipient MNO's export file)
sourceFeedstringMNP source identifier (afghan-wireless-mnp-2026-04-20.csv etc.)
reconRunIdstringFK → ReconciliationRun
prevChainHashbytes (32)Links into the per-MSISDN hash chain
recordHashbytes (32)`sha256(canonicalPayload
signingKeyIdstringVault Transit key version at write time
observedAtInstantWhen reconciliation inserted this row

Invariants

  • Audit-immortal. portability_history is 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 by portDate then seq. The chain root uses prevChainHash = 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.

FieldTypeNotes
probeIdprb_<ULID>Identity
msisdnHashbytes (32)Subject
mnoHintMnoId | nullPrefix-derived hint passed to the gateway
transportProbeTransportMAP_SRI_SM · REST_ADAPTER
gccInvokeIdint | nullMAP invoke ID for SS7 PCAP correlation (when transport = MAP)
statusProbeStatusOK · TIMEOUT · MAP_ABORT · REST_5XX · THROTTLED · ADAPTER_DOWN
durationMsintEnd-to-end
resultSnapshotJSONBThe live attribution returned (if OK)
startedAt, endedAtInstants

Invariants

  • TPS-governed. Probes are issued only through the Redis token bucket numint:tps:hlr:{mno}; status = THROTTLED is recorded when the bucket denies.
  • Cost attribution. Every OK probe against a MAP adapter is a billable SS7 message; counters drive the per-MNO numint_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.

FieldTypeNotes
auditIdnia_<ULID>
seqbigintMonotonic per-partition
tenantIdUUID
actorSubstringJWT subject
msisdnHashbytes (32)Salted with per-tenant salt so cross-tenant re-derivation requires that salt
resultClassLookupResultClassSUCCESS · INVALID_MSISDN · QUOTA_EXCEEDED · RATE_LIMITED · ERROR
resultMnoMnoId | nullPopulated when resultClass = SUCCESS
stalenessSecondsintFrom response
sourceAttributionSource
tierLookupLatencyClassLRU · REDIS · PG · LIVE · FALLBACK
ipAddressinet | nullTenant IP (if observable through Kong)
userAgentstring | null
requestIdstringW3C traceparent
prevHash, recordHashbytes (32)Hash-chained (RFC 8785 canonicalisation)
signingKeyIdstring
occurredAtInstant

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.

FieldTypeNotes
mnoIdMnoIdPrimary key
namestring"Afghan Wireless", "MTN Afghanistan" etc.
countryISO 3166-1 alpha-2
prefixesstring[]Owned E.164 prefix ranges (MSISDN ranges assigned by ATRA)
hlrEndpointHlrEndpoint VOSIGTRAN (point code, SSN, GT) or REST (URL, auth profile)
mnpSftpEndpointstringMNP file source
tpsLimitintContracted SS7 TPS
restTimeoutMs, mapTimeoutMsintGateway timeouts
activeboolean
updatedAtInstant

ReconciliationRun

One row per MNP or EIR daily ingest.

FieldTypeNotes
runIdrcn_<ULID>
kindReconKindMNP · EIR
mnoIdMnoId | nullnull for EIR ATRA-wide runs
fileSha256stringTamper-evidence of the source file
totalRecords, accepted, rejectedint
conflictsCountintCount of rows entering ReconciliationConflict
durationMsint
prevChainHash, recordHashbytes (32)Hash-chained across runs per MNO
startedAt, completedAtInstants
statusReconStatusPENDING · RUNNING · COMPLETED · FAILED

ReconciliationConflict

Two MNOs claim the same ported number on overlapping dates — genuine discrepancy requiring manual resolution.

FieldTypeNotes
conflictIdcfl_<ULID>
msisdnHashbytes (32)
candidateA, candidateBJSONB{ mnoId, portDate, sourceFeed } per candidate
severityConflictSeverityHIGH if port dates differ ≥ 7 days; MEDIUM otherwise
resolutionConflictResolution | nullA_WINS · B_WINS · KEEP_BOTH_PENDING_VENDOR_CONFIRM · DISCARDED
resolvedByUUID | nullPlatform admin
resolvedAtInstant | null
createdAtInstant

Invariants

  • Unresolved conflicts hold the corresponding PortabilityRecord insert; the active NumberRecord.mnpStatus remains at its previous value until resolution.

EirRecord

IMEI-keyed device status across ATRA + per-MNO CEIR feeds.

FieldTypeNotes
imeistring15-digit; Luhn-valid
imeiHashbytes (32)`sha256(imei
statusEirStatusWHITELIST · GREYLIST · BLACKLIST · UNKNOWN
reasonCodestring | nullSTOLEN, LOST, COUNTERFEIT, GOVT_ACTION, etc.
reportedByMnoId[] + "ATRA"Multi-source aggregation
effectiveStatusEirStatusDerived: most-restrictive across reporters (BLACKLIST > GREYLIST > WHITELIST)
firstSeenAt, lastUpdatedAtInstants

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.

FieldTypeNotes
msisdnHashbytes (32)
imeiHashbytes (32)
firstObservedAt, lastObservedAtInstants
observationCountintNumber of HLR responses including this IMEI
sourceAttributionSourceOnly LIVE_HLR_MAP or LIVE_HLR_REST permitted

TenantLookupQuota

Per-tenant rate & quota state; mirrored from billing-service.tenant_plans.

FieldNotes
tenantIdUUID
rpsLimitint — Redis token-bucket capacity
monthlyQuotaint — per calendar month
currentMonthUsedint — incremented on each chargeable call
freshLookupRpsLimitint — lower cap for maxStaleness < 86400 (which forces live HLR)
overageModeOverageModeREJECT · THROTTLE_TO_PLAN
planVersionint — for plan-change idempotency
updatedAtInstant

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

VOShapeInvariants
MsisdnstringCanonical E.164 (^\+[1-9]\d{6,14}$); NFKC-normalised; no whitespace; no separators
MsisdnHashbytes(32)`sha256(msisdn
MnoIdstring sluge.g. afghan-wireless, mtn-afghanistan, etisalat-af, roshan, salaam; registered in operator-management-service
LineTypeenumMOBILE · FIXED · VOIP · UNKNOWN
MnpStatusenumNATIVE · PORTED_IN · PORTED_OUT · UNKNOWN
ConfidenceenumHIGH (live-HLR ≤ 5 min, or MNP ≤ 24 h) · MEDIUM (PG cache ≤ 24 h) · LOW (PG cache > 24 h, or throttled) · UNKNOWN (no data)
LookupLatencyClassenumLRU · REDIS · PG · LIVE · FALLBACK — bound to the Prometheus tier label
RiskFlagenumSTOLEN_DEVICE · MNP_DIVERGENCE · ABNORMAL_MNP_CHURN · PREFIX_MISMATCH · UNUSUAL_VLR
AttributionSourceenumSee NumberRecord.source values
HlrEndpointdiscriminated union{ kind: "MAP", pc, ssn, gt, mapContext: "shortMsgGatewayContext-v3" } or { kind: "REST", url, authProfile }
Imeistring15 digits; Luhn valid (3GPP TS 23.003 §6.2.1)
PortDirectionenumIN · OUT
ConflictResolutionenumSee ReconciliationConflict

4. Domain Events (produced)

Detailed schemas in EVENT_SCHEMAS.md.

EventTrigger
numint.attribution.changed.v1NumberRecord.mnoId or mnpStatus changes vs previous row
numint.mnp.changed.v1New PortabilityRecord inserted (per-MSISDN port transition)
numint.mnp.divergence.v1LookupPorting or ResolveMsisdn detects MNP registry vs live HLR disagreement
numint.cache.refreshed.v1Warm-on-deploy or manual warm completes (ops marker)
numint.reconciliation.completed.v1MNP or EIR daily run finishes (success or fail)
numint.reconciliation.conflict.v1New ReconciliationConflict row
numint.hlr_probe.completed.v1Live HLR probe finishes — for fraud-intel correlation
numint.eir.flagged.v1IMEI transitions to BLACKLIST from non-blacklist
numint.lookup.billed.v1One chargeable Public Lookup API call; consumed by billing-service
numint.audit.v1Administrative 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 with confidence: LOW is always safer than returning no answer — downstream routing-engine has a prefix-table fallback. The one exception is LookupEir for a BLACKLIST check, which must not false-negative; on total failure it returns status = UNKNOWN so 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: UNKNOWN as actionable without a fallback are misusing the contract; routing-engine, sms-firewall-service, compliance-engine each document their fallback policy (prefix table, allow-by-default, geo-default respectively).
  • Tenant-salted audit hash. LookupAuditEntry.msisdnHash uses 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-service DATA_MODEL §3.2 — documented so reviewers do not flag it as an omission.