high priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

getAttributionCountsForMentor(mentorId) returns an AttributionCounts object with fields: totalClicks (int), pendingAttributions (int), confirmedAttributions (int), conversionRate (double, 0.0–1.0)
conversionRate is calculated as confirmedAttributions / totalClicks; returns 0.0 when totalClicks is 0 to avoid division-by-zero
When the mentor has no click events or attributions, all counts are 0 and conversionRate is 0.0 — no exception is thrown
The aggregation query fetches click count by joining click_events to referral_codes by mentor_id, and attribution counts from attribution_records by mentor_id
Result is suitable for direct binding in the Recruitment Stats Widget without further transformation
The method does not return raw records — only the aggregated summary is exposed to callers
Unit tests cover: mentor with mixed pending/confirmed attributions, mentor with only clicks and no attributions, mentor with no activity, zero-division guard
Coordinator Dashboard can request counts for any mentor (not just the current user); mentorId parameter is not restricted to self

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase PostgREST REST API (GET with aggregate or Supabase RPC for server-side COUNT)
Supabase RPC (optional: rpc('get_attribution_counts', params: {mentor_id: ...}) for efficient server-side aggregation)
data models
AttributionCounts (totalClicks, pendingAttributions, confirmedAttributions, conversionRate)
ClickEvent (referral_code_id → mentor_id join)
AttributionRecord (mentor_id, status)
performance requirements
Prefer a single Supabase RPC call for server-side aggregation over fetching all records and counting client-side
Response must be cacheable; consumer layers (Riverpod providers) should apply a short cache TTL (e.g., 60 seconds) to avoid repeated calls during dashboard navigation
Aggregation query must use indexed mentor_id columns on both click_events (via referral_codes) and attribution_records
security requirements
Coordinators and org admins may query any mentor's counts; peer mentors may only query their own — enforce via RLS or service-layer role check
mentorId must be validated as a non-empty UUID before querying
Do not expose individual click event details or member PII in the aggregated result
ui components
Recruitment Stats Widget (consumer)
Coordinator Dashboard (consumer)

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Implement repository aggregation as a Supabase RPC function (`get_attribution_counts`) if performance testing shows client-side counting is too slow for mentors with thousands of click events. The RPC takes `p_mentor_id uuid` and returns a single row with columns `total_clicks bigint, pending_count bigint, confirmed_count bigint`. For the initial implementation, two parallel queries are acceptable: (1) COUNT of click_events joined to referral_codes where mentor_id matches; (2) COUNT of attribution_records by mentor_id grouped by status. Use `Future.wait([clickQuery, attributionQuery])` to run them in parallel.

Build the AttributionCounts model from the results and compute conversionRate in Dart. Add a `@freezed` annotation (or manual copyWith/equality) to AttributionCounts so Riverpod's select() diffing works correctly when used in widgets. Expose a `referralAttributionCountsProvider(mentorId)` FutureProvider.family alongside the service method for convenient widget consumption.

Testing Requirements

Unit tests (flutter_test + Mockito): mock IRecruitmentAttributionRepository with controlled return values. Test cases: (1) 10 clicks, 3 pending, 2 confirmed → conversionRate = 0.2; (2) 0 clicks, 0 attributions → all zeros, no exception; (3) 5 clicks, 0 attributions → conversionRate = 0.0; (4) repository throws → exception propagated. Assert that the AttributionCounts fields are calculated correctly, not just non-null. Test the zero-division guard explicitly.

No UI tests required at this layer; widget tests for Recruitment Stats Widget are out of scope for this task.

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.