high priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

A `outlier_thresholds` table (or JSONB config column on `organisations`) stores per-org underactivity_min and overload_max values
When no org-specific threshold exists, the system falls back to a global default (e.g. underactivity < 2 activities/period, overload > 20 activities/period)
Thresholds are loaded once per summary generation run and cached for the duration of that run (not re-queried per peer mentor)
An admin API endpoint (or Supabase RPC) allows authorised users to update thresholds for their organisation
Threshold changes take effect on the next scheduled generation run, not retroactively on past summaries
The outlier detection service correctly classifies a peer mentor as underactive when their activity count falls below the resolved threshold
The outlier detection service correctly classifies a peer mentor as overloaded when their activity count exceeds the resolved threshold
Threshold values are validated: must be positive integers, underactivity_min must be strictly less than overload_max

Technical Requirements

frameworks
Dart
Supabase Edge Functions (Deno/TypeScript)
Riverpod (for Flutter-side config display if needed)
apis
Supabase PostgREST REST API
outlier_thresholds table or organisations config column
data models
OutlierThreshold
Organisation
PeerMentorActivitySummary
performance requirements
Threshold resolution must not add more than one additional DB query per summary generation batch
Global defaults must be compile-time constants requiring no DB read
security requirements
Only org admins and global admins may update org-specific thresholds
RLS policy ensures an org admin cannot read or modify another org's thresholds
Threshold values must be server-validated before persistence (no negative numbers, no inverted ranges)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Represent thresholds as a simple Dart data class `OutlierThreshold { int underactivityMin; int overloadMax; }`. Implement a `ThresholdResolver.resolve(orgId)` method that first queries org-specific config, then falls back to `OutlierThreshold.globalDefault()`. Store org thresholds as a JSONB column `outlier_config` on the `organisations` table to avoid a separate table migration — this keeps the schema simpler. Global defaults should be defined as named constants (e.g.

`kDefaultUnderactivityMin = 2`, `kDefaultOverloadMax = 20`) and documented in code comments referencing the business rationale from the workshop notes (HLF peer mentor with 380 registrations represents extreme overload context). Ensure the classification is exclusive: a peer mentor is either normal, underactive, or overloaded — never both.

Testing Requirements

Unit tests: verify threshold resolution logic returns org-specific value when present and global default when absent. Test boundary values: activity count equal to threshold (should not trigger outlier). Test invalid threshold configurations (min >= max) are rejected. Integration test: update an org threshold via RPC, run summary generation, assert outlier classification uses the updated value.

Test with organisation that has no threshold record to verify fallback behaviour.

Component
Outlier Detection Service
service medium
Epic Risks (4)
high impact medium prob technical

Supabase pg_cron or Edge Function retries could trigger multiple concurrent generation runs for the same period and organisation, producing duplicate summaries and sending multiple push notifications to users — a serious UX regression.

Mitigation & Contingency

Mitigation: Implement a database-level run-lock using an INSERT … ON CONFLICT DO NOTHING pattern keyed on (organisation_id, period_type, period_start). Only the first successful insert proceeds; subsequent attempts read the existing lock and exit early. Test with concurrent invocations in a Deno test suite.

Contingency: If duplicate summaries are detected post-deployment, add a deduplication cleanup job that removes all but the most recent summary per (user_id, period_type, period_start) and sends a corrective push notification.

medium impact low prob integration

FCM and APNs have different payload structures and size limits. An oversized or malformed payload could cause silent notification drops on iOS or delivery failures on Android, meaning mentors never learn their summary is ready.

Mitigation & Contingency

Mitigation: Build the PushNotificationDispatcher with separate FCM and APNs payload constructors, enforce a 256-byte body limit on the preview text, and run integration tests against the Firebase Emulator and a test APNs sandbox.

Contingency: Fall back to a generic 'Your periodic summary is ready' message if personalised preview text construction fails, ensuring delivery even when the personalisation pipeline encounters an error.

medium impact high prob scope

Outlier thresholds that are too tight will flag most mentors as outliers (alert fatigue for coordinators), while thresholds that are too loose will miss genuinely underactive mentors — directly undermining HLF's follow-up goal.

Mitigation & Contingency

Mitigation: Implement thresholds as configurable per-organisation database settings rather than hardcoded constants. Provide sensible defaults (underactive < 2 sessions/period, overloaded > 20 sessions/period) and document the tuning process for coordinators in the admin portal.

Contingency: If coordinators report threshold miscalibration after launch, expose a threshold configuration UI in the coordinator admin screen and allow real-time threshold adjustment without requiring a code deployment.

low impact high prob scope

The app may not have 12 months of historical activity data for all organisations at launch, making year-over-year comparison impossible for most users and rendering the comparison widget empty, which could disappoint users expecting Wrapped-style insights.

Mitigation & Contingency

Mitigation: Design the generation service to gracefully handle missing prior-year data by setting the yoy_delta field to null rather than zero. The UI must treat null as 'no comparison available' with appropriate placeholder copy rather than showing a misleading 0% delta.

Contingency: If historical data import from legacy Excel/Word sources becomes feasible, add a one-time backfill Edge Function that populates prior-year activity records from imported spreadsheets. Until then, explicitly communicate the data-availability limitation in the first summary each user receives.