high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

invalidateCode(mentorId) sets the referral code record's status to 'inactive' in Supabase for the given mentor
Method reads current code status before writing; if code is already inactive, no write is performed and no error is thrown
If no referral code exists for the mentor, the method completes successfully and returns a CodeNotFoundResult without throwing
A ReferralCodeInvalidatedEvent is emitted via the event stream after a successful status write
Any downstream consumer observing the code stream receives the invalidation event and treats the deep-link URL as expired
Invalidation is atomic: if the Supabase write fails, no event is emitted and the error is propagated to the caller
Unit tests cover: successful invalidation, already-inactive code (no-op), missing code (graceful return), and repository write failure
Method is idempotent: calling it twice for the same mentor produces the same final state without errors

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase PostgREST REST API (PATCH /referral_codes)
Supabase Realtime (optional event broadcast)
data models
ReferralCode (id, mentor_id, code, status, created_at, invalidated_at)
ReferralCodeInvalidatedEvent
performance requirements
Read-before-write must complete within a single async round-trip; avoid N+1 queries
Invalidation operation (read + conditional write) must complete within 2 seconds under normal network conditions
security requirements
Only authenticated users with the 'coordinator' or 'org_admin' role may trigger mentor deactivation flows that invoke invalidateCode
Row-Level Security (RLS) on the referral_codes table must prevent direct status mutation from the client without service-layer mediation
mentorId parameter must be validated as a non-empty UUID before any database call

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Pattern: follow the existing ReferralCodeService method structure established in tasks 001–002. The method signature should be: `Future invalidateCode(String mentorId)`. Use a sealed class or enum for the return type (InvalidationResult: success, alreadyInactive, notFound) to avoid stringly-typed error handling. Read the current record using `repository.getCodeForMentor(mentorId)`, then branch on status.

The invalidation event should be emitted via a `StreamController` already managed by the service; do not create a new stream per call. Ensure `invalidated_at` timestamp is written alongside the status update using `DateTime.now().toUtc()`. Use Supabase's `.eq('mentor_id', mentorId).eq('status', 'active')` filter on the PATCH to make the write conditional — this prevents a race condition where two concurrent deactivation calls both attempt to write.

Testing Requirements

Write unit tests using flutter_test. Mock the ReferralCodeRepository using a fake implementation or Mockito-generated mock. Test cases: (1) happy path — active code is invalidated and event emitted; (2) idempotency — inactive code triggers no write and no duplicate event; (3) missing code — method returns CodeNotFoundResult without exception; (4) repository failure — SupabaseException is propagated and no event emitted. Aim for 100% branch coverage of invalidateCode.

No integration tests required at this stage; integration coverage is provided by repository-layer tests.

Component
Referral Code Service
service medium
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.