critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

ThresholdCriteriaEvaluator implements CriteriaEvaluator<ThresholdCriteriaConfig, PeerMentorStats> from the interfaces defined in task-001
ThresholdCriteriaConfig includes fields: metric ('activity_count' | 'honorar_count' | 'blindeforbundet_honorar_milestone'), threshold (number), organizationId (string)
For metric 'activity_count': evaluate returns met=true when stats.activityCount >= config.threshold, currentValue=stats.activityCount, requiredValue=config.threshold
For metric 'honorar_count': evaluate returns met=true when stats.honorarCount >= config.threshold
For metric 'blindeforbundet_honorar_milestone': evaluate returns met=true when stats.honorarCount is exactly 3 or exactly 15 (milestone thresholds), using the threshold config value to select which milestone (3 or 15) to check
Blindeforbundet honorar milestone logic: a peer mentor who has completed exactly their 3rd assignment receives the kontorhonorar trigger; one who has completed exactly their 15th receives the higher-rate trigger — evaluate correctly distinguishes between these two badges by comparing stats.honorarCount === config.threshold (not >=)
Calling evaluate() with an unsupported metric value throws a BadCriteriaConfigError with a descriptive message listing valid metric values
evaluate() is a pure function with no side effects — it does not write to the database or call any external API
EvaluationResult.metadata includes the metric used and the raw count value for debugging purposes

Technical Requirements

frameworks
Deno
TypeScript (strict mode)
apis
No external API calls in this class — all data received via PeerMentorStats input parameter
data models
PeerMentorStats
ThresholdCriteriaConfig
EvaluationResult
BadgeMilestone (Blindeforbundet-specific honorar milestones: 3rd and 15th assignment)
performance requirements
evaluate() executes in O(1) time — no loops or database queries; all aggregation already done in PeerMentorStats
No memory allocation beyond the EvaluationResult object
security requirements
ThresholdCriteriaConfig.threshold must be validated as a positive integer — reject negative, zero, or non-integer values with BadCriteriaConfigError
organizationId in config must match the organizationId in ActivityRecord entries — add an assertion in validate() to prevent cross-organization badge evaluation

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The key business rule for Blindeforbundet: the 3rd and 15th assignment milestones are exact-match checks (===), not cumulative (>=). A peer mentor who has 16 honorar assignments should NOT re-trigger the 15th milestone badge — the badge is awarded once at the crossing point. The calling service is responsible for ensuring evaluate() is only called when a new assignment is registered (event-driven), but the evaluator itself should be defensive and use === for milestone metrics. For the 'activity_count' and 'honorar_count' metrics, use >= because those badges represent sustained achievement levels, not one-time milestones.

Keep the evaluate() method short (under 30 lines) — all branching should be handled via a private strategy map (Record EvaluationResult>) to keep the main method readable and easy to extend with new metric types. Do not import from Flutter or Dart packages — this is a Deno/TypeScript Supabase Edge Function.

Testing Requirements

Unit tests with Deno's built-in test runner (Deno.test). Required test cases: (1) activity_count: met when count equals threshold; met when count exceeds threshold; not met when count is below threshold. (2) honorar_count: same three cases. (3) blindeforbundet_honorar_milestone threshold=3: met when honorarCount===3; not met when honorarCount===2; not met when honorarCount===4 (already past, no re-trigger).

(4) blindeforbundet_honorar_milestone threshold=15: same pattern. (5) Invalid metric string throws BadCriteriaConfigError. (6) Negative threshold in config throws BadCriteriaConfigError. Aim for 100% branch coverage.

Use assertEquals and assertThrows from Deno's std/assert module.

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

Supabase Edge Functions may experience cold start latency of 500ms–2s when they have not been invoked recently. If evaluation latency consistently exceeds the 2-second UI expectation, the celebration overlay timing SLA cannot be met without the optimistic UI fallback from the UI epic.

Mitigation & Contingency

Mitigation: Keep the edge function warm by scheduling a lightweight health-check invocation every 5 minutes in production. Optimise the function size to minimise Deno module load time. Implement the optimistic UI path in badge-bloc (from the UI epic) as the primary UX path so cold start only affects server-side reconciliation, not perceived responsiveness.

Contingency: If cold starts remain problematic, migrate badge evaluation to a Supabase database function (pl/pgsql) triggered directly by a database trigger on activity insert, eliminating the Edge Function overhead entirely for the evaluation logic while keeping Edge Function only for FCM notification dispatch.

high impact low prob integration

Supabase database webhooks can fail silently if the edge function returns a non-2xx response or times out. A missed webhook means a peer mentor does not receive a badge they earned, which is both a functional defect and a trust issue for organisations relying on milestone tracking.

Mitigation & Contingency

Mitigation: Implement idempotent webhook processing: the edge function reads the activity ID from the webhook payload and checks whether evaluation for this activity has already run (via an audit log query) before proceeding. Add Supabase webhook retry configuration (3 retries with exponential backoff). Monitor webhook failure rates via Supabase logs alert.

Contingency: Implement a nightly reconciliation job (Supabase scheduled function) that scans all activities from the past 24 hours, re-evaluates badge criteria for any peer mentor with no corresponding evaluation log entry, and awards any missing badges. Alert operations if reconciliation awards more than 5% of badges, indicating systematic webhook failure.

high impact low prob security

The evaluation service loads badge definitions per organisation, but a misconfigured RLS policy or incorrect organisation scoping in the edge function could cause one organisation's badge criteria to be evaluated against another organisation's peer mentor activity data, leading to incorrect or cross-contaminated badge awards.

Mitigation & Contingency

Mitigation: The edge function must extract organisation_id from the webhook payload activity record and pass it explicitly to every database query. Write a security test that seeds two organisations with distinct badge definitions and verifies that evaluating a peer mentor in org A never reads or awards org B definitions. Use Supabase service role key only within the edge function, never the anon key.

Contingency: If cross-org contamination is detected in audit logs, immediately disable the edge function webhook, run a targeted SQL query to identify and revoke incorrectly awarded badges, notify affected organisations, and perform a full security review of all RLS policies on badge-related tables before re-enabling.