Files
nick-doc/04 - Flows/Referral Flow.md

8.5 KiB
Raw Blame History

title, tags, related_models, related_apis
title tags related_models related_apis
Referral Flow
flow
referral
points
growth
User
PointTransaction
LevelConfig
POST /api/points/generate-referral-code
GET /api/points/referrals
GET /api/points/leaderboard

Referral Flow

Each user can generate a personal referral code, share a short URL, and earn points/commission when their referees sign up and complete purchases. Codes can also be entered at sign-up time (Phase 1 attribution) — see Registration Flow.

Actors

  • Referrer — the user with the code.
  • Referred user — the new sign-up.
  • BackendPointsService (backend/src/services/points/PointsService.ts), points routes at backend/src/routes/pointsRoutes.ts.
  • MongoDBusers (referralCode, referredBy, referralStats, points), pointtransactions, levelconfigs.
  • Socket.IOreferral-signup and level-up events.

Preconditions

  • Authenticated user (for referral-code generation and points endpoints).
  • The 8-character code is unique (alphabet excludes I/O/0/1 to avoid confusion — PointsService.ts:13).

Step-by-step narrative

1. Code generation

  1. User opens /dashboard/account/referrals. If they don't have a code yet, they click "Generate code".
  2. Frontend POSTs POST /api/points/generate-referral-code.
  3. PointsService.generateReferralCode(userId) (:12-31):
    • Loops generating an 8-character code from ABCDEFGHJKLMNPQRSTUVWXYZ23456789 until uniqueness is confirmed by User.findOne({ referralCode }).
    • Saves the code to the user.
    • Returns it.
  4. Frontend renders the share URL https://amn.gg/r/{code} and a copy button.

2. Short-URL redirect

  1. When a friend clicks the short URL, GET /r/:code (backend/src/app.ts:274-278) redirects to ${FRONTEND_URL}/auth/jwt/sign-up?ref={code}.
  2. The sign-up form reads ?ref= and pre-fills the referral field (hidden or visible).

3. Attribution at sign-up

  1. During Registration Flow verification (or Google OAuth Flow sign-up), the controller looks up User.findOne({ referralCode }):
    • Sets user.referredBy = referrer._id on the new user.
    • Increments referrer.referralStats.totalReferrals.
    • Emits referral-signup to user-{referrerId} with the referee's name, email, and updated total.
  2. At this point the referee is counted but no points have changed hands yet. Points award on subsequent business events.

4. Points awarding

  1. PointsService.addPoints(userId, amount, source, metadata) (:36-100) is called by other services on triggering events:
    • Purchase completion (intended): when a referred user finishes an order, the referrer should get a commission. The hook point is PurchaseRequestService notifyTransactionCompleted — the exact wiring is implementation-specific; the service exposes source: 'purchase' | 'referral' | 'bonus' | 'admin'.
    • Bonus: ad-hoc admin grants.
  2. Inside addPoints:
    • Transaction-scoped Mongo session.
    • user.points.total += amount; user.points.available += amount.
    • PointTransaction.create({ type:'earn', source, amount, balance, metadata }).
    • updateUserLevel(userId, session) recomputes the user's tier from LevelConfig.
    • Emits level-up on user-{userId} if the level changed (:91-99).
  3. Both the referrer and the referee may earn points (e.g. "give 100, get 100" growth model). The current code awards per addPoints call — design decision lives in the caller, not in PointsService.

5. Redemption / payout

  1. Users see their balance under /dashboard/account/points and can spend via POST /api/points/redeem (e.g. for service-credit or discount codes).
  2. PointTransaction records type: 'spend' with negative amount, keeping balance running.

Sequence diagram

sequenceDiagram
    autonumber
    actor R as Referrer
    actor N as New User
    participant FE as Frontend
    participant BE as Backend
    participant DB as MongoDB
    participant IO as Socket.IO

    R->>FE: Generate referral code
    FE->>BE: POST /api/points/generate-referral-code
    BE->>DB: User.findByIdAndUpdate(referralCode=...)
    BE-->>FE: { code }
    R->>R: share https://amn.gg/r/{code}

    N->>BE: GET /r/{code}
    BE-->>N: 302 → /auth/jwt/sign-up?ref={code}
    N->>FE: Fills sign-up, completes email verification
    FE->>BE: POST /api/auth/verify-email-code (with referralCode in TempVerification)
    BE->>DB: User.create
    BE->>DB: referrer.referralStats.totalReferrals += 1
    BE->>IO: emit user-{R} 'referral-signup'

    Note over BE,DB: Later, when N completes a purchase
    BE->>BE: PointsService.addPoints(R, +X, 'referral', {referredUserId:N})
    BE->>DB: add X points to user balance
    BE->>DB: create PointTransaction record
    BE->>BE: updateUserLevel → maybe 'level-up'
    BE->>IO: emit user-{R} 'level-up'

API calls

Method Endpoint Purpose
POST /api/points/generate-referral-code Generate or rotate referral code
GET /api/points/my-points Balance + level
GET /api/points/transactions History
GET /api/points/referrals Referred users list
GET /api/points/leaderboard Global top referrers
GET /api/points/levels Level config (public)
POST /api/points/redeem Spend points
POST /api/points/admin/add Admin-only manual grant
GET /r/:code Short-URL redirect to sign-up

Database writes

  • users: referralCode on generation, referredBy on referee creation, referralStats.{totalReferrals, activeReferrals, totalEarned} and points.{total, available, level} on point events.
  • pointtransactions: one document per earn/spend/refund.
  • levelconfigs: read-only at runtime (seeded at deploy).

Socket events emitted

  • referral-signupuser-{referrerId} on referee creation.
  • level-upuser-{userId} when crossing a tier.
  • new-notification → standard notification channel for points-related milestones.

Side effects

  • The referee never sees the referrer's identity unless surfaced in UI.
  • points.available is the spendable balance; points.total is the lifetime accumulation (used for level tiers).
  • Transactions are wrapped in a Mongo session for atomicity (addPoints:47-88).

Error / edge cases

  • Code collision — extremely unlikely with 32^8 ≈ 1.1 × 10¹² combinations; the while-loop in generateReferralCode is a hard guarantee.
  • Self-referral — not blocked at controller level. Add a check if (referrer._id.equals(user._id)) return in verifyEmailWithCode and googleSignUp to prevent gaming.
  • Referral code entered with leading/trailing spaces.trim() is applied (authController.ts:74, :127).
  • Referrer deletedreferredBy still points to the deleted user; the new user is effectively un-attributed. Soft-delete preservation is acceptable.
  • Points overflowNumber is sufficient up to 2⁵³; no overflow risk in practice.
  • Race on level-up — the Mongo session ensures user.points and PointTransaction are atomically updated, but two parallel addPoints calls might both trigger level-up emit. Idempotent in practice (frontend shows toast once).
  • activeReferrals — defined in referralStats but no code path increments it currently. Define "active" (e.g. referee has at least one completed purchase) and update accordingly.

[!tip] Track conversion, not just sign-ups totalReferrals is incremented on sign-up; consider also tracking convertedReferrals (completed purchases) to measure real growth value.

Linked flows

Source files

  • Backend: backend/src/services/points/PointsService.ts
  • Backend: backend/src/controllers/pointsController.ts
  • Backend: backend/src/routes/pointsRoutes.ts
  • Backend: backend/src/models/PointTransaction.ts
  • Backend: backend/src/models/LevelConfig.ts
  • Backend: backend/src/services/auth/authController.ts:411-433 (referral attribution on email signup)
  • Backend: backend/src/services/auth/authController.ts:817-838 (referral on Google signup)
  • Backend: backend/src/app.ts:274-278 (short-URL redirect)
  • Frontend: /dashboard/account/referrals view (see frontend/src/sections/account/)