Numbering Service — Jira-Ready Epics & User Stories
Status: populated Owner: Commerce Engineering Last updated: 2026-04-20 Service prefix: NUM Scope: MSISDN/short-code/alpha-ID inventory + lifecycle, reservation/hold/release workflow, and per-tenant pool management.
Epic Summary
| Epic ID | Title | Stories | Points |
|---|---|---|---|
| EP-NUM-01 | MSISDN/Short-Code Inventory + Lifecycle (lease, recall, expiry) | US-NUM-001 – US-NUM-006 | 31 |
| EP-NUM-02 | Number Reservation, Hold, and Release Workflow | US-NUM-007 – US-NUM-010 | 15 |
| EP-NUM-03 | Per-Tenant Number Pool Management | US-NUM-011 – US-NUM-014 | 18 |
Totals: 3 epics · 14 stories · 64 points
EP-NUM-01 · MSISDN/Short-Code Inventory + Lifecycle
Context: The canonical inventory ledger for every sendable identity (MSISDN, short code, alpha ID) with explicit lifecycle states and audit trail. Hot-path lease validation, MNO block import, recall flows, auto-renewal, and post-recall quarantine cool-down.
US-NUM-001 · Bulk Import MSISDN Block from MNO
Type: Feature | Points: 8
Description: As the numbering operations engineer, I need to import a signed MSISDN block CSV from a Mobile Network Operator so that the new block is added to the AVAILABLE inventory for tenant assignment.
Acceptance Criteria:
-
POST /v1/admin/numbering/blocks/importwith{operatorId, signature, csvFile}verifies signature againstnumbering.mno_signing_keys - Invalid signature → 422
SIGNATURE_INVALID+ audit event - E.164 validation (
+93[0-9]{9}) and prefix-vs-operator check - Duplicate MSISDNs reported as
duplicate(skipped, not errored) - Summary
{inserted, duplicates, invalid}returned +numbering.block.imported{operatorId, count}published - Inserted MSISDNs start in
AVAILABLEwithoriginatingBlockIdset - Future
validFromexcludes fromListAvailable()until date passes
US-NUM-002 · Validate Sender Identity Lease (Hot Path)
Type: Feature | Points: 5
Description: As the sms-orchestrator, I need to validate that a tenant currently holds an active lease on a sender identifier so that I can reject outbound messages using unleased or recalled identifiers.
Acceptance Criteria:
- gRPC
ValidateLease({identifier, tenantId})→ P95 ≤ 20 ms cache-hit - Cache miss → PG fallback P95 ≤ 50 ms; 60 s cache TTL
- LEASED to caller + not expired →
valid: true - LEASED to other tenant →
valid: false, reason: "WRONG_TENANT" - SUSPENDED →
valid: false, reason: "LEASE_SUSPENDED" - Unknown identifier →
valid: false, reason: "NOT_REGISTERED" - Redis down → PG fallback P95 ≤ 80 ms + alert
US-NUM-003 · Lease Identifier to Tenant
Type: Feature | Points: 5
Description: As a tenant via customer-portal, I need to lease an identifier to my tenant for a defined term so that I can begin using it for outbound messaging.
Acceptance Criteria:
-
POST /v1/numbers/{identifier}/leasewith{tenantId, term, autoRenew}transitions RESERVED|HELD (own) → LEASED - AVAILABLE direct lease only with bypass permission
- HELD by other tenant → 409
HELD_BY_OTHER_TENANT -
effectiveFrom = now(),effectiveUntil = now() + term;numbering.lease.createdpublished - At quota → 403
QUOTA_EXCEEDEDwithcurrentCount+quota - In QUARANTINE → 409
QUARANTINE_ACTIVEwithavailableAt
US-NUM-004 · Recall Identifier (Regulator / Abuse / Tenant-Initiated)
Type: Feature | Points: 5
Description: As a numbering admin or system actor, I need to recall a leased identifier and put it into post-recall quarantine so that the identifier is removed from the tenant and held off the available pool to prevent abuse rotation.
Acceptance Criteria:
-
POST /v1/numbers/{identifier}/recallwith{reason, actorId, ticketId?}→ RECALLED + event -
REGULATOR_ORDERandABUSErequireticketId; missing → 422 - MSISDN → 90-day QUARANTINE; short code → 30 days; alpha → 0 days
- Cached
ValidateLeaseinvalidated within 1 s -
compliance.tenant.suspendedtriggers cascade recall of all tenant leases under one ticket
US-NUM-005 · Lease Auto-Renewal
Type: Feature | Points: 5
Description: As a tenant with autoRenew enabled, I need to have my lease automatically renewed before expiry so that my active sender identities do not lapse and disrupt campaigns.
Acceptance Criteria:
- Daily renewal job extends leases with
autoRenew=truewhoseeffectiveUntilis within 7 days - Billing rejection → no renewal +
numbering.lease.renewal_failed{reason: "BILLING_REJECTED"} - Suspended tenant → skip with
TENANT_SUSPENDED - New row in
numbering.lease_historyreferencing priorleaseId - Renewal vs current quota: existing leases grandfathered; warning event emitted
US-NUM-006 · Quarantine Cool-Down Enforcement
Type: Feature | Points: 3
Description: As the platform integrity guard, I need to prevent re-leasing of recalled identifiers until the quarantine period elapses so that bad actors cannot rotate phishing numbers across tenant identities.
Acceptance Criteria:
- Lease attempt on QUARANTINE identifier → 409
QUARANTINE_ACTIVEwithavailableAt - Sweep job (5 min) transitions to AVAILABLE on cool-down end +
numbering.identifier.releasedevent - Self-bypass forbidden (same tenant who recalled cannot re-lease)
- Admin override via
POST /v1/admin/numbers/{id}/quarantine/releasewith justification + audit - Daily override report surfaced in compliance dashboard
EP-NUM-02 · Number Reservation, Hold, and Release Workflow
Context: Two-phase pinning model. RESERVED (15 min) for browsing; HELD (24h) for build-phase. Concurrent attempts resolved by partial unique index. TTL auto-release driven by Redis keyspace notifications with PG sweep safety net.
US-NUM-007 · Reserve Identifier (15-min TTL)
Type: Feature | Points: 5
Description: As a tenant browsing the customer portal, I need to reserve a candidate identifier for 15 minutes while I evaluate it so that it does not disappear from the pool while I am deciding.
Acceptance Criteria:
-
POST /v1/numbers/{identifier}/reserve→ AVAILABLE → RESERVED + Redisnum:reserve:{id}TTL 900s - Non-AVAILABLE → 409 with current state
- Concurrent attempts → exactly one wins via partial unique index
(identifier) WHERE state IN ('RESERVED','HELD','LEASED') - TTL expiry → AVAILABLE +
numbering.reservation.released{reason: "TTL_EXPIRED"} - Tenant exceeds
maxActiveReservations→ 403RESERVATION_QUOTA -
numbering.reservation.held{kind: "RESERVE"}event published
US-NUM-008 · Hold Identifier (24-h TTL)
Type: Feature | Points: 3
Description: As a tenant in a build-phase workflow, I need to promote a reservation to a 24-hour hold so that I have a longer window to complete KYC, billing, or campaign config before commit.
Acceptance Criteria:
-
POST /v1/numbers/{identifier}/hold→ RESERVED (own) → HELD with TTL 86400s - Wrong tenant or state → 409
- HELD expiry → AVAILABLE + event
- Starter plan limit > 5 holds → 403
HOLD_QUOTA - PG + Redis updated atomically; Redis-only failure → PG row remains, sweep handles release
US-NUM-009 · Release Reservation or Hold
Type: Feature | Points: 2
Description: As a tenant, I need to release a reservation or hold I no longer need so that the identifier returns to the available pool and I free a quota slot.
Acceptance Criteria:
-
POST /v1/numbers/{identifier}/release→ AVAILABLE + Redis key delete - LEASED → 409
USE_RECALL_FOR_LEASES - Held by other tenant → 403
-
numbering.reservation.released{reason: "TENANT_RELEASE"}published - Releasing alpha-ID reservation does not cascade to leased numbers
US-NUM-010 · Auto-Release on TTL Expiry
Type: Feature | Points: 5
Description: As the pool integrity guard, I need to automatically release expired reservations and holds within 2 seconds of TTL so that the pool stays current and tenants do not see stale unavailable identifiers.
Acceptance Criteria:
- Redis keyspace expiry → worker transitions PG row within 2 s
- Fallback sweep job every 60 s for missed notifications
-
numbering.reservation.released{reason: "TTL_EXPIRED"}published - Race-safe via
UPDATE … WHERE state = 'RESERVED' - Metrics:
num_reservations_auto_released_total,num_reservations_active
EP-NUM-03 · Per-Tenant Number Pool Management
Context: Tenant-scoped quotas (MSISDN, short code, alpha, active reservations). Vanity short-code long-term reservation with auto-renew and extended quarantine. Pool capacity monitoring with exhaustion alerts. Tenant browse with filters and pricing quotes.
US-NUM-011 · Per-Tenant Pool Quota Enforcement
Type: Feature | Points: 5
Description: As the platform, I need to enforce per-tenant quotas on leased MSISDNs, short codes, and alpha IDs so that tenant plans and billing tiers are honored.
Acceptance Criteria:
- At quota → 403
QUOTA_EXCEEDED {quota, current, identifierClass} -
numbering.tenant_quotascached in Redis 5 min TTL - Admin update emits
numbering.quota.changed+ invalidates cache immediately - Plan upgrade via billing-service applies within 60 s
- Reservation quota independent: exceed →
RESERVATION_QUOTA
US-NUM-012 · Vanity Short-Code Long-Term Reservation
Type: Feature | Points: 5
Description: As a premium tenant, I need to reserve a vanity short code for a multi-year term with auto-renew so that my brand short code is protected from competitors.
Acceptance Criteria:
-
POST /v1/numbers/{shortCode}/leasewith{vanityFlag: true, term: "1y"|"3y", autoRenew}creates lease - Non-vanity-eligible short code → 422
NOT_VANITY_ELIGIBLE -
numbering.leases.vanity = true; vanity tier pricing applied - Recall → 365-day quarantine (configurable)
- Renewal failure → 14-day grace + daily reminder events
US-NUM-013 · Pool Capacity Monitoring
Type: Feature | Points: 3
Description: As a platform operator, I need alerts when an MSISDN block or short-code range is approaching exhaustion so that I can request a new block from the MNO before tenants are blocked.
Acceptance Criteria:
-
numbering_block_capacity_remainingper (operatorId, blockPrefix) exported - Remaining < 5 % →
numbering.pool.exhaustedevent - Remaining < 1 % → SEV1
numbering_block_critical+ page -
GET /v1/admin/numbering/pools/capacityreturns utilisation + projected exhaustion - New block import recalculates + auto-resolves alerts
- Short-code range alert references manual ATRA-request runbook
US-NUM-014 · Tenant Pool Browse with Filters
Type: Feature | Points: 5
Description: As a tenant via customer portal, I need to browse the available identifier pool with filters so that I can choose identifiers fitting my campaign budget and routing needs.
Acceptance Criteria:
-
GET /v1/numbers/available?identifierClass=&operatorId=&prefix=&vanity=&page=paginated with per-tenant pricing - Cursor-based pagination, stable sort
- Per-tenant operator allowlist filters results
- Rate limit 60 req/min per tenant → 429 with
Retry-After - Wildcard alpha search
?identifierClass=ALPHA&pattern=BANK*
End of report — 3 epics, 14 stories, 64 points