Implement milestone threshold evaluator in trigger engine
epic-scenario-push-engagement-core-engine-task-006 — Add the milestone scenario evaluator to the trigger engine: query cumulative session count for the current calendar year per peer mentor, compare against configurable milestone thresholds (e.g. 50 sessions), and trigger a congratulatory notification exactly once per threshold crossing. Store the triggered milestone in the scenario notification repository to prevent re-firing.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
The milestone record in `scenario_notifications` table should store `milestone_value` (the integer threshold) and the year it was triggered. When checking for prior notifications, filter by BOTH userId AND year AND milestone_value — this ensures that achieving 50 sessions in 2026 does not block the 50-session milestone trigger in 2027. The evaluator should process thresholds in ascending order and short-circuit on the first uncrossed threshold — if a mentor has 75 sessions, only check 50 (crossed, notified?), 100 (not crossed — stop). This prevents false positives from evaluating thresholds the mentor cannot yet reach.
Consider using `annual_summary` if it already stores yearly session counts to avoid recounting raw activity rows on every engine run — this is a significant performance optimization for mentors with hundreds of activities. If using annual_summary, define a clear fallback to COUNT(activity) when the summary is not yet computed for the current year.
Testing Requirements
Unit tests: test that a mentor with exactly 49 sessions triggers no milestones; test mentor with exactly 50 sessions triggers milestone=50; test mentor with 201 sessions who has no prior notifications triggers all three thresholds [50,100,200]; test mentor with 201 sessions who has existing records for 50 and 100 triggers only 200; test opted-out mentor returns empty trigger list or all shouldTrigger=false; test year boundary — notifications from a prior year (different year column in milestone record) do not block current-year triggers if milestone records are year-scoped. Verify that evaluator returns an array even when zero milestones are triggered (empty array, not null).
The scenario-edge-function-scheduler must evaluate all active peer mentors within the 30-second Supabase Edge Function timeout. For large organisations, a sequential evaluation loop may exceed this limit, causing partial runs and missed notifications.
Mitigation & Contingency
Mitigation: Design the trigger engine to batch mentor evaluations using database-side SQL queries (bulk inactivity check via a single query rather than per-mentor calls), and add a performance test against 500 mentors during development. Document the evaluated mentor count per scenario type in scenario-evaluation-config to allow selective scenario execution per run.
Contingency: If single-run execution is insufficient, split evaluation into per-scenario-type scheduled functions (inactivity check, milestone check, expiry check) on separate cron schedules, dividing the computational load across multiple invocations.
A race condition between concurrent scheduler invocations or retried cron triggers could cause the same scenario notification to be dispatched multiple times to a mentor, severely degrading trust in the feature.
Mitigation & Contingency
Mitigation: Implement cooldown enforcement using a database-level upsert with a unique constraint on (user_id, scenario_type, cooldown_window_start) so that a second invocation within the same window is rejected at the persistence layer rather than the application layer.
Contingency: Add an idempotency key derived from (user_id, scenario_type, evaluation_date) to the notification record insert; if a duplicate key violation is caught, log it as a warning and skip dispatch without error.
The trigger engine queries peer mentor activity history across potentially multiple organisations and chapters. RLS policies configured for app-user roles may block the Edge Function's service-role queries, or query performance may degrade on large activity tables.
Mitigation & Contingency
Mitigation: Confirm the Edge Function runs with the Supabase service role key (bypassing RLS) and add composite indexes on (user_id, activity_date) to the activity tables before implementing the inactivity detection query.
Contingency: If service-role access is restricted by organisational policy, implement a dedicated database function (SECURITY DEFINER) that performs the inactivity aggregation and is callable by the Edge Function with limited scope.