high priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

evaluateEligibility(mentorId, orgId) returns a TierEligibility object containing the highest eligible tier and delta-to-next-tier values for all defined tiers
When an organisation has custom tier thresholds, those thresholds are used instead of global defaults
When tiers are disabled for an organisation, evaluateEligibility returns TierEligibility with tier set to null and an explicit tiersEnabled=false flag
Delta-to-next-tier values are non-negative and accurately reflect the difference between current stats and the next tier threshold for each metric (assignments, streak, training)
If a mentor already holds the highest tier, delta-to-next-tier is 0 for all metrics
Method throws a typed MentorNotFoundException when mentorId does not exist in the database
Method throws a typed OrgNotFoundException when orgId does not exist
Evaluation is deterministic: same inputs always return the same TierEligibility
PeerMentorStatsAggregator is called exactly once per evaluateEligibility invocation (no redundant queries)
BadgeConfigurationService is called exactly once per invocation to load thresholds (leverages cache)

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
apis
Supabase REST API
Supabase RPC
data models
MentorStats
TierEligibility
TierThreshold
BadgeConfiguration
OrgConfiguration
performance requirements
evaluateEligibility must complete within 500ms including Supabase round-trips
No N+1 queries: aggregate all required stats in a single PeerMentorStatsAggregator call
BadgeConfigurationService cache must be used so threshold loading adds <10ms overhead
security requirements
Row-level security (RLS) on Supabase must enforce that a mentor can only evaluate their own eligibility unless caller is coordinator or admin
orgId must be validated against the authenticated user's org membership before query execution
No mentor PII must be logged during evaluation

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Define a TierEligibility value class with fields: eligibleTier (nullable TierLevel enum), tiersEnabled (bool), deltaToNextTier (Map). Implement tier comparison as a sorted list of thresholds (descending) — iterate and return the first tier whose all-metrics conditions are satisfied. Use a sealed class or enum for TierLevel (e.g., bronze, silver, gold) so exhaustive switch is enforced at compile time. Guard against orgs with no thresholds configured by checking BadgeConfigurationService result before iterating — return lowest delta map pointing to global defaults if no org config exists.

Keep evaluateEligibility a pure function that accepts stats and thresholds as parameters internally so it is trivially unit-testable without mocking the service collaborators. The public method simply resolves collaborators and delegates to the pure function.

Testing Requirements

Unit tests (flutter_test) covering: (1) correct highest-tier selection when mentor qualifies for multiple tiers, (2) correct delta calculation for each metric, (3) tiers-disabled org returns null tier + tiersEnabled=false, (4) custom org thresholds override global defaults, (5) MentorNotFoundException thrown for unknown mentorId, (6) OrgNotFoundException thrown for unknown orgId, (7) PeerMentorStatsAggregator called exactly once. Mock both PeerMentorStatsAggregator and BadgeConfigurationService. Target 90%+ branch coverage on evaluateEligibility business logic.

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.