high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

StreakCriteriaEvaluator implements CriteriaEvaluator<StreakCriteriaConfig, PeerMentorStats> from task-001 interfaces
StreakCriteriaConfig includes fields: requiredStreakLength (number, minimum 2), windowUnit ('week' | 'month'), windowSize (number), activityTypeFilter (string[] | null — null means any activity type counts)
evaluate() sorts stats.activityHistory by completedAt descending and groups activities into consecutive time windows of size windowUnit × windowSize
A 'streak' is defined as consecutive non-empty windows with no gap — a single missing window breaks the streak
evaluate() returns met=true when the computed consecutive window count >= config.requiredStreakLength
EvaluationResult.currentValue is the computed streak length; EvaluationResult.requiredValue is config.requiredStreakLength
When activityTypeFilter is non-null, only ActivityRecord entries whose activityTypeId is in the filter array are counted toward streak windows
An empty activityHistory array results in met=false, currentValue=0 without throwing an error
Streak computation is deterministic — calling evaluate() twice with the same inputs always returns the same result
Window boundary calculation uses UTC dates to prevent timezone-related streak miscalculation across daylight saving time transitions

Technical Requirements

frameworks
Deno
TypeScript (strict mode)
apis
Deno std/datetime for date arithmetic (addDays, startOfWeek, startOfMonth)
No external API calls — all data received via PeerMentorStats
data models
PeerMentorStats
ActivityRecord
StreakCriteriaConfig
EvaluationResult
StreakWindow (internal computation type: { windowStart: Date, windowEnd: Date, activityCount: number })
performance requirements
evaluate() runs in O(n log n) time where n = activityHistory.length — sort once, then linear pass for window grouping
For peer mentors with up to 2000 activity records, evaluate() must complete in under 50ms
security requirements
requiredStreakLength must be validated as an integer ≥ 2 — a streak of 1 is meaningless and indicates a misconfigured badge definition
activityHistory entries from different organizationIds must be filtered out before streak computation — assert that all records belong to the evaluator's target organizationId

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The hardest part is the window-grouping algorithm. Recommended approach: (1) Filter by activityTypeFilter if set. (2) Sort by completedAt descending (most recent first). (3) Determine the current window boundaries from 'now' (pass a clock injectable for testability).

(4) Walk backward through time, incrementing streak count for each window that has ≥1 activity, and stopping (breaking streak) at the first window with 0 activities. Do NOT use a calendar-month loop — compute window boundaries programmatically using Deno std/datetime's startOfWeek (Monday) or a custom month-start function. Make 'now' injectable via the constructor (StreakCriteriaEvaluator(private readonly clock: () => Date = () => new Date())) so tests can pin the reference date and produce deterministic results without mocking global Date. For weekly windows, define week start as Monday (ISO 8601) to align with Norwegian work-week conventions.

Add StreakWindow[] to EvaluationResult.metadata so the calling service can log exactly which windows were counted — invaluable for debugging badge award disputes.

Testing Requirements

Unit tests covering: (1) Exact streak met: n consecutive non-empty weekly windows = met=true, currentValue=n. (2) Streak broken by one gap: n-1 windows + gap + windows = streak resets, met=false if n-1 < required. (3) Single-activity week counts as one window. (4) Multiple activities in same window count as one window (not additive).

(5) activityTypeFilter: activities of filtered-out type do not contribute to windows; verify gap is created if filtered activities were the only events in a window. (6) Empty activityHistory: returns met=false, currentValue=0. (7) Month-based windows: verify January → February boundary handled correctly (different month lengths). (8) UTC boundary test: activity at 23:00 local time (next UTC day) is placed in the correct window.

Use Deno.test with assertEquals. Aim for 100% branch coverage on the streak computation logic.

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.