Implement Peer Mentor Stats Aggregator
epic-achievement-badges-ui-task-010 — Build the peer-mentor-stats-aggregator service that computes activity counts, unique contact counts, and other metrics needed to evaluate badge criteria. Read from the badge-repository for earned badge counts, aggregate activity data, and expose a typed stats model consumed by badge-evaluation-service.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Prefer a Supabase RPC function (PostgreSQL stored procedure) for the core aggregation rather than multiple separate queries — this reduces round-trips and leverages the database's query planner. Create an rpc('get_mentor_activity_stats', {mentor_id: uuid}) that returns a single JSON object with all count fields. The Dart service then maps this JSON to PeerMentorStats. This is both more performant and more maintainable than orchestrating 3–4 separate Supabase queries in Dart.
For the watchStats stream, use a StreamController.broadcast() and call add() in invalidateCache() after clearing the cache entry — consumers can then call getStats() to get fresh data. The PeerMentorStats model should be immutable (const constructor with copyWith) so that badge-evaluation-service can safely store a snapshot without risk of mutation.
Testing Requirements
Unit tests with mocktail mocking all repository dependencies: (1) empty mentor returns PeerMentorStats.empty(); (2) correct aggregation with mixed activity types; (3) unique contact count de-duplicates repeated contact IDs; (4) cache hit on second getStats call — repositories called exactly once; (5) invalidateCache followed by getStats triggers fresh repository calls; (6) watchStats emits after invalidation. Integration test against Supabase test instance: seed known activity data and assert aggregation results match expected counts. Minimum 90% branch coverage.
The badge-earned-celebration overlay must appear within 2 seconds of the triggering activity being saved, but badge evaluation runs server-side in an edge function triggered by a database webhook. Network latency, edge function cold start, and Supabase Realtime delivery delays could cause the overlay to appear late or not at all, breaking the motivational loop.
Mitigation & Contingency
Mitigation: Implement an optimistic UI path: after activity save, badge-bloc immediately checks whether any badge thresholds are crossed client-side using cached stats and badge definitions, showing the overlay speculatively before server confirmation. The server result then reconciles. Subscribe to Supabase Realtime on the earned_badges table for authoritative confirmation.
Contingency: If Realtime delivery is unreliable in production, add a polling fallback: badge-bloc polls for new earned badges 3 seconds after an activity save and shows the overlay if a new record is detected, accepting up to 5-second latency as a fallback SLA.
The celebration overlay uses animation for positive reinforcement, but motion sensitivity (prefers-reduced-motion) and screen reader users require a non-animated or text-only alternative. Failing to handle this risks excluding Blindeforbundet users or triggering vestibular discomfort for motion-sensitive volunteers.
Mitigation & Contingency
Mitigation: Check MediaQuery.disableAnimations in badge-earned-celebration-overlay and skip animation entirely when true, showing a static card instead. Add an ExcludeSemantics wrapper around the decorative animation widget and a separate Semantics node with a live region announcement of the badge name and congratulatory message.
Contingency: If accessibility issues are identified in TestFlight testing with Blindeforbundet's test group, fast-track a patch that defaults to the static card path and gates the animation behind a user preference setting in notification preferences.