critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

Service accepts a list of mentor IDs with lapsed certificates and a boolean dryRun flag
When dryRun is false, each mentor's listing_status in the peer_mentor_visibility table is set to 'deactivated' and the suppression_reason is set to 'certificate_lapsed'
When dryRun is true, no database writes are made; the service returns the same result shape with all records in a 'would_deactivate' state for inspection
Deactivation preserves the previous listing_status value in a previous_status column so reactivation can restore the exact original state
Each suppression action produces a structured log entry with: mentor_id, certificate_type, previous_status, new_status, dry_run flag, and ISO-8601 timestamp
Mentors whose listing_status is already 'deactivated' (for any reason) are skipped and logged as 'already_suppressed' — not double-written
Service returns a typed result object listing: suppressed count, skipped (already deactivated) count, dry-run count, and failed count
Batch deactivation uses a single Supabase upsert/update call where possible — not one call per mentor
Unit tests cover: happy-path batch deactivation, dry-run mode produces no writes, already-deactivated mentors are skipped, partial database failure is caught per record and logged

Technical Requirements

frameworks
Flutter (Dart)
Riverpod
apis
Supabase REST API (peer_mentor_visibility table update)
data models
PeerMentorVisibility
MentorSuppressionRecord
SuppressionResult
performance requirements
Batch update of up to 500 mentor records must use a single SQL UPDATE with WHERE mentor_id = ANY(...) via Supabase RPC or bulk filter — not a loop of individual updates
Must complete within 5 seconds for a batch of 500
security requirements
Only service-role or coordinator role may invoke this service — enforce via Supabase RLS policy on peer_mentor_visibility table
Dry-run mode must be enforced server-side; do not rely solely on client-side flag to prevent accidental writes
All deactivation events must be written to the audit log table regardless of dry-run mode (dry-run entries marked with dry_run: true)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define listing_status as a Dart enum (active, deactivated, paused, suspended) — use a database CHECK constraint for the same set. Store previous_status as a nullable column to avoid overwriting a legitimate previous deactivation reason; only write previous_status when transitioning TO deactivated state from a non-deactivated state. For bulk update, use Supabase's .update().in_('id', mentorIds) pattern — confirm the Supabase Dart client version supports this without N+1 queries. The dry-run flag should be a named parameter with a default of true to force explicit opt-in for real writes: suppress({required List mentorIds, bool dryRun = true}).

This prevents accidental production writes during development. The audit log write should happen inside the same service method after the visibility update, wrapped in a try/catch so an audit failure does not roll back the visibility change — log the audit failure separately.

Testing Requirements

Unit tests (flutter_test): mock Supabase client, assert update called with correct filter when dryRun=false, assert update NOT called when dryRun=true, assert skip logic for already-deactivated records, assert result object counts. Integration test against Supabase local emulator: seed 10 mentors (5 active, 3 already deactivated, 2 with other statuses), run suppressor for all 10, verify only 7 rows updated (5 active + 2 other), verify 3 already-deactivated skipped, verify previous_status preserved. Also test dry-run: seed same data, run with dryRun=true, assert zero rows changed. Coverage target: 85%.

Component
Peer Mentor Visibility Suppressor
service medium
Epic Risks (4)
high impact medium prob technical

If the daily edge function runs more than once in a 24-hour window due to a Supabase scheduling anomaly or manual re-trigger, the orchestrator could dispatch duplicate push notifications to the same mentor and coordinator for the same threshold, eroding user trust.

Mitigation & Contingency

Mitigation: Implement idempotency at the notification record level using a unique constraint on (mentor_id, threshold_days, certification_id). The orchestrator checks for an existing record before dispatching. Use a database-level upsert with ON CONFLICT DO NOTHING.

Contingency: If duplicate notifications are reported in production, add a rate-limiting guard in the edge function that aborts if a notification for the same mentor and threshold was created within the last 20 hours, and add an alerting rule to Supabase logs for duplicate dispatch attempts.

medium impact medium prob scope

The mentor visibility suppressor relies on the daily edge function to detect expiry and update suppression_status. A mentor whose certificate expires at midnight may remain visible for up to 24 hours if the cron runs at a fixed time, violating HLF's requirement that expired mentors disappear promptly.

Mitigation & Contingency

Mitigation: Schedule the edge function to run at 00:05 UTC to minimise lag after midnight transitions. Additionally, the RLS policy can include a direct date comparison (certification_expiry_date < now()) as a secondary predicate that does not rely on suppression_status, providing real-time enforcement at the database level.

Contingency: If the cron lag is unacceptable after launch, implement a Supabase database trigger on the certifications table that fires on UPDATE of expiry_date and calls the suppressor immediately, reducing lag to near-zero for renewal and expiry events.

medium impact low prob integration

The orchestrator needs to resolve the coordinator assigned to a specific peer mentor to dispatch coordinator-side notifications. If the assignment relationship is not normalised or is missing for some mentors, coordinator notifications will silently fail.

Mitigation & Contingency

Mitigation: Query the coordinator assignment from the existing assignments or user_roles table before dispatch. Log a structured warning (missing_coordinator_assignment: mentor_id) when no coordinator is found. Add a data quality check in the edge function that reports mentors without coordinators.

Contingency: If coordinator assignments are missing at scale, fall back to notifying the chapter-level admin role for the mentor's chapter, and surface a data quality report to the admin dashboard showing mentors without assigned coordinators.

medium impact low prob dependency

The course enrollment prompt service generates deep-link URLs targeting the course administration feature. If the course administration feature changes its deep-link schema or the Dynamics portal URL structure changes, enrollment prompts will navigate to broken destinations.

Mitigation & Contingency

Mitigation: Define the deep-link contract between the certificate expiry feature and the course administration feature as a shared constant in a cross-feature navigation config. Version the deep-link schema and validate the generated URL format in unit tests.

Contingency: If the deep-link breaks in production, the course enrollment prompt service should gracefully fall back to opening the course administration feature root screen with a query parameter indicating the notification context, allowing the user to manually locate the correct course.