high priority medium complexity backend pending backend specialist Tier 6

Acceptance Criteria

ClearAllNotifications event handler clears the visible notification list in state to an empty list without issuing any database delete call
After ClearAllNotifications, the unread count in state is 0
The full cached notification list (used as the source for filtering) is preserved internally after ClearAllNotifications, so that subsequent filter changes can still operate on the original data
ApplyNotificationFilter event handler accepts a NotificationFilter value object describing type and/or read-state criteria
On ApplyNotificationFilter, the bloc updates the activeFilter in state and re-applies the filter predicate via the Role-Aware Notification Filter Service against the full cached list
The filtered result is emitted as the new visible notifications list without triggering a network fetch
Switching from a restricted filter back to 'all' restores the full cached list (respecting role scope)
ApplyNotificationFilter with the same filter value as the current active filter is a no-op (no redundant emits)
The unread count in state always reflects the count of unread items in the currently visible (filtered) list
State shape separates allNotifications (full cache) from visibleNotifications (filtered view) to support this architecture
BLoC state transitions remain immutable throughout both event handlers

Technical Requirements

frameworks
Flutter
BLoC (bloc ^8.x)
flutter_bloc
data models
Notification
NotificationFilter (type, readState)
NotificationState (allNotifications, visibleNotifications, unreadCount, activeFilter)
Role-Aware Notification Filter Service
performance requirements
Filter re-application must complete synchronously on the cached list — no async gaps
Filter predicate must run in O(n) against the cached list with no redundant passes
State emission on ApplyNotificationFilter must happen in a single emit call
security requirements
Filter predicates must respect role scope — a peer mentor must never see notifications outside their role scope even after filter changes
ClearAllNotifications must not expose the full cached list to the UI layer directly

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

The key architectural decision here is maintaining two lists in state: allNotifications (the authoritative cache from repository/Realtime) and visibleNotifications (the filtered projection). ClearAllNotifications sets visibleNotifications to [] but leaves allNotifications intact. ApplyNotificationFilter delegates to the Role-Aware Notification Filter Service, passing allNotifications and the new filter, and places the result in visibleNotifications. This avoids re-fetching and keeps filtering synchronous.

For the no-op guard on duplicate filter, compare filter value objects using == (ensure NotificationFilter implements Equatable). The unread count should always be computed from visibleNotifications.where((n) => !n.isRead).length rather than stored as a separate mutable integer to avoid drift. Use Equatable on NotificationState for bloc_test expect() comparisons.

Testing Requirements

Unit tests using flutter_test and bloc_test. Cover: (1) ClearAllNotifications emits empty visible list with unread=0, (2) full cache is preserved internally after clear, (3) ApplyNotificationFilter with type filter emits correctly filtered list, (4) ApplyNotificationFilter with read-state filter emits only matching items, (5) switching back to 'all' filter restores full role-scoped list, (6) duplicate filter value triggers no additional emit, (7) unread count after filter reflects count of unread in visible list only, (8) role scope is not bypassed by filter change. Use mocktail to mock Role-Aware Notification Filter Service. Target 90%+ line coverage on both handlers.

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.