From 9698ec580905641a0b1f09dc73ebb25ef6e04a1f Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 29 May 2026 14:57:47 +0400 Subject: [PATCH] docs: align API reference and data model docs with code reality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API Reference (9 files updated): - Marketplace API: corrected offer endpoints (scoped under /purchase-requests/:id/offers), marked phantom /search /stats /seller/:sellerId /withdraw routes as NOT IMPLEMENTED, documented PUT→PATCH mismatches, removed invalid SellerOffer 'active' status - Dispute API: corrected resolve schema (action enum), categories (no 'fraud'), removed 'under_review' status, added security callouts (3 unguarded endpoints), route shadowing documented, all socket events marked as TODO stubs - Notification API: corrected mark-all-read method+path, fixed broken GET /:id, added unread-count-update event, 90-day TTL documented - Payment API: /create→/save, removed 10+ phantom endpoints, fixed release/refund paths (no /shkeeper/ segment), added 3 unauthenticated endpoint security warnings, stats undercounting documented, export privilege gap documented - Authentication API: 8-digit→6-digit code, no-complexity warning on reset-with-code, rate limiter counts all attempts, passkey stub claims removed, deleteAccount bug noted - Admin API: PUT→PATCH bug documented, wrong status values documented, hard vs soft delete clarified, scanner no-auth security bug, 3 NOT IMPLEMENTED endpoints - Chat API: file upload wrong endpoint bug, archive PUT→PATCH bug, rate limits added - Points API: corrected redeem schema, referral triggers on 'completed' only, leaderboard period ignored, removed 'refund' PointTransaction type - Socket Events: removed request-cancelled, notification-read; added unread-count-update; dispute events all stubs; referral-signup is auth-domain not points-domain Data Models (3 files updated): - SellerOffer: removed 'active' from status enum, withdrawOffer() is dead code - PurchaseRequest: added pending_payment/active statuses, added 'urgent' urgency, corrected description minimum (5 chars), removed finalized/archived - Dispute: corrected action enum, categories (no fraud), removed under_review, security callout on unguarded status/resolve endpoints Co-Authored-By: Claude Sonnet 4.6 --- 02 - Data Models/Dispute.md | 20 ++++++++ 02 - Data Models/PurchaseRequest.md | 12 ++++- 02 - Data Models/SellerOffer.md | 14 ++++++ 03 - API Reference/Admin API.md | 34 +++++++++++--- 03 - API Reference/Authentication API.md | 27 ++++++++--- 03 - API Reference/Chat API.md | 20 ++++++-- 03 - API Reference/Dispute API.md | 59 +++++++++++++++++------- 03 - API Reference/Marketplace API.md | 39 ++++++++++++++-- 03 - API Reference/Notification API.md | 24 +++++++++- 03 - API Reference/Payment API.md | 55 +++++++++++++--------- 03 - API Reference/Points API.md | 31 +++++++++---- 03 - API Reference/Socket Events.md | 27 +++++++---- 12 files changed, 287 insertions(+), 75 deletions(-) diff --git a/02 - Data Models/Dispute.md b/02 - Data Models/Dispute.md index ef730f5..a7ef223 100644 --- a/02 - Data Models/Dispute.md +++ b/02 - Data Models/Dispute.md @@ -6,6 +6,8 @@ aliases: [Complaint, IDispute] # Dispute +> **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)) + Buyer-raised complaint tied to a [[PurchaseRequest]]. Captures the reason, priority, category, an array of evidence uploads, a chronological `timeline` of actions, an optional resolution, and SLA deadlines. An admin (`adminId`) is assigned during triage and resolves the dispute with a structured action (`refund`, `replacement`, `compensation`, `warning_seller`, `ban_seller`, or `no_action`). > [!note] Implementation status @@ -13,6 +15,8 @@ Buyer-raised complaint tied to a [[PurchaseRequest]]. Captures the reason, prior > > Source: `backend/src/models/Dispute.ts` — schema definition and model export. +> ⚠️ **SECURITY** — The dispute `status` update endpoint and the `resolve` endpoint currently have **no role guards**. Any authenticated user (not just admins) can modify dispute status or submit a resolution. This is a known gap pending a role-guard audit. + ## Schema | Field | Type | Required | Default | Validation | Index | Description | @@ -49,6 +53,22 @@ Buyer-raised complaint tied to a [[PurchaseRequest]]. Captures the reason, prior | `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. | | `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. | +### Category enum + +Valid values: `product_quality` · `delivery_delay` · `wrong_item` · `payment_issue` · `seller_behavior` · `other` + +**Note:** `fraud` is **not** a valid category value. Use `seller_behavior` or `other` for fraud-related complaints. + +### Status enum + +Valid values: `pending` · `in_progress` · `waiting_response` · `resolved` · `rejected` · `closed` + +**Note:** `under_review` does **not** exist in the schema. The equivalent lifecycle state is `in_progress`. + +### Resolution action enum + +Valid values: `refund` · `replacement` · `compensation` · `warning_seller` · `ban_seller` · `no_action` + > [!note] `messages` in the interface > The TypeScript interface mentions an optional embedded `messages[]` array, but the actual Mongoose schema does not declare it — messages live in [[Chat]] via `chatId`. diff --git a/02 - Data Models/PurchaseRequest.md b/02 - Data Models/PurchaseRequest.md index 29f46a7..ebc5dab 100644 --- a/02 - Data Models/PurchaseRequest.md +++ b/02 - Data Models/PurchaseRequest.md @@ -6,6 +6,8 @@ aliases: [Purchase Request, Buy Request, IPurchaseRequest] # PurchaseRequest +> **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)) + The central buyer-side document. A `PurchaseRequest` captures what a buyer wants to acquire (physical product, digital product, service, or consultation), the budget envelope, urgency, delivery details, and the entire lifecycle from creation through payment, delivery, and completion. Sellers respond by attaching [[SellerOffer]] documents; the buyer accepts one, a [[Payment]] is opened, and delivery is verified by a 6-digit code. > [!note] Source @@ -18,7 +20,7 @@ The central buyer-side document. A `PurchaseRequest` captures what a buyer wants | --- | --- | --- | --- | --- | --- | --- | | `buyerId` | ObjectId → [[User]] | yes | — | — | yes | Buyer that owns the request. | | `title` | String | yes | — | trim, maxlength 200 | — | Short headline. | -| `description` | String | yes | — | trim, maxlength 2000 | — | Long form description. | +| `description` | String | yes | — | trim, minlength 5 (frontend), maxlength 2000 | — | Long form description. Frontend enforces a 5-character minimum; the field is optional in the raw schema but the form will reject shorter values. | | `categoryId` | ObjectId → [[Category]] | yes | — | — | yes | Category the request belongs to. | | `productType` | String | no | `physical_product` | enum: `physical_product` / `digital_product` / `service` / `consultation` | yes | What kind of fulfilment is expected. | | `productLink` | String | no | — | trim, must match `/^https?:\/\/.+/` | — | Reference URL for the desired product. | @@ -31,7 +33,7 @@ The central buyer-side document. A `PurchaseRequest` captures what a buyer wants | `budget.max` | Number | no | — | min 0 | — | Upper bound. | | `budget.currency` | String | no | `USD` | enum: `USD` / `EUR` / `IRR` | — | Budget currency. | | `urgency` | String | no | `medium` | enum: `low` / `medium` / `high` / `urgent` | yes | Buyer urgency. | -| `status` | String | no | `pending` | enum (13 values, see below) | yes | Lifecycle state. | +| `status` | String | no | `pending` | enum (13 values — see State Transitions below) | yes | Lifecycle state. | | `isPublic` | Boolean | no | `true` | — | — | Public marketplace listing vs. private request. | | `tags[]` | String[] | no | — | trim | — | Free-form tags. | | `specifications[].key` | String | yes | — | trim | — | Spec key. | @@ -84,6 +86,12 @@ The central buyer-side document. A `PurchaseRequest` captures what a buyer wants | `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. | | `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. | +### Status enum — all valid values + +`pending_payment` · `pending` · `active` · `received_offers` · `in_negotiation` · `payment` · `processing` · `delivery` · `delivered` · `confirming` · `completed` · `seller_paid` · `cancelled` + +**Note:** `finalized` and `archived` are **not** valid status values and do not appear in the `IPurchaseRequest` frontend type or the Mongoose schema enum. Using either would cause a validation error. + ## Virtuals None defined. diff --git a/02 - Data Models/SellerOffer.md b/02 - Data Models/SellerOffer.md index f900bdd..b19ec32 100644 --- a/02 - Data Models/SellerOffer.md +++ b/02 - Data Models/SellerOffer.md @@ -6,6 +6,8 @@ aliases: [Seller Offer, Bid, ISellerOffer] # SellerOffer +> **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)) + A seller's bid against a [[PurchaseRequest]]. Stores the proposed price, the delivery time commitment, optional notes/attachments, and a small status machine (`pending` / `accepted` / `rejected` / `withdrawn`). The parent `PurchaseRequest` keeps the array of offer ids in `offers[]` and the chosen one in `selectedOfferId`. > [!note] Source @@ -31,6 +33,8 @@ A seller's bid against a [[PurchaseRequest]]. Stores the proposed price, the del | `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. | | `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. | +> **Status enum note:** Valid values are `pending | accepted | rejected | withdrawn` only. `'active'` is **not** a valid status and would throw a Mongoose `ValidationError` if passed. + ## Virtuals None defined. @@ -56,6 +60,16 @@ None defined. None defined. +## Service notes + +### `createOffer` — eligible parent request statuses + +`createOffer` in `SellerOfferService` permits offers against a `PurchaseRequest` whose status is **`pending`**, **`received_offers`**, or **`active`**. Attempts against any other status are rejected. + +### `withdrawOffer()` — dead code + +`SellerOfferService.withdrawOffer()` exists in the source but is **not exposed via any HTTP route**. It cannot be called through the API. Any frontend references to a withdraw endpoint will receive a `404`. + ## Relationships - **References**: [[User]] (`sellerId`), [[PurchaseRequest]] (`purchaseRequestId`). diff --git a/03 - API Reference/Admin API.md b/03 - API Reference/Admin API.md index b1b0316..f75207a 100644 --- a/03 - API Reference/Admin API.md +++ b/03 - API Reference/Admin API.md @@ -5,6 +5,8 @@ tags: [api, admin, reference] # Admin API +> **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)) + There is no single `/api/admin` namespace — admin-only endpoints are scattered across the service routers. This page catalogs them in one place. All require `Bearer JWT` with `req.user.role === 'admin'`. The two enforcement patterns are: - Middleware: `authorizeRoles('admin')` after `authenticateToken` (used by the dispute, data-cleanup, blog routers). @@ -17,7 +19,7 @@ See full descriptions in [[User API]]. | Endpoint | Action | | --- | --- | | `POST /api/user/admin/create` | Create user with role/status | -| `DELETE /api/user/admin/:userId` | Delete user (admins cannot delete each other) | +| `DELETE /api/user/admin/:userId` | Soft delete user — sets `status='deleted'` (admins cannot delete each other) | | `PATCH /api/user/admin/:userId/status` | Activate / suspend | | `PATCH /api/user/admin/:userId/toggle-status` | Flip active flag | | `PATCH /api/user/admin/:userId/role` | Change role | @@ -30,6 +32,12 @@ See full descriptions in [[User API]]. | `PATCH /api/users/admin/:userId/password` | Force password reset (wipes refresh tokens) | | `POST /api/users/admin/:userId/resend-verification` | Resend verification email | +**⚠️ KNOWN BUG — HTTP verb mismatch (status/role updates):** The frontend Redux actions for `updateUserStatus` and `updateUserRole` send `PUT` requests, but the backend registers these handlers under `PATCH`. These calls will receive `404 Method Not Found` responses until the frontend is corrected to use `PATCH`. + +**⚠️ KNOWN BUG — Status value mismatch:** The frontend sends `'inactive'` and `'pending'` as status values when updating user status. The backend only accepts `'active'`, `'suspended'`, or `'deleted'`. Sending `'inactive'` or `'pending'` will be rejected or silently ignored. + +**Hard vs. soft delete note:** The legacy route `DELETE /users/admin/:id` performs a **hard delete** (`findByIdAndDelete`). The current route `DELETE /api/user/admin/:userId` performs a **soft delete** (sets `status='deleted'`). Always use the current `/api/user/admin/:userId` route to preserve data integrity. + ## Listing / marketplace moderation See [[Marketplace API]]. Admins can use most marketplace endpoints with elevated privileges (e.g. delete any purchase request, override offer status). Specific admin-only actions: @@ -62,14 +70,16 @@ See [[Payment API]]. | `POST /api/payment/payments/cleanup-pending` | Delete stale pending payments | | `POST /api/payment/payments/:id/fetch-tx` | Re-query chain for missing tx hash | | `POST /api/payment/payments/auto-fetch-missing` | Batch tx-hash backfill | -| `POST /api/payment/shkeeper/:id/release` | Build escrow-release tx | -| `POST /api/payment/shkeeper/:id/release/confirm` | Confirm release tx hash | -| `POST /api/payment/shkeeper/:id/refund` | Build refund tx | -| `POST /api/payment/shkeeper/:id/refund/confirm` | Confirm refund tx hash | +| `POST /api/payment/:id/release` | Build escrow-release tx | +| `POST /api/payment/:id/release/confirm` | Confirm release tx hash | +| `POST /api/payment/:id/refund` | Build refund tx | +| `POST /api/payment/:id/refund/confirm` | Confirm refund tx hash | | `POST /api/payment/shkeeper/payout` | Create payout task | | `GET /api/payment/shkeeper/webhook-stats` | Webhook telemetry | | `POST /api/payment/decentralized/admin-payout` | Direct admin-wallet payout | +**⚠️ Path correction:** Release/refund routes do **not** include a `/shkeeper/` segment. The correct paths are `/api/payment/:id/release`, `/api/payment/:id/release/confirm`, etc. (Previously documented incorrectly as `/api/payment/shkeeper/:id/…`.) + ## Points (admin) See [[Points API]]. @@ -125,12 +135,24 @@ Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/se **Description:** Seeds users, addresses, and templates in dependency order. Used to bootstrap a fresh staging environment. +## Scanner / monitoring + +### GET /api/admin/scanner/status + +**Description:** Returns the current state of the blockchain scanner / wallet monitor. +**⚠️ SECURITY BUG — NO AUTHENTICATION:** Despite being mounted under `/api/admin/` and documented as admin-only, this endpoint has **no** `authenticateToken` or `authorizeRoles` guard. Any unauthenticated request can read scanner state. + +> ⚠️ **NOT IMPLEMENTED:** The following endpoints do not exist in the codebase: +> - `GET /api/admin/settings/confirmation-thresholds/history` — only the current-values `GET /api/admin/settings/confirmation-thresholds` and per-chain `PATCH /api/admin/settings/confirmation-thresholds/:chainId` exist. +> - `POST /api/admin/rn/networks/reload` — the network registry cannot be reloaded at runtime via HTTP. +> - `POST /api/admin/rn/networks/probe/:chainId` — no per-chain probe endpoint exists. + ## Analytics There is no dedicated analytics router. Admin dashboards stitch together: - `GET /api/users/admin/stats` (user metrics) -- `GET /api/payment/stats` (payment aggregates) +- `GET /api/payment/stats` (payment aggregates — note: `'completed'` status is excluded from `successfulPayments` count) - `GET /api/disputes/statistics` (dispute KPIs) - `GET /api/admin/cleanup/stats` (collection sizes) - `GET /api/payment/shkeeper/webhook-stats` (provider health) diff --git a/03 - API Reference/Authentication API.md b/03 - API Reference/Authentication API.md index 8716466..230a3f8 100644 --- a/03 - API Reference/Authentication API.md +++ b/03 - API Reference/Authentication API.md @@ -5,15 +5,19 @@ tags: [api, auth, reference] # Authentication API +> **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)) + All endpoints are mounted under `/api/auth/*` in `backend/src/app.ts`. The routes file is [`backend/src/services/auth/authRoutes.ts`](../../backend/src/services/auth/authRoutes.ts) and the WebAuthn sub-routes are in [`passkeyRoutes.ts`](../../backend/src/services/auth/passkeyRoutes.ts). Controller logic lives in [`authController.ts`](../../backend/src/services/auth/authController.ts) and [`authService.ts`](../../backend/src/services/auth/authService.ts). Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[TempVerification]] document that holds pending registration data until the email code is confirmed. Tokens are signed JWTs (access + refresh) created in `authService`. See [[Authentication Flow]] for the high-level lifecycle diagram. +**Token refresh behaviour:** The Axios interceptor handles `401` responses to trigger a token refresh. `403` errors are **not** intercepted and propagate directly to callers. + ## Registration ### POST /api/auth/register -**Description:** Start a new registration. Creates a [[TempVerification]] document and emails an 8-digit verification code. The actual [[User]] is only created once the code is verified. +**Description:** Start a new registration. Creates a [[TempVerification]] document and emails a **6-digit** verification code. The actual [[User]] is only created once the code is verified. **Auth required:** No **Request body:** ```ts @@ -45,7 +49,7 @@ Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[Temp ```ts { email: string; - code: string; // 8 digits + code: string; // 6 digits (generated by authService.generateVerificationCode()) password?: string; // required if not provided at register } ``` @@ -76,7 +80,7 @@ Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[Temp ### POST /api/auth/resend-verification -**Description:** Re-issues the 8-digit code for a pending or unverified user. +**Description:** Re-issues the 6-digit code for a pending or unverified user. **Auth required:** No **Request body:** `{ email: string }` **Response 200:** `{ "success": true, "message": "Verification code resent" }` @@ -116,6 +120,9 @@ Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[Temp - `401` invalid credentials - `403` email not verified - `423` account locked (after repeated failures, tracked in Redis via `rateLimitService`) + +**⚠️ Rate limiter behaviour:** The attempt counter increments on **every** attempt (before password validation), not only on failures. 5 total attempts within 15 minutes triggers lockout — a user burning 5 attempts with typos will be locked out even if they never had a valid password. + **Side effects:** - Updates `user.lastLoginAt`. - Pushes refresh token onto `user.refreshTokens`. @@ -194,7 +201,9 @@ Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[Temp ## Passkey / WebAuthn -Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyService.ts`. +Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyService.ts`. These routes go directly to the Express backend via the `next.config.ts` rewrite rule (`/api/:path*` → backend). No Next.js route handlers exist for passkey paths. + +**Implementation status:** Passkey attestation is **fully implemented** using `@simplewebauthn/server`. The registration and authentication flows are production-ready. ### POST /api/auth/passkey/authenticate/challenge @@ -247,7 +256,7 @@ Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyServi ### POST /api/auth/reset-password -**Description:** Sets a new password using a token from the reset email. Wipes refresh tokens. +**Description:** Sets a new password using a token from the reset email. Wipes refresh tokens. Enforces password complexity via `passwordResetValidation`. **Auth required:** No **Request body:** ```ts @@ -261,10 +270,11 @@ Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyServi ### POST /api/auth/reset-password-with-code -**Description:** Alternative reset flow using a numeric code instead of a tokenised URL. +**Description:** Alternative reset flow using a **6-digit** numeric code instead of a tokenised URL. **Auth required:** No **Request body:** `{ email, code, password }` **Response 200:** `{ "success": true }` +**⚠️ No password complexity validation:** Unlike `POST /api/auth/reset-password` (token-based), this endpoint does **not** run `passwordResetValidation`. Any non-empty password will be accepted without complexity checks. ### POST /api/auth/change-password @@ -280,6 +290,7 @@ Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyServi **Response 200:** `{ "success": true, "message": "Password updated" }` **Errors:** `400` validation, `401` wrong current password. **Side effects:** Clears `user.refreshTokens` (forces re-login on other devices). +**⚠️ No frontend UI:** This endpoint exists and is functional in the backend, but no frontend page currently exposes a change-password form. It can only be called directly. ## Current user / profile @@ -316,13 +327,15 @@ Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyServi ### DELETE /api/auth/account -**Description:** Permanently deletes the caller's account after re-authenticating with password. +**Description:** Permanently deletes the caller's account after re-authenticating with password. Requires `{ password }` in the request body and runs `deleteAccountValidation`. **Auth required:** Bearer JWT **Request body:** `{ password: string }` **Response 200:** `{ "success": true, "message": "Account deleted" }` **Errors:** `401` bad password. **Side effects:** Removes [[User]] document, clears Redis session, cascades configured by `dataCleanupService`. +**⚠️ KNOWN BUG — Frontend calls wrong endpoint:** The frontend currently calls `DELETE /user/profile` instead of `DELETE /api/auth/account`. Account deletion initiated from the frontend UI will fail or hit the wrong handler. + ## Error codes summary | HTTP | App code | Meaning | diff --git a/03 - API Reference/Chat API.md b/03 - API Reference/Chat API.md index 6e6146c..03922c4 100644 --- a/03 - API Reference/Chat API.md +++ b/03 - API Reference/Chat API.md @@ -5,10 +5,20 @@ tags: [api, chat, reference] # Chat API +> **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)) + All chat endpoints live under `/api/chat/*`. The router is [`backend/src/services/chat/chatRoutes.ts`](../../backend/src/services/chat/chatRoutes.ts), controller is `chatController`, service is `ChatService`. Every endpoint requires `Bearer JWT` — the router applies `authenticateToken` globally. Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat-`. Clients must call `join-chat-room` after connecting. See [[Socket Events]] for `new-message`, `messages-read`, `message-edited`, `message-deleted`, `participants-added`, `participant-removed`, and `user-typing` payloads. +## Rate limits and constraints + +| Rule | Value | +| --- | --- | +| Messages per user per minute | **20** | +| Edit window | **15 minutes** after send | +| Maximum message length | **5 000 characters** | + ## Conversations ### POST /api/chat @@ -61,9 +71,11 @@ Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat- ⚠️ **KNOWN BUG** — The frontend `archiveConversation` helper sends `PUT /api/chat/:id/archive` but the backend route is registered as `PATCH`. The request will receive a `404` until the frontend is corrected to use `PATCH`. + ### POST /api/chat/:id/participants **Description:** Add a participant to a group chat. @@ -112,16 +124,18 @@ Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat- ⚠️ **KNOWN BUG** — The frontend `sendFileMessage` function incorrectly posts to `POST /api/chat/:id/messages` (the plain-text endpoint) instead of `POST /api/chat/:id/messages/file`. File uploads are currently broken as a result; the attachment is silently dropped or the request is rejected. + ### PATCH /api/chat/:id/messages/read -**Description:** Mark all unread messages up to the latest as read for the caller. +**Description:** Mark messages as read for the caller. Passing an empty `messageIds` array (or omitting it) marks **all** messages in the chat as read. **Auth required:** Bearer JWT (participant) **Response 200:** `{ success, data: { modifiedCount } }` **Side effects:** Emits `messages-read` on `chat-`. ### PUT /api/chat/:id/messages/:messageId -**Description:** Edit an existing message (author only, within edit window). +**Description:** Edit an existing message (author only, within the 15-minute edit window). **Auth required:** Bearer JWT (message author) **Request body:** `{ content: string }` **Side effects:** Emits `message-edited` on `chat-`. diff --git a/03 - API Reference/Dispute API.md b/03 - API Reference/Dispute API.md index b3d2760..00f6b41 100644 --- a/03 - API Reference/Dispute API.md +++ b/03 - API Reference/Dispute API.md @@ -5,13 +5,21 @@ tags: [api, dispute, reference] # Dispute API +> **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)) + > [!note] Current implementation > The Dispute module now has a Mongoose model, controller routes, dashboard routes, and release-hold helper routes mounted under `/api/disputes`. Keep this page aligned with both `backend/src/routes/disputeRoutes.ts` and `backend/src/services/dispute/disputeRoutes.ts`. Endpoints live under `/api/disputes/*`. `backend/src/routes/disputeRoutes.ts` delegates to `DisputeController` (`backend/src/controllers/disputeController.ts`) for CRUD/triage. `backend/src/services/dispute/disputeRoutes.ts` provides lightweight release-hold endpoints (`raise`, `resolve`, `status`) used by escrow release gating. The routers apply `authenticateToken` globally — every endpoint requires `Bearer JWT`. -> [!warning] Route overlap to verify -> Both route modules define a `POST /:id-or-purchaseRequestId/resolve` shape. Because `app.ts` mounts the full controller router before the lightweight hold router, confirm the intended handler before wiring automation to the lightweight resolve endpoint. +> [!warning] Route shadowing — both dispute routers are mounted at `/api/disputes` +> The dashboard router is mounted **first** in `app.ts`. Its `POST /:id/resolve` intercepts requests before the admin-guarded release-hold router's resolve handler. Confirm which handler will run before wiring automation to either resolve endpoint. + +> [!danger] Security issues — see individual endpoint notes below +> Several endpoints that are documented as admin-only have **no role guard** in the current codebase. Any authenticated user can call them. These are noted per-endpoint. + +> [!note] Real-time events +> All socket events from `DisputeService` are currently **TODO stubs**. No real-time events fire from dispute mutations. Notifications are delivered via `POST /api/notifications` → `new-notification` socket event only. Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[Payment]] context and is the input to the mediation workflow that can lead to refund, replacement, compensation, warning/ban, or no-action. Release/refund execution should go through the ledger-gated [[Payment API]] and [[Payout Flow]]. @@ -25,12 +33,15 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P ```ts { purchaseRequestId: string; - reason: "not_delivered" | "wrong_item" | "damaged" | "quality" | "other"; + reason: "product_quality" | "delivery_delay" | "wrong_item" | "payment_issue" | "seller_behavior" | "other"; description: string; evidence?: string[]; // URLs from [[File API]] paymentId?: string; } ``` + +> **Note:** Valid `reason` values are `product_quality | delivery_delay | wrong_item | payment_issue | seller_behavior | other`. The value `fraud` does not exist. + **Response 201:** `{ success: true, data: { dispute } }` **Errors:** `400` validation, `403` not a participant of the request, `409` dispute already open for this request. **Side effects:** @@ -39,14 +50,14 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P ### POST /api/disputes/:purchaseRequestId/raise -**Description:** Lightweight release-hold endpoint that marks a purchase request and related payments as disputed. +**Description:** Lightweight release-hold endpoint that marks a purchase request and related payments as disputed. Exists in the backend but has no corresponding frontend action. **Auth required:** Bearer JWT (buyer who owns the request or admin) **Request body:** `{ reason?: string }` **Response 200:** `{ success, message, data }` ### GET /api/disputes/:purchaseRequestId/status -**Description:** Returns release-hold flags for a purchase request, including whether release is currently blocked. +**Description:** Returns release-hold flags for a purchase request, including whether release is currently blocked. Exists in the backend but has no corresponding frontend action. **Auth required:** Bearer JWT (buyer, preferred seller, or admin) ## Read @@ -56,9 +67,13 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P **Description:** List disputes the caller can see (their own as buyer/seller, all for admins). **Auth required:** Bearer JWT **Query params:** -- `status` (`open` | `under_review` | `resolved_buyer` | `resolved_seller` | `closed`) +- `status` (`open` | `in_progress` | `resolved_buyer` | `resolved_seller` | `closed`) + + > **Note:** The status value `under_review` does not exist. Use `in_progress`. + - `purchaseRequestId` - `page`, `limit`, `sortBy`, `sortOrder` + **Response 200:** `{ success, data: { disputes, pagination } }` ### GET /api/disputes/statistics @@ -77,41 +92,53 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P ### POST /api/disputes/:id/assign -**Description:** Assign an admin moderator to the dispute. Sets `assignedAdminId` and transitions status to `under_review`. -**Auth required:** Bearer JWT (admin) +**Description:** Assign an admin moderator to the dispute. Sets `assignedAdminId` and transitions status to `in_progress`. +**Auth required:** Bearer JWT + +> ⚠️ **SECURITY — NO ROLE GUARD:** Despite being documented as admin-only, there is no role guard on this endpoint. Any authenticated user can self-assign as mediator on any dispute. + **Request body:** `{ adminId: string }` **Side effects:** Notifies all participants. ### PATCH /api/disputes/:id/status **Description:** Generic status update (e.g. close without resolution). -**Auth required:** Bearer JWT (admin) +**Auth required:** Bearer JWT + +> ⚠️ **SECURITY — NO ROLE GUARD:** There is no role guard on this endpoint. Any authenticated user can change dispute status despite documentation claiming admin-only access. + **Request body:** `{ status: string; note?: string }` ### POST /api/disputes/:id/resolve **Description:** Final adjudication. Records the decision and triggers the appropriate escrow action. -**Auth required:** Bearer JWT (admin) +**Auth required:** Bearer JWT + +> ⚠️ **SECURITY — NO ROLE GUARD:** This is the dashboard router's resolve handler (mounted first). There is no role guard. Any authenticated user can resolve a dispute, including issuing `action=ban_seller`. + +> ⚠️ **ROUTE SHADOWING:** Because the dashboard router is mounted before the admin-guarded release-hold router, this handler intercepts all `POST /api/disputes/:id/resolve` requests. The admin-guarded release-hold resolve endpoint is unreachable at this path. + **Request body:** ```ts { - decision: "buyer" | "seller" | "split"; - refundAmount?: number; // required when "split" - releaseAmount?: number; // required when "split" - reasoning: string; + action: "refund" | "replacement" | "compensation" | "warning_seller" | "ban_seller" | "no_action"; + amount?: string; // optional, e.g. for partial refund or compensation amount + notes?: string; } ``` **Response 200:** `{ success, data: { dispute, paymentAction } }` **Side effects:** - `action === "refund"` → create/approve the corresponding refund instruction through the ledger-gated payment release/refund flow. - `action === "no_action"` or seller-favorable outcome → clear hold only after release checks pass. -- split outcomes require explicit partial release/refund instructions. - Notifies both participants and updates [[PurchaseRequest]] status to `disputed_resolved`. ### POST /api/disputes/:purchaseRequestId/resolve **Description:** Lightweight release-hold endpoint that clears the disputed hold flags on a purchase request and related payments. **Auth required:** Bearer JWT (admin) + +> ⚠️ **ROUTE SHADOWING:** This endpoint is on the release-hold router which is mounted **after** the dashboard router. The dashboard router's `POST /:id/resolve` matches first, making this handler unreachable in practice. See the route shadowing warning at the top of this page. + **Response 200:** `{ success, message, data }` ## Evidence and messages @@ -136,7 +163,7 @@ Direct messages between disputants and the admin moderator are handled via a ded ## Real-time -Dispute mutations emit notifications via `POST /api/notifications` which delivers `new-notification` socket events to each participant's `user-` room. See [[Socket Events]] for payload shape. +> ⚠️ All socket events from `DisputeService` are currently **TODO stubs** — no real-time events fire from dispute mutations. Dispute notifications are delivered only via `POST /api/notifications`, which in turn emits `new-notification` to the relevant `user-` room. See [[Socket Events]] for payload shape. ## Related diff --git a/03 - API Reference/Marketplace API.md b/03 - API Reference/Marketplace API.md index 8223c6c..f5d2dd9 100644 --- a/03 - API Reference/Marketplace API.md +++ b/03 - API Reference/Marketplace API.md @@ -5,6 +5,8 @@ tags: [api, marketplace, reference] # Marketplace API +> **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)) + All marketplace endpoints live under `/api/marketplace/*`. The router is composed of several files mounted from `app.ts`: - New controller-pattern routes: [`backend/src/services/marketplace/controllerRoutes.ts`](../../backend/src/services/marketplace/controllerRoutes.ts) (`marketplaceControllerRouter`) @@ -96,6 +98,16 @@ The buyer-facing CRUD plus seller-side workflow endpoints. Model: [[PurchaseRequ **Auth required:** Bearer JWT **Query params:** `status`, `categoryId`, `urgency`, `search`, `page`, `limit`, `sortBy`, `sortOrder` +> **Note:** Use query params on this endpoint for filtering/searching. The separate search and stats endpoints documented in earlier versions do not exist — see below. + +### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/purchase-requests/search + +This endpoint does not exist. Use query params (`search`, `status`, `categoryId`, etc.) on `GET /api/marketplace/purchase-requests` instead. + +### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/purchase-requests/stats + +This endpoint does not exist in the backend. + ### GET /api/marketplace/purchase-requests/my **Description:** Shortcut for the caller's own purchase requests. @@ -112,6 +124,8 @@ The buyer-facing CRUD plus seller-side workflow endpoints. Model: [[PurchaseRequ **Description:** Buyer edits draft / pending request fields. **Auth required:** Bearer JWT (owner) +> ⚠️ **KNOWN BUG:** The frontend sends `PUT` but the backend registers `PATCH`. Requests from clients using `PUT` will receive `404`. Use `PATCH`. + ### PATCH /api/marketplace/purchase-requests/:id/status **Description:** Transition the request status (`draft` → `pending` → `payment` → `processing` → `delivery` → `delivered` → `seller_paid` → `completed`, or `cancelled`). @@ -213,10 +227,15 @@ Six-digit codes the buyer hands to the seller at handover. Backed by `deliverySe Model: [[SellerOffer]]. +Valid `status` values: `pending | accepted | rejected | withdrawn` + +> **Note:** The status value `active` does not exist on SellerOffer. Earlier docs were incorrect. + ### POST /api/marketplace/purchase-requests/:id/offers -**Description:** Submit an offer against a purchase request. +**Description:** Submit an offer against the purchase request identified by `:id` in the path. The purchase request must be in `pending`, `received_offers`, or `active` status. **Auth required:** Bearer JWT (seller) +**Path param:** `:id` — the `purchaseRequestId` (not a body field) **Request body:** ```ts { @@ -248,11 +267,21 @@ Model: [[SellerOffer]]. **Description:** Fetch a specific seller's offer on a request. **Auth required:** No +### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/offers/request/:requestId + +This endpoint does not exist. Use `GET /api/marketplace/purchase-requests/:id/offers` instead. + +### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/offers/seller/:sellerId + +This endpoint does not exist. `getOffersBySeller()` is an internal service method and is not exposed via HTTP. + ### PATCH /api/marketplace/offers/:id **Description:** Seller edits their pending offer (price, delivery estimate, notes). **Auth required:** Bearer JWT (offer owner) +> ⚠️ **KNOWN BUG:** The frontend sends `PUT` but the backend registers `PATCH`. Requests from clients using `PUT` will receive `404`. Use `PATCH`. + ### DELETE /api/marketplace/offers/:id **Description:** Seller withdraws their offer. @@ -260,9 +289,13 @@ Model: [[SellerOffer]]. ### PUT /api/marketplace/offers/:id/status -**Description:** Direct status mutation (admin override / counter-offer states). +**Description:** Direct status mutation (admin override / counter-offer states). This is also the correct way to withdraw an offer programmatically — send `{ status: 'withdrawn' }`. **Auth required:** Bearer JWT -**Request body:** `{ status: "pending" | "accepted" | "rejected" | "withdrawn" | "countered" }` +**Request body:** `{ status: "pending" | "accepted" | "rejected" | "withdrawn" }` + +### ⚠️ NOT IMPLEMENTED: POST /api/marketplace/offers/:id/withdraw + +This endpoint does not exist. To withdraw an offer use `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`. ### POST /api/marketplace/purchase-requests/:id/select-offer diff --git a/03 - API Reference/Notification API.md b/03 - API Reference/Notification API.md index cea56d4..8642a44 100644 --- a/03 - API Reference/Notification API.md +++ b/03 - API Reference/Notification API.md @@ -5,6 +5,8 @@ tags: [api, notification, reference] # Notification API +> **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)) + Endpoints live under `/api/notifications/*`. Two routers are mounted: - New controller pattern: [`notificationControllerRoutes.ts`](../../backend/src/services/notification/notificationControllerRoutes.ts) (controller-backed, requires auth) @@ -12,7 +14,7 @@ Endpoints live under `/api/notifications/*`. Two routers are mounted: Both routers are mounted at `/api`, so the paths collide; the controller router wins for the shared paths (it is mounted first). The legacy router is still used by background scripts and admin tools that have no JWT context. -Model: [[Notification]]. Real-time delivery is via `new-notification` and `unread-count-update` Socket.IO events on `user-`. See [[Socket Events]]. +Model: [[Notification]]. Notifications are **auto-deleted after 90 days**. Real-time delivery is via `new-notification` and `unread-count-update` Socket.IO events on `user-`. See [[Socket Events]]. ## List @@ -47,6 +49,8 @@ Model: [[Notification]]. Real-time delivery is via `new-notification` and `unrea **Auth required:** Bearer JWT **Errors:** `404` not found, `403` not owner. +> ⚠️ **KNOWN BUG:** The controller fetches only the 1 most-recent notification for the user and does an in-memory ID match. Any notification that is not the user's single latest will return `404` even if it exists and belongs to the user. Do not rely on this endpoint for fetching arbitrary notifications by id. + ## Mutations ### PATCH /api/notifications/:id/read @@ -62,6 +66,8 @@ Model: [[Notification]]. Real-time delivery is via `new-notification` and `unrea **Auth required:** Bearer JWT **Response 200:** `{ "success": true, "data": { "modifiedCount": 12 } }` +> **Note:** Earlier versions of this documentation incorrectly listed this as `POST /api/notifications/read-all`. The correct path and method are `PATCH /notifications/mark-all-read`. + ### PATCH /api/notifications/bulk/mark-read **Description:** Mark a list of notifications as read. @@ -99,10 +105,26 @@ Model: [[Notification]]. Real-time delivery is via `new-notification` and `unrea **Response 201:** `{ success, data: { notification } }` **Side effects:** Emits `new-notification` to `user-`; also increments unread count via `unread-count-update`. +## Real-time socket events + +### `new-notification` + +Emitted to `user-` when a new notification is created for that user. + +### `unread-count-update` + +Emitted to `user-` whenever the unread notification count changes (e.g. after marking one or all as read, or after a new notification arrives). This is the canonical cross-tab sync event. + +> **Note:** Earlier docs referenced a `notification-read` socket event for cross-tab sync. That event does not exist. The real event is `unread-count-update`. + ## Preferences Notification preferences live on [[User]] (`preferences.notifications.email | sms | push`). They are read and written through the [[User API]] (`GET /api/user/profile`, `PUT /api/user/profile`). +## Data retention + +Notifications are automatically deleted after **90 days**. + ## Related - [[Notification]] diff --git a/03 - API Reference/Payment API.md b/03 - API Reference/Payment API.md index 9a75ee4..01392e5 100644 --- a/03 - API Reference/Payment API.md +++ b/03 - API Reference/Payment API.md @@ -5,6 +5,8 @@ tags: [api, payment, reference, request-network, escrow] # Payment API +> **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)) + The payment surface is split across provider-neutral payment routers, Request Network checkout/webhook routes, derived-destination custody routes, and admin safety routes: | Path prefix | File | Purpose | @@ -92,11 +94,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip **Auth required:** Bearer JWT **Errors:** `404` not found. -### GET /api/payment/:id/debug - -**Description:** Debug bundle including the raw payment, blockchain metadata, and wallet-monitor status. -**Auth required:** Bearer JWT -**Notes:** Intended for admin / development. +> ⚠️ **NOT IMPLEMENTED:** `GET /payment/:id/status`, `POST /payment/:id/confirm`, and `DELETE /payment/:id` do not exist in the codebase. Do not call these paths. ### GET /api/payment/user/:userId @@ -108,12 +106,16 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip **Description:** Aggregated counts and sums per status. **Auth required:** Bearer JWT +**⚠️ Known undercounting:** Only payments with status `'confirmed'` are counted as `successfulPayments`. Payments with status `'completed'` (the terminal state for SHKeeper and DePay) are **not** included in this count and are therefore under-reported. ### GET /api/payment/export / GET /api/payment/export/:userId **Description:** Export payments as `json` or `csv`. **Auth required:** Bearer JWT **Query params:** `format=json|csv` +**⚠️ Privilege gap:** The controller-pattern route for this endpoint has no admin guard. Any authenticated user (not just admins) can export payment data. + +> ⚠️ **NOT IMPLEMENTED:** `/payment/history`, `/payment/methods`, `/payment/validate`, `/payment/transactions`, and `/payment/escrow/balance` do not exist. Do not call these paths. ### POST /api/payment/payments/cleanup-pending @@ -124,15 +126,20 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip ### POST /api/payment/payments/:id/fetch-tx **Description:** Re-queries the blockchain to fetch the missing `transactionHash` for a completed payment. -**Auth required:** Bearer JWT +**⚠️ SECURITY — NO AUTHENTICATION:** This endpoint has no authentication guard. Any unauthenticated caller can trigger a blockchain re-query for any payment ID. **Response 200:** `{ success, transactionHash, network, source, message }` ### POST /api/payment/payments/auto-fetch-missing **Description:** Batch tx-hash backfill across the database. -**Auth required:** Bearer JWT +**⚠️ SECURITY — NO AUTHENTICATION:** This endpoint has no authentication guard. Any unauthenticated caller can trigger a full database backfill scan. **Request body:** `{ limit?: number }` (default 10) +### GET /api/payment/payments/:id/debug + +**Description:** Debug bundle including the raw payment, blockchain metadata, and wallet-monitor status. Intended for admin / development. +**⚠️ SECURITY — NO AUTHENTICATION:** Despite exposing full payment data, this endpoint has no authentication guard. Any unauthenticated caller can retrieve complete payment details for any payment ID. + ### POST /api/payment/callback **Description:** Generic payment callback (called by the older client SDK). @@ -175,6 +182,8 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip **Auth required:** No (signature-protected) **Response:** `200` when processed or duplicate; `202` when accepted but safety checks are pending; `401` for invalid signature. +> ⚠️ **NOT IMPLEMENTED:** `POST /api/payment/request-network/:id/payout/initiate`, `POST /api/payment/request-network/:id/payout/confirm`, `POST /api/payment/request-network/:id/release/confirm`, and `POST /api/payment/request-network/:id/refund/confirm` do not exist in the codebase. Do not call these paths. + ## Legacy SHKeeper - Pay-in > [!warning] Historical route family @@ -218,11 +227,13 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip **Body:** The SHKeeper callback envelope (`external_id`, `crypto`, `addr`, `fiat`, `balance_fiat`, `balance_crypto`, `paid`, `status`, `transactions[]`). **Response 200:** `{ success: true }` **Side effects:** -- Updates the matching [[Payment]] to `completed` (`OVERPAID` and `PAID` both count). +- Updates the matching [[Payment]] to `completed` (`OVERPAID` and `PAID` both count). Note: `'completed'` is the terminal state for SHKeeper payments but is **not** counted as `successfulPayments` in `GET /api/payment/stats`. - Releases or rejects [[SellerOffer]] siblings (the chosen offer becomes `accepted`, others `rejected`). - Updates [[PurchaseRequest]] status to `payment` / `processing`. - Emits `seller-offer-update` to each affected seller room and `purchase-request-update` to the request room. +> ⚠️ **NOT IMPLEMENTED:** `GET /api/payment/shkeeper/status/:paymentId` does not exist. SHKeeper payment status is delivered via socket events only — there is no HTTP polling endpoint. + ### POST /api/payment/shkeeper/confirm-transaction **Description:** Manual fallback when the webhook misses — the frontend calls this after the buyer signs the EVM transaction directly. Coordinated through `PaymentCoordinator` to avoid double updates. @@ -270,25 +281,27 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip These build an admin-signed transaction off-chain and require a follow-up confirm with the broadcast tx hash. Source: `shkeeperService.buildAdminSignedTxPayload` and `confirmAdminTx`. -### POST /api/payment/shkeeper/:id/release +**⚠️ Path correction:** The `/shkeeper/` segment is NOT present in the actual release/refund routes. The correct paths are under `/api/payment/:id/…` (not `/api/payment/shkeeper/:id/…`). + +### POST /api/payment/:id/release **Description:** Prepares the admin-signed payload to release escrow to the seller. Returns the raw payload — the admin client signs and broadcasts. **Auth required:** Bearer JWT (admin) **Response 200:** `{ success: true, data: { /* tx payload */ } }` -### POST /api/payment/shkeeper/:id/release/confirm +### POST /api/payment/:id/release/confirm **Description:** Records the broadcast transaction hash for the release; marks the payment as released, updates [[PurchaseRequest]] to `seller_paid` and emits `purchase-request-update` (`type: payment_released`). **Auth required:** Bearer JWT (admin) **Request body:** `{ txHash: string }` **Errors:** `400` missing `txHash`. -### POST /api/payment/shkeeper/:id/refund +### POST /api/payment/:id/refund **Description:** Mirror of release, but returns the escrow to the buyer. **Auth required:** Bearer JWT (admin) -### POST /api/payment/shkeeper/:id/refund/confirm +### POST /api/payment/:id/refund/confirm **Description:** Records the refund tx hash; emits `purchase-request-update` (`type: payment_refunded`). **Auth required:** Bearer JWT (admin) @@ -332,7 +345,9 @@ Historical payouts were SHKeeper-side outbound transfers. Current routine releas **Auth required:** No (signature checked) **Response 200/400:** `{ success, message, data }` -## Legacy Web3 Wallet-Direct +## Legacy Web3 Wallet-Direct (DePay) + +> ⚠️ **NOT IMPLEMENTED:** `POST /payment/depay/intents` (`createDePayIntent`) does not exist in the codebase. ### POST /api/payment/decentralized/save @@ -377,7 +392,7 @@ Historical payouts were SHKeeper-side outbound transfers. Current routine releas ### POST /api/payment/decentralized/verify/:paymentId -**Description:** Re-verifies a single decentralized payment against the chain. +**Description:** Re-verifies a single decentralized payment against the chain. `paymentId` is a **path parameter** as shown. **Auth required:** Bearer JWT (owner or admin) ### POST /api/payment/decentralized/verify-all-pending @@ -491,8 +506,8 @@ Same result shape as above, but for a single destination. - `pending` - intent created, awaiting on-chain settlement - `processing` - settlement seen, awaiting confirmations -- `confirmed` - fully credited (intermediate; sometimes skipped) -- `completed` - confirmed, escrow funded +- `confirmed` - fully credited (intermediate; sometimes skipped). **Note:** this is the only status counted as `successfulPayments` in `GET /api/payment/stats`. +- `completed` - confirmed, escrow funded. Terminal state for SHKeeper and DePay. **Not** counted in `successfulPayments` stats — see stats undercounting note above. - `failed` - intentionally failed (expired, declined, refused) - `cancelled` - cancelled by user/admin - `released` - escrow released to seller through the release/refund orchestration and custody signer @@ -530,6 +545,8 @@ Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded } ``` +> ⚠️ **NOT IMPLEMENTED:** `GET /api/admin/settings/confirmation-thresholds/history` does not exist. Only the current-values GET and per-chain PATCH endpoints are implemented. + ## Payments awaiting confirmation (admin) ### `GET /api/admin/payments/awaiting-confirmation` @@ -586,11 +603,7 @@ Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded } ``` -### `POST /api/admin/rn/networks/reload` - -**Auth:** Admin only -**Description:** Reload the in-memory chain + token registry from JSON files on disk. Call after editing `supportedChains.json` or `tokens.json` in the container. -**Response 200:** `{ "success": true, "message": "Registry reloaded from disk" }` +> ⚠️ **NOT IMPLEMENTED:** `POST /api/admin/rn/networks/reload` and `POST /api/admin/rn/networks/probe/:chainId` do not exist in the codebase. ## Related diff --git a/03 - API Reference/Points API.md b/03 - API Reference/Points API.md index 8087f57..84f56bc 100644 --- a/03 - API Reference/Points API.md +++ b/03 - API Reference/Points API.md @@ -5,10 +5,14 @@ tags: [api, points, reference] # Points API +> **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)) + Endpoints live under `/api/points/*`. The router is [`backend/src/routes/pointsRoutes.ts`](../../backend/src/routes/pointsRoutes.ts), delegating to [`PointsController`](../../backend/src/controllers/pointsController.ts) and `PointsService` ([`backend/src/services/points/PointsService.ts`](../../backend/src/services/points/PointsService.ts)). The router applies `authenticateToken` globally — every endpoint requires `Bearer JWT`. Models: [[PointTransaction]], [[LevelConfig]]. Points are minted automatically by the platform (referral signup, successful purchases, reviews) and can be redeemed for discounts or marketplace credits. Levels progress as the user's lifetime points cross configured thresholds. +> **Note on `PointTransaction.type`** — Valid values are `earn | spend | expire` only. There is **no** `refund` type; a financial refund does not create a points transaction. + ## Balance and history ### GET /api/points/my-points @@ -36,7 +40,7 @@ Models: [[PointTransaction]], [[LevelConfig]]. Points are minted automatically b **Auth required:** Bearer JWT **Query params:** - `page` (default 1), `limit` (default 20) -- `type` (`earn` | `redeem` | `referral` | `purchase` | `review` | `admin_grant` | `admin_deduct`) +- `type` (`earn` | `spend` | `expire` | `admin_grant` | `admin_deduct`) — note: `redeem`, `referral`, `purchase`, `review` are **not** valid filter values - `from` / `to` (ISO dates) **Response 200:** ```json @@ -49,18 +53,24 @@ Models: [[PointTransaction]], [[LevelConfig]]. Points are minted automatically b } ``` +> ⚠️ **Missing frontend pages** — `/dashboard/points/transactions`, `/dashboard/points/referrals`, and `/dashboard/points/levels` are referenced in documentation but **do not exist** in the frontend. Users cannot access these views through the UI. + ### GET /api/points/referrals **Description:** Users referred by the caller plus the points earned from each. **Auth required:** Bearer JWT **Response 200:** `{ success, data: { referrals: [{ userId, name, joinedAt, pointsEarned, status }] } }` +> ⚠️ **Missing frontend page** — `/dashboard/points/referrals` does not exist. + ### GET /api/points/levels **Description:** Public list of every configured level (from [[LevelConfig]]). Used by the marketing / levels page. **Auth required:** Bearer JWT (but data is non-sensitive) **Response 200:** `{ success, data: { levels: [LevelConfig, ...] } }` +> ⚠️ **Missing frontend page** — `/dashboard/points/levels` does not exist. + ### GET /api/points/leaderboard **Description:** Top referrers by referral count and points earned. Used for community displays. @@ -68,18 +78,19 @@ Models: [[PointTransaction]], [[LevelConfig]]. Points are minted automatically b **Query params:** `limit` (default 10), `period` (`all` | `month` | `week`) **Response 200:** `{ success, data: { entries: [{ userId, name, avatar, referrals, pointsEarned }] } }` +> ⚠️ **Known limitation** — The `period` query parameter (`all` | `month` | `week`) is **silently ignored** by the backend. The leaderboard always returns all-time results regardless of the value passed. + ## Mutations ### POST /api/points/redeem -**Description:** Redeem points for a marketplace credit / discount. Server validates available balance and configured redemption rate. +**Description:** Redeem points against an in-progress purchase. Server validates available balance and configured redemption rate. **Auth required:** Bearer JWT **Request body:** ```ts { - amount: number; // points to redeem - purpose?: "wallet_credit" | "discount_code"; - purchaseRequestId?: string; // when applying to an in-progress purchase + pointsToUse: number; // points to redeem + purchaseRequestId: string; // the in-progress purchase to apply the discount to } ``` **Response 200:** @@ -88,8 +99,8 @@ Models: [[PointTransaction]], [[LevelConfig]]. Points are minted automatically b "success": true, "data": { "transaction": { /* PointTransaction */ }, - "redemption": { "creditAmount": 3.20, "currency": "USD", "code": "DISC-..." }, - "newBalance": 0 + "discount": { "creditAmount": 3.20, "currency": "USD" }, + "remainingPoints": 0 } } ``` @@ -127,7 +138,11 @@ The short link redirect (`GET /r/:code`) is mounted at the app root in `app.ts` `PointsService` emits Socket.IO events on level-up and referral rewards: - `level-up` on `user-` when a transaction crosses a level threshold. -- `referral-reward` on `user-` when a referred user triggers a reward. +- `referral-reward` on `user-` when a referred user triggers a reward. This fires only when the referred user's purchase reaches **`'completed'`** status — it does **not** fire on `'delivered'`. + +`authController` (not `PointsService`) emits: + +- `referral-signup` on `user-` when a referred user completes registration. See [[Socket Events]] for payload shape. diff --git a/03 - API Reference/Socket Events.md b/03 - API Reference/Socket Events.md index 4ecd40f..3b824c1 100644 --- a/03 - API Reference/Socket Events.md +++ b/03 - API Reference/Socket Events.md @@ -5,6 +5,8 @@ tags: [api, socket, realtime, reference] # Socket Events +> **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)) + The backend runs a Socket.IO server on the same HTTP port as the REST API. It is initialised in [`backend/src/app.ts`](../../backend/src/app.ts) and exposed globally as `global.io`. Helper functions for emitting events from services live in [`backend/src/infrastructure/socket/socketService.ts`](../../backend/src/infrastructure/socket/socketService.ts): ```ts @@ -58,11 +60,10 @@ Grouped by the service that emits them. | Event | Room | Payload | Source | | --- | --- | --- | --- | -| `new-purchase-request` | `sellers` | `PurchaseRequest` document | `marketplaceController.createPurchaseRequest` | +| `new-purchase-request` | `sellers` (shared global room) | `PurchaseRequest` document | `marketplaceController.createPurchaseRequest` | | `new-offer` | `buyer-` | `{ requestId, offer, sellerId }` | `marketplaceController.createSellerOffer` | | `seller-offer-update` | `seller-` (and global on payment confirm) | `{ sellerId, requestId, eventType: "payment-completed" \| "offer-rejected" \| "offer-accepted", data: { offerId, isSelected, paymentId?, transactionHash?, reason? } }` | `marketplaceController`, `shkeeperRoutes`, `shkeeperWebhook`, `SellerOfferService` | | `purchase-request-update` | `request-` | `{ type, requestId, status?, paymentId?, txHash?, provider? }` | `marketplaceController`, `PurchaseRequestService`, `shkeeperRoutes`, `paymentCoordinator` | -| `request-cancelled` | `user-`, `user-` | `{ requestId, reason }` | `PurchaseRequestService` | | `transaction-completed` | `user-`, `user-` | `{ requestId, paymentId, amount, currency }` | `marketplaceController` | | `delivery-code-generated` | `request-` | `{ requestId, deliveryCode }` (only the seller UI uses this) | `DeliveryService` | | `delivery-update` | `request-` | `{ requestId, status, carrier?, trackingNumber? }` | `DeliveryService` | @@ -72,6 +73,8 @@ Grouped by the service that emits them. | `template-checkout-payment-pending` | global | `{ checkoutId }` | `templateCheckoutWebhook` | | `template-checkout-payment-failed` | global | `{ checkoutId, reason }` | `templateCheckoutWebhook` | +> **Note:** There is **no** `request-cancelled` event. When a purchase request is cancelled, `PurchaseRequestService` emits `purchase-request-update` with `eventType: 'status-changed'` to the `request-` room. Any code listening for `request-cancelled` will never fire. + ### Payment | Event | Room | Payload | Source | @@ -98,6 +101,8 @@ Grouped by the service that emits them. Sources: [`ChatService.ts`](../../backend/src/services/chat/ChatService.ts), [`chatController.ts`](../../backend/src/services/chat/chatController.ts), and `app.ts` socket handlers. +> **Note:** There is **no** `notification-read` event. Cross-tab unread badge synchronisation is handled by `unread-count-update` (see Notification table below), not by a dedicated read event. + ### Notification | Event | Room | Payload | @@ -107,15 +112,21 @@ Sources: [`ChatService.ts`](../../backend/src/services/chat/ChatService.ts), [`c Source: [`NotificationService.ts`](../../backend/src/services/notification/NotificationService.ts). +`unread-count-update` is the canonical cross-tab sync mechanism for the notification badge. It is emitted whenever the unread count changes (new notification or mark-as-read). + ### Points -| Event | Room | Payload | -| --- | --- | --- | -| `level-up` | `user-` | `{ oldLevel, newLevel, lifetimePoints, perks }` | -| `referral-reward` | `user-` | `{ referredUserId, points, transactionId }` | -| `referral-signup` | `user-` | `{ referredUserId, name, joinedAt }` | +| Event | Room | Payload | Source | +| --- | --- | --- | --- | +| `level-up` | `user-` | `{ oldLevel, newLevel, lifetimePoints, perks }` | `PointsService` | +| `referral-reward` | `user-` | `{ referredUserId, points, transactionId }` | `PointsService` | +| `referral-signup` | `user-` | `{ referredUserId, name, joinedAt }` | `authController` (auth domain, **not** `PointsService`) | -Sources: [`PointsService.ts`](../../backend/src/services/points/PointsService.ts), [`authController.ts`](../../backend/src/services/auth/authController.ts). +> **Note on `referral-signup`** — This event is emitted by `authController` when a referred user completes registration, not by `PointsService`. It belongs to the authentication domain. `PointsService` emits only `level-up` and `referral-reward`. + +### Disputes + +> ⚠️ **TODO stubs** — `DisputeService` does not currently emit any Socket.IO events. All socket event handlers in `DisputeService` are placeholder stubs. No real-time dispute notifications fire regardless of dispute status changes. ## Online status