Story NOTIF-008: Tenant per-day send cap¶
Module: notifications
Slice: v0.1.0 NOTIF — tenant daily cap (PRD V0 Envelope v0.1.0 "le cap par tenant ... deviennent actifs"; V1 PRD §15.2; NOTIF-US-04)
Side: [BACKEND]
Version target: V0
Sub-version: v0.1.0
Priority: 12 (v0.1.0; after v0.0.2)
Depends on: NOTIF-001a, NOTIF-001b (dispatch + SMS adapter); TENANT notif.daily_cap setting; AUDIT catalog extended with NOTIF_CAP_EXCEEDED
Can develop concurrently with: NOTIF-009, NOTIF-010
Merge order: Any order within v0.1.0 (independent of NOTIF-009 / NOTIF-010)
Estimated complexity: M
PRD User Stories: NOTIF-US-04 (AC-04.1, AC-04.2, AC-04.3, AC-04.4)
Wireframe: N/A — backend only
⚠️ [ARCH-PENDING] — statut PRÉLIMINAIRE¶
Architecture is v0.0.0-only; the cap is "designed-for but inactive" there (architecture §0 + §2.3 reserves the
PENDING_CAPstatus precisely so it can activate later without a migration). This story is drafted from that reserved hook + V1 PRD §15.2 + NOTIF-US-04. Reconcile with the ARCHITECT v0.1.0 pass before dev-start — particularly the counter store (DB vs. Redis, see NOTIF-010) and the midnight-rollover scheduler.
Objective¶
v0.1.0 activates the per-tenant daily send cap so a misconfigured campaign cannot generate runaway provider cost. When a tenant exceeds its daily cap, further sends are held (not dropped) in the reserved PENDING_CAP status and auto-released at the next local midnight. The cap default is 5000/day, configurable per tenant via TENANT.
Backend Scope¶
Entities¶
- Reuses
notification.status— activates the reserved valuePENDING_CAP(declared TEXT in v0.0.0 for exactly this; architecture §2.3 "reserved but unreachable in v0.0.0"). - A per-tenant per-local-day counter (proposed: Redis counter from NOTIF-010, or a
notif_daily_count (tenant_id, local_day, count)table — decide in ARCHITECT pass).
Migrations¶
- If a DB counter is chosen:
Vxxx__notif_daily_count.sql—CREATE TABLE notif_daily_count (tenant_id UUID, local_day DATE, count INT, PRIMARY KEY (tenant_id, local_day)). No schema change needed forstatus(already TEXT).
Service Layer¶
DailyCapGuard.check(tenant_id)invoked before each dispatch. Reads the effective cap (tenant.notif.daily_cap, default 5000) and the current local-day count.- Day boundary = Africa/Abidjan midnight (AC-04.1), not UTC.
- On cap reached: set
status = PENDING_CAP, do not submit to provider, emitNOTIF_CAP_EXCEEDEDonce per tenant per cap-rollover (deduplicated within the day). CapReleaseJobruns at local midnight: releasesPENDING_CAProws in FIFO order subject to the fresh day's budget.
API Endpoints¶
None new (cap is enforced inside the existing dispatch path). GET /api/notifications/{id} will surface PENDING_CAP as the current status.
Validation Rules¶
- Effective cap =
tenant.notif.daily_capif set, else default 5000. A cap< 1is rejected at TENANT write-time (TENANT-side AC-04.2); NOTIF trusts the validated value. - Counter increments only on an actually-submitted send (held sends do not consume budget until released).
Multi-Tenant Considerations¶
- Cap is per tenant (the billable legal entity), not per agency. Counter keyed by
tenant_id+ local day. Both V0 DB profiles must work. - Local day uses the tenant's country timezone resolved from
country_profile(CI → Africa/Abidjan).
Audit & Logging¶
NOTIF_CAP_EXCEEDED— emitted once per tenant per cap-rollover (dedup within the day). Required fields per AUDIT producer contract (§B.3):tenant_id,local_day,cap,correlation_id. AUDIT catalog must admit thisevent_typebefore this story ships.
Frontend Scope¶
N/A — backend only. (Operator banner for cap is V1 / OP-owned.)
Acceptance Criteria¶
AC-008.1: Default cap applies
Given a tenant without an explicit notif.daily_cap
When NOTIF evaluates the cap
Then daily_cap = 5000 sends per local day (rolled at Africa/Abidjan midnight) applies
AC-008.2: Configured cap is honored
Given a tenant whose notif.daily_cap was set via TENANT
When NOTIF reads tenant config at dispatch
Then it honors the configured cap instead of the default
AC-008.3: Cap reached holds the send
Given a tenant has reached its daily cap
When the (cap+1)-th send is dispatched in the current local day
Then NOTIF holds the notification in PENDING_CAP (does not submit to provider)
And emits exactly one NOTIF_CAP_EXCEEDED audit event for that tenant for that cap-rollover
AC-008.4: Auto-release at local midnight
Given notifications held in PENDING_CAP
When local (Africa/Abidjan) midnight rolls
Then held notifications resume in FIFO order, subject to the fresh day's cap budget
Compliance Rules¶
- No direct compliance obligation; the cap is a cost/abuse safeguard (V1 PRD §15.2). ARTCI B5/B10 production BLOQUANT unchanged.
NOTIF_CAP_EXCEEDEDmust be in the AUDIT §B.1 catalog (critical rule #3 — AUDIT rejects unknownevent_type).
Standards & Conventions¶
docs/standards/critical-rules.md(#3 audit-log obligation; tenant isolation).docs/standards/java-spring-guidelines.md(scheduled job conventions).docs/standards/db-guidelines.md(counter table if DB-backed).docs/standards/multi-tenant-model.md(cap is per-tenant, country timezone resolution).
Testing Requirements¶
Unit Tests¶
- Default vs. configured cap selection.
NOTIF_CAP_EXCEEDEDdeduplicated to one per tenant per day.- Local-day boundary computed at Africa/Abidjan midnight, not UTC.
Integration Tests¶
- (cap+1)-th send →
PENDING_CAP, not submitted; one audit event. - Midnight rollover releases held sends FIFO within fresh budget.
- Two tenants' caps are independent.
What QA Will Validate¶
- A flooded tenant is held at exactly its cap; held sends release next local midnight in order; the audit event fires once per rollover.
Out of Scope¶
- Platform kill-switch (NOTIF-009).
- Redis cross-cutting rate-limit + retry policy (NOTIF-010).
- Per-agency cap (not in scope; cap is per-tenant).
- Operator UI for cap state (V1).
Definition of Done¶
- [ ] Cap enforced before provider submit; held sends go to
PENDING_CAP - [ ] Default 5000 + configured cap both honored
- [ ]
CapReleaseJobreleases FIFO at Africa/Abidjan midnight - [ ]
NOTIF_CAP_EXCEEDEDemitted once per tenant per rollover; present in AUDIT catalog - [ ] All acceptance criteria pass manually
- [ ] Unit + integration tests written and passing
- [ ] Tenant isolation verified (per-tenant counter; both V0 DB profiles tested)
- [ ] No silent failures on network errors
- [ ] [ARCH-PENDING] counter store + scheduler reconciled with the ARCHITECT v0.1.0 pass before merge to a release branch