Implement check_activity_duplicates Supabase RPC
epic-duplicate-activity-detection-foundation-task-002 — Create the PostgreSQL function check_activity_duplicates as a Supabase RPC. The function accepts peer_mentor_id, activity_type_id, date, and an optional exclusion ID, and returns candidate rows ordered by similarity score. Must respect chapter-scoped RLS policies so coordinators can only query within their chapter membership. Validate query plan uses the composite index.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Define the similarity score as a deterministic integer or decimal: full match on all three key fields (peer_mentor_id + activity_type_id + date) = 1.0; partial matches (e.g., same peer_mentor_id + activity_type_id but different date within ±7 days) = 0.7; same peer_mentor_id + date only = 0.4. Document the scoring formula in a SQL block comment above the function. Use SECURITY DEFINER so the function runs with elevated privileges but filter chapter membership inside the function body using auth.uid() to maintain logical row-level security. Avoid using SECURITY INVOKER if RLS on activities does not cover the coordinator's view adequately.
The function should use a CTE for readability: first CTE resolves the coordinator's chapter memberships, second CTE fetches candidate activities, third applies scoring. This structure also allows the query planner to optimise each CTE independently.
Testing Requirements
Write SQL test scripts (or pgTAP tests) covering: (1) call with no matching activities returns empty set; (2) call with exact duplicate present returns that activity with highest similarity_score; (3) call with p_exclude_id equal to the duplicate's ID returns empty set; (4) call by a coordinator not in the activity's chapter returns empty set (RLS enforcement); (5) EXPLAIN ANALYZE output shows Index Scan on idx_activities_peer_mentor_type_date_status. Run tests via `supabase db reset && psql -f tests/rpc/check_activity_duplicates_test.sql`. Also test Dart integration: call via supabase.rpc() in an integration test environment and assert the returned list maps correctly to DuplicateCandidate models.
The `check_activity_duplicates` RPC may not meet the 500ms target on production-scale data if the composite index is not applied correctly or if Supabase RLS evaluation adds unexpected overhead, causing the duplicate check to noticeably delay activity submission.
Mitigation & Contingency
Mitigation: Write the RPC with an explicit EXPLAIN ANALYZE in development against a seeded dataset representative of a large chapter (10,000+ activities). Pin the index hint in the RPC body and verify the query plan in Supabase's SQL editor before merging.
Contingency: If the 500ms target cannot be met with the RPC approach, introduce an async post-submit check pattern where the activity is saved first and the duplicate warning is surfaced as a follow-up notification, preserving submission speed at the cost of real-time blocking UX.
RLS policies for the coordinator_duplicate_queue view must correctly scope results to the coordinator's chapters. Incorrect policies could expose duplicate records from other chapters (privacy violation) or hide legitimate duplicates (functional regression).
Mitigation & Contingency
Mitigation: Write explicit integration tests that verify RLS behaviour using at least three distinct coordinator + chapter combinations, including a peer mentor belonging to two chapters. Use Supabase's built-in RLS testing utilities.
Contingency: If RLS proves too complex for the queue view, move the chapter-scoping filter into the DuplicateQueueRepository query layer at the application level, trading database-enforced isolation for application-enforced scoping with full test coverage.
Adding the duplicate_reviewed column to the activities table and the composite index requires a migration against a live table. If the migration locks the table for an extended period, it could disrupt active coordinators submitting activities.
Mitigation & Contingency
Mitigation: Use PostgreSQL's `CREATE INDEX CONCURRENTLY` to avoid table lock. Add the duplicate_reviewed column with a DEFAULT false so no backfill update lock is required. Schedule the migration during a low-traffic window.
Contingency: If concurrent index creation fails or takes too long, fall back to a smaller partial index scoped to the last 90 days of activities, then expand it incrementally.