Overall Status
NON-COMPLIANT
1 missing criterion — AC-047 (Pact contract tests, LOW severity)
Reviewed Commit
d3f704ab
2026-03-19
46
Criteria Met
0
Partial
1
Missing
98%
Compliance
Spec coverage
46 / 47
Resolved This Cycle 4 criteria fixed
AC-043
Quiet hours: defer notification during quiet window
PARTIAL MET
commit 2f33b1c
Now sets initial_status = Some(Queued) when recipient is in quiet window. Deferred notifications are distinguishable from normal Pending ones by status.
AC-044
Dockerfile port consistency with runtime default
PARTIAL MET
commit 2f33b1c
Corrected from EXPOSE 8080 to EXPOSE 8085, now matching SERVER_PORT default in src/config.rs.
AC-045
Template events emitted on state changes
MISSING MET
commit fd4a2cf
Template handlers accept Option<EventProducer> and emit template.created, template.updated, template.deleted. Graceful degradation on publish failure.
AC-046
Soft delete on preferences (no hard deletes)
MISSING MET
commit 5981652
Migration 002_preferences_soft_delete.sql adds deleted_at + partial index. Repository delete() now issues UPDATE SET deleted_at = now(). Upsert resets deleted_at = NULL.
Remaining Gap 1 missing
AC-047
Pact contract tests between notification-hub and consumer services
MISSING LOW severity
No pact crate in Cargo.toml dev-dependencies. No contract test files found under tests/ or anywhere in the project tree. Global CLAUDE.md requires “Contract tests: Pact between each service pair” and the service checklist mandates “Write API contract tests (Pact) FIRST.” This is a process and testing compliance gap — it does not affect runtime behavior of the deployed service.
Spec ref: Global CLAUDE.md — TDD Discipline / Contract tests · Service checklist item 2
All Criteria 47 total
ID Criterion Status Evidence
AC-001Notification entity domain model with required fieldsMETsrc/domain/notification.rs — Notification struct with all fields incl. deleted_at
AC-002Template entity with subject/body templates and is_active flagMETsrc/domain/template.rs
AC-003Preference entity with channel, enabled flag, quiet hoursMETsrc/domain/preference.rs — now includes deleted_at (migration 002)
AC-004Channel enum: Email, Sms, InApp, WebhookMETsrc/domain/enums.rs + migrations/001_init.sql channel_type ENUM
AC-005Priority enum: Low, Normal (default), High, UrgentMETsrc/domain/enums.rs — Normal is #[default]; SQL DEFAULT 'normal'
AC-006NotificationStatus: Pending, Queued, Sent, Delivered, Failed, ReadMETsrc/domain/enums.rs — all 6 variants
AC-007Validation: notification requires body XOR template_idMETsrc/domain/dto.rs:66-84
AC-008Validation: template name and body_template not emptyMETsrc/domain/dto.rs:86-104
AC-009Template rendering: mustache-style variable substitutionMETsrc/domain/template.rs:23-47 — render_body() + render_subject()
AC-010Quiet hours logic with midnight-wrap supportMETsrc/domain/preference.rs:21-33 — handles 22:00–06:00 wrap
AC-011Reject notification if channel disabledMETsrc/service/notification_service.rs:31-37
AC-012Reject if referenced template is inactiveMETsrc/service/notification_service.rs:55-60
AC-013POST /api/v1/notifications → 201 CreatedMETsrc/api/notifications.rs:12-36
AC-014GET /api/v1/notifications → paginated list scoped to tenantMETsrc/api/notifications.rs:39-46 — per_page capped at 100
AC-015GET /api/v1/notifications/{id}METsrc/api/notifications.rs:49-57
AC-016GET /api/v1/notifications/recipient/{recipient_id}METsrc/api/notifications.rs:60-71
AC-017PATCH /api/v1/notifications/{id}/read → mark as readMETsrc/api/notifications.rs:74-98 — sets read_at timestamp
AC-018PATCH /api/v1/notifications/{id}/statusMETsrc/api/notifications.rs:102-136
AC-019DELETE /api/v1/notifications/{id} → soft delete 204METsrc/api/notifications.rs:139-155
AC-020POST /api/v1/templates → 201 CreatedMETsrc/api/templates.rs:11-30
AC-021GET /api/v1/templates → paginated listMETsrc/api/templates.rs:33-39
AC-022GET /api/v1/templates/{id}METsrc/api/templates.rs:43-51
AC-023PUT /api/v1/templates/{id} → partial updateMETsrc/api/templates.rs:54-71 — COALESCE for optional fields
AC-024DELETE /api/v1/templates/{id} → soft delete 204METsrc/api/templates.rs:74-90
AC-025PUT /api/v1/preferences/{user_id} → upsertMETsrc/api/preferences.rs:11-30 — ON CONFLICT DO UPDATE; resets deleted_at = NULL
AC-026GET /api/v1/preferences/{user_id}METsrc/api/preferences.rs:33-42 — filters WHERE deleted_at IS NULL
AC-027GET /health → liveness 200 OKMETsrc/api/health.rs
AC-028GET /health/ready → readiness + DB connectivityMETsrc/api/health.rs — SELECT 1; returns 503 if DB unreachable
AC-029PostgreSQL schema notifications with all required tablesMETmigrations/001_init.sql — 3 tables + 3 enum types
AC-030RLS policies on all 3 tablesMETmigrations/001_init.sql — ENABLE ROW LEVEL SECURITY + CREATE POLICY tenant_isolation_* on all 3
AC-031RLS enforced at runtime via SET LOCAL app.tenant_idMETsrc/db.rs:begin_tenant_tx() — integration test test_rls_enforced_at_database_level
AC-032Soft delete on notificationsMETsrc/repository/notifications.rs:220-241
AC-033Soft delete on templatesMETsrc/repository/templates.rs:156-177
AC-034Unique constraint on templates (tenant_id, name)METmigrations/001_init.sql — constraint + AppError::Conflict mapping
AC-035Performance indexes on tenant-scoped tablesMETmigrations/001_init.sql + partial idx_preferences_active in 002
AC-036JWT signature verification with cryptographic validationMETsrc/api/auth.rs:verify_jwt() — jsonwebtoken crate; 13+ unit tests
AC-037tenant_id from JWT → TenantContextMETsrc/api/auth.rs — X-Tenant-Id header fallback for M2M
AC-038EventProducer instantiated and wired in main.rsMETsrc/main.rs — used by notification service and template handlers
AC-039Notification events emitted on all state changesMETsrc/service/notification_service.rs:86-101, 149-171
AC-040CloudEvents v1.0 envelope formatMETsrc/events/producer.rs — CloudEvent struct with all required fields
AC-041Topic events.notification-hub; tenant_id as partition keyMETsrc/events/producer.rs — const TOPIC; FutureRecord::key(&tenant_id)
AC-042JSON structured logging with tenant_id + correlation_idMETsrc/main.rs — tracing_subscriber::fmt().json()
AC-043Quiet hours: defer notification when in quiet windowMET FIXEDdeferred=true → initial_status=Some(Queued) in notification_service.rs
AC-044Dockerfile port consistency with runtime defaultMET FIXEDDockerfile EXPOSE 8085 matches SERVER_PORT default in src/config.rs
AC-045Template events emitted (created / updated / deleted)MET FIXEDsrc/api/templates.rs — create(), update(), soft_delete() emit CloudEvents
AC-046Soft delete on preferences (no hard deletes)MET FIXEDmigrations/002_preferences_soft_delete.sql + repository UPDATE SET deleted_at
AC-047Pact contract tests between notification-hub and consumersMISSINGNo pact crate in Cargo.toml. No contract test files found anywhere.
47 criteria evaluated 46 MET · 0 PARTIAL · 1 MISSING 4 resolved this cycle
Review Notes & Context

Incremental review from c386b451 (2026-03-19). Four of five previously-flagged deviations resolved across commits 5981652, fd4a2cf, and 2f33b1c.

Quiet-hours implementation: The deferral approach sets initial_status = Some(NotificationStatus::Queued) when a recipient is in their quiet window. Deferred notifications are distinguishable from normally-pending ones by status. A background worker to dispatch Queued notifications at the end of the quiet window is not yet implemented but is a separate concern outside the current acceptance criteria.

Preferences soft-delete: The upsert operation correctly resets deleted_at = NULL on conflict, restoring previously-deleted preferences when the user re-enables them. The partial index idx_preferences_active WHERE deleted_at IS NULL keeps query performance unaffected by accumulating soft-deleted rows.

Template CloudEvents: Graceful degradation (log error, do not abort API response) is consistent with the notification event pattern — the service does not make API success dependent on Redpanda availability.

Remaining gap (AC-047): LOW severity, does not affect runtime correctness. Should be addressed before the service enters production integration with consumer services (Workflow Engine, Form Engine) that listen on events.notification-hub.

Service is at 46/47 criteria met (98%). Status remains non-compliant per the rule that criteria_missing == 0 is required for compliance.