docs: complete code-reality alignment for remaining docs + reconcile issue set
Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
added undocumented endpoints (ton-proof challenge, profile email verify,
GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
90-day notification TTL, soft-delete semantics, wallet fields
Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation
Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,15 +7,17 @@ related_apis: ["POST /api/points/generate-referral-code", "GET /api/points/refer
|
||||
|
||||
# Referral Flow
|
||||
|
||||
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
||||
|
||||
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`.
|
||||
- **Backend** — `PointsService` (`backend/src/services/points/PointsService.ts`), `authController` (`backend/src/services/auth/authController.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.
|
||||
- **Socket.IO** — `referral-signup` (auth domain) and `referral-reward` / `level-up` (points domain) events.
|
||||
|
||||
## Preconditions
|
||||
|
||||
@@ -26,17 +28,19 @@ Each user can generate a personal referral code, share a short URL, and earn poi
|
||||
|
||||
### 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`.
|
||||
1. User opens the points dashboard. If they don't have a code yet, they receive one automatically (`getUserPoints` lazily generates one — `PointsService.ts:216-219`).
|
||||
2. A manual `POST /api/points/generate-referral-code` is also available.
|
||||
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.
|
||||
- **ALWAYS overwrites** the user's existing code via `User.findByIdAndUpdate(userId, { referralCode: code })` (`:29`). There is **no idempotency / no `force` flag** — any param in the request body is ignored. Calling this endpoint rotates (replaces) the code every time, invalidating previously shared links.
|
||||
- Returns it.
|
||||
4. Frontend renders the share URL `https://amn.gg/r/{code}` and a copy button.
|
||||
4. Frontend renders the share URL `${NEXT_PUBLIC_API_URL}/r/${referralCode}` (pointing to the **backend** API URL, not a frontend URL) and a copy button. This is constructed in `frontend/src/sections/points/points-invite-friends.tsx:35-36`.
|
||||
> [!warning] Share link points at the wrong base
|
||||
> The link is built from `NEXT_PUBLIC_API_URL` (the backend) rather than the frontend origin. The `/r/:code` redirect on the backend then bounces the user to the frontend sign-up — so it functions, but the surfaced URL is the API host, which is not the intended public-facing brand URL.
|
||||
|
||||
### 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}`.
|
||||
5. When a friend clicks the share 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
|
||||
@@ -44,26 +48,38 @@ Each user can generate a personal referral code, share a short URL, and earn poi
|
||||
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.
|
||||
- Emits **`referral-signup`** to `user-{referrerId}` with the referee's name, email, and updated total — emitted from `authController.ts`, not from PointsService.
|
||||
8. At this point the referee is **counted** but no points have changed hands yet. Points award on subsequent business events.
|
||||
|
||||
> [!danger] No self-referral guard
|
||||
> There is **no check** preventing a user from using their own referral code. A user who enters their own code at sign-up (or any flow that sets `referredBy`) is not blocked at the controller or service level. This is a known gap — add a guard such as `if (referrer._id.equals(user._id)) return` in the email and Google sign-up paths.
|
||||
|
||||
### 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`:
|
||||
9. The **only** caller that awards referral points is `marketplaceController.ts`, which invokes `PointsService.processReferralReward(id)` **only when an order transitions to `'completed'`** (`marketplaceController.ts:473-475`, inside `if (newStatus === 'completed')`). It is **NOT** triggered on `'delivered'`, `'delivery'`, `'seller_paid'`, or any other status.
|
||||
10. `PointsService.processReferralReward(purchaseRequestId)` (`:372-429`):
|
||||
- Loads the purchase request, finds the buyer and the buyer's `referredBy` referrer (returns `null` if either is missing).
|
||||
- Computes `referralPoints = Math.floor(amount * 0.02)` — a flat **2% commission** on the selected offer's price.
|
||||
- Calls `PointsService.addPoints(referrerId, referralPoints, 'referral', {...})`.
|
||||
- Recomputes `referrer.referralStats.activeReferrals` as a count of **ALL** users with `referredBy = referrer._id` (`:409-411`) — this includes referrals that never purchased; it is **not** scoped to converted referrals.
|
||||
- Increments `referrer.referralStats.totalEarned`.
|
||||
- Emits **`referral-reward`** to `user-{referrerId}` (`:417`).
|
||||
11. Inside `addPoints` (`:36-113`):
|
||||
- Transaction-scoped Mongo session.
|
||||
- `user.points.total += amount; user.points.available += amount`.
|
||||
- `PointTransaction.create({ type:'earn', source, amount, balance, metadata })`.
|
||||
- `PointTransaction.create({ type:'earn', source, amount, balance, metadata })`. For `source === 'referral'`, `metadata.commission` is set to the amount.
|
||||
- `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.
|
||||
12. Note: only the **referrer** earns points via this path. There is no "referee also earns" reward in the current code — the referee gets nothing automatically.
|
||||
|
||||
### 5. Redemption / payout
|
||||
### 5. Redemption
|
||||
|
||||
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.
|
||||
13. Users see their balance under `/dashboard/points` and can spend via `POST /api/points/redeem` (applied as a discount against a specific purchase request).
|
||||
14. `redeemPoints(userId, pointsToUse, purchaseRequestId)` (`:118-167`):
|
||||
- Requires both `purchaseRequestId` and `pointsToUse` (controller returns `400` if either is missing or `pointsToUse <= 0`).
|
||||
- Throws `Insufficient points` if `user.points.available < pointsToUse`.
|
||||
- Decrements `available`, increments `used`, and records a `PointTransaction` with `type: 'spend'`, `source: 'redemption'`.
|
||||
- The controller computes `discount = pointsToUse * 1000` (1 point = 1000 IRR, **always**) and returns `{ transaction, discount, remainingPoints }`. There are **no** `amount` / `purpose` / `newBalance` / `redemption` fields in the response.
|
||||
|
||||
## Sequence diagram
|
||||
|
||||
@@ -77,11 +93,11 @@ sequenceDiagram
|
||||
participant DB as MongoDB
|
||||
participant IO as Socket.IO
|
||||
|
||||
R->>FE: Generate referral code
|
||||
R->>FE: Generate referral code (or auto-assigned)
|
||||
FE->>BE: POST /api/points/generate-referral-code
|
||||
BE->>DB: User.findByIdAndUpdate(referralCode=...)
|
||||
BE-->>FE: { code }
|
||||
R->>R: share https://amn.gg/r/{code}
|
||||
BE->>DB: User.findByIdAndUpdate(referralCode=...) (ALWAYS overwrites)
|
||||
BE-->>FE: { referralCode }
|
||||
R->>R: share ${NEXT_PUBLIC_API_URL}/r/{code} (backend URL)
|
||||
|
||||
N->>BE: GET /r/{code}
|
||||
BE-->>N: 302 → /auth/jwt/sign-up?ref={code}
|
||||
@@ -89,67 +105,96 @@ sequenceDiagram
|
||||
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'
|
||||
BE->>IO: emit user-{R} 'referral-signup' (authController)
|
||||
|
||||
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
|
||||
Note over BE,DB: ONLY when N's order reaches status 'completed'
|
||||
BE->>BE: marketplaceController → PointsService.processReferralReward(id)
|
||||
BE->>BE: addPoints(R, floor(amount*0.02), 'referral', {...})
|
||||
BE->>DB: add points to balance + create PointTransaction
|
||||
BE->>BE: updateUserLevel → maybe 'level-up'
|
||||
BE->>IO: emit user-{R} 'level-up'
|
||||
BE->>DB: activeReferrals = count(referredBy=R) (ALL, not just buyers)
|
||||
BE->>IO: emit user-{R} 'referral-reward' (PointsService)
|
||||
```
|
||||
|
||||
## 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 |
|
||||
> [!note] All points routes require authentication
|
||||
> `router.use(authenticateToken)` is applied to **every** route in `pointsRoutes.ts:8`. None of these endpoints — including `GET /api/points/levels` — are public.
|
||||
|
||||
| Method | Endpoint | Auth | Body / Query | Response data |
|
||||
|---|---|---|---|---|
|
||||
| `POST` | `/api/points/generate-referral-code` | user | (ignored) | `{ referralCode }` — always rotates the code |
|
||||
| `GET` | `/api/points/my-points` | user | — | `{ points, referral, currentLevel, nextLevel }` |
|
||||
| `GET` | `/api/points/transactions` | user | `page`, `limit`, `type` (`earn`/`spend`/`expire` only) | `{ transactions, pagination }` |
|
||||
| `GET` | `/api/points/referrals` | user | `page`, `limit` | `{ referrals, pagination }` |
|
||||
| `GET` | `/api/points/leaderboard` | user | `limit` only (**`period` is NOT supported**) | `{ leaderboard, total }` |
|
||||
| `GET` | `/api/points/levels` | user (**NOT public**) | — | `{ levels }` |
|
||||
| `POST` | `/api/points/redeem` | user | `{ pointsToUse, purchaseRequestId }` (both required) | `{ transaction, discount, remainingPoints }` |
|
||||
| `POST` | `/api/points/admin/add` | admin | `{ userId, amount, description }` | `{ transaction, user, levelChanged, newLevel }` |
|
||||
| `GET` | `/r/:code` | public | — | `302` redirect to sign-up |
|
||||
|
||||
### Endpoint notes (verified against code)
|
||||
|
||||
- **`GET /api/points/transactions` — `type` filter** only accepts `earn`, `spend`, or `expire` (`PointsService.ts:250-265`). There is **no source-based filtering**: you cannot filter by `referral` / `purchase` / `admin` / `redemption`.
|
||||
- **`GET /api/points/leaderboard` — the `period` filter (`all`/`month`/`week`) does not exist and is silently ignored.** `getLeaderboard(limit)` only honors `limit` and always returns all-time data sorted by `totalReferrals` then `totalEarned` (`PointsService.ts:434-479`).
|
||||
- **`POST /api/points/admin/add`** reads `{ userId, amount, description }` (the field is `description`, **not** `reason`). However the `description` is **read but never persisted** — the controller calls `addPoints(userId, amount, 'admin', {})` with an empty metadata object (`pointsController.ts:209`), so admin-granted points store **no human-readable reason**. The stored description is the generic auto-generated `'admin'` label from `getTransactionDescription`.
|
||||
|
||||
## 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.
|
||||
- **`users`**: `referralCode` on generation/rotation, `referredBy` on referee creation, `referralStats.{totalReferrals, activeReferrals, totalEarned}` and `points.{total, available, used, level}` on point events. `activeReferrals` is set by `PointsService.processReferralReward` (`:409`) as a count of **all** users with `referredBy = referrer._id`, regardless of purchase history.
|
||||
- **`pointtransactions`**: one document per `earn` / `spend` event. (`expire` is defined in the schema but **never written** — see below.)
|
||||
- **`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.
|
||||
- **`referral-signup`** → `user-{referrerId}` on referee creation — emitted by `authController.ts`; this is an **auth-domain** event (NOT emitted by `PointsService`).
|
||||
- **`referral-reward`** → `user-{referrerId}` when `PointsService.processReferralReward` runs — emitted by `PointsService.ts:417`; this is the **points-domain** event. (There is no `referral-signup` emitted from PointsService.)
|
||||
- **`level-up`** → `user-{userId}` when crossing a tier (`PointsService.ts:92`).
|
||||
|
||||
## 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`).
|
||||
- `points.available` is the spendable balance; `points.total` is the lifetime accumulation (used for level tiers); `points.used` tracks redeemed points.
|
||||
- Transactions are wrapped in a Mongo session for atomicity (`addPoints:47-88`, `redeemPoints:123-153`).
|
||||
|
||||
## 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.
|
||||
- **Self-referral** — **NOT blocked** at any level (see danger callout above). Known gap.
|
||||
- **Code rotation on regenerate** — calling `generate-referral-code` again replaces the existing code, breaking previously shared links. There is no opt-out.
|
||||
- **Referrer deleted** — `referredBy` still points to the deleted user; the new user is effectively un-attributed.
|
||||
- **Point expiry never enforced** — the `expiresAt` field and the `'expire'` transaction type exist in the schema, and there is a sparse index for expiry sweeps, but **no cron job, TTL index, or service ever creates `expire`-type transactions**. Points never actually expire today.
|
||||
- **`activeReferrals` semantics** — counts **all** referred users, not just those who completed a purchase. If conversion tracking is the intent, this counter is misleading.
|
||||
|
||||
> [!tip] Track conversion, not just sign-ups
|
||||
> `totalReferrals` is incremented on sign-up; consider also tracking `convertedReferrals` (completed purchases) to measure real growth value.
|
||||
> `totalReferrals` is incremented on sign-up and `activeReferrals` counts all referees regardless of purchase; neither distinguishes converted referrals. Consider a dedicated `convertedReferrals` counter incremented only inside `processReferralReward`.
|
||||
|
||||
## Frontend coverage (known gaps)
|
||||
|
||||
The following routes are referenced conceptually but **do NOT exist** — navigating to them returns **404**:
|
||||
|
||||
- `/dashboard/points/referrals` — 404 (no page file)
|
||||
- `/dashboard/points/transactions` — 404 (no page file)
|
||||
- `/dashboard/points/levels` — 404 (no page file)
|
||||
|
||||
Only `/dashboard/points` (`frontend/src/app/dashboard/points/page.tsx`) exists.
|
||||
|
||||
The following frontend actions are defined in `frontend/src/actions/points.ts` but have **no UI callers** (dead code from the UI's perspective):
|
||||
|
||||
- `redeemPoints` — no caller.
|
||||
- `generateReferralCode` — no caller (codes are auto-assigned server-side via `getUserPoints`).
|
||||
- `getLevels` — no caller.
|
||||
- `getReferrals` — no caller.
|
||||
- `adminAddPoints` — no caller.
|
||||
|
||||
Only `getMyPoints`, `getTransactions`, and `getLeaderboard` are actually invoked by the UI (`points-main-view.tsx`, `points-leaderboard.tsx`).
|
||||
|
||||
## 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.
|
||||
- [[Notification Flow]] — `referral-signup`, `referral-reward`, `level-up` surface here.
|
||||
- [[Escrow Flow]] — order reaching `'completed'` is the **sole** trigger for awarding referral commission.
|
||||
|
||||
## Source files
|
||||
|
||||
@@ -158,7 +203,8 @@ sequenceDiagram
|
||||
- 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/services/marketplace/marketplaceController.ts:473-475` (referral reward triggered ONLY on `'completed'`)
|
||||
- Backend: `backend/src/services/auth/authController.ts` (referral attribution + `referral-signup` emit on email/Google signup)
|
||||
- Backend: `backend/src/app.ts:274-278` (short-URL redirect)
|
||||
- Frontend: `/dashboard/account/referrals` view (see `frontend/src/sections/account/`)
|
||||
- Frontend: `frontend/src/sections/points/points-invite-friends.tsx:35-36` (builds share URL from `NEXT_PUBLIC_API_URL`)
|
||||
- Frontend: `frontend/src/actions/points.ts` (action layer; several actions have no UI callers)
|
||||
|
||||
Reference in New Issue
Block a user