high priority high complexity backend pending backend specialist Tier 1

Acceptance Criteria

shouldDeliver(userId, category) returns false if the user has no registered FCM token in Supabase
shouldDeliver returns false if the user has disabled the specified notification category in their preferences
shouldDeliver returns true only when: FCM token exists AND category is enabled AND user account is active
Category preferences are read from NotificationPreferencesRepository which is backed by Supabase user_notification_preferences table
Supported categories match those defined in NotificationCategory enum (e.g., activity_reminder, expense_approval, certification_expiry, coordinator_message)
shouldDeliver is async and caches preferences per user for a maximum of 60 seconds to avoid redundant Supabase reads in batch dispatch scenarios
Cache is invalidated immediately when the user updates preferences via NotificationSettingsScreen
shouldDeliver handles Supabase errors gracefully — returns false (fail-safe) if preferences cannot be read
NotificationTriggerService is usable from both client-side (Dart) and server-side Supabase Edge Function contexts (pure Dart logic, no Flutter dependencies)
Unit tests cover all guard conditions: no token, category disabled, category enabled, Supabase error, cache hit, cache expiry

Technical Requirements

frameworks
Dart (pure, no Flutter dependency)
Riverpod (client-side provider)
Supabase Dart client
apis
Supabase select on user_notification_preferences (userId, category, enabled)
Supabase select on user_fcm_tokens (userId, token)
NotificationPreferencesRepository.getPreferences(userId)
FCMTokenManager.activeToken stream
data models
NotificationPreference (user_id, category, enabled, updated_at)
FCMToken (user_id, token, platform)
NotificationCategory (enum: activity_reminder, expense_approval, certification_expiry, coordinator_message, system_alert)
performance requirements
shouldDeliver must respond within 100ms for cached results
Cache must support concurrent calls for the same userId without issuing duplicate Supabase reads (use a pending-request map or Mutex)
Batch guard evaluation for up to 50 users must complete within 5 seconds
security requirements
Service must never expose one user's preferences to another — always scope queries by userId
RLS policies on user_notification_preferences enforce row-level isolation server-side
Cache keys must include userId to prevent cross-user cache pollution in multi-user session scenarios
shouldDeliver must not throw — always returns bool (fail-safe false on error)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement NotificationTriggerService as a plain Dart class (not a Flutter widget or Riverpod notifier) so it is reusable in Edge Functions. Expose it as a Riverpod Provider on the client. Use an in-memory Map for the 60-second TTL cache keyed by userId. Wrap the repository call with a Map> pendingRequests guard to coalesce concurrent calls.

Prefer a synchronous shouldDeliverSync() overload that reads from cache-only for hot paths (foreground banner filter in PushNotificationService). Define NotificationCategory as a Dart enum with a fromString() factory that returns a nullable value — unknown categories default to false (opt-in model). This conservative default aligns with the project's accessibility commitment: users are never spammed with uncategorised push notifications.

Testing Requirements

Unit tests (dart test, no flutter dependency): mock NotificationPreferencesRepository and FCMTokenManager. Test matrix: (token=null, category=any) → false; (token=valid, category=disabled) → false; (token=valid, category=enabled) → true; (Supabase throws) → false. Test cache: second call within 60s does not call repository again. Test cache invalidation: calling invalidateCache(userId) causes next call to re-fetch.

Test concurrent calls for same userId issue only one repository call. Integration test: wire up against a local Supabase instance and verify end-to-end preference enforcement. Benchmark test: 50-user batch guard completes within 5s.

Component
Notification Settings Screen
ui low
Epic Risks (2)
medium impact medium prob technical

The notification badge widget depends on a persistent Supabase Realtime websocket subscription for live unread count updates. On mobile, network transitions (WiFi to cellular, background app state) can silently drop the websocket, resulting in a stale badge count that does not update until the next app foreground — reducing trust in the notification system.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management in the badge widget's BLoC that re-subscribes on app foreground and on network reconnection events. Add a fallback polling query (every 60 seconds when app is foregrounded) to reconcile the badge count if the Realtime subscription is interrupted.

Contingency: If Realtime reliability proves insufficient in production, replace the live subscription with a polling approach using a configurable interval, accepting slightly delayed badge updates in exchange for reliability.

medium impact medium prob technical

The notification list item widget requires merged semantics combining title, body, timestamp, read state, and role-context icon into a single VoiceOver/TalkBack announcement. Getting the merged semantics structure right for both iOS (VoiceOver) and Android (TalkBack) simultaneously is non-trivial and common to break silently when widgets are refactored.

Mitigation & Contingency

Mitigation: Use the project's existing semantics-wrapper-widget pattern with explicit Semantics widgets and excludeSemantics on decorative children. Write accessibility widget tests using Flutter's SemanticsController to assert the exact announcement string. Test on physical devices with VoiceOver and TalkBack enabled before release.

Contingency: If merged semantics cannot be achieved cleanly on both platforms, implement platform-specific semantic trees using defaultTargetPlatform branching, ensuring each platform receives an optimal announcement even if the implementation differs.