high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

awardBadge(mentorId, badgeDefinitionId) returns the EarnedBadge model on successful first award
Calling awardBadge with the same (mentorId, badgeDefinitionId) pair a second time returns the existing EarnedBadge without inserting a duplicate record
Service throws BadgeDefinitionNotFoundException if the provided badgeDefinitionId does not exist in badge-definition-repository
Service throws UnauthorizedBadgeAwardException if the calling context does not have the 'award_badge' permission
A BadgeEarnedEvent is emitted to DomainEventBus exactly once per unique award (not on idempotent re-award)
Database write and event emission are treated as a logical unit — if the write fails, no event is emitted
Service is stateless and thread-safe (safe for concurrent calls from background isolates)
All inputs are validated: mentorId and badgeDefinitionId must be non-empty UUIDs
Unit tests cover duplicate detection, successful award, definition-not-found, and unauthorized scenarios

Technical Requirements

frameworks
Flutter
Supabase
flutter_test
apis
BadgeRepository.findEarnedBadge(mentorId, badgeDefinitionId)
BadgeRepository.insertEarnedBadge(EarnedBadge)
BadgeDefinitionRepository.getById(id)
DomainEventBus.emit(BadgeEarnedEvent)
data models
EarnedBadge
BadgeDefinition
BadgeEarnedEvent
performance requirements
Duplicate check + write must complete within 500ms under normal Supabase latency
No N+1 queries — duplicate check and insert combined in a single upsert where possible
security requirements
Permission check performed before any database read/write
mentorId validated against authenticated session — service must not accept an arbitrary mentorId from untrusted callers
Supabase Row Level Security (RLS) must be the final enforcement layer

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement idempotency using a Supabase upsert with ON CONFLICT DO NOTHING on the (mentor_id, badge_definition_id) unique constraint, then inspect the returned row count to determine whether this was a new award or a duplicate. This avoids a separate SELECT before INSERT and removes the race condition window. Emit the BadgeEarnedEvent only when row count > 0 (i.e., a new row was inserted). The service should be a plain Dart class injected via the dependency injection container — avoid any Flutter widget dependencies.

Wrap the upsert + event emission in a try/catch; if the upsert succeeds but event emission fails, log the error but do not roll back (event bus failure is non-fatal; the badge is already persisted).

Testing Requirements

Unit tests using mocktail: mock BadgeRepository and BadgeDefinitionRepository. Test cases: (1) first award → insert called, event emitted, EarnedBadge returned; (2) duplicate award → insert NOT called, event NOT emitted, existing record returned; (3) unknown badgeDefinitionId → BadgeDefinitionNotFoundException; (4) invalid UUID inputs → ArgumentError. Integration test against a Supabase test instance: verify upsert idempotency at the database level. 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.