critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

confirmAttribution(newMemberId) fetches the attribution record for the given member and transitions status from 'pending' to 'confirmed'
If the record is already in 'confirmed' status, the method returns AlreadyConfirmed without performing a write
If no attribution record exists for newMemberId, the method returns AttributionNotFound without throwing
Confirmation write includes a confirmed_at UTC timestamp
After successful confirmation, publishMilestoneEvent is triggered (or a ConfirmationEvent is emitted) so the badge/gamification layer can react
Double-confirmation is prevented at both the application layer (status check before write) and database layer (UNIQUE constraint or conditional UPDATE)
The conditional UPDATE uses a WHERE status='pending' clause to make the write atomic and race-condition safe
Unit tests cover: successful confirmation, already confirmed (no-op), record not found, repository update failure

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase PostgREST REST API (PATCH /attribution_records with conditional filter)
data models
AttributionRecord (id, new_member_id, status, confirmed_at)
ConfirmationEvent or MilestoneEvent
performance requirements
Use a single conditional PATCH (UPDATE ... WHERE status='pending') rather than read-then-write to minimize round-trips and eliminate race conditions
Total execution must complete within 2 seconds
security requirements
Confirmation must only be triggerable by a trusted server-side event (e.g., membership verification webhook via Supabase Edge Function) or by a coordinator/admin role — not by the peer mentor themselves
Validate newMemberId as a valid UUID before any DB call
RLS policy must ensure peer mentors cannot directly update attribution_records status

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement as a conditional PATCH to avoid the read-then-write antipattern. In Supabase Dart client: `supabase.from('attribution_records').update({'status': 'confirmed', 'confirmed_at': DateTime.now().toUtc().toIso8601String()}).eq('new_member_id', newMemberId).eq('status', 'pending')`. Check the count of affected rows in the response — if 0 rows updated, distinguish between 'already confirmed' and 'not found' by doing a follow-up read (this is the only acceptable use of read-then-write here, as a diagnostic step after a no-op update). After a successful update, call `publishMilestoneEvent(mentorId, MilestoneType.attributionConfirmed)` — resolve mentorId from the fetched record.

Return sealed class: ConfirmationResult (Confirmed, AlreadyConfirmed, NotFound).

Testing Requirements

Unit tests (flutter_test + Mockito): mock IRecruitmentAttributionRepository. Test: (1) pending record transitions to confirmed and confirmed_at is set; (2) already-confirmed record — repository update not called, AlreadyConfirmed returned; (3) no record for member — AttributionNotFound returned; (4) repository throws — exception propagates; (5) publishMilestoneEvent is called exactly once on success and zero times on AlreadyConfirmed/NotFound. Use argument captors to assert the PATCH payload contains status='confirmed' and a valid confirmed_at timestamp.

Component
Referral Attribution Service
service high
Epic Risks (3)
high impact medium prob integration

Confirmed registration events originate from the membership system (Dynamics portal for HLF), which may call back asynchronously with significant delay. If the attribution service only accepts synchronous confirmation at registration time, late callbacks will fail to match the originating referral code, resulting in under-counted conversions.

Mitigation & Contingency

Mitigation: Design the attribution confirmation path as a webhook endpoint (Supabase Edge Function) that accepts a referral_code + new_member_id pair at any time after click. The service matches by code string, not by session. Persist pending_signup events immediately at onboarding screen submission so there is always a record to upgrade to 'confirmed' when the webhook fires.

Contingency: If the membership system cannot reliably call the webhook, implement a polling reconciliation job (Supabase pg_cron, daily) that queries the membership system for recently registered members and back-fills any unmatched attribution records.

medium impact medium prob technical

If confirmRegistration() is called more than once for the same new member (e.g., idempotency retry from the webhook), duplicate milestone events could be emitted, causing the badge system to award badges multiple times.

Mitigation & Contingency

Mitigation: Use a UNIQUE constraint on (referral_code_id, new_member_id) in the referral_events table for confirmed events. The confirmRegistration() method uses upsert semantics; milestone evaluation reads the confirmed count from the aggregation query rather than counting individual calls.

Contingency: If duplicate awards occur in production, the badge system should support idempotent award checks (query existing badges before awarding). Add a deduplication guard in BadgeCriteriaIntegration as a secondary defence.

medium impact medium prob scope

Stakeholder review may expand attribution requirements mid-epic to include click-through tracking per channel (WhatsApp vs SMS vs email), which is not currently in scope but was mentioned in user story discussions. This would require schema changes in the foundation epic and delay delivery.

Mitigation & Contingency

Mitigation: Capture per-channel data in the device_metadata JSONB field from day one as an unstructured field (share_channel: 'whatsapp'). This preserves data without requiring a schema column, allowing structured querying to be added later without migrations.

Contingency: If channel-level analytics become a hard requirement during this epic, timebox the change to adding a nullable channel column to referral_events and a corresponding filter parameter on the aggregation query, deferring dashboard UI to a separate task.