critical priority low complexity database pending database specialist Tier 0

Acceptance Criteria

BadgeRepository class exposes at minimum: fetchEarnedBadges(String peerMentorId) → Future<List<EarnedBadge>>, insertBadgeAward(EarnedBadge badge) → Future<EarnedBadge>, and fetchBadgeHistory(String peerMentorId, {DateRange? range}) → Future<List<EarnedBadge>>
All Supabase queries include the peer_mentor_id filter — no query may return badges belonging to other peer mentors
RLS policy on the earned_badges Supabase table is documented and verified to block cross-organization reads in a test
insertBadgeAward uses upsert semantics with a unique constraint on (peer_mentor_id, badge_definition_id) to prevent duplicate awards
Returned EarnedBadge objects are fully typed Dart models — no raw Map<String, dynamic> escapes the repository boundary
Repository methods throw typed domain exceptions (BadgeRepositoryException) on Supabase errors — no raw PostgrestException surfaces to callers
All methods are tested with unit tests using a mocked Supabase client
Repository is registered as a Riverpod provider so it can be injected into BLoC/Cubit layers

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
apis
Supabase REST API
Supabase RLS
data models
EarnedBadge
BadgeDefinition
PeerMentor
performance requirements
fetchEarnedBadges must return results within 2 seconds on a standard mobile connection
Repository must not perform redundant queries — one fetchEarnedBadges call per screen load
security requirements
RLS policies must enforce organization-level isolation — a peer mentor may only read their own earned badges
No badge data may be written without an authenticated Supabase session
Badge award insertion must validate that the badge_definition_id exists before writing

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Follow the repository pattern already established in the codebase — use an abstract BadgeRepositoryInterface and a concrete SupabaseBadgeRepository implementation to support future swapping or testing. Define EarnedBadge as an immutable Dart class with a fromJson factory and toJson method. Table name: earned_badges. Key columns expected: id (uuid), peer_mentor_id (uuid, FK), badge_definition_id (text), awarded_at (timestamptz), organization_id (uuid).

Register as a Riverpod Provider pointing at SupabaseBadgeRepository. Keep the repository free of UI or BLoC logic — it is a pure data access object. For the RLS policy: enable RLS on earned_badges, create policy 'peer mentor reads own badges' using auth.uid() = peer_mentor_id, and create policy 'org admin reads org badges' scoped by organization_id.

Testing Requirements

Unit tests using flutter_test with a mocked Supabase client (use mockito or manual fakes). Test cases must cover: (1) successful fetch returns correctly mapped EarnedBadge list, (2) empty result returns empty list without throwing, (3) Supabase error is wrapped in BadgeRepositoryException, (4) upsert on duplicate award does not throw and returns existing record, (5) fetchBadgeHistory with a DateRange filters results correctly. Integration test against Supabase test instance verifying RLS blocks cross-organization reads.

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.