high priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

getShareableUrl(mentorId, orgId) returns a fully-qualified HTTPS URL of the form https://{orgDeepLinkDomain}/join/{referralCode}
URL scheme is fetched from the organisation's configuration in the repository — not hardcoded
If the repository returns null or empty string for the deep-link scheme, the method throws a typed MissingDeepLinkConfigError (not a generic Exception)
If the scheme does not start with 'https://', the method throws a typed InvalidDeepLinkSchemeError
The resulting URL passes Uri.parse() without throwing and Uri.isAbsolute == true
The URL is valid as an iOS Universal Link: HTTPS scheme, no port, path starts with /join/
The URL is valid as an Android App Link: same HTTPS requirements as Universal Link
The referral code embedded in the URL contains only URL-safe characters (verified by Uri encoding check — no percent-encoding needed)
getShareableUrl calls generateCode internally (or accepts a pre-generated code) — does not duplicate code generation logic

Technical Requirements

frameworks
Flutter
Riverpod
apis
ReferralCodeRepository.getDeepLinkScheme(orgId)
ReferralCodeService.generateCode() (internal)
data models
assignment
performance requirements
URL construction (excluding code generation) must be synchronous after repository fetch — no additional async operations
security requirements
Only HTTPS schemes are accepted — HTTP must be rejected with InvalidDeepLinkSchemeError
The referral code in the URL must not expose mentor PII — code is opaque by design (see task-002)
Deep-link domain must be validated against an allowlist of known organisation domains stored server-side — reject arbitrary domains to prevent open redirect attacks

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Keep the method simple: await repository.getDeepLinkScheme(orgId), validate, then string-interpolate: '$scheme/join/$code'. Use Dart's Uri class for final validation rather than string regex. Define MissingDeepLinkConfigError and InvalidDeepLinkSchemeError as typed exception classes (extend AppException or equivalent base class used in the codebase). For the domain allowlist, fetch it alongside the deep-link scheme from the same repository call to avoid a second round-trip.

This URL will be passed to share_plus for sharing — ensure it does not contain any characters that share_plus would encode unexpectedly (test with the package's share method in a widget test).

Testing Requirements

Unit tests: assert correct URL format for a valid scheme + code combination. Assert MissingDeepLinkConfigError thrown for null/empty scheme. Assert InvalidDeepLinkSchemeError thrown for HTTP scheme. Assert InvalidDeepLinkSchemeError thrown for a scheme with an unrecognised domain (not in allowlist).

Assert Uri.parse(result).isAbsolute == true. Assert the referral code in the URL matches the output of generateCode for the same mentor/org. All tests use mocked repository — no real Supabase calls.

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.