critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

Dart class `NotificationPreferenceService` exists in the service layer with a single public interface
Method `isNotificationAllowed(String userId, String scenarioType)` returns `Future<bool>`: returns false if user has disabled all push notifications globally; returns false if user has opted out of the specific scenarioType; returns true otherwise
Method `getAllPreferences(String userId)` returns `Future<NotificationPreferences>` typed model with all current opt-out flags
Global push opt-out (all notifications disabled) takes precedence over per-scenario settings — checked first, short-circuits the per-scenario query
Service caches preferences per userId for the duration of a single engine evaluation cycle (in-memory, not persisted) to avoid redundant DB reads when evaluating multiple scenario types for the same user
Cache is invalidated at the start of each new engine invocation — never stale across separate scheduled runs
Service constructor accepts a preferences repository parameter for dependency injection
When the preferences record does not exist for a user, the service defaults to `true` (notifications allowed) — opt-in by default behavior
Service logs (debug level only) each gate decision with userId and scenarioType — no PII in log output
Method contracts documented with Dart doc comments including param descriptions and return semantics

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase PostgreSQL 15
data models
accessibility_preferences
device_token
performance requirements
Preference lookup must add under 10ms overhead per evaluated user when served from in-memory cache
Cold DB fetch for preference record must complete in under 100ms — single-row lookup by user_id PK
security requirements
Service must never expose preference state of one user to another — user_id always scoped to the requesting context
Opt-out state is user-controlled and must be respected without override — no coordinator or admin bypass for notification suppression
Debug log output must not include personally identifiable fields — log user_id only as an opaque reference

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Keep the service stateless except for the single-cycle in-memory cache — a simple `Map` keyed by userId is sufficient. Do not use a persistent cache (Hive, SharedPreferences) for preferences here — the source of truth is always the database; the in-memory cache is only a within-invocation optimization. Follow the repository pattern: the service wraps a repository, the repository wraps the Supabase client. This layering makes each level independently testable.

Avoid adding business logic (e.g. cooldown checks) to this service — its only responsibility is the preference gate. The `isNotificationAllowed` method should be a pure async query with no side effects. Define `NotificationPreferences` as an immutable Dart class with `copyWith` support in case future fields are added.

Testing Requirements

Unit tests with flutter_test using a mock preferences repository: test global opt-out returns false regardless of scenario type; test per-scenario opt-out returns false only for the opted-out type, true for others; test missing preference record defaults to true (allowed); test that a second call for the same userId within a cycle hits the cache (verify repository mock called exactly once). Test that cache does not persist across two separate `NotificationPreferenceService` instantiations. All tests must be deterministic and require no network access.

Component
Notification Preference Service
service low
Epic Risks (3)
high impact medium prob technical

The scenario-edge-function-scheduler must evaluate all active peer mentors within the 30-second Supabase Edge Function timeout. For large organisations, a sequential evaluation loop may exceed this limit, causing partial runs and missed notifications.

Mitigation & Contingency

Mitigation: Design the trigger engine to batch mentor evaluations using database-side SQL queries (bulk inactivity check via a single query rather than per-mentor calls), and add a performance test against 500 mentors during development. Document the evaluated mentor count per scenario type in scenario-evaluation-config to allow selective scenario execution per run.

Contingency: If single-run execution is insufficient, split evaluation into per-scenario-type scheduled functions (inactivity check, milestone check, expiry check) on separate cron schedules, dividing the computational load across multiple invocations.

high impact low prob technical

A race condition between concurrent scheduler invocations or retried cron triggers could cause the same scenario notification to be dispatched multiple times to a mentor, severely degrading trust in the feature.

Mitigation & Contingency

Mitigation: Implement cooldown enforcement using a database-level upsert with a unique constraint on (user_id, scenario_type, cooldown_window_start) so that a second invocation within the same window is rejected at the persistence layer rather than the application layer.

Contingency: Add an idempotency key derived from (user_id, scenario_type, evaluation_date) to the notification record insert; if a duplicate key violation is caught, log it as a warning and skip dispatch without error.

medium impact medium prob integration

The trigger engine queries peer mentor activity history across potentially multiple organisations and chapters. RLS policies configured for app-user roles may block the Edge Function's service-role queries, or query performance may degrade on large activity tables.

Mitigation & Contingency

Mitigation: Confirm the Edge Function runs with the Supabase service role key (bypassing RLS) and add composite indexes on (user_id, activity_date) to the activity tables before implementing the inactivity detection query.

Contingency: If service-role access is restricted by organisational policy, implement a dedicated database function (SECURITY DEFINER) that performs the inactivity aggregation and is callable by the Edge Function with limited scope.