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:
Siavash Sameni
2026-05-29 15:15:02 +04:00
parent 9698ec5809
commit 7a616744f4
118 changed files with 2833 additions and 1788 deletions

View File

@@ -5,6 +5,7 @@ aliases: [Conversation, IChat, IMessage]
---
# Chat
> **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))
Conversation container with embedded messages. Used for buyer-seller direct chats, group chats, and support tickets. Each chat carries a list of participants, an embedded `messages[]` array (with reactions, replies, edit history), a denormalised `lastMessage` snapshot for list views, and per-user `unreadCounts`. A chat can be linked to any other entity through the `relatedTo` discriminator (currently `PurchaseRequest`, `SellerOffer`, or `Transaction`).
@@ -16,6 +17,9 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
> [!warning] Embedded messages
> Messages live inside the chat document. Very long-running chats can grow past the 16 MB document limit. Treat this as a known constraint of the current schema.
> [!warning] `relatedTo` is NOT set via `POST /api/chat`
> Although `relatedTo` exists in the schema, it is **not accepted** by the `POST /api/chat` create endpoint. Purchase-request linkage is established server-side through the dedicated `POST /api/chat/purchase-request`, not by passing `relatedTo` to the generic create endpoint.
## Schema — Chat
| Field | Type | Required | Default | Validation | Index | Description |
@@ -27,10 +31,10 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
| `participants[].role` | String | no | `member` | enum: `member` / `admin` / `owner` | — | Member role. |
| `participants[].joinedAt` | Date | no | `Date.now` | — | — | Join time. |
| `participants[].lastSeen` | Date | no | — | — | — | Last activity. |
| `participants[].leftAt` | Date | no | — | — | — | If left, when. |
| `participants[].isActive` | Boolean | no | `true` | — | — | Still a participant. |
| `participants[].leftAt` | Date | no | — | — | — | Set when the participant is removed (soft removal). |
| `participants[].isActive` | Boolean | no | `true` | — | — | Still a participant. Set to `false` on soft removal (subdocument is kept). |
| `messages[]` | Subdocument[] | no | `[]` | — | yes (`messages.timestamp`) | Embedded messages. |
| `relatedTo.type` | String | no | — | enum: `PurchaseRequest` / `SellerOffer` / `Transaction` | yes (compound) | Linked entity kind. |
| `relatedTo.type` | String | no | — | enum: `PurchaseRequest` / `SellerOffer` / `Transaction` | yes (compound) | Linked entity kind. **Not accepted via `POST /api/chat`** — set only via `POST /api/chat/purchase-request`. |
| `relatedTo.id` | ObjectId | no | — | — | yes (compound) | Linked entity id. |
| `lastMessage.content` | String | no | — | — | — | Snapshot for list views. |
| `lastMessage.senderId` | ObjectId → [[User]] | no | — | — | — | Last sender. |
@@ -50,13 +54,16 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
> [!note] No top-level `timestamps`
> Unlike most models, this schema does not pass `{ timestamps: true }`. It uses its own `metadata.createdAt` / `metadata.updatedAt` instead, maintained by the pre-save hook.
> [!note] Soft removal of participants
> Removing a participant (via `DELETE /api/chat/:id/participants/:participantId`) does **not** delete the subdocument. It is a soft removal: `isActive` is set to `false` and `leftAt` is timestamped, preserving message attribution and history.
## Schema — Message (embedded)
| Field | Type | Required | Default | Validation | Description |
| --- | --- | --- | --- | --- | --- |
| `senderId` | ObjectId → [[User]] | yes | — | — | Author. |
| `senderType` | String | no | `User` | — | Currently fixed. |
| `content` | String | yes | — | maxlength 5000 | Message body. |
| `content` | String | yes | — | **maxlength 5000** | Message body. Enforced at both schema and controller. |
| `messageType` | String | no | `text` | enum: `text` / `image` / `file` / `system` | Body kind. |
| `fileUrl` | String | no | — | — | If file/image. |
| `fileName` | String | no | — | — | Original filename. |
@@ -65,10 +72,14 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
| `isRead` | Boolean | no | `false` | — | Read flag. |
| `isEdited` | Boolean | no | `false` | — | Edited flag. |
| `editedAt` | Date | no | — | — | When edited. |
| `deletedAt` | Date | no | — | — | Set on soft-delete; `content` is cleared but the subdocument is kept. |
| `replyTo` | ObjectId | no | — | — | Reply target message id. |
| `reactions[].userId` | ObjectId → [[User]] | no | — | — | Reacting user. |
| `reactions[].reaction` | String | no | — | maxlength 10 | Emoji. |
> [!note] Messages are soft-deleted
> Deleting a message sets `deletedAt` and clears `content` (the body becomes empty). The message subdocument is **not** physically removed from `messages[]`, and a `message-deleted` socket event is emitted.
## Virtuals
| Virtual | Returns | Definition |
@@ -97,7 +108,7 @@ Defined at `backend/src/models/Chat.ts:243-247`:
| --- | --- |
| `getUnreadCount(userId: Types.ObjectId): number` | Returns the unread counter for a participant. `backend/src/models/Chat.ts:264` |
| `addMessage(messageData: Partial<IMessage>): IMessage` | Pushes a message, updates `lastMessage`, increments unread counters for everyone except the sender, and bumps `lastActivity`. `backend/src/models/Chat.ts:270` |
| `markAsRead(userId, messageIds?: Types.ObjectId[]): void` | Marks listed messages (or all) as read for the user, zeros their unread counter, and updates `lastSeen`. `backend/src/models/Chat.ts:308` |
| `markAsRead(userId, messageIds?: Types.ObjectId[]): void` | Marks listed messages (or all when `messageIds` is empty/omitted) as read for the user, zeros their unread counter, and updates `lastSeen`. `backend/src/models/Chat.ts:308` |
## Static Methods

View File

@@ -6,7 +6,9 @@ aliases: [User Notification, INotification]
# Notification
Per-user notification entry. Each row binds to one `userId` (stored as a string rather than ObjectId), carries a typed severity (`info` / `success` / `warning` / `error`) and a domain category, optionally references another entity via `relatedId`, and supports an `actionUrl` for deep-linking. Old notifications are auto-purged by a 90-day TTL index.
> **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))
Per-user notification entry. Each row binds to one `userId` (stored as a string rather than ObjectId), carries a typed severity (`info` / `success` / `warning` / `error`) and a domain category, optionally references another entity via `relatedId`, and supports an `actionUrl` for deep-linking. Old notifications are auto-purged by a 90-day TTL index (`createdAt` with `expireAfterSeconds = 7,776,000`).
> [!note] Source
> `backend/src/models/Notification.ts:18` — schema definition
@@ -15,6 +17,12 @@ Per-user notification entry. Each row binds to one `userId` (stored as a string
> [!warning] String userId
> `userId` is a String, not an ObjectId, and is not declared with `ref`. Consumers must cast to `ObjectId` if they want to `populate()` it as a [[User]].
> [!warning] `category` enum vs reality
> The schema enum is `purchase_request` / `offer` / `payment` / `delivery` / `system`, but in practice:
> - `notificationController.createNotification` defaults the category to **`'general'`** (`category = 'general'`) when the caller omits it. `'general'` is **not** in the schema enum — Mongoose enum validation will reject it on a strict save, so callers must supply a valid value or the write fails. Treat `'general'` as a value you may encounter in payloads even though it is not an enum member.
> - The frontend socket hook `use-notifications.ts` hardcodes `category: 'system'` for every realtime-injected notification, so most client-side notifications surface as `'system'` regardless of their true domain.
> - `NotificationService.notifyRequestStatusChanged` always writes `category: 'system'` for purchase-request status changes.
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
@@ -23,13 +31,13 @@ Per-user notification entry. Each row binds to one `userId` (stored as a string
| `title` | String | yes | — | maxlength 200 | — | Headline. |
| `message` | String | yes | — | maxlength 1000 | — | Body. |
| `type` | String | yes | `info` | enum: `info` / `success` / `warning` / `error` | — | Severity. |
| `category` | String | yes | — | enum: `purchase_request` / `offer` / `payment` / `delivery` / `system` | yes (compound) | Domain bucket. |
| `category` | String | yes | — | enum: `purchase_request` / `offer` / `payment` / `delivery` / `system` | yes (compound) | Domain bucket. ⚠️ `notificationController` defaults to `'general'` (not in the enum) and the realtime socket hook + `notifyRequestStatusChanged` hardcode `'system'`. See warning above. |
| `relatedId` | String | no | — | — | yes | Id of the related entity (e.g. [[PurchaseRequest]]). |
| `metadata` | Mixed | no | — | — | — | Arbitrary payload. |
| `actionUrl` | String | no | — | maxlength 500 | — | Deep link. |
| `isRead` | Boolean | no | `false` | — | yes (compound) | Read flag. |
| `readAt` | Date | no | — | — | — | When read. |
| `createdAt` | Date | auto | — | — | yes (compound + TTL) | Mongoose timestamp. |
| `createdAt` | Date | auto | — | — | yes (compound + TTL) | Mongoose timestamp. Auto-deleted after 90 days by TTL index. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
The collection name is overridden to `notifications` via `collection: 'notifications'`.
@@ -46,7 +54,7 @@ Defined at `backend/src/models/Notification.ts:71-77`:
- `{ userId: 1, isRead: 1 }` — unread badge.
- `{ userId: 1, category: 1 }` — category filter.
- `{ relatedId: 1 }` — lookup by linked entity.
- `{ createdAt: 1 }` with `expireAfterSeconds: 60 * 60 * 24 * 90` — auto-delete after 90 days.
- `{ createdAt: 1 }` with `expireAfterSeconds: 60 * 60 * 24 * 90` (7,776,000 s) — MongoDB TTL index; the database hard-deletes documents automatically after 90 days.
Plus the implicit index from `userId` having `index: true` at the field level.
@@ -62,6 +70,13 @@ None defined.
None defined.
## Status-change notification coverage
`NotificationService.notifyRequestStatusChanged` maps a [[PurchaseRequest]] status to a human label via an internal `statusMessages` table. That table covers `pending`, `active`, `received_offers`, `in_negotiation`, `payment`, `processing`, `delivery`, `delivered`, `confirming`, `completed`, and `cancelled`.
> [!warning] Missing status templates
> The `pending_payment` and `seller_paid` [[PurchaseRequest]] statuses have **no entry** in the `statusMessages` table and no dedicated notification template. Transitions into these states do not produce a meaningful status-change notification (the label falls back to the raw status string, and several flows skip notification entirely). If you rely on notifications for `pending_payment` / `seller_paid`, they will not arrive as expected.
## Relationships
- **References**: [[User]] indirectly through `userId` (string); arbitrary entity via `relatedId`.

View File

@@ -6,6 +6,8 @@ aliases: [Payment Record, Escrow, IPayment]
# Payment
> **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))
Records every monetary movement in the marketplace: buyer pay-ins, seller payouts, and refunds. The current model is centered on Request Network pay-in, in-house checkout metadata, on-chain transaction verification, escrow state, and provider request IDs. The `provider` and `direction` discriminators let one collection hold incoming buyer payments, outgoing seller releases, refunds, and legacy/other provider records.
> [!note] Source
@@ -15,6 +17,22 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout
> [!warning] Mixed types
> `purchaseRequestId`, `sellerOfferId`, and `sellerId` use `Schema.Types.Mixed`. They are usually `ObjectId`s, but the template-checkout flow passes string ids that do not yet exist in the database, so the schema accepts both.
> [!warning] `provider` values (schema enum vs reality)
> The declared schema enum for `provider` is only `['request.network', 'other']`, yet production code writes additional values. The full set of providers that actually appear is: `request.network`, `shkeeper`, `decentralized`, `test`, `other`.
> - `paymentCoordinator.ts` and `RequestTemplateService.ts` create `Payment` docs with `provider: 'shkeeper'`.
> - The decentralized/on-chain flow uses `decentralized`.
> - ⚠️ **Frontend type bug:** the frontend `PaymentProvider` TypeScript type (`frontend/src/types/payment.ts`) is `'request.network' | 'test' | 'other'` — it is **missing `shkeeper` and `decentralized`**, so the client cannot represent payments created by those providers.
> [!warning] `confirmed` vs `completed` — stats undercount
> Payment stats (`paymentService.getPaymentStats`) only increment `successfulPayments` for status **`confirmed`**:
> ```ts
> case "confirmed": stats.successfulPayments += stat.count; break;
> ```
> The terminal SHKeeper / DePay state is **`completed`**, which has no case in the switch and is therefore **not** counted as a successful payment. ⚠️ This causes successful-payment stats to undercount any payment that reached `completed`.
> [!warning] `SIM_` payment-hash bypass — security concern
> In both `payment/paymentRoutes.ts` and `marketplace/routes.ts`, a `paymentHash` that starts with `SIM_` (or a short `0x...` hash under 64 chars) is treated as a simulated transaction and **skips on-chain verification entirely** (`isVerified = true`). There is **no environment guard** (e.g. no `NODE_ENV !== 'production'` check) around this branch, so the bypass is reachable in production. ⚠️ A caller can mark a payment verified without any real on-chain settlement.
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
@@ -25,7 +43,7 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout
| `sellerId` | Mixed (ObjectId or String) | yes | — | — | yes (compound) | Seller receiving (or template seller). |
| `amount.amount` | Number | yes | — | — | — | Numeric amount. |
| `amount.currency` | String | yes | `USDT` | — | — | Settlement currency. |
| `provider` | String | no | `request.network` | enum: `request.network` / `other` | yes (compound, partial) | Payment processor. |
| `provider` | String | no | `request.network` | enum (declared): `request.network` / `other`. Values written in practice: `request.network`, `shkeeper`, `decentralized`, `request.network`, `test`, `other` | yes (compound, partial) | Payment processor. ⚠️ See provider note below — code writes `shkeeper` and `decentralized` even though they are not in the declared schema enum, and the frontend `PaymentProvider` type is missing both. |
| `direction` | String | no | `in` | enum: `in` / `out` / `refund` | yes (compound, partial) | Flow direction. |
| `blockchain.network` | String | no | — | — | — | Network identifier. |
| `blockchain.transactionHash` | String | no | — | — | yes (sparse) | On-chain tx hash. |
@@ -35,8 +53,8 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout
| `blockchain.receiver` | String | no | — | — | — | Destination address. |
| `blockchain.confirmedAt` | Date | no | — | — | — | When tx confirmed. |
| `blockchain.confirmations` | Number | no | `0` | — | — | Confirmation count. |
| `status` | String | no | `pending` | enum: `pending` / `processing` / `confirmed` / `completed` / `failed` / `cancelled` / `refunded` | yes (compound) | Lifecycle status. |
| `escrowState` | String | no | — | enum: `funded` / `releasable` / `released` / `refunded` / `releasing` / `failed` / `cancelled` / `partial` | — | Escrow lifecycle. |
| `status` | String | no | `pending` | enum: `pending` / `processing` / `confirmed` / `completed` / `failed` / `cancelled` / `refunded` | yes (compound) | Lifecycle status. ⚠️ `confirmed` vs `completed`: only `confirmed` is counted as a successful payment in stats. See status note below. |
| `escrowState` | String | no | — | enum: `funded` / `releasable` / `released` / `refunded` / `releasing` / `failed` / `cancelled` / `partial` | — | Escrow lifecycle. Note the intermediate states `releasable` (delivery confirmed, ready to pay out) and `releasing` (payout in flight) between `funded` and `released`. |
| `providerPaymentId` | String | no | — | — | yes (sparse) | External provider id for idempotency. |
| `metadata.userAgent` | String | no | — | — | — | Browser UA. |
| `metadata.ipAddress` | String | no | — | — | — | Client IP. |

View File

@@ -4,9 +4,19 @@ tags: [data-model, mongoose]
aliases: [Point Ledger, Loyalty Transaction, IPointTransaction]
---
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
# PointTransaction
Append-only ledger of loyalty point movements. Each row represents one earn / spend / expire event for a user, with a source attribution (`purchase` / `referral` / `bonus` / `admin` / `redemption`), the amount moved, and the resulting balance snapshot. Metadata is flexible to support different sources (order amount, commission, level changes, referenced [[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))
Append-only ledger of loyalty point movements. Each row represents one `earn` / `spend` / `expire` event for a user, with a source attribution (`purchase` / `referral` / `bonus` / `admin` / `redemption`), the amount moved, and the resulting balance snapshot. Metadata is flexible to support different sources (order amount, commission, level changes, referenced [[PurchaseRequest]]).
> [!warning] `type` enum is `earn` / `spend` / `expire` ONLY
> There is **no `refund` type** (nor any other value). The `enum` at `PointTransaction.ts:35` is exactly `['earn', 'spend', 'expire']`. Referral earns are identified by `source: 'referral'` + `type: 'earn'`, **not** by a dedicated type.
> [!danger] `expire` is defined but never produced
> The `expiresAt` field and the `'expire'` type exist in the schema, and there is a sparse `{ expiresAt: 1 }` index intended for expiry sweeps — but **no service, cron job, or TTL ever creates an `expire`-type transaction**. Point expiry is **not enforced** anywhere in the codebase today; points effectively never expire.
> [!note] Source
> `backend/src/models/PointTransaction.ts:25` — schema definition
@@ -18,7 +28,7 @@ Append-only ledger of loyalty point movements. Each row represents one earn / sp
| --- | --- | --- | --- | --- | --- | --- |
| `user` | ObjectId → [[User]] | yes | — | — | yes (single + compound) | Owner of the transaction. |
| `type` | String | yes | — | enum: `earn` / `spend` / `expire` | yes (compound) | Movement direction. |
| `source` | String | yes | — | enum: `purchase` / `referral` / `bonus` / `admin` / `redemption` | yes (compound) | Source bucket. |
| `source` | String | yes | — | enum: `purchase` / `referral` / `bonus` / `admin` / `redemption` | yes (compound) | Source bucket. **Referral earns are identified by `source='referral'` (with `type='earn'`), not by type.** Redemptions use `source='redemption'`; admin grants use `source='admin'`. |
| `amount` | Number | yes | — | — | — | Points moved (positive integer; semantics by `type`). |
| `balance` | Number | yes | — | — | — | Available balance after the move. |
| `order` | ObjectId → Order | no | — | — | — | Linked order id (legacy ref, see warning). |
@@ -67,7 +77,7 @@ None defined.
## State Transitions
No status field — entries are immutable once written. A consumer scans for `expiresAt < now` to create offsetting `type: 'expire'` rows.
No status field — entries are immutable once written. The schema anticipates a consumer scanning for `expiresAt < now` to create offsetting `type: 'expire'` rows, but **no such consumer exists**: nothing in the codebase ever writes an `expire` row, so in practice only `earn` and `spend` entries are ever created.
## Common Queries

View File

@@ -6,12 +6,20 @@ aliases: [User Model, IUser, Account]
# User
> **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 core identity document for every actor in the marketplace: buyers, sellers, and admins. Stores credentials (password + WebAuthn passkeys), profile/preference data, referral bookkeeping, point balances, and a soft-delete status flag. Almost every other model carries an `ObjectId` reference back to `User`, so this collection is the relational hub of the system.
> [!note] Source
> `backend/src/models/User.ts:70` — schema definition
> `backend/src/models/User.ts:257` — model export
> [!note] Email change re-verification
> When a profile update (`PUT /api/user/profile`, `userController.updateUserProfile`) changes `email` to a new value, the controller sets `isEmailVerified = false`, generates a **6-digit** `emailVerificationCode` (valid 15 minutes), stores it on `emailVerificationCode` / `emailVerificationCodeExpires`, and emails the code to the new address. The user must then confirm via `POST /api/user/profile/email/verify` (or request a new code with `POST /api/user/profile/email/resend-verification`).
> [!note] Wallet ownership proof
> `PATCH /api/user/wallet-address` accepts both EVM and TON wallets. EVM addresses require an EIP-191 signature (`ethers.verifyMessage`); TON addresses are format-validated and may include an optional TonProof. A successful proof sets `profile.walletProofVerified = true` and `profile.walletProofTimestamp`.
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
@@ -21,7 +29,7 @@ The core identity document for every actor in the marketplace: buyers, sellers,
| `firstName` | String | no | `"کاربر"` | trim | — | Persian default ("user"). |
| `lastName` | String | no | `"جدید"` | trim | — | Persian default ("new"). |
| `role` | String | yes | `"buyer"` | enum: `admin` / `buyer` / `seller` | yes | Authorisation tier. |
| `isEmailVerified` | Boolean | no | `false` | — | — | Set to true after [[TempVerification]] is consumed. |
| `isEmailVerified` | Boolean | no | `false` | — | — | Set to true after the email verification code is consumed. ⚠️ Changing the email via `PUT /api/user/profile` **resets this to `false`** and dispatches a fresh **6-digit** verification code to the new address (see Email verification note below). |
| `authProvider` | String | yes | `"email"` | enum: `email` / `google` / `telegram` | yes | Provider used to create the account. Existing email/password accounts remain `email`; Telegram-only users are `telegram`. |
| `telegramVerified` | Boolean | no | `false` | — | — | Set when Telegram identity has been signature-verified and linked through `TelegramLink`. |
| `emailVerificationToken` | String | no | — | — | — | Legacy token-based email verification. |
@@ -48,7 +56,11 @@ The core identity document for every actor in the marketplace: buyers, sellers,
| `profile.address.country` | String | no | — | — | — | — |
| `profile.bio` | String | no | — | — | — | Free-form bio. |
| `profile.website` | String | no | — | — | — | Personal website URL. |
| `profile.walletAddress` | String | no | — | — | — | On-chain wallet address. |
| `profile.walletAddress` | String | no | — | — | — | On-chain wallet address (EVM `0x…` or TON). Set via `PATCH /api/user/wallet-address`. |
| `profile.walletType` | String | no | — | enum: `evm` / `ton` | — | Which chain family the stored `walletAddress` belongs to. |
| `profile.walletProvider` | String | no | — | — | — | Wallet provider label (e.g. `evm`, `telegram-wallet`). Defaults to `telegram-wallet` for TON, `evm` otherwise. |
| `profile.walletProofVerified` | Boolean | no | — | — | — | True when ownership was proven — EIP-191 signature for EVM, or a verified TonProof for TON. |
| `profile.walletProofTimestamp` | Date | no | — | — | — | When the wallet proof was last verified (only set when `walletProofVerified` is true). |
| `profile.isPublic` | Boolean | no | `false` | — | — | Whether the profile is publicly visible. |
| `preferences.language` | String | no | `"en"` | — | — | UI language. |
| `preferences.currency` | String | no | `"USD"` | — | — | Display currency. |
@@ -57,7 +69,7 @@ The core identity document for every actor in the marketplace: buyers, sellers,
| `preferences.notifications.push` | Boolean | no | `true` | — | — | Opt-in for push notifications. |
| `status` | String | no | `"active"` | enum: `active` / `suspended` / `deleted` | yes | Soft-delete and moderation flag. |
| `lastLoginAt` | Date | no | — | — | — | Updated by auth middleware. |
| `refreshTokens[]` | String[] | no | `[]` | — | — | Outstanding JWT refresh tokens. |
| `refreshTokens[]` | String[] | no | `[]` | — | — | Array of currently active JWT refresh tokens. ⚠️ Reset to `[]` on password change and on password reset, which invalidates every outstanding session and forces re-login everywhere. |
| `referralCode` | String | no | — | — | unique, sparse | **Not yet implemented** in `User.ts` — planned for referral programme. |
| `referredBy` | ObjectId → User | no | — | — | yes | **Not yet implemented** in `User.ts` — planned for referral programme. |
| `points.total` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts` — planned for loyalty system. |