high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

getStats(mentorId) returns a PeerMentorStats object containing: total activity count, unique contact count, earned badge count, activity counts by type, and streak data (consecutive active weeks)
All counts are accurate against the current state of the Supabase database at time of call
Unique contact count counts distinct contact IDs across all activities — a contact appearing in 10 activities counts as 1
Activity count by type returns a Map<ActivityType, int> covering all activity types defined in the system
getStats returns PeerMentorStats.empty() (zero counts, no error) for a mentor with no recorded activities
Stats are cached per mentorId with a 2-minute TTL to avoid redundant aggregation queries
Cache can be manually invalidated per mentorId via invalidateCache(mentorId) — called by badge-evaluation-service after a new activity is recorded
Service exposes a Stream<PeerMentorStats> via watchStats(mentorId) that emits an updated stats object whenever the cache is invalidated
Database queries use aggregation (COUNT, COUNT DISTINCT) — no full record fetches and client-side counting
Unit tests cover: empty mentor, single activity, multiple activities with repeated contacts, cache hit/miss/invalidation

Technical Requirements

frameworks
Flutter
Supabase
flutter_test
apis
Supabase RPC or view: rpc('get_mentor_activity_stats', params: {mentor_id})
BadgeRepository.countEarnedBadges(mentorId)
ActivityRepository.getActivityTypeCounts(mentorId)
ActivityRepository.getUniqueContactCount(mentorId)
data models
PeerMentorStats
ActivityType
EarnedBadge
performance requirements
getStats cache hit must resolve synchronously
Full aggregation query on cache miss must complete within 1 second for mentors with up to 1000 activities
Use Supabase server-side aggregation (RPC or materialized view) — no client-side loops over full datasets
watchStats StreamController must be a broadcast stream to support multiple subscribers
security requirements
RLS on Supabase must prevent cross-mentor data leakage at the database level
mentorId validated against authenticated session before issuing queries
Aggregation queries must not expose raw contact PII in their return values — counts only

Execution Context

Execution Tier
Tier 1

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.

Component
Badge BLoC
infrastructure medium
Epic Risks (2)
medium impact medium prob integration

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.

high impact low prob technical

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.