high priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

RoleAccessValidator exposes a single method: Future<StatsScope> resolveScope(String userId) or StatsScope resolveScope(SessionModel session)
For a user with role 'coordinator', resolveScope returns ChapterScope(chapterId) where chapterId is read from the user's primary chapter membership
For a user with role 'org_admin', resolveScope returns OrganisationScope(orgId) where orgId is read from the user's organisation membership
For a user with role 'peer_mentor', resolveScope throws StatsAccessDeniedException — peer mentors do not have access to coordinator-level stats
For an unauthenticated or null session, resolveScope throws StatsAccessDeniedException
If a coordinator belongs to multiple chapters, resolveScope returns the primary chapter (or throws if no primary chapter is defined — not silently pick one)
RoleAccessValidator does not perform network calls — it reads only from the in-memory session state provided by the Supabase auth client or a session provider
The validator is a pure Dart class injectable via Riverpod and has no Flutter dependencies
All role resolution paths are covered by unit tests with no real Supabase connection needed

Technical Requirements

frameworks
Dart
Riverpod
apis
Supabase Auth (session metadata)
data models
StatsScope
ChapterScope
OrganisationScope
user_roles
chapters
SessionModel
performance requirements
resolveScope must be synchronous or complete in under 10ms — no network calls allowed
security requirements
Role must be read from the server-verified JWT claims or a trusted session object — never from a client-supplied parameter
Chapter and org IDs must be sourced from the authenticated session, not from any UI state or route parameters
StatsAccessDeniedException must not reveal whether a user exists or what their actual role is in its message

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Source the user's role from the Supabase JWT `app_metadata` or `user_metadata` field that is set server-side during user provisioning — do not read role from a separate database query in this validator (that would make it async and introduce latency). Define a typed `SessionModel` or use the existing auth session wrapper already in the codebase — do not parse raw JWT strings in the validator; delegate that to the auth layer. Use Dart 3 sealed class pattern matching to make role handling exhaustive: `switch (role) { case Role.coordinator => ..., case Role.orgAdmin => ..., case Role.peerMentor => throw StatsAccessDeniedException() }`. If the app already has a `CurrentUserProvider` or `AuthBloc`, source the session from there via Riverpod `ref.read()` rather than injecting a raw Supabase client.

Testing Requirements

Unit tests with flutter_test: (1) Construct a mock SessionModel with role='coordinator' and chapterId='ch-1' — assert ChapterScope('ch-1') returned. (2) Mock role='org_admin' with orgId='org-1' — assert OrganisationScope('org-1') returned. (3) Mock role='peer_mentor' — assert StatsAccessDeniedException thrown. (4) Null session — assert StatsAccessDeniedException thrown.

(5) Coordinator with no primary chapter — assert exception thrown with a meaningful message. (6) Verify that resolveScope does not call any async Supabase methods (mock the provider and assert zero calls). All tests must pass without a network connection.

Component
Coordinator Statistics Service
service high
Epic Risks (3)
medium impact medium prob technical

fl_chart's default colour palette may not meet WCAG 2.2 AA contrast requirements when rendered on the app's dark or light backgrounds. If segment colours are insufficient, the donut chart will fail accessibility audits, which is a compliance blocker for all three organisations.

Mitigation & Contingency

Mitigation: Define all chart colours in the design token system with pre-validated contrast ratios. Run the contrast-ratio-validator against every chart colour during the adapter's unit tests. Use the contrast-safe-color-palette as the source palette.

Contingency: If a colour fails validation, replace with the nearest compliant token. If activity types exceed the available token set, implement a deterministic hashing algorithm that maps activity type IDs to compliant colours.

medium impact medium prob technical

StatsBloc subscribing to the activity registration stream creates a long-lived subscription. If the subscription is not disposed correctly when the dashboard is closed, it will cause a stream leak and potentially trigger re-fetches on a disposed BLoC, resulting in uncaught errors in production.

Mitigation & Contingency

Mitigation: Implement subscription disposal in the BLoC's close() override. Write a widget test that navigates away from the dashboard and asserts no BLoC events are emitted after disposal.

Contingency: If leaks are detected in QA, add a mounted check guard before emitting states from async callbacks, and audit all other BLoC stream subscriptions in the codebase for the same pattern.

low impact low prob scope

PersonalStatsService's Phase 4 gamification data structure is designed against an assumed future schema. If the Phase 4 Spotify Wrapped feature defines a different data contract when it is developed, the structure built now will require a breaking change and migration.

Mitigation & Contingency

Mitigation: Document the contribution data structure with explicit field semantics and versioning comments. Keep the Phase 4 fields as optional/nullable so they do not break existing consumers if the schema evolves.

Contingency: If the Phase 4 schema diverges significantly, the personal stats data can be re-mapped in a thin adapter layer without changing PersonalStatsService's core implementation.