high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

A `BadgeCriterionType` enum is defined with values: `assignmentCount`, `streakLength`, `trainingCompletion`, `milestoneThreshold`; any string outside this set fails validation with a descriptive error
`validateCriteria` rejects a criteria list that is empty or null with error message 'At least one criterion is required'
Any criterion with a numeric threshold of 0 or negative fails validation with 'Threshold must be a positive integer'
Any criterion with a non-integer threshold (e.g. 3.5, null) fails validation with 'Threshold must be an integer'
A criterion referencing a `badge_definition_id` that belongs to a different org than the current context fails validation with 'Cross-organisation badge references are not permitted'
All validation errors are collected and returned together (not fail-fast) so the caller receives a complete list of violations in a single pass
Validation runs synchronously with no Supabase calls for rules 1–4; the cross-org reference check (rule 5) is the only rule that may require an async lookup
The cross-org check uses the cached config from `loadOrgBadgeConfig` (task-007) rather than issuing a new Supabase query
Passing `validateCriteria` with a valid single-criterion list of each enum type returns an empty error list
All validation rules are covered by isolated unit tests that do not require a Supabase connection

Technical Requirements

frameworks
Flutter
Dart
apis
Indirect Supabase access via loadOrgBadgeConfig cache for cross-org reference check only
data models
BadgeCriteria
BadgeCriterionType
BadgeDefinition
performance requirements
Synchronous validation rules (1–4) must complete in < 1 ms regardless of criteria list size
Cross-org reference check must use the in-memory cache and not incur additional network round-trips
security requirements
Cross-org reference validation must be enforced server-side as well (Supabase RLS policy) — client-side check is a UX convenience, not the security boundary
Criterion type enum must be validated against the known set before any processing to prevent unexpected values from reaching the database

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Model `BadgeCriterionType` as a Dart enum with a `fromString` factory that throws a `BadgeCriteriaValidationException` for unknown values — this keeps the enum exhaustive and avoids scattered string comparisons. Use a collect-all pattern: accumulate errors in a `List errors` and return at the end rather than throwing on the first failure. This enables the UI to show all validation errors simultaneously in a form. For the cross-org check, pass the `orgId` as a parameter to `validateCriteria` (or as a field on the validator) and compare each referenced `badge_definition_id` against the IDs in the cached `OrgBadgeConfig`.

If the cache is cold (not yet loaded), call `loadOrgBadgeConfig` asynchronously first — make `validateCriteria` return `Future>` to accommodate this. Document clearly that the cross-org check is defence-in-depth and Supabase RLS is the authoritative enforcement layer.

Testing Requirements

Pure unit tests for every validation rule: (a) empty criteria list, (b) null threshold, (c) zero threshold, (d) negative threshold, (e) float threshold, (f) unknown criterion type string, (g) cross-org badge ID, (h) valid single criterion for each enum value. Test that all errors are returned together (not just the first). Test the async cross-org path with a mocked cache returning a known set of badge IDs. Verify that no Supabase calls are made for rules 1–4 by using a mock that asserts it is never called.

Achieve 100% branch coverage on the validation function.

Component
Badge Configuration 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.