critical priority medium complexity backend pending backend specialist Tier 5

Acceptance Criteria

MarkNotificationRead event handler exists in NotificationBloc and accepts a notificationId parameter
MarkAllNotificationsRead event handler exists in NotificationBloc with no required parameters
On MarkNotificationRead, the bloc delegates mutation to Notification Read State Service before emitting new state (optimistic update)
After successful mark-read, the targeted notification in state has is_read=true and a non-null read_at timestamp matching the value returned by the service
After MarkNotificationRead, the unread count in state is decremented by exactly 1 (or stays at 0 if already 0)
After MarkAllNotificationsRead, all notifications in state have is_read=true and the unread count is 0
On rollback signal from the service (e.g., repository failure), the notification list and unread count revert to the exact pre-mutation snapshot
On rollback, the bloc emits a NotificationError state or side-effect that surfaces an error message to the UI layer
Pre-mutation snapshot is taken immediately before optimistic update and discarded after confirmed success
Marking an already-read notification does not decrement unread count below 0 or alter read_at
BLoC state transitions are immutable — new state objects are emitted, never mutated in place
All event handlers are implemented as EventTransformer-compatible handlers (sequential or droppable as appropriate)

Technical Requirements

frameworks
Flutter
BLoC (bloc ^8.x)
flutter_bloc
apis
Supabase REST/RPC for mark-read mutations
Supabase Realtime (read receipts sync)
data models
Notification (id, is_read, read_at, unread_count)
NotificationState (notifications list, unreadCount, activeFilter)
NotificationReadStateService
performance requirements
Optimistic update must be emitted within one frame of event receipt (no async gap before UI reflects change)
Mark-all-read must update state in a single emit, not one emit per notification
Pre-mutation snapshot must not clone entire notification list unnecessarily — use List.unmodifiable or copyWith patterns
security requirements
Mark-read RPC/REST call must include authenticated Supabase session token
Users may only mark their own notifications as read — validate ownership in service layer
read_at timestamp must originate from server response, not client clock, to prevent manipulation

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Use the standard BLoC pattern: emit an optimistic state immediately, then await the service call. Store the pre-mutation state as a local variable before emitting. If the service returns a rollback signal (throws or returns a failure result type), emit the saved snapshot state followed by an error state/event. For MarkAllNotificationsRead, build the updated list with a single map() pass and emit once — avoid multiple emits.

Use copyWith on the Notification model for immutability. The read_at value should be extracted from the service response, not DateTime.now(). Consider using a sealed Result type from the service to distinguish success/rollback cleanly. Ensure the EventTransformer for MarkNotificationRead is sequential (not concurrent) to prevent race conditions if the user taps multiple notifications quickly.

Testing Requirements

Unit tests using flutter_test and bloc_test. Cover: (1) MarkNotificationRead emits optimistic state before service resolves, (2) state after successful mark-read has correct is_read and read_at values, (3) unread count decrements correctly, (4) MarkAllNotificationsRead sets all is_read=true and unread=0 in one emit, (5) rollback on service failure restores pre-mutation snapshot exactly, (6) error side-effect is emitted on rollback, (7) marking an already-read notification is a no-op on unread count. Use mocktail to mock the Notification Read State Service. Minimum 90% line coverage on the event handler code paths.

Component
Notification BLoC
service high
Epic Risks (3)
medium impact medium prob technical

A Realtime INSERT event arriving during an in-flight mark-all-read operation can cause the new notification to be incorrectly marked read in the optimistic state update, silently hiding it from the user.

Mitigation & Contingency

Mitigation: Process Realtime events sequentially in the BLoC event queue using bloc_concurrency's sequential transformer. The mark-all-read event should only affect notifications whose IDs were fetched before the operation started.

Contingency: Add a reconciliation step after mark-all-read that re-fetches the unread count from the repository and corrects the BLoC state if it diverges from the server value.

medium impact medium prob technical

A coordinator assigned to many peer mentors may trigger a query returning hundreds or thousands of notifications. Without pagination and query optimisation, the initial load will be slow and memory-heavy.

Mitigation & Contingency

Mitigation: Enforce server-side pagination (50 items per page) in the Role-Aware Filter's query predicates. Add a composite index on (org_id, user_id, created_at DESC) and profile query plans before shipping.

Contingency: If query performance is insufficient for large coordinator scopes, introduce a server-side RPC function that pre-aggregates visible notification IDs and returns only the first page, deferring full scope resolution to lazy-load.

medium impact low prob scope

If the read-state optimistic update rolls back frequently due to intermittent connectivity, users will observe notifications toggling between read and unread, creating confusion and distrust of the feature.

Mitigation & Contingency

Mitigation: Queue failed mutations in a local retry store and re-attempt on next connectivity event using a connectivity-aware retry service. Show a non-intrusive banner if offline rather than applying optimistic updates.

Contingency: Disable optimistic updates for mark-as-read in low-connectivity scenarios detected by the connectivity provider, instead showing a loading indicator until server confirmation.