critical priority high complexity backend pending backend specialist Tier 2

Acceptance Criteria

matchRegistrationToCode(newMemberId, deviceFingerprint, sessionToken, registeredAt) queries click events for the given device fingerprint or session token within the configured time window (default 72 hours before registeredAt)
When multiple click events exist in the window, the most recent one is selected as the attribution source
A pending AttributionRecord is created in the repository with status 'pending', linked to: new_member_id, referral_code_id, mentor_id, matched_click_event_id, and created_at
When no matching click event is found within the time window, the method returns NoMatchFound without creating any record
Time window is configurable via a constant or environment-level config; changing the default (72h) does not require code changes in the method body
If a pending attribution already exists for the new member, the method returns AlreadyMatched without creating a duplicate record
Attribution record creation is atomic: partial writes do not leave orphaned records
Unit tests cover: successful match (most recent click selected), no match found, existing attribution (no duplicate), multiple click events (most recent wins), boundary condition (click exactly at 72h boundary)
The method is resilient to concurrent calls for the same newMemberId (database unique constraint + application-level guard)

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase PostgREST REST API (GET /click_events with filters, POST /attribution_records)
data models
ClickEvent (device_fingerprint, session_token, clicked_at, referral_code_id)
AttributionRecord (id, new_member_id, referral_code_id, mentor_id, matched_click_event_id, status, created_at)
ReferralCode (mentor_id)
performance requirements
The click event query must use indexed columns (device_fingerprint, clicked_at) to avoid full table scans
Total method execution should complete within 3 seconds including two sequential DB round-trips
The attribution record insert and the click event lookup should be wrapped in a Supabase RPC call or sequential awaits with error rollback
security requirements
newMemberId must be a valid UUID belonging to the currently authenticated session or a trusted server-side trigger; prevent spoofing by validating against the members table
Device fingerprint used for matching is the hashed value only — never log or expose raw device identifiers
Attribution records must not be readable by the peer mentor before confirmation to avoid gaming the system; enforce via RLS

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Method signature: `Future matchRegistrationToCode(String newMemberId, String deviceFingerprint, String? sessionToken, DateTime registeredAt)`. Lookup strategy: first query by device_fingerprint, then by session_token if no fingerprint match found — union the results and take the most recent. Define the time window as a class-level constant `static const Duration _attributionWindow = Duration(hours: 72)` with a constructor parameter override for testing.

To prevent duplicates, add a UNIQUE constraint on attribution_records(new_member_id) at the DB level, and catch the unique violation in the Dart layer to return AlreadyMatched. The Supabase query should be: `.from('click_events').select().eq('device_fingerprint', fp).gte('clicked_at', cutoff).order('clicked_at', ascending: false).limit(1)`. After finding the click event, resolve mentor_id by joining referral_codes. Create the AttributionRecord in a single insert.

Use sealed classes for MatchResult: MatchFound(attributionId), NoMatchFound, AlreadyMatched.

Testing Requirements

Unit tests (flutter_test + Mockito): mock IRecruitmentAttributionRepository. Test cases must include: (1) single click event within window — attribution created; (2) two click events within window — most recent used; (3) click event at exactly 72h boundary — included (use inclusive comparison); (4) click event at 72h+1s — excluded; (5) no click events — NoMatchFound returned; (6) existing pending attribution — AlreadyMatched returned without DB insert; (7) repository insert failure — exception propagated, no partial state. Use fake DateTime injection to control 'now' in tests. Aim for 95% branch coverage.

Add a concurrency test using Future.wait with two simultaneous calls to confirm only one attribution record is created.

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.