critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

generateCode(mentorId, orgId) returns the same code on repeated calls with the same inputs (deterministic)
Generated code contains only alphanumeric characters from the set [A-Z0-9] excluding ambiguous characters (0, O, I, 1, L) — verified by regex assertion in tests
Generated code length is between 8 and 12 characters inclusive
If a code already exists for the mentor+org pair in the repository, generateCode returns the existing code without creating a duplicate
If a collision is detected (different mentor produces same hash), a suffix is appended and re-checked — loop terminates within 5 iterations or throws a typed CollisionExhaustedError
generateCode persists the new code via ReferralCodeRepository.save() exactly once on first call for a mentor
generateCode is idempotent — calling it 10 times for the same mentor produces exactly 1 database write
Code generation does not use Math.random() or any non-deterministic source — algorithm is pure given inputs

Technical Requirements

frameworks
Flutter
Riverpod
apis
ReferralCodeRepository.save()
ReferralCodeRepository.findByMentorAndOrg()
ReferralCodeRepository.existsByCode()
data models
assignment
performance requirements
Code generation (hashing + encoding) must complete in under 5ms on a mid-range mobile device
Maximum 5 repository round-trips for collision resolution before throwing
security requirements
Algorithm must not embed mentorId or orgId in plaintext within the code — use a one-way hash (e.g., SHA-256 truncated)
Codes must not be guessable or enumerable — derived from a keyed HMAC with a per-organisation secret stored server-side, not hardcoded
personnummer and other PII must never be used as inputs to the generation algorithm

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Recommended algorithm: HMAC-SHA256(key=orgSecret, message=mentorId+orgId), take first 8 bytes of digest, encode with base32-crockford alphabet (removes ambiguous chars automatically, URL-safe). This gives determinism, unguessability, and a clean character set in one step. orgSecret should be fetched from the repository (stored per organisation in Supabase, never in the app binary). For collision handling: append a 1-char numeric suffix (code + '2', '3', ...) and re-hash or simply append — document the strategy.

Store the generated code via ReferralCodeRepository immediately after generation so subsequent calls hit the early-exit path. Use Dart's crypto package (dart:convert + crypto) which is already available in Flutter projects.

Testing Requirements

Unit tests: assert same output for same inputs across 100 iterations. Assert character set validity with regex. Assert length bounds. Assert collision detection calls existsByCode() and retries.

Assert idempotency: second call to generateCode with same mentor returns early from repository lookup without calling save() again. Property-based test (if fast_check or equivalent available in Dart): generate codes for 1000 random mentor/org UUID pairs and assert no two produce the same code (birthday-paradox check for the chosen code length and alphabet).

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.