critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

TierLevel enum is defined with values: bronze, silver, gold, platinum, in ascending order of prestige
RecognitionTier class has fields: id (String), level (TierLevel), name (String), description (String), requiredAssignments (int), requiredStreakWeeks (int?), and iconAssetPath (String)
TierEligibility class has fields: mentorId (String), currentTier (TierLevel?), nextTier (TierLevel?), assignmentsMet (bool), streakMet (bool), overallEligible (bool), and progressToNextTier (double, 0.0–1.0)
TierAwardPeriod class has fields: id (String), periodName (String), startDate (DateTime), endDate (DateTime), orgId (String), and isActive (bool)
TierAssignment class has fields: id (String), mentorId (String), tier (TierLevel), awardedAt (DateTime), periodId (String), orgId (String), and awardedBy (String?)
TierLevel enum implements a compareTo-compatible ordering so that bronze < silver < gold < platinum
All classes implement fromJson/toJson, copyWith, ==, hashCode, toString
TierAwardPeriod includes an isCurrentlyActive() method returning bool based on current DateTime vs startDate/endDate
Unit tests confirm TierLevel ordering and TierEligibility.overallEligible logic

Technical Requirements

frameworks
Flutter
Dart
data models
RecognitionTier
TierEligibility
TierAwardPeriod
TierAssignment
performance requirements
isCurrentlyActive() must be pure synchronous — no I/O
TierLevel ordering must not depend on enum index position — use explicit int values for stability
security requirements
orgId required on TierAwardPeriod and TierAssignment for multi-tenant data isolation

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Represent TierLevel as an enum with an explicit integer value field (e.g., final int value, with bronze=1, silver=2, gold=3, platinum=4) to enable reliable ordering comparisons that are not fragile against enum declaration order changes. Use DateTime.now().isAfter(startDate) && DateTime.now().isBefore(endDate) in isCurrentlyActive(), but inject a clock abstraction if testability is a concern. TierAwardPeriod.isActive should be a stored field (from DB) indicating admin has enabled this period, while isCurrentlyActive() is a computed runtime check. This distinction matters: a period can be set active=true but not currently within its date range.

The recognition tier system maps to the gamification requirement from NHF and HLF — showing mentors their status (e.g., 'Gold Mentor') in a 'Ditt likepersonsår' (Your Peer Mentor Year) summary view.

Testing Requirements

Unit tests for: (1) TierLevel enum ordering — assert bronze < silver < gold < platinum using compareTo or index-based comparison, (2) fromJson/toJson round-trip for all four classes, (3) TierAwardPeriod.isCurrentlyActive() returns true when now is between start and end, false otherwise including boundary conditions, (4) TierEligibility.overallEligible is true only when both assignmentsMet and streakMet are true, (5) progressToNextTier is clamped between 0.0 and 1.0. Place tests in test/features/badges/data/models/.

Component
Recognition Tier 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.