Skip to content

ChatGPT 5.4: Referral algorithm review: missing qualification loop, unsafe application flow, and unusable credit balances #52

@teetangh

Description

@teetangh

Summary

The mobile referral implementation is much thinner than the web system and currently stops at the signup bonus. The highest-signal problems are:

  • the referral lifecycle never advances past SIGNED_UP
  • reward/counter data is not snapshotted or linked cleanly at referral creation
  • application and code-generation flows are not concurrency-safe
  • referral credits can be earned and displayed, but not redeemed in mobile checkout
  • signup/referral UX is coupled to a fire-and-forget post-auth call with no pre-validation or visible failure path

This issue expands the referral bucket into a full backend review because the data model already supports a much richer lifecycle, but the mobile repo currently drives only a small part of it.

Scope

Primary mobile files:

  • backend/lib/database/repositories/referral_repository.dart
  • backend/routes/api/referrals/apply.dart
  • backend/routes/api/referrals/code/index.dart
  • backend/routes/api/referrals/credits/available.dart
  • lib/data/datasources/remote/referral_remote_source.dart
  • lib/features/auth/screens/sign_up_screen.dart
  • backend/routes/api/checkout/index.dart

Supporting schema/model evidence:

  • backend/lib/database/schema_registry_builder.dart
  • backend/lib/generated/prisma_client.dart
  • backend/lib/generated/models/payment.dart
  • backend/lib/generated/models/referral_credit.dart

Web parity references:

  • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts
  • /Users/kaustavghosh/Desktop/familiarise_web/app/api/referrals/apply/route.ts
  • /Users/kaustavghosh/Desktop/familiarise_web/app/api/referrals/code/check/[code]/route.ts
  • /Users/kaustavghosh/Desktop/familiarise_web/lib/payments/webhooks/handlers.ts
  • /Users/kaustavghosh/Desktop/familiarise_web/lib/payments/operations/checkout.ts

Findings

1. The referral lifecycle stops at SIGNED_UP, so referrers never receive their reward and referral stats never converge

Evidence:

  • Mobile applyReferralCode() only:
    • finds the code
    • creates Referral(status = SIGNED_UP)
    • increments totalReferrals
    • creates the referee signup bonus credit
    • backend/lib/database/repositories/referral_repository.dart:15-124
  • The mobile referral route surface is limited to:
    • POST /api/referrals/apply
    • GET|POST /api/referrals/code
    • GET /api/referrals/credits/available
    • backend/routes/api/referrals/apply.dart:9-79
    • backend/routes/api/referrals/code/index.dart:10-102
    • backend/routes/api/referrals/credits/available.dart:8-45
  • I could not find any mobile-side equivalent of web processQualifyingAction() or any payment-success hook that rewards the referrer after the first paid booking.
  • The web repo explicitly processes the qualifying action and updates stats/credits.
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:204-287
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/payments/webhooks/handlers.ts:383-389

Impact:

  • referrers may never receive their promised reward from mobile-driven flows
  • successfulReferrals and totalEarned can remain permanently wrong
  • referral status stays stuck at SIGNED_UP, so the state machine never reflects actual business outcomes

Proposed fix:

  1. Add a backend referral qualification step triggered from payment success for the user’s first paid booking.
  2. Implement a mobile equivalent of web processQualifyingAction():
    • qualification window
    • one-way transition from SIGNED_UP
    • referrer bonus creation
    • stats updates
  3. Add exactly-once tests for repeated webhook/payment-success delivery.

2. Referral creation does not snapshot reward amounts and does not link the referee’s credit back to the referral

Evidence:

  • The mobile schema already includes referral lifecycle fields:
    • referrerRewardAmount
    • refereeRewardAmount
    • referrerRewardPaidAt
    • qualifiedAt
    • qualifyingAction
    • backend/lib/database/schema_registry_builder.dart:1584-1607
  • But mobile applyReferralCode() creates the Referral with only:
    • referralCodeId
    • referredUserId
    • status
    • signedUpAt
    • backend/lib/database/repositories/referral_repository.dart:74-85
  • The signup bonus ReferralCredit is also created without referralId.
    • backend/lib/database/repositories/referral_repository.dart:100-117
  • The web repo snapshots both reward amounts on the Referral row and links the referee credit to that referral.
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:158-191

Impact:

  • historical reward obligations are not preserved if the referral code’s reward amounts change later
  • the referee signup bonus is not traceably linked to the originating referral
  • analytics, auditing, support investigation, and later reward/reversal logic lose context they should already have

Proposed fix:

  1. Persist referrerRewardAmount and refereeRewardAmount when the referral is created.
  2. Link the referee’s signup bonus credit with referralId.
  3. Use those snapshotted values for all future qualification and refund logic, not the mutable ReferralCode defaults.

3. applyReferralCode() is vulnerable to TOCTOU races around maxReferrals and duplicate application

Evidence:

  • Mobile applyReferralCode() reads:
    • the referral code
    • maxReferrals
    • totalReferrals
    • existing referral by referredUserId
    • before entering the transaction
    • backend/lib/database/repositories/referral_repository.dart:20-66
  • It then updates totalReferrals using totalReferrals + 1, which is a stale read-modify-write pattern.
    • backend/lib/database/repositories/referral_repository.dart:88-98
  • There is no serializable transaction or retry wrapper.
  • The web repo does the whole apply flow inside a serializable transaction with retry.
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:17-44
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:134-202

Impact:

  • concurrent applications can overrun maxReferrals
  • totalReferrals updates can be lost or duplicated under contention
  • duplicate-apply races are left to unique constraints and exception paths instead of a deliberate state transition

Proposed fix:

  1. Move validation + create + counter update fully inside a serializable transaction.
  2. Replace totalReferrals + 1 with atomic increment semantics.
  3. Add retries for serialization failures.
  4. Add concurrent tests around:
    • last available referral slot
    • two parallel applies for the same referred user

4. Referral code generation is not uniqueness-safe and can fail with avoidable collisions

Evidence:

  • Mobile createReferralCode() generates a code and immediately inserts it.
    • backend/lib/database/repositories/referral_repository.dart:138-167
  • _generateCode() derives the code from the user name plus 2-3 random digits, but never checks the database for uniqueness before returning it.
    • backend/lib/database/repositories/referral_repository.dart:198-222
  • It also does not explicitly guard collisions against existing customCode values.
  • The web repo uses generateUniqueCode() and checks the database until it finds a non-conflicting value.
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:75-109

Impact:

  • referral code creation can fail with a server error when two similar usernames collide
  • this is more likely under seeded/demo/test environments or popular short names
  • the failure mode is avoidable and should be resolved before insert, not by leaking DB uniqueness errors

Proposed fix:

  1. Add a uniqueness loop similar to web generateUniqueCode().
  2. Check collisions against both code and customCode.
  3. Treat duplicate-code insertion as a retriable generation failure, not a user-visible 500.

5. Mobile signup applies referral codes only after authentication, with no pre-validation route and no visible failure handling

Evidence:

  • The signup screen applies the referral code only after auth state becomes authenticated.
    • lib/features/auth/screens/sign_up_screen.dart:56-64
  • That call is fire-and-forget; it is not awaited and the user is not shown whether referral application succeeded or failed.
    • lib/features/auth/screens/sign_up_screen.dart:60-64
  • The mobile backend has no code-check route analogous to web /api/referrals/code/check/[code].
    • mobile route surface: backend/routes/api/referrals/apply.dart, backend/routes/api/referrals/code/index.dart, backend/routes/api/referrals/credits/available.dart
    • web check route: /Users/kaustavghosh/Desktop/familiarise_web/app/api/referrals/code/check/[code]/route.ts:14-63
  • The web apply route also rate-limits referral application.
    • /Users/kaustavghosh/Desktop/familiarise_web/app/api/referrals/apply/route.ts:13-15

Impact:

  • invalid/expired/self referral codes are only discovered after account creation
  • signup UX can silently fail to apply a referral and the user may believe the code worked
  • support/debugging becomes harder because the referral action is detached from the signup completion UX

Proposed fix:

  1. Add a referral code validation endpoint for pre-signup checking.
  2. Surface referral application result explicitly in the signup flow.
  3. Decide whether referral application should remain post-auth or be incorporated into a backend signup transaction.
  4. Add throttling to POST /api/referrals/apply.

6. Referral credits are visible in mobile, but the mobile checkout stack has no way to redeem them

Evidence:

  • The mobile repo exposes:
    • referral credit balance endpoint
    • referral credit repository/provider
    • referral summary dashboard UI
    • backend/routes/api/referrals/credits/available.dart:8-45
    • lib/data/datasources/remote/referral_remote_source.dart:120-147
  • But the mobile checkout API and client do not accept a useReferralCredits parameter.
    • backend/routes/api/checkout/index.dart:30-55
    • lib/data/datasources/remote/checkout_remote_source.dart:18-57
    • lib/data/datasources/remote/checkout_remote_source.dart:99-150
  • The mobile checkout UI shows discount code handling only; there is no referral-credit payment path.
    • lib/features/checkout/screens/checkout_screen.dart:196-253
  • Meanwhile the mobile generated schema already supports ReferralCreditUsage and Payment.creditUsages.
    • backend/lib/generated/prisma_client.dart:286-287
    • backend/lib/generated/models/payment.dart:43-50
    • backend/lib/generated/models/referral_credit.dart:27-31
  • The web checkout stack actively applies referral credits and creates usage-ledger entries.
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:289-363
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/payments/operations/checkout.ts:400-409
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/payments/operations/checkout.ts:1604-1624

Impact:

  • users can earn referral credits on mobile but cannot actually spend them there
  • credit balances become a dead-end product promise in the mobile app
  • the schema supports a more complete model than the exposed mobile checkout flow

Proposed fix:

  1. Decide whether mobile should support referral-credit redemption now or explicitly hide balances until it does.
  2. If supported, add:
    • useReferralCredits to checkout request/response contracts
    • FIFO credit application logic
    • per-payment usage ledger entries
  3. Add end-to-end tests for partial/full credit application.

7. The mobile backend has no referral expiry, credit expiry maintenance, or credit-reversal path

Evidence:

  • I could not find any mobile backend equivalent of:
    • processQualifyingAction()
    • reverseCreditsForPayment()
    • expireStaleReferrals()
    • expireStaleCredits()
  • The web repo has all of those maintenance and reversal functions.
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:204-287
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:377-469
    • /Users/kaustavghosh/Desktop/familiarise_web/lib/referrals/service.ts:552-585

Impact:

  • stale referral rows can remain in SIGNED_UP indefinitely
  • credit lifecycle depends only on read-time filtering, not actual maintenance
  • if/when mobile starts spending credits, there is no reversal path for failed/expired/refunded payments

Proposed fix:

  1. Add referral maintenance jobs/routes for:
    • expiring stale referrals
    • expiring stale credits
  2. Implement credit reversal logic before exposing credit redemption.
  3. Reconcile referral counters and statuses against actual rewards/credits as part of a one-time backfill if needed.

Recommended fix plan

  1. Introduce a dedicated referral service layer in mobile backend instead of keeping all logic inside one repository method.
  2. Port the core web referral lifecycle:
    • serializable apply
    • qualifying action on first paid booking
    • referrer credit creation
    • stats updates
  3. Normalize referral data integrity:
    • snapshot reward amounts on Referral
    • link ReferralCredit rows to Referral
    • add credit-usage ledgering before redemption is exposed
  4. Align product surface:
    • pre-validate codes
    • expose application results
    • either enable credit redemption in checkout or stop advertising credits as spendable value
  5. Add maintenance and abuse controls:
    • route throttling
    • stale referral/credit cleanup
    • refund/failed-payment reversal hooks

Acceptance criteria

  • referrers receive their reward exactly once after the referred user’s qualifying first paid booking
  • referral stats (totalReferrals, successfulReferrals, totalEarned) converge to actual business events
  • concurrent referral applications cannot exceed maxReferrals or double-apply
  • referral code creation cannot fail from avoidable code collisions
  • signup can validate referral codes before account completion and surfaces apply failures explicitly
  • referral credits are either redeemable in mobile checkout with ledgering or are not presented as available spendable value
  • stale referrals and stale credits have a defined maintenance path

Regression test matrix

  • concurrent apply requests cannot overrun the final available referral slot
  • duplicate apply attempts for the same referred user do not create two referrals
  • referral creation snapshots reward amounts and links the referee bonus credit to the referral
  • referral code generation retries on collisions instead of failing with 500
  • invalid/self/expired referral codes can be checked before signup completion
  • first paid booking rewards the referrer exactly once even under repeated webhook delivery
  • mobile checkout either accepts and applies referral credits correctly or hides the feature entirely
  • stale signed-up referrals expire after the qualification window
  • refunded/failed payments restore consumed referral credits once redemption is enabled

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions