critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

PauseNotificationBuilder.buildCoordinatorPayload(mentorName: String, returnDate: DateTime?, organisationId: String) method defined as a static method returning CoordinatorPausePayload
Title is built from the localisation key 'notifications.pause.coordinator.title' with no interpolation (generic: 'Peer Mentor on Pause')
Body is built from localisation key 'notifications.pause.coordinator.body' interpolating mentorName and formatted returnDate; if returnDate is null, uses localisation key 'notifications.pause.coordinator.body_no_date'
returnDate is formatted as locale-aware short date (e.g. '15 Apr 2026') using the Dart intl package with locale resolved from organisationId settings
If interpolated body exceeds 240 characters, mentorName is truncated to fit within the constraint and a Unicode ellipsis (…) is appended
If interpolated title exceeds 65 characters after localisation (possible in future locales), title is truncated with ellipsis
Returned CoordinatorPausePayload.toFcmPayloadMap() produces a valid FCM data map consumed by task-006 dispatcher
Builder does not perform I/O, database queries, or async operations — it is a pure synchronous function
Builder is covered by tests that assert correct interpolation, truncation behaviour, and null returnDate handling

Technical Requirements

frameworks
Flutter
Dart
intl (Dart package for date formatting)
data models
CoordinatorPausePayload
PauseNotificationPayload
PauseNotificationRecipientType
performance requirements
Builder method must complete synchronously with no blocking calls
String interpolation and truncation must complete in O(n) time relative to string length
security requirements
mentorName must be sanitised before interpolation: strip any HTML/markdown characters to prevent notification injection if mentorName is displayed in a WebView
No PII beyond display name and return date in the payload — do not include user_id, role_id, or internal IDs in the FCM data map values visible to device

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement PauseNotificationBuilder as a class with only static methods (no instance state) to keep it stateless and easily testable without construction. Localisation strings should be resolved via Flutter's AppLocalizations (generated from .arb files); the builder receives a BuildContext or an AppLocalizations instance as a parameter to stay testable without a widget tree. Truncation logic: compute the maximum allowed length for mentorName as maxBodyLength - templateLengthWithEmptyName - 1 (for ellipsis), then truncate mentorName before interpolation. This avoids truncating in the middle of multi-byte Unicode characters — use String.characters from the characters package for grapheme-cluster-safe truncation.

Do not truncate in the middle of words if possible: prefer truncating at the last word boundary before the limit. Date formatting: use DateFormat.yMMMd(locale) from the intl package. Locale string should be derived from the organisation's configured locale stored in Riverpod state, not from the device locale, to ensure notifications match the organisation's language even on devices with different system language.

Testing Requirements

Unit tests using flutter_test. All tests are synchronous. Required test cases: (1) standard input with mentorName and returnDate produces correct interpolated title and body, (2) returnDate null uses body_no_date template, (3) very long mentorName (>100 chars) triggers truncation and body remains ≤ 240 chars, (4) very long mentorName where even truncated version with ellipsis does not fit triggers further truncation correctly, (5) toFcmPayloadMap() on returned payload contains required FCM keys, (6) returned recipientType is PauseNotificationRecipientType.coordinator. Use test fixtures for localisation strings rather than loading real .arb files — inject strings via a TestLocalisationDelegate or simple map.

Component
Pause Notification Payload Builder
service low
Epic Risks (3)
high impact medium prob integration

The org membership table structure used to resolve coordinator relationships may differ from what the repository assumes, causing incorrect coordinator lookup or missing rows for mentors in multi-chapter scenarios.

Mitigation & Contingency

Mitigation: Review the existing org membership table schema and RLS policies before writing repository queries. Align query logic with the patterns already used by peer-mentor-status-repository and multi-chapter-membership-service.

Contingency: If schema differs, add an adapter layer in the repository that normalises the membership resolution and document the discrepancy for the data team. Fall back to coordinator lookup via the feature's own stored coordinator_id field if org membership join fails.

high impact medium prob technical

Device tokens stored in the database may be stale or unregistered, causing FCM dispatch failures that silently drop coordinator notifications — the primary coordination safeguard of this feature.

Mitigation & Contingency

Mitigation: Implement token validation on every dispatch call and handle FCM's NOT_REGISTERED error by flagging the token as invalid in the database. Reuse the token refresh pattern already established by fcm-token-manager.

Contingency: If push delivery fails after retry, ensure the in-app notification record is always written regardless of push outcome so coordinators can still see the event in the notification centre.

medium impact low prob technical

The optional reason field may contain special characters, emoji, or non-Latin scripts that exceed the 200-character byte limit when FCM encodes the payload, causing delivery failures.

Mitigation & Contingency

Mitigation: Enforce the 200-character limit on Unicode code point count, not byte count, in the payload builder. Add a unit test with multi-byte input strings.

Contingency: If an oversized payload is detected at dispatch time, strip the reason field from the push notification body and note 'See in-app notification for full reason' to preserve delivery.