high priority medium complexity testing pending testing specialist Tier 3

Acceptance Criteria

Test file is named referral_code_service_test.dart and resides in test/unit/services/
All tests use flutter_test and a hand-written fake (or Mockito-generated mock) for ReferralCodeRepository — no real Supabase calls
Code generation: same mentor_id + organisation_id inputs always produce the same code string (deterministic) — tested with 10 identical calls asserting equal output
Uniqueness: 1000 generated codes for different mentor_ids contain no duplicates — tested with a Set membership check
URL construction: valid base URL produces 'https://app.example.com/join?ref=CODE' format — asserted with Uri.parse validation
URL construction: missing scheme in config throws ConfigurationException with message containing 'scheme' — tested with expect(throws)
URL construction: empty code string throws InvalidCodeException before any URL construction attempt
Invalidation: calling invalidateCode transitions code status from active to inactive in the repository — verified via mock capture
Invalidation: calling invalidateCode on an already-inactive code throws CodeAlreadyInvalidException
Invalidation: calling invalidateCode on a non-existent code ID throws CodeNotFoundException
Repository failure: repository.save throws StorageException → service propagates as ReferralServiceException wrapping the original cause
Repository failure: repository.findByMentor returns empty → service returns null (not throws)
Branch coverage report generated via flutter test --coverage shows 100% for lib/services/referral_code_service.dart

Technical Requirements

frameworks
Flutter
flutter_test
Riverpod
data models
assignment
contact
performance requirements
Full test suite must complete in < 3 seconds — all synchronous or using fake async
1000-iteration uniqueness test must run in < 500ms
security requirements
Test data must not contain real personnummer, real phone numbers, or production UUIDs — use clearly fake UUIDs (00000000-0000-0000-0000-000000000001 pattern)
Mock repository must not write to disk or network — assert no file I/O in test teardown

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Prefer hand-written fakes over Mockito-generated mocks for this service — fakes are more readable and don't require code generation. The FakeReferralCodeRepository should maintain an in-memory Map and expose a List callLog for assertions. For the deterministic code generation test, inject a seeded Random or a fixed-seed hash function via constructor parameter so production code remains pure and tests are deterministic. If the service uses DateTime.now() internally, inject a Clock abstraction (clock package) so tests can control time.

Document each test group with a brief comment explaining what invariant it guards.

Testing Requirements

Pure unit tests using flutter_test. Organise with group() blocks: 'generateCode', 'constructShareableUrl', 'invalidateCode', 'repository error propagation'. Use setUp() to initialise a fresh fake repository and service instance per test. Use fake_async package for any timer-based logic.

Assert both the return value and the exact method calls on the mock repository using verify(). Generate coverage report with `flutter test --coverage test/unit/services/referral_code_service_test.dart` and assert lcov.info shows 100% line and branch coverage for the service file before marking task complete.

Component
Referral Code Service
service medium
Dependencies (4)
Implement the getShareableUrl method in ReferralCodeService that fetches the organisation's configured deep-link scheme from the repository and combines it with the referral code to produce a fully-qualified, shareable URL. Handle missing or misconfigured scheme values with a typed error. Ensure the URL is valid for both iOS universal links and Android App Links. epic-membership-recruitment-core-services-task-003 Add the invalidateCode method to ReferralCodeService that marks a mentor's referral code as inactive when their account is deactivated. The method must: read current code status before invalidating, write the inactive state via the repository, and emit an invalidation event so any cached deep-link URLs are treated as expired by downstream consumers. Handle the case where no code exists for the mentor. epic-membership-recruitment-core-services-task-004 Create the ReferralCodeService class as a Riverpod provider with constructor injection of the ReferralCodeRepository dependency. Define the public interface: generateCode, getShareableUrl, invalidateCode, and getCodeForMentor. Wire up the provider registration and ensure it compiles cleanly against the repository abstraction from the foundation epic. epic-membership-recruitment-core-services-task-001 Implement the deterministic, URL-safe code generation algorithm in ReferralCodeService. The algorithm must produce one unique code per peer mentor per organisation, be reproducible given the same mentor and org identifiers, use a character set safe for deep-link URLs (alphanumeric, no ambiguous characters), and be short enough for display (8–12 chars). Include collision detection via the repository before persisting. epic-membership-recruitment-core-services-task-002
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.