critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

EarnedBadge class has fields: id (String UUID), mentorId (String), badgeDefinitionId (String), earnedAt (DateTime), orgId (String), and metadata (Map<String, dynamic>?)
BadgeDefinition class has fields: id (String), name (String), description (String), criteria (BadgeCriteria), iconAssetPath (String), isActive (bool), and orgId (String?)
BadgeCriteria class has fields: criteriaType (BadgeCriteriaType enum), threshold (int?), streakWeeks (int?), courseIds (List<String>?), and additionalParams (Map<String, dynamic>?)
BadgeCriteriaType is a Dart enum with values: assignmentCount, streakWeeks, trainingCompletion, honorarMilestone
OrgBadgeConfig class has fields: orgId (String), enabledBadgeIds (List<String>), customThresholds (Map<String, int>), and isGamificationEnabled (bool)
BadgeAwardResult class has fields: wasAwarded (bool), earnedBadge (EarnedBadge?), reason (String), idempotencyKey (String), and awardedAt (DateTime?)
EarnedBadge includes a deriveIdempotencyKey() method returning a deterministic String based on mentorId + badgeDefinitionId + orgId to prevent duplicate awards
BadgeDefinition includes a isEligible(PeerMentorStats stats) validation helper returning bool — delegates to criteria evaluation
All classes implement fromJson/toJson, copyWith, ==, hashCode, toString
Unit tests for idempotency key derivation confirm identical inputs always produce identical output and different inputs produce different output

Technical Requirements

frameworks
Flutter
Dart
apis
Supabase (for schema alignment of JSON keys)
data models
EarnedBadge
BadgeDefinition
BadgeCriteria
OrgBadgeConfig
BadgeAwardResult
PeerMentorStats
performance requirements
deriveIdempotencyKey() must be pure and synchronous — no async or I/O
isEligible() must be pure — no side effects or network calls
security requirements
orgId must be present on EarnedBadge and OrgBadgeConfig to enforce multi-tenant isolation
idempotencyKey must not be user-guessable — use a hash of concatenated IDs rather than plain concatenation

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

The idempotencyKey derivation should use a simple but stable hash: concatenate mentorId + ':' + badgeDefinitionId + ':' + orgId and apply a consistent hash (e.g., using Dart's built-in Object.hash() or a SHA-256 from the crypto package if already a dependency). Avoid DateTime in idempotency keys as awards can be retried across different timestamps. The OrgBadgeConfig model is critical for the multi-org deployment: NHF, Blindeforbundet, and HLF each have different badge sets enabled and potentially different thresholds (e.g., Blindeforbundet's honorar at 3rd and 15th assignment). customThresholds allows overriding default criteria thresholds per org without schema changes.

Ensure BadgeCriteria uses an enum (not a String) for criteriaType to prevent typo-driven bugs in badge evaluation logic.

Testing Requirements

Unit tests for each class covering: fromJson/toJson round-trip, copyWith field isolation, equality, and null-safety for optional fields. For BadgeDefinition.isEligible(), write parameterized tests covering each BadgeCriteriaType with stats values both above and below threshold. For deriveIdempotencyKey(), verify: same inputs → same key (called 100 times), and that changing any single input field changes the output key. Place all tests in test/features/badges/data/models/.

Component
Badge Award Service
service medium
Epic Risks (3)
high impact medium prob technical

peer-mentor-stats-aggregator must compute streaks and threshold counts across potentially hundreds of activity records per peer mentor. Naive queries (full table scans or N+1 patterns) will cause slow badge evaluation, especially when triggered on every activity save for all active peer mentors.

Mitigation & Contingency

Mitigation: Design aggregation queries using Supabase RPCs with window functions or materialised views from the start. Add database indexes on (peer_mentor_id, activity_date, activity_type) before writing any service code. Profile all aggregation queries against a dataset of 500+ activities during development.

Contingency: If query performance is insufficient at launch, implement incremental stat caching: maintain a peer_mentor_stats snapshot table updated on each activity insert via a database trigger, so the aggregator reads from pre-computed values rather than scanning raw activity rows.

medium impact low prob technical

badge-award-service must be idempotent, but if two concurrent edge function invocations evaluate the same peer mentor simultaneously (e.g., from a rapid double-save), both could pass the uniqueness check before either commits, resulting in duplicate badge records.

Mitigation & Contingency

Mitigation: Rely on the database-level uniqueness constraint (peer_mentor_id, badge_definition_id) as the final guard. In the service layer, use an upsert with ON CONFLICT DO NOTHING and return the existing record. Add a Postgres advisory lock or serialisable transaction for the award sequence during the edge function integration epic.

Contingency: If duplicate records are discovered in production, run a deduplication migration to remove extras (keeping earliest earned_at) and add a unique index if not already present. Alert engineering via Supabase database webhook on constraint violations.

medium impact medium prob scope

The badge-configuration-service must validate org admin-supplied criteria JSON on save, but the full range of valid criteria types (threshold, streak, training-completion, tier-based) may not be fully enumerated during development, leading to either over-permissive or over-restrictive validation that frustrates admins.

Mitigation & Contingency

Mitigation: Define a versioned Dart sealed class hierarchy for CriteriaType before writing the validation logic. Review the hierarchy with product against all known badge types across NHF, Blindeforbundet, and HLF before implementation. Build the validator against the sealed class so new criteria types require an explicit code addition.

Contingency: If admins encounter validation rejections for legitimate criteria, expose a 'criteria_raw' escape hatch (JSON passthrough, admin-only) with a product warning, and schedule a sprint to formalise the new criteria type properly.