Initial commit: nick docs
This commit is contained in:
163
04 - Flows/Referral Flow.md
Normal file
163
04 - Flows/Referral Flow.md
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
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: user.points += X; PointTransaction.create
|
||||
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.
|
||||
- [[Payment Flow - SHKeeper]] — 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/`)
|
||||
Reference in New Issue
Block a user