critical priority high complexity backend pending backend specialist Tier 2

Acceptance Criteria

CoordinatorStatsService exposes getCoordinatorStats(String userId, StatsFilter filter) that internally uses RoleAccessValidator to determine scope (chapter vs full org)
Coordinator scope: aggregates only activities within the coordinator's assigned chapter(s) — determined by contact_chapter membership
Org-admin scope: aggregates all activities across the entire organization_id — no chapter restriction
Both scopes use the same repository query interface with a ScopeFilter parameter — no if/else branching duplicating query construction
CoordinatorStatsViewModel is fully populated: totalActivities, activeMentorsCount, contactsReachedCount, monthlyBarSeries (List<BarChartSeriesViewModel>), activityTypeDonut (List<DonutSegmentViewModel>), mentorStatsList (List<MentorKpiRow>)
DonutSegmentViewModel contains: activityTypeId, activityTypeName, count, percentage (summing to 100.0 with rounding correction on last segment)
Active mentors count = distinct peer_mentor_id values with at least one activity in the selected period
Cache key incorporates userId + resolved scope type + filter period hash — coordinator and org-admin for the same org get different cache keys
Service returns Result<CoordinatorStatsViewModel> — never throws; Supabase errors map to Result.failure
MentorStatsList is sorted by activity count descending — top contributors appear first
If any activity_type in the period has zero activities it is excluded from the donut, not shown as 0%
Percentage rounding: last donut segment absorbs floating point remainder so total always equals exactly 100%

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
apis
Supabase PostgreSQL 15 (via StatsRepository)
Supabase Auth JWT claims for role resolution
data models
activity
activity_type
assignment
contact_chapter
annual_summary
performance requirements
Aggregation query must use GROUP BY server-side — never pull raw rows to client for aggregation
Cache hit must resolve in under 5ms
Full org-admin query across 1000+ activities must complete within 3 seconds
MentorStatsList capped at 50 entries in the view model to prevent unbounded list rendering
security requirements
RoleAccessValidator must read the user's role from Supabase Auth JWT claims — not from a client-supplied parameter
Coordinator scope enforced by Supabase RLS on the repository query — even if a coordinator manually crafts a request for org-wide data, RLS blocks it
Org-admin scope verification: confirm organization_id in JWT claim matches the requested org before executing full-org query
MentorStatsList must not expose contact-level PII — only mentor name, count, and mentorId

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

The scope abstraction is the critical design challenge. Model it as a sealed class: `sealed class StatsScope { const factory StatsScope.chapter(String chapterId) = ChapterScope; const factory StatsScope.organization(String organizationId) = OrganizationScope; }` — this is passed to the repository and mapped to the appropriate SQL WHERE clause without branching in the service. RoleAccessValidator should be async (it may need to query the database for chapter assignments). Cache key hashing: use a stable toString or toJson on StatsScope + StatsFilter period so keys are deterministic.

Donut percentage: compute raw percentages first, then apply largest-remainder method to ensure exactly 100%. For MentorStatsList, consider a separate repository method that returns pre-sorted rows to avoid in-memory sorting of large lists.

Testing Requirements

Unit tests required using flutter_test with mocked StatsRepository and RoleAccessValidator. Test cases: (1) coordinator role → scoped query called with chapter filter, (2) org-admin role → full-org query called without chapter filter, (3) DonutSegmentViewModel percentages sum exactly to 100.0 with rounding correction, (4) zero-activity types excluded from donut, (5) active mentors count uses distinct logic, (6) MentorStatsList sorted descending by activity count, (7) cache hit prevents repository call, (8) cache miss stores result, (9) Supabase error → Result.failure, (10) coordinator cannot access org-admin scope even if they pass org-admin userId. Integration test: deploy two Supabase test accounts (coordinator and org-admin) and verify scope isolation via actual RLS enforcement.

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.