high priority high complexity testing pending testing specialist Tier 7

Acceptance Criteria

Test file is named referral_attribution_service_test.dart and resides in test/unit/services/
Click deduplication: second recordClick with same visitor_fingerprint + referral_code within 1 hour is silently ignored — repository.save called exactly once
Click deduplication: same fingerprint after > 1 hour gap is recorded as a new click — repository.save called twice
Registration matching: new member registration within 72-hour attribution window matches the correct referral code — asserting matched_referral_code_id on the created attribution record
Registration matching: registration received 73 hours after click does not match — attribution record has matched_referral_code_id = null
Registration matching: multiple overlapping clicks from different codes within the window — most recent click wins (last-touch attribution)
Confirmation transition: confirmAttribution moves status from 'pending' to 'confirmed' and sets confirmed_at timestamp
Confirmation transition: confirmAttribution on a 'confirmed' record throws AttributionAlreadyConfirmedException
Confirmation transition: confirmAttribution on a 'rejected' record throws InvalidAttributionStateException
Double-confirmation guard: calling confirmAttribution twice concurrently (using fake async) results in exactly one state change and one event publish
Aggregate count: getAggregatedCount returns correct sum after 0, 1, 5, and 10 confirmed attributions
Milestone detection: threshold boundary tests for counts [0→1, 4→5, 9→10, 24→25, 49→50] — event published for each crossed threshold
Milestone detection: count 3→4 (no threshold) — no event published
Milestone detection: count 8→11 (skips 10) — exactly one event published for threshold 10
Event publishing: BadgeCriteriaIntegration.onMilestoneEvent receives correct MilestoneEvent payload including mentor_id, threshold, and cumulative_count
Event publishing: BadgeCriteriaIntegration throws → exception is caught, attribution remains confirmed, no retry
Branch coverage of lib/services/referral_attribution_service.dart is 100% per lcov.info

Technical Requirements

frameworks
Flutter
flutter_test
Riverpod
data models
assignment
badge_definition
activity
performance requirements
Full test suite (all groups) must complete in < 10 seconds
Concurrency tests must use fake_async to avoid real timer waits
No test should depend on execution order — each uses isolated setUp()
security requirements
Test mentor_ids and organisation_ids use clearly synthetic UUIDs
No real Supabase credentials, API keys, or production data referenced in test files
Visitor fingerprint values in tests must be clearly fictional strings (e.g., 'test-fingerprint-001')

Execution Context

Execution Tier
Tier 7

Tier 7 - 84 tasks

Can start after Tier 6 completes

Implementation Notes

This is a high-complexity test suite — budget time for the fake implementations, not just the tests. The FakeRecruitmentAttributionRepository needs to simulate concurrent access for the double-confirmation guard test; use a Completer-based mechanism inside the fake to inject a delay between read and write. For milestone boundary tests, parameterise using a data-driven pattern: define a list of (before_count, after_count, expected_threshold_or_null) tuples and loop over them in one group — this avoids copy-paste and makes boundary coverage obvious. Extract shared test fixtures (sample AttributionRecord, sample MilestoneEvent) into a test_helpers.dart file in test/helpers/ to keep the main test file readable.

Ensure the SpyBadgeCriteriaIntegration records events in a List for assertion, not just a call count.

Testing Requirements

Pure unit tests with flutter_test. Use fake_async for all time-dependent scenarios (attribution window, deduplication cooldown). Organise with nested group() blocks: 'click recording' > 'deduplication', 'registration matching' > 'within window' / 'outside window' / 'multi-code conflict', 'confirmation transitions' > 'happy path' / 'guard conditions', 'aggregate counts', 'milestone detection' > 'boundary values', 'event publishing' > 'success' / 'badge layer throws'. Use a FakeRecruitmentAttributionRepository with an in-memory list and a SpyBadgeCriteriaIntegration that records all received events.

Generate coverage with flutter test --coverage and confirm 100% branch coverage on the service file. Run flutter analyze on the test file to confirm no lints.

Component
Referral Attribution Service
service high
Dependencies (7)
Implement the getAttributionCountsForMentor method in ReferralAttributionService. This method queries the repository for all attribution records belonging to a specific mentor and returns a structured summary containing: total clicks, pending attributions, confirmed attributions, and conversion rate. The result must be suitable for direct consumption by the Recruitment Stats Widget and Coordinator Dashboard components. epic-membership-recruitment-core-services-task-009 Implement the publishMilestoneEvent method in ReferralAttributionService. After each confirmed attribution, evaluate whether the mentor's cumulative confirmed count has crossed a recruitment milestone threshold (e.g., 1st, 5th, 10th recruit). If a threshold is crossed, publish a structured milestone event to the BadgeCriteriaIntegration layer using the agreed event contract so the badge system can subscribe and award the appropriate badge. epic-membership-recruitment-core-services-task-010 Wire the notification path from ReferralAttributionService to the BadgeCriteriaIntegration layer so the badge system can subscribe to confirmed-registration events in real time. Define the event stream or callback interface, register the subscription in the Riverpod provider graph, and ensure the connection is initialised at app startup. Verify that events survive provider dispose/rebuild cycles. epic-membership-recruitment-core-services-task-011 Create the ReferralAttributionService class as a Riverpod provider with constructor injection of the RecruitmentAttributionRepository and a reference to the BadgeCriteriaIntegration layer. Define the full public interface: recordClickEvent, matchRegistrationToCode, confirmAttribution, getAttributionCountsForMentor, and publishMilestoneEvent. Ensure the provider compiles against all injected abstractions. epic-membership-recruitment-core-services-task-005 Implement the recordClickEvent method in ReferralAttributionService. This method receives a referral code and device metadata, validates that the code is active, writes a click event record via the repository, and handles the network call to the Supabase Edge Function endpoint. Include idempotency logic so duplicate clicks from the same device within a short window are deduplicated before persistence. epic-membership-recruitment-core-services-task-006 Implement the matchRegistrationToCode method in ReferralAttributionService. When a new member registration is confirmed, the service looks up the most recent click event for that device or session token, resolves the originating referral code, and creates a pending attribution record linked to both the new member and the referring peer mentor. Implement time-window matching (configurable, default 72 hours) and handle the case where no matching click event is found. epic-membership-recruitment-core-services-task-007 Implement the confirmAttribution method in ReferralAttributionService. When the membership system signals that a referred new member's membership is fully verified, this method transitions the pending attribution record to confirmed status via the repository. Include guard logic to prevent double-confirmation and to handle cases where the attribution record no longer exists (e.g., was manually removed). epic-membership-recruitment-core-services-task-008
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.