Story NOTIF-007: Per-agency dispatch queue partitioning (agency_id)¶
Module: notifications
Slice: v0.0.2 NOTIF — per-agency send queue (PRD V0 Envelope v0.0.2; architecture §2.1 note "agency_id nullable in v0.0.0 (NOT NULL from v0.0.2)" + §4.3)
Side: [BACKEND]
Version target: V0
Sub-version: v0.0.2
Priority: 11 (v0.0.2; after v0.0.1 stories; aligned with TENANT introducing agency in the model)
Depends on: NOTIF-001a, NOTIF-001b (dispatch + SMS adapter); TENANT v0.0.2 agency model (agency rows + tenant.default_agency_id)
Can develop concurrently with: None (single v0.0.2 NOTIF slice)
Merge order: After TENANT v0.0.2 agency migration is merged
Estimated complexity: M
PRD User Stories: N/A — technical slice derived from PRD V0 Envelope v0.0.2 row + architecture forward-compat hooks
Wireframe: N/A — backend only
⚠️ [ARCH-PENDING] — statut PRÉLIMINAIRE¶
Architecture is v0.0.0-only; v0.0.2 ("per-agency queue") is a forward-compat checkpoint, not a designed slice. The v0.0.0 schema pre-provisioned
agency_id(nullable) and the index strategy for exactly this. This story is drafted from those hooks + the PRD V0 Envelope. Reconcile with the ARCHITECT v0.0.2 pass before dev-start — especially the queue/worker partitioning mechanism (in-process executor key vs. dedicated queue) and the backfill strategy for existing rows.
Objective¶
v0.0.2 is when the agency enters the TENANT model (a tenant may have N domestic agencies). NOTIF must isolate dispatch ordering per agency so one agency's large campaign cannot starve another agency's sends within the same tenant. This story makes agency_id mandatory on the notification, backfills existing rows, and partitions the send queue/worker by (tenant_id, agency_id). No rate-limit or quota yet — only queue isolation (PRD V0 Envelope: "Pas encore de quota / rate-limit ; uniquement isolation des files").
Backend Scope¶
Entities¶
notification.agency_idUUID — change from nullable (v0.0.0) to NOT NULL (v0.0.2). Already a column since v0.0.0 per architecture §2.1.
Migrations¶
Vxxx__notif_agency_id_backfill.sql:- Backfill
notification.agency_idfor existing rows fromtenant.default_agency_id(per architecture §4.3 "writetenant.default_agency_idwhen available"). ALTER TABLE notification ALTER COLUMN agency_id SET NOT NULL;- Add/confirm an index supporting the partitioned scan, e.g.
(tenant_id, agency_id, status)and the dispatch-ordering index(tenant_id, agency_id, enqueued_at).
Service Layer¶
DispatchQueuepartition key changes fromtenant_idto(tenant_id, agency_id). Sends are ordered/fair per agency; one agency's backlog does not block another's within the tenant.- The
POST /dispatchpayload already carriesagency_id(architecture §3.2). v0.0.2 makes it required (was optional/derivable in v0.0.0). SmsDispatcher/ WA cascade readagency_idfrom the row; no behavioral change to provider submit, only to which partition the work is pulled from.
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| POST | /api/notifications/dispatch |
now requires non-null agency_id |
202 {notification_id, status} or 400 {reason: "agency_id_required"} |
Service (mTLS/internal) |
Validation Rules¶
agency_idis now mandatory; a missing/nullagency_id→ HTTP 400agency_id_required(was tolerated in v0.0.0 by defaulting totenant.default_agency_id; v0.0.2 producers must send it).agency_idMUST belong to thetenant_id(verified against TENANT config cache) → else 400agency_not_in_tenant.
Multi-Tenant Considerations¶
agency_idis a sub-unit within a tenant, not a separate tenant (CLAUDE.md LOCKED tenant definition). Isolation remains primarily bytenant_id;agency_idadds intra-tenant fairness, not a new isolation boundary.- Every query stays scoped by
tenant_idfirst, thenagency_id. Both V0 DB profiles (SHARED, BYO) must work; BYO arrives at v0.0.4 but the partition key must already be correct.
Audit & Logging¶
- No new audit event. Existing send events (
NOTIF_SENDING/NOTIF_SENT/NOTIF_FAILURE) MUST now includeagency_idin their payload (it was nullable before). Confirm the AUDIT producer contract accepts it.
Frontend Scope¶
N/A — backend only.
Acceptance Criteria¶
AC-007.1: agency_id is mandatory and tenant-checked
Given a producer call to POST /dispatch
When agency_id is missing or does not belong to tenant_id
Then NOTIF rejects with HTTP 400 (agency_id_required or agency_not_in_tenant)
And no notification row is created
AC-007.2: dispatch is partitioned per agency
Given two agencies of the same tenant each enqueue a batch of sends
When the dispatcher runs
Then sends are pulled fairly per (tenant_id, agency_id) partition
And a large backlog in agency A does not starve agency B within the same tenant
AC-007.3: existing rows are backfilled and column is NOT NULL
Given notification rows created under v0.0.0 with null agency_id
When the v0.0.2 migration runs
Then every row gets agency_id from tenant.default_agency_id
And agency_id is enforced NOT NULL afterwards
Compliance Rules¶
- No new compliance obligation. Tenant-isolation critical rule (CLAUDE.md #1) extended with the
agency_idsub-scope. ARTCI B5/B10 production BLOQUANT unchanged.
Standards & Conventions¶
docs/standards/critical-rules.md(tenant isolation;agency_idsub-scope).docs/standards/multi-tenant-model.md(agency = intra-tenant sub-unit).docs/standards/db-guidelines.md(migration safety: backfill before SET NOT NULL).docs/standards/java-spring-guidelines.md.
Testing Requirements¶
Unit Tests¶
agency_idrequired-field validation; tenant membership check.- Partition key resolves to
(tenant_id, agency_id).
Integration Tests¶
- Fairness: agency A floods the queue; agency B's sends still progress (no starvation).
- Migration test: rows with null
agency_idare backfilled; column becomes NOT NULL; no data loss. - Tenant isolation preserved with the added agency sub-scope.
What QA Will Validate¶
- Two agencies in one tenant get fair dispatch ordering; a missing
agency_idis rejected; the backfill migration is reversible-safe and idempotent.
Out of Scope¶
- Per-tenant or per-agency rate-limit / daily cap (v0.1.0 — NOTIF-008).
- Platform kill-switch (v0.1.0 — NOTIF-009).
- Cross-agency consolidation/reporting (V2).
- BYO-DB datasource routing (v0.0.4).
Definition of Done¶
- [ ]
agency_idenforced NOT NULL after backfill migration - [ ]
POST /dispatchrejects missing / cross-tenantagency_id - [ ] Dispatch partitioned and fair per
(tenant_id, agency_id) - [ ] Send audit events include
agency_id - [ ] All acceptance criteria pass manually
- [ ] Unit + integration tests written and passing (incl. starvation + migration tests)
- [ ] Tenant isolation verified (
tenant_id+agency_id; both V0 DB profiles tested) - [ ] No silent failures on network errors
- [ ] [ARCH-PENDING] queue-partitioning + backfill mechanism reconciled with the ARCHITECT v0.0.2 pass before merge to a release branch