critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

RecruitmentAttributionRepository class lives in lib/features/recruitment/data/repositories/ and accepts SupabaseClient via constructor injection
recordEvent(referralCodeId, eventType, metadata) inserts a row into referral_events via an edge function RPC (not direct table insert) and completes without returning data; it throws RecruitmentAttributionException on failure
getEventsForCode(referralCodeId) returns all ReferralEvent objects for the given code ordered by occurred_at ASC
getConversionFunnelForOrg(orgId, dateRange) returns an AttributionFunnel with clicked_count, registered_count, and converted_count for the given org and date window; implemented via a Supabase RPC function to avoid N+1 queries
getTotalConversionsByMentor(orgId) returns a List<MentorConversionStats> (mentorId, mentorName, conversionCount) sorted by conversionCount DESC; implemented via a Supabase RPC or database view
All aggregation RPCs are defined in a companion migration file and return stable JSON contracts
RLS is respected: a coordinator calling getConversionFunnelForOrg for another org receives an empty/zero result, not a Postgres error
All Supabase exceptions are caught and mapped to typed domain exceptions with descriptive messages
No Supabase types leak beyond the repository; domain models (ReferralEvent, AttributionFunnel, MentorConversionStats) are pure Dart classes
DateRange parameter for getConversionFunnelForOrg accepts start and end DateTime and is validated (start < end) before the RPC call

Technical Requirements

frameworks
Flutter
supabase_flutter
Riverpod
apis
Supabase PostgREST (referral_events table reads)
Supabase RPC (aggregation functions: get_conversion_funnel, get_mentor_conversion_totals)
Supabase Edge Functions (for write path via recordEvent)
data models
ReferralEvent
AttributionFunnel
MentorConversionStats
referral_events (table)
referral_codes (table, for mentor name join)
performance requirements
getConversionFunnelForOrg RPC must complete in < 300 ms for up to 50 000 events within a 90-day window
getTotalConversionsByMentor must return within < 500 ms for orgs with up to 100 mentors
Aggregation RPCs must use the indexes on (referral_code_id, event_type) and (organisation_id, occurred_at) defined in task-002
security requirements
recordEvent must route through an edge function (service_role) because the referral_events table disallows direct authenticated writes (RLS from task-002)
Edge function endpoint must validate the JWT and extract organisation_id from the authenticated user — never trust client-supplied organisation_id for write operations
RPC functions must be defined with SECURITY INVOKER so they execute under the calling user's RLS context

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Define the two aggregation Postgres functions (get_conversion_funnel and get_mentor_conversion_totals) in a dedicated migration file alongside the referral_events migration (task-002) or in a follow-up migration. The functions should accept org_id, start_date, end_date parameters and return JSON. On the Dart side, use `supabase.rpc('function_name', params: {...})` with typed response deserialization. For recordEvent, call the edge function via `supabase.functions.invoke('record-referral-event', body: {...})` — keep the edge function thin (validate JWT, insert row, return 200).

Model MentorConversionStats as an immutable Dart class with a fromJson factory. Expose the repository via a Riverpod Provider and colocate it with ReferralCodeRepository in the same provider file for discoverability.

Testing Requirements

Unit tests with flutter_test and a mocked SupabaseClient covering: (1) recordEvent calls the correct RPC/edge function with expected parameters; (2) getEventsForCode returns an empty list when no events exist; (3) getConversionFunnelForOrg correctly maps RPC response to AttributionFunnel; (4) getTotalConversionsByMentor maps RPC response to sorted MentorConversionStats list; (5) PostgrestException is mapped to RecruitmentAttributionException; (6) invalid DateRange (start >= end) throws ArgumentError before making any network call. Integration tests (local Supabase) verifying: funnel counts match inserted test events; cross-org isolation (coordinator cannot see another org's data). Target 85% line coverage on the repository.

Component
Recruitment Attribution Repository
data medium
Epic Risks (3)
high impact medium prob technical

iOS Universal Links and Android App Links have distinct configuration requirements (apple-app-site-association, assetlinks.json, entitlements). A misconfiguration causes the OS to open the referral URL in a browser instead of the app, completely breaking the onboarding funnel for new members on one platform.

Mitigation & Contingency

Mitigation: Configure both Universal Links and App Links from the start of this epic using the project's existing Supabase-hosted domain. Write an E2E test on both simulators that taps a referral URL and asserts the onboarding screen is reached. Document the required server-side JSON files alongside the migration.

Contingency: If platform deep-link configuration cannot be resolved before the UI epics need the handler, implement a fallback custom-scheme URI (e.g., likeperson://referral?code=XYZ) that works unconditionally, and schedule Universal/App Link fix as a follow-up task.

high impact high prob security

Referral click events must be writable without an authenticated session (a new member who has not yet registered is tapping the link). Standard Supabase RLS cannot grant anonymous inserts without opening a security hole. If this is not solved early it blocks the entire attribution pipeline.

Mitigation & Contingency

Mitigation: Design referral_events writes to go exclusively through a Supabase Edge Function that validates the referral code exists and is active before inserting. The Edge Function uses the service-role key server-side; the client only calls the function endpoint. This is documented in the feature spec.

Contingency: If the Edge Function approach is delayed, temporarily allow anon inserts restricted by a CHECK constraint that event_type = 'click' and new_member_id IS NULL, then tighten to Edge Function writes in a follow-up migration before the feature goes to production.

medium impact low prob dependency

The qr_flutter package version pinned in pubspec may conflict with the current Flutter SDK version or with other packages in the monorepo, causing build failures that block QR code delivery.

Mitigation & Contingency

Mitigation: Verify qr_flutter compatibility against the project's Flutter SDK version as the very first task in this epic. If a conflict exists, resolve it before any other work proceeds.

Contingency: If qr_flutter cannot be made compatible, evaluate mobile_scanner (already likely in pubspec for QR scanning) which also supports generation, or implement QR generation via a lightweight Dart port as a last resort.