critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

recordClickEvent(referralCode, deviceMetadata) validates the code is active before writing; if code is inactive or not found, returns a ClickRejectedResult without writing to the database
A ClickEvent record is persisted via the repository with fields: referral_code, device_fingerprint, session_token, clicked_at (UTC), and ip_hash (hashed, not raw)
Idempotency window is configurable (default 5 minutes); a second call with the same device_fingerprint and referral_code within the window returns ClickDeduplicated without a second DB write
The Supabase Edge Function is invoked via HTTP POST for analytics/side-effect purposes; a non-2xx response is logged but does NOT fail the method — the DB write is the authoritative action
Method returns a typed result: ClickRecorded(clickEventId), ClickDeduplicated, ClickRejected(reason)
Device metadata is never stored in raw form; only a hashed fingerprint is persisted to meet GDPR/privacy requirements
Unit tests cover: new click recorded, duplicate within window deduplicated, inactive code rejected, Edge Function failure does not block DB write
Integration test (with Supabase local emulator or mock) verifies the click record appears in the database after a successful call

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase Edge Function (POST /functions/v1/record-referral-click)
Supabase PostgREST REST API (POST /click_events, GET /referral_codes)
data models
ClickEvent (id, referral_code_id, device_fingerprint, session_token, clicked_at, ip_hash)
ReferralCode (status field)
DeviceMetadata (raw input, never persisted)
performance requirements
DB write and Edge Function call should be parallelized using Future.wait where possible to minimize latency
Deduplication check (read from DB or local cache) must complete within 500ms
Total method execution must not block the UI thread; use compute() if hashing is CPU-intensive
security requirements
Device fingerprint must be hashed (SHA-256 minimum) before persistence; raw IP and device identifiers must never be stored
Session token must be validated as a non-null, non-empty string before use as a deduplication key
Edge Function call must use the authenticated Supabase client so the request carries a valid JWT
GDPR compliance: click events must be subject to data retention policy; include created_at for automated purging

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

For deduplication, query the repository for a click event matching (device_fingerprint, referral_code_id) within the last N minutes before inserting. If a match is found, return ClickDeduplicated immediately. Do NOT use an in-memory cache for deduplication — the device may open the deep link in a different app session; the DB is the source of truth. For fingerprinting, use a combination of platform (iOS/Android), device model, and a randomly generated install ID (stored in secure storage on first launch) — hash the concatenation with SHA-256.

The Edge Function call is fire-and-forget for analytics; wrap it in a try/catch and log failures with a structured logger. Supabase Edge Functions are invoked via `supabase.functions.invoke('record-referral-click', body: payload)`. Return sealed class results, not exceptions, for all expected failure modes.

Testing Requirements

Unit tests (flutter_test + Mockito): mock IRecruitmentAttributionRepository and assert correct method calls for each scenario. Test the deduplication logic by simulating two rapid calls with the same fingerprint and asserting the repository insert is called only once. Test that Edge Function HTTP failure (simulated by mock returning 500) does not propagate as an exception. Integration test (optional, against Supabase local): call recordClickEvent with a valid active code, then query the click_events table directly and assert one record exists.

Test coverage target: 90% of method branches.

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.