Story CP-010: Notification Backend Service + Templates¶
Module: control-plane Slice: Slice 10 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 10 Depends on: CP-001, CP-002, CP-003 Estimated complexity: M
Objective¶
Build the shared notification backend service used by ALL modules: in-app notifications (via WebSocket STOMP), email delivery, and template rendering. Seed the 13 MVP email templates. Notification UI (bell, dropdown, unread count) is handled in CP-027.
Backend Scope¶
Entities & Records¶
Notification entity (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL |
| recipient_user_id | UUID | NOT NULL |
| template_code | String | NOT NULL |
| channel | NotificationChannel enum | NOT NULL |
| priority | NotificationPriority enum | NOT NULL |
| title | String | NOT NULL |
| body | String | NOT NULL |
| is_read | boolean | NOT NULL, default false |
| metadata | JSONB | Nullable (extra context) |
| created_date | Instant | NOT NULL |
NotificationChannel: IN_APP, EMAIL, BOTH
NotificationPriority: LOW, NORMAL, HIGH, URGENT
NotificationTemplate entity (platform DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK |
| code | String | NOT NULL, unique |
| channel | NotificationChannel | NOT NULL |
| subject_template | String | NOT NULL (for email) |
| body_template | String | NOT NULL (Mustache/Handlebars) |
| priority | NotificationPriority | NOT NULL |
MVP Templates (from PRD FR-CP-091):
| Code | Trigger | Channel |
|---|---|---|
| ONBOARDING_WELCOME_A | Email verification | |
| ONBOARDING_WELCOME_B_PASSWORD | Channel B: password link | |
| ONBOARDING_WELCOME_B_PAYMENT | Channel B: payment link | |
| EMAIL_VERIFICATION | Resend verification | |
| PASSWORD_RESET | Password recovery | |
| INVOICE_GENERATED | Monthly invoice | |
| PAYMENT_RECEIVED | Payment confirmed | Email + In-App |
| PAYMENT_FAILED | Payment failed | Email + In-App |
| SUBSCRIPTION_EXPIRING | 30/15/7 days before expiry | Email + In-App |
| ACCOUNT_SUSPENDED | Tenant suspended | |
| ACCOUNT_REACTIVATED | Tenant reactivated | Email + In-App |
| USER_INVITATION | User invited | |
| MISSING_LEGAL_INFO | RCCM/NIF reminder | Email + In-App |
Repository Layer¶
NotificationRepository.java— Tenant-aware: findByRecipientUserId(Pageable), countUnreadByRecipientUserId, markAsRead, markAllAsReadNotificationTemplateRepository.java— Platform-scoped: findByCode
Service Layer¶
NotificationService.java (shared — consumed by all modules):
- send(UUID tenantId, UUID recipientUserId, NotificationChannel channel, String templateCode, Map<String,Object> params, NotificationPriority priority) → void
1. Resolve template by code
2. Render template with params (Mustache/Handlebars)
3. IF channel includes IN_APP: persist Notification + broadcast via WebSocket
4. IF channel includes EMAIL: queue email for async delivery
- getNotifications(UUID userId, UUID tenantId, Pageable) → Page<NotificationResponse>
- getUnreadCount(UUID userId, UUID tenantId) → int
- markAsRead(UUID notificationId) → void
- markAllAsRead(UUID userId, UUID tenantId) → void
EmailService.java: - Async email delivery via provider (Brevo/Mailjet/SES — provider adapter pattern) - Papillon-branded email templates (French, with butterfly logo) - Retry: 3 attempts with exponential backoff
TemplateRenderer.java: - Mustache/Handlebars template rendering - Supports: {{fullName}}, {{companyName}}, {{amount}}, {{date}}, {{link}}, etc.
WebSocket Configuration:
- WebSocketConfig.java — STOMP over SockJS configuration
- User-specific topics: /user/queue/notifications
- Tenant-scoped: notifications only delivered to users of the same tenant
- SockJS fallback for African proxy networks that may block WebSocket
NotificationBroadcaster.java: - Listens for new in-app notifications - Sends to connected WebSocket clients - Queues for offline users (delivered on next login)
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/notifications | ?page=&size= | Page |
Any authenticated |
| GET | /api/v1/notifications/unread-count | — | {count} |
Any authenticated |
| PATCH | /api/v1/notifications/{id}/read | — | 204 | Any authenticated |
| PATCH | /api/v1/notifications/read-all | — | 204 | Any authenticated |
| POST | /api/v1/notifications | Internal API (service-to-service) | 204 | Internal |
Multi-Tenant Considerations¶
- Notifications in tenant DB (tenant-scoped)
- Templates in platform DB (shared across tenants)
- WebSocket: STOMP user destination ensures notifications go to the right user/tenant
- No cross-tenant notification delivery possible
Flyway Migrations¶
V005__create_notification_templates.sql— Platform DB: notification_templates table + seed 13 MVP templatesV009__create_notification.sql— Tenant DB: notifications table
Frontend Scope¶
None for this story. Notification bell, dropdown, and unread count UI are in CP-027.
Acceptance Criteria¶
AC-107: Send in-app notification
Given user "Moussa" is logged in
When a notification is sent via NotificationService.send()
Then Moussa sees the notification in his bell dropdown in real-time (WebSocket)
AC-108: Send email notification
Given the PAYMENT_RECEIVED template exists
When a payment is confirmed
Then an email is sent to the tenant admin with correct template rendering
AC-112: Template rendering
Given the template "Bonjour {{fullName}}, votre paiement de {{amount}} FCFA a été confirmé."
When rendered with {fullName: "Moussa", amount: "15 000"}
Then the output is "Bonjour Moussa, votre paiement de 15 000 FCFA a été confirmé."
AC-113: Tenant-scoped notifications
Given user A belongs to tenant X and user B belongs to tenant Y
When a notification is sent to tenant X
Then only user A receives it (not user B)
OHADA & Regulatory Rules¶
- No direct OHADA regulatory requirements for notifications.
- Email templates must be in French (primary language for CI).
- Data protection: notification content may reference employee names or financial data — ensure notifications are tenant-scoped and user-scoped.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §WebSocket STOMP, §Async processing, §Template renderingdocs/standards/api-guidelines.md— §Notification endpoints
Testing Requirements¶
Unit Tests¶
- TemplateRenderer: all 13 templates render correctly with sample params
- NotificationService: channel routing (IN_APP only, EMAIL only, BOTH)
- NotificationService: tenant isolation — cannot send to wrong tenant
Integration Tests¶
- WebSocket: connect → receive notification in real-time
- Email: send via mock provider → verify rendered content
- Tenant isolation: notification sent to tenant X not visible to tenant Y
What QA Will Validate¶
- WebSocket delivery works in real-time
- Email templates render correctly (verify via email testing tool)
- All 13 MVP templates seeded and functional
Out of Scope¶
- Notification UI (bell, dropdown, unread count) — CP-027
- Offline notification queuing — CP-027
- Mark as read / mark all as read UI — CP-027
- SMS notifications — V2
- Push notifications (mobile) — V2
- Notification preferences (opt out of specific types) — V2
- Tenant logo in email templates — V2
- Admin console notification management — CP-012
- Email provider selection (Brevo vs Mailjet vs SES) — Infrastructure decision, abstract behind EmailService
Definition of Done¶
- [ ] All backend endpoints implemented and returning correct responses
- [ ] All acceptance criteria pass manually
- [ ] Unit tests written and passing
- [ ] Integration tests written and passing
- [ ] Multi-tenant isolation verified (no cross-tenant data leaks)
- [ ] No Java raw types — all generics explicit
- [ ] API responses follow standard envelope format
- [ ] All 13 MVP templates seeded via Flyway migration