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

165 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Referral Flow
tags: [flow, referral, points, growth]
related_models: ["[[User]]", "[[PointTransaction]]", "[[LevelConfig]]"]
related_apis: ["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.
- **Backend** — `PointsService` (`backend/src/services/points/PointsService.ts`), points routes at `backend/src/routes/pointsRoutes.ts`.
- **MongoDB** — `users` (`referralCode`, `referredBy`, `referralStats`, `points`), `pointtransactions`, `levelconfigs`.
- **Socket.IO** — `referral-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
5. 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}`.
6. The sign-up form reads `?ref=` and pre-fills the referral field (hidden or visible).
### 3. Attribution at sign-up
7. 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.
8. At this point the referee is **counted** but no points have changed hands yet. Points award on subsequent business events.
### 4. Points awarding
9. `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.
10. 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`).
11. 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
12. Users see their balance under `/dashboard/account/points` and can spend via `POST /api/points/redeem` (e.g. for service-credit or discount codes).
13. `PointTransaction` records `type: 'spend'` with negative `amount`, keeping `balance` running.
## Sequence diagram
```mermaid
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-signup`** → `user-{referrerId}` on referee creation.
- **`level-up`** → `user-{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 deleted** — `referredBy` still points to the deleted user; the new user is effectively un-attributed. Soft-delete preservation is acceptable.
- **Points overflow** — `Number` 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
- [[Registration Flow]] — attribution point.
- [[Google OAuth Flow]] — also supports `referralCode`.
- [[Notification Flow]] — `referral-signup`, `level-up`, and points events surface here.
- [[PRD - Request Network In-House Checkout]] / [[Escrow Flow]] — completion of a purchase is the canonical trigger for awarding referral commission.
## 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/`)