Initial commit: nick docs

This commit is contained in:
moojttaba
2026-05-23 20:35:34 +03:30
commit 0da235ae27
90 changed files with 18268 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
---
title: Address
tags: [data-model, mongoose]
aliases: [Shipping Address, IAddress]
---
# Address
User-owned address book entry. Each row carries the recipient name, optional phone, full address text, city/state/country/zip, an address type (`Home` / `Office` / `Other`), and a `primary` flag. A pre-save hook enforces a single primary address per user by demoting the user's other addresses when the saving document is primary.
> [!note] Source
> `backend/src/models/Address.ts:20` — schema definition
> `backend/src/models/Address.ts:89` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `userId` | ObjectId → [[User]] | yes | — | — | yes (single + compound) | Owner. |
| `name` | String | yes | — | trim | — | Recipient name. |
| `phoneNumber` | String | no | — | trim | — | Recipient phone. |
| `fullAddress` | String | yes | — | trim | — | Address line. |
| `city` | String | yes | — | trim | — | City. |
| `state` | String | yes | — | trim | — | State / province. |
| `country` | String | yes | — | trim | — | Country. |
| `zipCode` | String | no | — | trim | — | Postal code. |
| `addressType` | String | no | `Home` | enum: `Home` / `Office` / `Other` | — | Address type label. |
| `primary` | Boolean | no | `false` | — | yes (compound, desc) | Whether this is the default address. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
- `{ userId: 1 }` — from field-level `index: true` at `backend/src/models/Address.ts:25`.
- `{ userId: 1, primary: -1 }``backend/src/models/Address.ts:75`.
## Pre/Post Hooks
| Hook | Behaviour |
| --- | --- |
| `pre('save')` (`backend/src/models/Address.ts:78`) | If the document being saved is `primary: true`, demotes (`primary: false`) every other address belonging to the same `userId`. |
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`userId`).
- **Referenced by**: none directly. Address text is also embedded into [[PurchaseRequest]] `deliveryInfo.deliveryAddress` at request time, denormalised so historical requests do not change if the address book changes later.
## State Transitions
No status field. The boolean `primary` flag follows a simple at-most-one invariant maintained by the pre-save hook.
## Common Queries
```ts
// User's address book
Address.find({ userId }).sort({ primary: -1, updatedAt: -1 });
// Primary address
Address.findOne({ userId, primary: true });
// Set a new primary (hook handles demotion)
const addr = await Address.findById(id);
addr.primary = true;
await addr.save();
// Delete an address
Address.deleteOne({ _id, userId });
```
Related: [[User]], [[PurchaseRequest]].

View File

@@ -0,0 +1,113 @@
---
title: BlogPost
tags: [data-model, mongoose]
aliases: [Blog Post, Article, IBlogPost]
---
# BlogPost
Editorial content for the marketplace's blog. Each post has a title, an auto-generated slug, rich `content`, optional cover image, gallery, and embedded videos (YouTube / Vimeo / Aparat / other). Carries publication workflow (`draft` / `published` / `archived`), denormalised author info, SEO metadata, and counters for views, likes, and comments. Two pre-save hooks handle slug generation and `publishedAt` stamping.
> [!note] Source
> `backend/src/models/BlogPost.ts:39` — schema definition
> `backend/src/models/BlogPost.ts:182` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `title` | String | yes | — | trim, maxlength 200 | — | Post title. |
| `slug` | String | no | (auto-generated) | lowercase, trim | unique, sparse | URL slug. |
| `description` | String | yes | — | maxlength 500 | — | Short summary. |
| `content` | String | yes | — | — | — | Full body (markdown / HTML). |
| `coverImage` | String | no | — | — | — | Hero image URL. |
| `images[]` | String[] | no | — | — | — | Gallery URLs. |
| `videos[].url` | String | yes | — | — | — | Video URL. |
| `videos[].title` | String | no | — | — | — | Video title. |
| `videos[].platform` | String | no | `youtube` | enum: `youtube` / `vimeo` / `aparat` / `other` | — | Platform. |
| `videos[].embedId` | String | no | — | — | — | Embed id (if applicable). |
| `author.id` | ObjectId → [[User]] | yes | — | — | — | Author user. |
| `author.name` | String | yes | — | — | — | Denormalised author name. |
| `author.avatar` | String | no | — | — | — | Avatar URL. |
| `category` | String | yes | `tutorial` | enum: `tutorial` / `news` / `guide` / `tips` / `announcement` / `other` | yes (compound) | Editorial category. |
| `tags[]` | String[] | no | — | trim | yes | Free-form tags. |
| `status` | String | no | `draft` | enum: `draft` / `published` / `archived` | yes (compound) | Workflow state. |
| `publishedAt` | Date | no | — | — | yes (compound) | Auto-set when status → `published`. |
| `views` | Number | no | `0` | — | — | View counter. |
| `likes` | Number | no | `0` | — | — | Like counter. |
| `comments` | Number | no | `0` | — | — | Comment counter. |
| `readTime` | Number | no | `5` | — | — | Estimated read time (minutes). |
| `featured` | Boolean | no | `false` | — | yes (compound) | Front-page promotion. |
| `seo.metaTitle` | String | no | — | — | — | SEO title. |
| `seo.metaDescription` | String | no | — | — | — | SEO description. |
| `seo.metaKeywords[]` | String[] | no | — | — | — | SEO keywords. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
Virtuals are enabled in `toJSON` and `toObject` even though none are declared on the schema.
## Virtuals
None defined (but enabled in serialisation).
## Indexes
Defined at `backend/src/models/BlogPost.ts:148-151`. Plus the implicit unique sparse index on `slug`:
- `{ status: 1, publishedAt: -1 }` — published feed.
- `{ category: 1, status: 1 }` — category page.
- `{ tags: 1 }` — tag lookup.
- `{ featured: 1, status: 1 }` — featured posts.
## Pre/Post Hooks
| Hook | Behaviour |
| --- | --- |
| `pre('save')` (`backend/src/models/BlogPost.ts:154`) | Auto-generates `slug` from the title (English letters only) plus a timestamp suffix; falls back to `post-<timestamp>` for non-Latin titles. |
| `pre('save')` (`backend/src/models/BlogPost.ts:175`) | When `status` is modified to `published` and `publishedAt` is empty, sets `publishedAt = new Date()`. |
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`author.id`).
- **Referenced by**: none.
## State Transitions
```mermaid
stateDiagram-v2
[*] --> draft
draft --> published : author publishes
published --> archived : admin archives
published --> draft : unpublish
archived --> published : restore
archived --> [*]
```
## Common Queries
```ts
// Public feed
BlogPost.find({ status: 'published' }).sort({ publishedAt: -1 }).limit(20);
// By slug (detail page)
BlogPost.findOne({ slug, status: 'published' });
// Featured carousel
BlogPost.find({ featured: true, status: 'published' }).sort({ publishedAt: -1 });
// Tag search
BlogPost.find({ tags: tag, status: 'published' });
// Increment views atomically
BlogPost.updateOne({ _id }, { $inc: { views: 1 } });
```
Related: [[User]].

View File

@@ -0,0 +1,87 @@
---
title: Category
tags: [data-model, mongoose]
aliases: [Category Model, Taxonomy, ICategory]
---
# Category
Hierarchical taxonomy node used by [[PurchaseRequest]] and [[RequestTemplate]]. Each row is bilingual (`name` in the local language, `nameEn` in English), supports parent/child via `parentId`, has an icon and a display `order`, and an `isActive` toggle for soft-removal.
> [!note] Source
> `backend/src/models/Category.ts:15` — schema definition
> `backend/src/models/Category.ts:60` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `name` | String | yes | — | trim | yes | Local language name. |
| `nameEn` | String | yes | — | trim | yes | English name. |
| `description` | String | no | — | trim | — | Description. |
| `icon` | String | no | — | trim | — | Icon identifier / URL. |
| `isActive` | Boolean | no | `true` | — | yes | Active flag. |
| `parentId` | ObjectId → [[Category]] | no | `null` | — | yes | Parent category (`null` for top level). |
| `order` | Number | no | `0` | — | — | Display order. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/Category.ts:55-58`:
- `{ name: 1 }`
- `{ nameEn: 1 }`
- `{ isActive: 1 }`
- `{ parentId: 1 }`
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[Category]] (self, via `parentId`).
- **Referenced by**: [[PurchaseRequest]] (`categoryId`), [[RequestTemplate]] (`categoryId`).
## State Transitions
No status field — only the `isActive` boolean for soft-disable.
```mermaid
stateDiagram-v2
[*] --> active
active --> inactive : admin disables
inactive --> active : admin re-enables
```
## Common Queries
```ts
// Top-level categories
Category.find({ parentId: null, isActive: true }).sort({ order: 1 });
// Children of a category
Category.find({ parentId, isActive: true }).sort({ order: 1 });
// Bilingual search
Category.find({ $or: [{ name: regex }, { nameEn: regex }], isActive: true });
// Full tree (basic, two-level)
const roots = await Category.find({ parentId: null }).sort({ order: 1 });
const children = await Category.find({ parentId: { $in: roots.map(r => r._id) } });
```
Related: [[PurchaseRequest]], [[RequestTemplate]].

144
02 - Data Models/Chat.md Normal file
View File

@@ -0,0 +1,144 @@
---
title: Chat
tags: [data-model, mongoose]
aliases: [Conversation, IChat, IMessage]
---
# Chat
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`).
> [!note] Source
> `backend/src/models/Chat.ts:130` — chat schema definition
> `backend/src/models/Chat.ts:69` — message subdocument schema
> `backend/src/models/Chat.ts:348` — model export
> [!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.
## Schema — Chat
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `type` | String | yes | `direct` | enum: `direct` / `group` / `support` | yes | Conversation type. |
| `name` | String | no | — | maxlength 100 | — | Display name (group chats). |
| `description` | String | no | — | maxlength 500 | — | Optional description. |
| `participants[].userId` | ObjectId → [[User]] | yes | — | — | yes | Member id. |
| `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. |
| `messages[]` | Subdocument[] | no | `[]` | — | yes (`messages.timestamp`) | Embedded messages. |
| `relatedTo.type` | String | no | — | enum: `PurchaseRequest` / `SellerOffer` / `Transaction` | yes (compound) | Linked entity kind. |
| `relatedTo.id` | ObjectId | no | — | — | yes (compound) | Linked entity id. |
| `lastMessage.content` | String | no | — | — | — | Snapshot for list views. |
| `lastMessage.senderId` | ObjectId → [[User]] | no | — | — | — | Last sender. |
| `lastMessage.timestamp` | Date | no | — | — | — | Last message time. |
| `lastMessage.messageType` | String | no | — | — | — | Last message type. |
| `unreadCounts[].userId` | ObjectId → [[User]] | no | — | — | — | User the counter belongs to. |
| `unreadCounts[].count` | Number | no | `0` | — | — | Number of unread messages. |
| `settings.isArchived` | Boolean | no | `false` | — | — | Archived flag. |
| `settings.isMuted` | Boolean | no | `false` | — | — | Muted flag. |
| `settings.mutedUntil` | Date | no | — | — | — | Mute expiry. |
| `settings.notifications` | Boolean | no | `true` | — | — | Per-chat notification toggle. |
| `metadata.createdBy` | ObjectId → [[User]] | yes | — | — | — | Original creator. |
| `metadata.createdAt` | Date | no | `Date.now` | — | — | Created timestamp. |
| `metadata.updatedAt` | Date | no | `Date.now` | — | — | Touched by pre-save. |
| `metadata.lastActivity` | Date | no | `Date.now` | — | yes (desc) | Sort key for chat lists. |
> [!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.
## 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. |
| `messageType` | String | no | `text` | enum: `text` / `image` / `file` / `system` | Body kind. |
| `fileUrl` | String | no | — | — | If file/image. |
| `fileName` | String | no | — | — | Original filename. |
| `fileSize` | Number | no | — | — | Bytes. |
| `timestamp` | Date | no | `Date.now` | — | Sent time. |
| `isRead` | Boolean | no | `false` | — | Read flag. |
| `isEdited` | Boolean | no | `false` | — | Edited flag. |
| `editedAt` | Date | no | — | — | When edited. |
| `replyTo` | ObjectId | no | — | — | Reply target message id. |
| `reactions[].userId` | ObjectId → [[User]] | no | — | — | Reacting user. |
| `reactions[].reaction` | String | no | — | maxlength 10 | Emoji. |
## Virtuals
| Virtual | Returns | Definition |
| --- | --- | --- |
| `participantsCount` | Count of active participants | `backend/src/models/Chat.ts:259` |
## Indexes
Defined at `backend/src/models/Chat.ts:243-247`:
- `{ 'participants.userId': 1 }`
- `{ 'metadata.lastActivity': -1 }`
- `{ 'relatedTo.type': 1, 'relatedTo.id': 1 }`
- `{ 'messages.timestamp': -1 }`
- `{ type: 1 }`
## Pre/Post Hooks
| Hook | Behaviour |
| --- | --- |
| `pre('save')` (`backend/src/models/Chat.ts:250`) | Updates `metadata.updatedAt` and refreshes `metadata.lastActivity` when there are messages. |
## Instance Methods
| Signature | Purpose |
| --- | --- |
| `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` |
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`participants[].userId`, `messages[].senderId`, `messages[].reactions[].userId`, `lastMessage.senderId`, `unreadCounts[].userId`, `metadata.createdBy`).
- **Referenced by**: [[Dispute]] (`chatId`), and any [[PurchaseRequest]] / [[SellerOffer]] indirectly through `relatedTo`.
## State Transitions
No top-level status. Chat-level archival is a boolean flag (`settings.isArchived`):
```mermaid
stateDiagram-v2
[*] --> active
active --> muted : user mutes
muted --> active : unmute / mute expires
active --> archived : user archives
archived --> active : restore
```
## Common Queries
```ts
// A user's recent chats
Chat.find({ 'participants.userId': userId, 'participants.isActive': true })
.sort({ 'metadata.lastActivity': -1 });
// Chat for a purchase request
Chat.findOne({ 'relatedTo.type': 'PurchaseRequest', 'relatedTo.id': prId });
// Append a message
const chat = await Chat.findById(id);
chat.addMessage({ senderId, content: 'hi', messageType: 'text' });
await chat.save();
// Mark read
chat.markAsRead(userId);
await chat.save();
```
Related: [[User]], [[PurchaseRequest]], [[SellerOffer]], [[Dispute]].

View File

@@ -0,0 +1,105 @@
---
title: Data Model Overview
tags: [data-model, mongoose, overview]
aliases: [Models Index, Schema Overview]
---
# Data Model Overview
This section documents every Mongoose model that backs the marketplace. The persistence layer lives in `backend/src/models/` and is exported through a single barrel file at `backend/src/models/index.ts`. All models target MongoDB via Mongoose, lean on `timestamps: true` for `createdAt` / `updatedAt`, and follow a consistent pattern: one schema per file, an exported `I<Name>` TypeScript interface, and named exports for the compiled model.
> [!note] Scope
> Sixteen models are documented here. The "File" concept exists only at the service layer (`backend/src/services/file/`) and is not persisted as its own Mongoose collection, so it is not listed below.
## Index of Models
- [[User]] — Core identity. Stores credentials, profile, preferences, referral data, points, and WebAuthn passkeys. Every other model that records "who did what" points back at a `User._id`. Buyers, sellers, and admins all live in this collection, differentiated by a `role` enum.
- [[PurchaseRequest]] — The buyer-side document at the heart of the marketplace. Captures what a buyer wants, the budget, urgency, delivery preferences, and the full lifecycle status (`pending_payment``seller_paid`). Aggregates [[SellerOffer]] references and tracks delivery codes.
- [[SellerOffer]] — A seller's bid against a [[PurchaseRequest]]. Holds price, delivery ETA, attachments, and a small status machine (`pending` / `accepted` / `rejected` / `withdrawn`).
- [[Payment]] — Records every monetary movement: buyer pay-in, seller payout, refund. Integrates with the SHKeeper crypto gateway and tracks escrow state plus on-chain transaction metadata.
- [[Chat]] — Conversation container with embedded messages, participants, unread counters, and reactions. Used for direct buyer-seller chats, group chats, and support tickets. Can be linked to a [[PurchaseRequest]] or [[SellerOffer]].
- [[Notification]] — Per-user notification with category, type, and 90-day TTL for automatic cleanup. References any related entity by stringified id.
- [[RequestTemplate]] — A seller-authored, sharable template that pre-fills a [[PurchaseRequest]]. Carries a public shareable link, usage counter, and an optional default proposal.
- [[Dispute]] — Buyer-raised complaint tied to a [[PurchaseRequest]]. Captures evidence uploads, a timeline of admin actions, deadlines, and a structured resolution.
- [[BlogPost]] — Editorial content: title, slug, rich content, media, SEO metadata, view/like counters, and a draft/published/archived workflow.
- [[Address]] — User shipping address book entry. Enforces a single primary address per user via a pre-save hook.
- [[Category]] — Hierarchical product/service taxonomy referenced by [[PurchaseRequest]] and [[RequestTemplate]]. Supports parent/child via `parentId` and bilingual `name` / `nameEn`.
- [[Review]] — Polymorphic 1-5 star review against either a seller or a [[RequestTemplate]] (`subjectType` discriminator). One review per reviewer per subject (compound unique index).
- [[PointTransaction]] — Ledger of point grants and spends per user. Sources include purchase, referral, bonus, admin grant, and redemption.
- [[LevelConfig]] — Static configuration of loyalty tiers (level number, point thresholds, benefits, icon, color). Driven by admins; consumed by the [[User]].points.level field.
- [[ShopSettings]] — One-to-one storefront branding for a seller: name, description, avatar, cover image, review toggles, and social links.
- [[TempVerification]] — Short-lived signup record that holds candidate user data and a verification code. Auto-purges via TTL when `emailVerificationCodeExpires` passes.
## Relationship Diagram
```mermaid
erDiagram
USER ||--o{ PURCHASE_REQUEST : "creates as buyer"
USER ||--o{ SELLER_OFFER : "submits as seller"
USER ||--o{ ADDRESS : "owns"
USER ||--o{ NOTIFICATION : "receives"
USER ||--o{ POINT_TRANSACTION : "earns/spends"
USER ||--o{ REQUEST_TEMPLATE : "authors as seller"
USER ||--o| SHOP_SETTINGS : "configures"
USER ||--o{ BLOG_POST : "publishes"
USER ||--o{ REVIEW : "writes as reviewer"
USER ||--o{ DISPUTE : "raises as buyer"
USER ||--o{ USER : "referred by"
PURCHASE_REQUEST }o--|| CATEGORY : "belongs to"
PURCHASE_REQUEST ||--o{ SELLER_OFFER : "receives"
PURCHASE_REQUEST ||--o{ PAYMENT : "settled by"
PURCHASE_REQUEST ||--o| CHAT : "discussed in"
PURCHASE_REQUEST ||--o{ DISPUTE : "may trigger"
PURCHASE_REQUEST ||--o| REVIEW : "rated by buyer"
SELLER_OFFER ||--o| PAYMENT : "funds"
SELLER_OFFER }o--|| PURCHASE_REQUEST : "responds to"
PAYMENT }o--|| USER : "buyer"
PAYMENT }o--|| USER : "seller"
CHAT }o--o{ USER : "participants"
CHAT ||--o{ DISPUTE : "support channel"
REQUEST_TEMPLATE }o--|| CATEGORY : "belongs to"
REQUEST_TEMPLATE ||--o{ REVIEW : "rated as subject"
CATEGORY ||--o{ CATEGORY : "parent of"
POINT_TRANSACTION }o--|| USER : "owner"
LEVEL_CONFIG ||..|| USER : "level lookup"
TEMP_VERIFICATION ||..|| USER : "promoted to"
```
## Conventions Across All Models
> [!note] Shared schema patterns
> - **Timestamps**: every model declares `{ timestamps: true }`, so `createdAt` and `updatedAt` are always present.
> - **ObjectId references**: foreign keys use `Schema.Types.ObjectId` with an explicit `ref` (e.g. `ref: 'User'`). The two exceptions are [[Notification]] and [[Payment]] which use string-typed or `Mixed` identifiers in places to support template-flow payments.
> - **Soft delete**: deletion is modelled as a `status` flag (e.g. `User.status = 'deleted'`, `BlogPost.status = 'archived'`) rather than physical removal.
> - **TTL indexes**: short-lived collections ([[Notification]], [[TempVerification]]) use `{ expireAfterSeconds: ... }` so MongoDB does the cleanup.
> - **toJSON sanitisation**: [[User]] overrides `toJSON` to strip credentials, refresh tokens, and verification codes before serialisation.
> [!warning] Index discipline
> Several schemas leave a comment noting that `unique: true` already creates an index — adding `schema.index({ field: 1 })` on top would produce a duplicate-index warning at startup. When introducing new indexes, search for `unique: true` first.
## Lifecycle View
The dominant happy-path flow exercises five collections in order:
1. A buyer (`User`) creates a `PurchaseRequest` with `status: 'pending'`.
2. Sellers (other `User`s) attach `SellerOffer` documents; the request transitions through `received_offers``in_negotiation` as the parties chat in a `Chat`.
3. The buyer accepts an offer; a `Payment` is opened against the SHKeeper provider with `escrowState: 'funded'`.
4. The seller marks the request `delivery``delivered`; the buyer confirms with the 6-digit `deliveryCode` and the request becomes `completed`.
5. The escrow `Payment` flips to `released` and a payout `Payment` (`direction: 'out'`) is issued. Optionally the buyer writes a `Review` and earns a `PointTransaction`.
If anything goes sideways, the buyer can open a `Dispute`, which freezes the flow until an admin resolves it (refund, replacement, compensation, or no-action).
## How to Navigate
Each model has its own note in this folder. Cross-references use `[[wikilinks]]` so backlinks work in Obsidian's graph view. Schemas are documented at field-level granularity — every field is listed with its type, default, validation, and indexing decisions. Where a model carries a meaningful state machine, a Mermaid `stateDiagram-v2` accompanies the schema table.
> [!note] Source of truth
> The information below is mirrored from the TypeScript schema definitions. If a field listed here disagrees with the code, the code wins — please update the note. All source citations use the form `backend/src/models/<File>.ts:<line>`.

127
02 - Data Models/Dispute.md Normal file
View File

@@ -0,0 +1,127 @@
---
title: Dispute
tags: [data-model, mongoose]
aliases: [Complaint, IDispute]
---
# Dispute
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] Source
> `backend/src/models/Dispute.ts:69` — schema definition
> `backend/src/models/Dispute.ts:238` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `purchaseRequestId` | ObjectId → [[PurchaseRequest]] | yes | — | — | yes | The disputed request. |
| `buyerId` | ObjectId → [[User]] | yes | — | — | yes | Complaining buyer. |
| `sellerId` | ObjectId → [[User]] | no | — | — | yes | Implicated seller. |
| `adminId` | ObjectId → [[User]] | no | — | — | yes (single + compound) | Admin owning the case. |
| `reason` | String | yes | — | trim, maxlength 200 | — | Short reason. |
| `description` | String | yes | — | trim, maxlength 2000 | — | Detailed description. |
| `priority` | String | no | `medium` | enum: `low` / `medium` / `high` / `urgent` | yes | Triage priority. |
| `category` | String | yes | — | enum: `product_quality` / `delivery_delay` / `wrong_item` / `payment_issue` / `seller_behavior` / `other` | yes | Issue type. |
| `status` | String | no | `pending` | enum: `pending` / `in_progress` / `waiting_response` / `resolved` / `rejected` / `closed` | yes (single + compound) | Lifecycle state. |
| `evidence[].type` | String | yes | — | enum: `image` / `document` / `screenshot` / `video` | — | Evidence kind. |
| `evidence[].url` | String | yes | — | — | — | Stored URL. |
| `evidence[].description` | String | no | — | — | — | Notes. |
| `evidence[].uploadedBy` | ObjectId → [[User]] | yes | — | — | — | Uploader. |
| `evidence[].uploadedAt` | Date | no | `Date.now` | — | — | Upload time. |
| `chatId` | ObjectId → [[Chat]] | no | — | — | — | Linked support chat. |
| `timeline[].action` | String | yes | — | — | — | Action label. |
| `timeline[].performedBy` | ObjectId → [[User]] | yes | — | — | — | Actor. |
| `timeline[].performedAt` | Date | no | `Date.now` | — | — | When. |
| `timeline[].details` | String | no | — | — | — | Free-form notes. |
| `resolution.action` | String | no | — | enum: `refund` / `replacement` / `compensation` / `warning_seller` / `ban_seller` / `no_action` | — | Outcome. |
| `resolution.amount` | Number | no | — | — | — | Monetary amount (refund/compensation). |
| `resolution.currency` | String | no | — | enum: `USD` / `EUR` / `IRR` / `USDT` | — | Currency. |
| `resolution.notes` | String | no | — | maxlength 1000 | — | Resolution notes. |
| `resolution.resolvedBy` | ObjectId → [[User]] | no | — | — | — | Admin who resolved. |
| `resolution.resolvedAt` | Date | no | — | — | — | When resolved. |
| `deadline` | Date | no | — | — | — | Overall SLA deadline. |
| `responseDeadline` | Date | no | — | — | — | Response SLA. |
| `tags[]` | String[] | no | — | trim | — | Filter tags. |
| `closedAt` | Date | no | — | — | — | When closed. |
| `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
> [!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`.
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/Dispute.ts:212-223`:
- `{ purchaseRequestId: 1 }`
- `{ buyerId: 1 }`
- `{ sellerId: 1 }`
- `{ adminId: 1 }`
- `{ status: 1 }`
- `{ priority: 1 }`
- `{ category: 1 }`
- `{ createdAt: -1 }`
- `{ status: 1, priority: -1 }` — admin queue
- `{ adminId: 1, status: 1 }` — per-admin workload
## Pre/Post Hooks
| Hook | Behaviour |
| --- | --- |
| `pre('save')` (`backend/src/models/Dispute.ts:226`) | On new documents pushes a `dispute_created` entry into `timeline` attributed to `buyerId`. |
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[PurchaseRequest]] (`purchaseRequestId`), [[User]] (`buyerId`, `sellerId`, `adminId`, evidence and timeline contributors, `resolution.resolvedBy`), [[Chat]] (`chatId`).
- **Referenced by**: none directly.
## State Transitions
```mermaid
stateDiagram-v2
[*] --> pending
pending --> in_progress : admin assigned
in_progress --> waiting_response : awaiting party
waiting_response --> in_progress : response received
in_progress --> resolved : action applied
in_progress --> rejected : invalid
resolved --> closed
rejected --> closed
closed --> [*]
```
## Common Queries
```ts
// Admin queue
Dispute.find({ status: { $in: ['pending', 'in_progress', 'waiting_response'] } })
.sort({ priority: -1, createdAt: 1 });
// Buyer's disputes
Dispute.find({ buyerId }).sort({ createdAt: -1 });
// Seller's open disputes
Dispute.find({ sellerId, status: { $nin: ['resolved', 'rejected', 'closed'] } });
// Append timeline entry atomically
Dispute.updateOne(
{ _id },
{ $push: { timeline: { action, performedBy: adminId, performedAt: new Date(), details } } }
);
```
Related: [[PurchaseRequest]], [[User]], [[Chat]], [[Payment]].

View File

@@ -0,0 +1,90 @@
---
title: LevelConfig
tags: [data-model, mongoose]
aliases: [Level, Loyalty Tier, ILevelConfig]
---
# LevelConfig
Admin-managed configuration of loyalty tiers. Each row defines one level (`level`, `name`, `nameEn`, point window via `minPoints` / `maxPoints`), the perks unlocked (`benefits.*`), and presentation details (`icon`, `color`, `order`). The `User.points.level` field is resolved against this collection.
> [!note] Source
> `backend/src/models/LevelConfig.ts:24` — schema definition
> `backend/src/models/LevelConfig.ts:93` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `level` | Number | yes | — | — | unique | Numeric level (1, 2, 3, ...). |
| `name` | String | yes | — | — | — | Local language name. |
| `nameEn` | String | yes | — | — | — | English name. |
| `minPoints` | Number | yes | `0` | — | yes | Inclusive lower bound. |
| `maxPoints` | Number | no | — | — | — | Inclusive upper bound (open if omitted). |
| `benefits.discountPercent` | Number | no | `0` | — | — | Percentage discount unlocked. |
| `benefits.freeShipping` | Boolean | no | `false` | — | — | Free shipping perk. |
| `benefits.prioritySupport` | Boolean | no | `false` | — | — | Priority support perk. |
| `benefits.specialOffers` | Boolean | no | `false` | — | — | Exclusive offers perk. |
| `icon` | String | no | `solar:medal-star-bold` | — | — | Icon identifier. |
| `color` | String | no | `#94a3b8` | — | — | Display color. |
| `order` | Number | yes | — | — | yes | Display order. |
| `isActive` | Boolean | no | `true` | — | yes | Active flag. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/LevelConfig.ts:89-91`. Plus the implicit unique index on `level`:
- `{ minPoints: 1 }`
- `{ order: 1 }`
- `{ isActive: 1 }`
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: none.
- **Referenced by**: indirectly by [[User]] (`points.level`) and [[PointTransaction]] (`metadata.levelBefore` / `metadata.levelAfter`).
## State Transitions
```mermaid
stateDiagram-v2
[*] --> active
active --> inactive : admin disables
inactive --> active : admin re-enables
```
## Common Queries
```ts
// All active levels (ordered)
LevelConfig.find({ isActive: true }).sort({ order: 1 });
// Resolve a point total to a level
LevelConfig.findOne({
isActive: true,
minPoints: { $lte: points },
$or: [{ maxPoints: { $gte: points } }, { maxPoints: { $exists: false } }],
}).sort({ minPoints: -1 });
// Fetch by level number
LevelConfig.findOne({ level });
```
Related: [[User]], [[PointTransaction]].

View File

@@ -0,0 +1,99 @@
---
title: Notification
tags: [data-model, mongoose]
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.
> [!note] Source
> `backend/src/models/Notification.ts:18` — schema definition
> `backend/src/models/Notification.ts:79` — model export
> [!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]].
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `userId` | String | yes | — | — | yes (single + compound) | Owner of the notification. |
| `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. |
| `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. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
The collection name is overridden to `notifications` via `collection: 'notifications'`.
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/Notification.ts:71-77`:
- `{ userId: 1, createdAt: -1 }` — user feed.
- `{ 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.
Plus the implicit index from `userId` having `index: true` at the field level.
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] indirectly through `userId` (string); arbitrary entity via `relatedId`.
- **Referenced by**: none.
## State Transitions
```mermaid
stateDiagram-v2
[*] --> unread
unread --> read : user opens
read --> [*] : TTL purge (90d)
unread --> [*] : TTL purge (90d)
```
## Common Queries
```ts
// User feed
Notification.find({ userId }).sort({ createdAt: -1 }).limit(50);
// Unread badge count
Notification.countDocuments({ userId, isRead: false });
// Mark all read
Notification.updateMany(
{ userId, isRead: false },
{ $set: { isRead: true, readAt: new Date() } }
);
// All notifications about a request
Notification.find({ relatedId: purchaseRequestId.toString() });
```
Related: [[User]], [[PurchaseRequest]], [[SellerOffer]], [[Payment]].

157
02 - Data Models/Payment.md Normal file
View File

@@ -0,0 +1,157 @@
---
title: Payment
tags: [data-model, mongoose]
aliases: [Payment Record, Escrow, IPayment]
---
# Payment
Records every monetary movement in the marketplace: buyer pay-ins, seller payouts, and refunds. Designed around the SHKeeper crypto payment gateway with explicit fields for blockchain network, transaction hash, escrow state, and provider invoice ids. The `provider` and `direction` discriminators let one collection hold all four flow types (incoming buyer payment, outgoing seller payout, refund, and "other" provider integrations).
> [!note] Source
> `backend/src/models/Payment.ts:3` — schema definition
> `backend/src/models/Payment.ts:257` — model export (default export)
> [!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.
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `purchaseRequestId` | Mixed (ObjectId or String) | yes | — | — | yes (compound, partial) | Linked [[PurchaseRequest]] id (or template id). |
| `sellerOfferId` | Mixed (ObjectId or String) | yes | — | — | — | Linked [[SellerOffer]] id (or template offer ref). |
| `buyerId` | ObjectId → [[User]] | yes | — | — | yes (compound) | Buyer paying. |
| `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 | `shkeeper` | enum: `shkeeper` / `other` | yes (compound, partial) | Payment processor. |
| `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. |
| `blockchain.blockchain` | String | no | — | enum: `ethereum` / `polygon` / `bsc` / `avalanche` / `solana` / `optimism` / `arbitrum` / `base` / `gnosis` | — | Chain. |
| `blockchain.token` | String | no | — | — | — | Token symbol. |
| `blockchain.sender` | String | no | — | — | — | Source address. |
| `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` | — | Escrow lifecycle. |
| `providerPaymentId` | String | no | — | — | yes (sparse) | External provider id for idempotency. |
| `metadata.userAgent` | String | no | — | — | — | Browser UA. |
| `metadata.ipAddress` | String | no | — | — | — | Client IP. |
| `metadata.walletType` | String | no | — | — | — | Wallet category. |
| `metadata.paymentMethod` | String | no | — | — | — | Payment method label. |
| `metadata.shkeeperUrl` | String | no | — | — | — | Invoice URL. |
| `metadata.shkeeperInvoiceId` | String | no | — | — | — | Invoice id. |
| `metadata.shkeeperData` | Mixed | no | — | — | — | Raw provider payload. |
| `metadata.shkeeperStatus` | String | no | — | — | — | Provider status string. |
| `metadata.balanceFiat` | String | no | — | — | — | Fiat-equivalent balance. |
| `metadata.balanceCrypto` | String | no | — | — | — | Crypto balance. |
| `metadata.cryptoName` | String | no | — | — | — | Crypto label. |
| `metadata.walletAddress` | String | no | — | — | — | Wallet address. |
| `metadata.shkeeperTaskId` | String | no | — | — | — | Payout task id. |
| `metadata.lastWebhookAt` | Date | no | — | — | — | Last webhook timestamp. |
| `metadata.webhookPayload` | Mixed | no | — | — | — | Last webhook body. |
| `metadata.createdVia` | String | no | — | — | — | Origin marker. |
| `metadata.payoutType` | String | no | — | — | — | Payout sub-type. |
| `metadata.error` | String | no | — | — | — | Last error message. |
| `metadata.failedAt` | Date | no | — | — | — | When it failed. |
| `createdAt` | Date | auto | `Date.now` | — | yes (compound) | Mongoose timestamp. |
| `processedAt` | Date | no | — | — | — | When processing started. |
| `completedAt` | Date | no | — | — | — | When fully settled. |
| `notes` | String | no | — | — | — | Free-form notes. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
| Virtual | Returns | Definition |
| --- | --- | --- |
| `paymentRef` | `PAY-<LAST_8_OF_ID_UPPERCASE>` | `backend/src/models/Payment.ts:191` |
The schema enables `toJSON: { virtuals: true }` and `toObject: { virtuals: true }` so the ref appears in API responses.
## Indexes
Defined at `backend/src/models/Payment.ts:174-188`:
- `{ status: 1, createdAt: -1 }` — admin queues.
- `{ buyerId: 1, status: 1 }` — buyer dashboard.
- `{ sellerId: 1, status: 1 }` — seller dashboard.
- `{ 'blockchain.transactionHash': 1 }` (sparse) — webhook lookup by hash.
- `{ providerPaymentId: 1 }` (sparse) — provider idempotency.
- `{ buyerId: 1, purchaseRequestId: 1, provider: 1, direction: 1 }` (unique, partial: `provider === 'shkeeper' && direction === 'in' && status === 'pending'`, name `uniq_pending_shkeeper_by_buyer_session`) — guards against duplicate pending invoices.
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`buyerId`, sometimes `sellerId`), [[PurchaseRequest]] (`purchaseRequestId`), [[SellerOffer]] (`sellerOfferId`).
- **Referenced by**: Indirectly through [[PurchaseRequest]] status transitions and [[Dispute]] resolution amounts; no model holds a direct foreign key back to `Payment`.
## State Transitions
Payment status:
```mermaid
stateDiagram-v2
[*] --> pending
pending --> processing : webhook received
processing --> confirmed : tx confirmed
confirmed --> completed : escrow released / payout done
pending --> cancelled : buyer aborts
processing --> failed : provider error
completed --> refunded : dispute resolved
failed --> [*]
cancelled --> [*]
completed --> [*]
refunded --> [*]
```
Escrow state (for `direction: 'in'`):
```mermaid
stateDiagram-v2
[*] --> funded : buyer pays
funded --> releasable : delivery confirmed
releasable --> releasing : payout initiated
releasing --> released : payout completed
funded --> refunded : dispute refund
releasing --> failed : payout error
released --> [*]
refunded --> [*]
failed --> [*]
```
## Common Queries
```ts
// Buyer history
Payment.find({ buyerId, direction: 'in' }).sort({ createdAt: -1 });
// Seller payouts
Payment.find({ sellerId, direction: 'out', status: 'completed' });
// Webhook lookup
Payment.findOne({ providerPaymentId });
// Pending escrows ready for release
Payment.find({ direction: 'in', escrowState: 'releasable' });
// Idempotent invoice creation (will fail by unique index if a pending one exists)
Payment.create({
buyerId, purchaseRequestId, provider: 'shkeeper', direction: 'in', status: 'pending', ...
});
```
Related: [[PurchaseRequest]], [[SellerOffer]], [[User]], [[Dispute]].

View File

@@ -0,0 +1,93 @@
---
title: PointTransaction
tags: [data-model, mongoose]
aliases: [Point Ledger, Loyalty Transaction, IPointTransaction]
---
# 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]]).
> [!note] Source
> `backend/src/models/PointTransaction.ts:25` — schema definition
> `backend/src/models/PointTransaction.ts:84` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `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. |
| `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). |
| `referredUser` | ObjectId → [[User]] | no | — | — | — | Referred user (for referral earns). |
| `description` | String | yes | — | — | — | Human label. |
| `metadata.orderAmount` | Number | no | — | — | — | Order amount snapshot. |
| `metadata.commission` | Number | no | — | — | — | Commission snapshot. |
| `metadata.levelBefore` | Number | no | — | — | — | Pre-level snapshot. |
| `metadata.levelAfter` | Number | no | — | — | — | Post-level snapshot. |
| `metadata.purchaseRequestId` | String | no | — | — | — | Linked [[PurchaseRequest]] id. |
| `expiresAt` | Date | no | — | — | yes (sparse) | When the points expire (for `earn`). |
| `createdAt` | Date | auto | — | — | yes (compound, desc) | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
> [!warning] `order` reference
> The schema declares `ref: 'Order'`, but there is no `Order` model in `backend/src/models/`. In practice this slot is used for the [[PurchaseRequest]] id; consumers should not rely on Mongoose `populate('order')` working.
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/PointTransaction.ts:80-82`. Plus the implicit index from `user` being declared with `index: true`:
- `{ user: 1, createdAt: -1 }` — user ledger view.
- `{ type: 1, source: 1 }` — analytics.
- `{ expiresAt: 1 }` (sparse) — expiry sweeps.
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`user`, `referredUser`).
- **Referenced by**: none. Loosely related to [[PurchaseRequest]] via `metadata.purchaseRequestId` (string).
## State Transitions
No status field — entries are immutable once written. A consumer scans for `expiresAt < now` to create offsetting `type: 'expire'` rows.
## Common Queries
```ts
// User ledger
PointTransaction.find({ user: userId }).sort({ createdAt: -1 }).limit(50);
// Latest balance (most recent row)
PointTransaction.findOne({ user: userId }).sort({ createdAt: -1 });
// Referral earnings
PointTransaction.find({ user: userId, source: 'referral', type: 'earn' });
// Points expiring soon
PointTransaction.find({ expiresAt: { $lte: oneWeekFromNow }, type: 'earn' });
// Analytics: total earned vs spent per source
PointTransaction.aggregate([
{ $group: { _id: { type: '$type', source: '$source' }, total: { $sum: '$amount' } } }
]);
```
Related: [[User]], [[LevelConfig]], [[PurchaseRequest]].

View File

@@ -0,0 +1,172 @@
---
title: PurchaseRequest
tags: [data-model, mongoose]
aliases: [Purchase Request, Buy Request, IPurchaseRequest]
---
# PurchaseRequest
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
> `backend/src/models/PurchaseRequest.ts:95` — schema definition
> `backend/src/models/PurchaseRequest.ts:387` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `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. |
| `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. |
| `size` | String | no | — | trim, maxlength 100 | — | Product size. |
| `color` | String | no | — | trim, maxlength 100 | — | Product color. |
| `brand` | String | no | — | trim, maxlength 100 | — | Brand preference. |
| `preferredSellerIds[]` | ObjectId → [[User]] | no | `[]` | — | — | Targeted sellers for a private request. |
| `quantity` | Number | no | `1` | min 1 | — | Unit count. |
| `budget.min` | Number | no | — | min 0 | — | Lower bound. |
| `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. |
| `isPublic` | Boolean | no | `true` | — | — | Public marketplace listing vs. private request. |
| `tags[]` | String[] | no | — | trim | — | Free-form tags. |
| `specifications[].key` | String | yes | — | trim | — | Spec key. |
| `specifications[].value` | String | yes | — | trim | — | Spec value. |
| `specifications[].label` | String | no | — | trim | — | Human label. |
| `deliveryInfo.deliveryType` | String | yes | `physical` | enum: `physical` / `online` | — | Delivery channel. |
| `deliveryInfo.address` | String | no | — | — | — | Physical address. |
| `deliveryInfo.preferredDate` | Date | no | — | — | — | Buyer's target date. |
| `deliveryInfo.notes` | String | no | — | — | — | Free-form notes. |
| `deliveryInfo.deliveryAddress.name` | String | no | — | — | — | Recipient name. |
| `deliveryInfo.deliveryAddress.phoneNumber` | String | no | — | — | — | Recipient phone. |
| `deliveryInfo.deliveryAddress.fullAddress` | String | no | — | — | — | Full address string. |
| `deliveryInfo.deliveryAddress.addressType` | String | no | — | — | — | e.g. Home / Office. |
| `deliveryInfo.email` | String | no | — | email regex | — | For digital delivery. |
| `deliveryInfo.sellerDeliveryInfo.estimatedDeliveryDate` | Date | no | — | — | — | Seller's ETA date. |
| `deliveryInfo.sellerDeliveryInfo.estimatedDeliveryTime` | String | no | — | — | — | Seller's ETA time. |
| `deliveryInfo.sellerDeliveryInfo.trackingNumber` | String | no | — | — | — | Carrier tracking. |
| `deliveryInfo.sellerDeliveryInfo.deliveryNotes` | String | no | — | — | — | Notes from seller. |
| `deliveryInfo.sellerDeliveryInfo.shippingMethod` | String | no | — | — | — | Method label. |
| `deliveryInfo.sellerDeliveryInfo.downloadLink` | String | no | — | — | — | Download URL for digital products. |
| `deliveryInfo.sellerDeliveryInfo.digitalFiles[]` | String[] | no | — | — | — | Digital file URLs. |
| `deliveryInfo.deliveryDateTime` | Date | no | — | — | — | Confirmed delivery datetime. |
| `deliveryInfo.deliveryDate` | Date | no | — | — | — | Confirmed delivery date. |
| `deliveryInfo.shippedAt` | Date | no | — | — | — | Timestamp of shipment. |
| `deliveryInfo.deliveryCode` | String | no | — | trim, length 6 | — | 6-digit handoff code. |
| `deliveryInfo.deliveryCodeGeneratedAt` | Date | no | — | — | — | When code was issued. |
| `deliveryInfo.deliveryCodeExpiresAt` | Date | no | — | — | — | When code expires. |
| `deliveryInfo.deliveryCodeUsed` | Boolean | no | `false` | — | — | Whether the code has been redeemed. |
| `deliveryInfo.deliveryCodeUsedAt` | Date | no | — | — | — | When it was redeemed. |
| `deliveryInfo.deliveryCodeUsedBy` | ObjectId → [[User]] | no | — | — | — | Seller that redeemed. |
| `deliveryInfo.deliveredAt` | Date | no | — | — | — | Final delivery timestamp. |
| `deliveryInfo.deliveryAttempts[].sellerId` | ObjectId → [[User]] | yes | — | — | — | Seller making the attempt. |
| `deliveryInfo.deliveryAttempts[].attemptedAt` | Date | no | `Date.now` | — | — | When attempted. |
| `deliveryInfo.deliveryAttempts[].success` | Boolean | yes | — | — | — | Whether it succeeded. |
| `deliveryInfo.deliveryAttempts[].code` | String | no | — | — | — | Code entered (only stored on success). |
| `serviceInfo.duration` | Number | no | — | min 0.5 | — | Hours, only for service/consultation. |
| `serviceInfo.sessionType` | String | no | — | enum: `online` / `in_person` / `hybrid` | — | Service session type. |
| `serviceInfo.location` | String | no | — | trim, maxlength 200 | — | Service location. |
| `serviceInfo.requirements[]` | String[] | no | — | trim | — | Pre-requisites. |
| `attachments[]` | String[] | no | — | — | — | Attached file URLs. |
| `offers[]` | ObjectId → [[SellerOffer]] | no | — | — | — | Offers received. |
| `selectedOfferId` | ObjectId → [[SellerOffer]] | no | `null` | — | — | Accepted offer. |
| `rating` | Number | no | `null` | min 1, max 5 | — | Buyer's post-delivery rating. |
| `feedback` | String | no | `null` | maxlength 1000 | — | Buyer's feedback text. |
| `deliveryConfirmed` | Boolean | no | `false` | — | — | Buyer confirmation flag. |
| `deliveryConfirmedAt` | Date | no | `null` | — | — | Confirmation timestamp. |
| `metadata.source` | String | no | `manual` | enum: `manual` / `template` / `api` | — | Where the request came from. |
| `metadata.templateId` | String | no | — | trim | — | Originating [[RequestTemplate]] id. |
| `metadata.version` | String | no | — | trim | — | Schema version. |
| `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
Single-field — `backend/src/models/PurchaseRequest.ts:376-381`:
- `{ buyerId: 1 }`
- `{ categoryId: 1 }`
- `{ productType: 1 }`
- `{ status: 1 }`
- `{ createdAt: -1 }`
- `{ urgency: 1 }`
Compound — `backend/src/models/PurchaseRequest.ts:384-385`:
- `{ productType: 1, status: 1 }`
- `{ categoryId: 1, productType: 1 }`
## Pre/Post Hooks
None declared at the schema level.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`buyerId`, `preferredSellerIds[]`, `deliveryInfo.deliveryCodeUsedBy`, `deliveryInfo.deliveryAttempts[].sellerId`), [[Category]] (`categoryId`), [[SellerOffer]] (`offers[]`, `selectedOfferId`).
- **Referenced by**: [[SellerOffer]] (`purchaseRequestId`), [[Payment]] (`purchaseRequestId`), [[Dispute]] (`purchaseRequestId`), [[Chat]] (`relatedTo.id` when `relatedTo.type === 'PurchaseRequest'`), [[Review]] (`purchaseRequestId`).
## State Transitions
```mermaid
stateDiagram-v2
[*] --> pending_payment
[*] --> pending
pending_payment --> pending : payment confirmed
pending --> active : published
active --> received_offers : first offer
received_offers --> in_negotiation : buyer engages
in_negotiation --> payment : offer accepted
payment --> processing : payment captured
processing --> delivery : shipped
delivery --> delivered : handed over
delivered --> confirming : code redeemed
confirming --> completed : buyer confirms
completed --> seller_paid : payout released
pending --> cancelled
active --> cancelled
received_offers --> cancelled
in_negotiation --> cancelled
completed --> [*]
seller_paid --> [*]
cancelled --> [*]
```
## Common Queries
```ts
// Buyer's open requests
PurchaseRequest.find({ buyerId, status: { $in: ['pending', 'active', 'received_offers'] } });
// Public marketplace feed
PurchaseRequest.find({ isPublic: true, status: 'active' }).sort({ createdAt: -1 });
// Sellers' eligible queue
PurchaseRequest.find({ productType, status: 'active', categoryId });
// Populate offers
PurchaseRequest.findById(id).populate('offers').populate('selectedOfferId');
// Redeem delivery code
PurchaseRequest.findOneAndUpdate(
{ _id: id, 'deliveryInfo.deliveryCode': code, 'deliveryInfo.deliveryCodeUsed': false },
{ $set: { 'deliveryInfo.deliveryCodeUsed': true, 'deliveryInfo.deliveryCodeUsedAt': new Date() } }
);
```
Related: [[SellerOffer]], [[Payment]], [[Chat]], [[Dispute]], [[Review]], [[RequestTemplate]], [[Category]].

View File

@@ -0,0 +1,134 @@
---
title: RequestTemplate
tags: [data-model, mongoose]
aliases: [Template, Request Template, IRequestTemplate]
---
# RequestTemplate
A reusable template authored by a seller. When a buyer visits the template's `shareableLink`, the front-end pre-fills a new [[PurchaseRequest]] with the template's category, urgency, specs, delivery info, and an optional default seller `proposal`. The schema mirrors `PurchaseRequest` for fast cloning, plus template-specific bookkeeping (`isActive`, `usageCount`, `maxUsage`, `expiresAt`).
> [!note] Source
> `backend/src/models/RequestTemplate.ts:65` — schema definition
> `backend/src/models/RequestTemplate.ts:295` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `sellerId` | ObjectId → [[User]] | yes | — | — | yes (compound) | Template author. |
| `title` | String | yes | — | trim, maxlength 200 | — | Headline. |
| `description` | String | yes | — | trim, maxlength 2000 | — | Description. |
| `categoryId` | ObjectId → [[Category]] | yes | — | — | yes (compound) | Category. |
| `productType` | String | no | `physical_product` | enum: `physical_product` / `digital_product` / `service` / `consultation` | yes (compound) | Fulfilment type. |
| `productLink` | String | no | — | URL regex | — | Reference URL. |
| `size` | String | no | — | trim, maxlength 100 | — | Size. |
| `color` | String | no | — | trim, maxlength 100 | — | Color. |
| `brand` | String | no | — | trim, maxlength 100 | — | Brand. |
| `quantity` | Number | no | `1` | min 1 | — | Default unit count. |
| `budget.min` | Number | no | — | min 0 | — | Lower bound. |
| `budget.max` | Number | no | — | min 0 | — | Upper bound. |
| `budget.currency` | String | no | `USD` | enum: `USD` / `EUR` / `IRR` | — | Currency. |
| `urgency` | String | no | `medium` | enum: `low` / `medium` / `high` / `urgent` | — | Urgency. |
| `tags[]` | String[] | no | — | trim | — | Tags. |
| `specifications[].key` | String | yes | — | trim | — | Spec key. |
| `specifications[].value` | String | yes | — | trim | — | Spec value. |
| `specifications[].label` | String | no | — | trim | — | Human label. |
| `deliveryInfo.deliveryType` | String | no | `physical` | enum: `physical` / `online` | — | Delivery channel. |
| `deliveryInfo.notes` | String | no | — | — | — | Notes. |
| `deliveryInfo.email` | String | no | — | email regex | — | Digital delivery email. |
| `serviceInfo.duration` | Number | no | — | min 0.5 | — | Hours. |
| `serviceInfo.sessionType` | String | no | — | enum: `online` / `in_person` / `hybrid` | — | Session type. |
| `serviceInfo.location` | String | no | — | trim, maxlength 200 | — | Location. |
| `serviceInfo.requirements[]` | String[] | no | — | trim | — | Pre-requisites. |
| `proposal.title` | String | no | — | trim, maxlength 200 | — | Default offer title. |
| `proposal.price` | Number | no | — | min 0.01 | — | Default offer price. |
| `proposal.deliveryTime` | Number | no | — | min 1, max 365 | — | Default ETA in days. |
| `proposal.description` | String | no | — | trim, maxlength 1000 | — | Default offer description. |
| `attachments[]` | String[] | no | — | — | — | File URLs. |
| `images[]` | String[] | no | — | trim | — | Image URLs. |
| `metadata.source` | String | no | `manual` | enum: `manual` / `template` / `api` | — | Origin. |
| `metadata.templateId` | String | no | — | trim | — | Originating template id. |
| `metadata.version` | String | no | — | trim | — | Schema version. |
| `isActive` | Boolean | no | `true` | — | yes (single + compound) | Active flag. |
| `shareableLink` | String | yes | — | trim | unique (+ compound) | Public link slug. |
| `usageCount` | Number | no | `0` | min 0 | — | Number of times used. |
| `maxUsage` | Number | no | `null` | min 1 | — | Optional cap. |
| `expiresAt` | Date | no | `null` | — | yes | Optional expiry. |
| `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/RequestTemplate.ts:283-293`:
- `{ categoryId: 1 }`
- `{ productType: 1 }`
- `{ isActive: 1 }`
- `{ createdAt: -1 }`
- `{ expiresAt: 1 }`
- `{ sellerId: 1, isActive: 1 }`
- `{ shareableLink: 1, isActive: 1 }`
- `{ productType: 1, isActive: 1 }`
- `{ categoryId: 1, productType: 1 }`
`shareableLink` and `sellerId` already get indexes from `unique: true` / field-level conventions (see source comment at line 282).
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`sellerId`), [[Category]] (`categoryId`).
- **Referenced by**: [[PurchaseRequest]] (`metadata.templateId` as string), [[Review]] (`subjectId` when `subjectType === 'template'`).
## State Transitions
```mermaid
stateDiagram-v2
[*] --> active : created
active --> inactive : seller toggles off
inactive --> active : seller toggles on
active --> expired : expiresAt passed
active --> capped : usageCount == maxUsage
expired --> [*]
capped --> [*]
```
> [!note] Soft state
> Only `isActive` is persisted directly. `expired` and `capped` are derived at query time using `expiresAt` and `usageCount`.
## Common Queries
```ts
// Seller's active templates
RequestTemplate.find({ sellerId, isActive: true }).sort({ createdAt: -1 });
// Public template by slug
RequestTemplate.findOne({ shareableLink: slug, isActive: true });
// Bump usage atomically
RequestTemplate.findOneAndUpdate(
{ _id, isActive: true, $or: [{ maxUsage: null }, { $expr: { $lt: ['$usageCount', '$maxUsage'] } }] },
{ $inc: { usageCount: 1 } },
{ new: true }
);
// Cleanup expired
RequestTemplate.find({ expiresAt: { $lt: new Date() }, isActive: true });
```
Related: [[PurchaseRequest]], [[User]], [[Category]], [[Review]].

View File

@@ -0,0 +1,95 @@
---
title: Review
tags: [data-model, mongoose]
aliases: [Rating, IReview]
---
# Review
Polymorphic 1-5 star review. The `subjectType` discriminator (`seller` or `template`) plus `subjectId` identifies what is being reviewed. `sellerId` is always present so per-seller aggregations work regardless of subject. A compound unique index on `(subjectType, subjectId, reviewerId)` prevents a reviewer from posting two reviews for the same subject.
> [!note] Source
> `backend/src/models/Review.ts:19` — schema definition
> `backend/src/models/Review.ts:38` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `subjectType` | String | yes | — | enum: `seller` / `template` | yes (compound) | Discriminator. |
| `subjectId` | ObjectId | yes | — | — | yes (compound) | Id of the seller [[User]] or [[RequestTemplate]]. |
| `sellerId` | ObjectId → [[User]] | yes | — | — | — | Seller associated with the review (always populated). |
| `reviewerId` | ObjectId → [[User]] | yes | — | — | yes (compound + unique) | Author. |
| `rating` | Number | yes | — | min 1, max 5 | — | Star rating. |
| `comment` | String | no | `""` | maxlength 1000 | — | Free-form comment. |
| `isVerifiedBuyer` | Boolean | no | `false` | — | — | Whether the reviewer actually bought from this seller. |
| `purchaseRequestId` | ObjectId → [[PurchaseRequest]] | no | `null` | — | — | Source request (if any). |
| `status` | String | no | `published` | enum: `published` / `pending` / `rejected` | — | Moderation status. |
| `createdAt` | Date | auto | — | — | yes (compound, desc) | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/Review.ts:34-36`:
- `{ subjectType: 1, subjectId: 1, createdAt: -1 }` — listing for a subject.
- `{ reviewerId: 1, subjectType: 1 }` — reviewer history.
- `{ subjectType: 1, subjectId: 1, reviewerId: 1 }`**unique**, one review per reviewer per subject.
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`sellerId`, `reviewerId`, and `subjectId` when `subjectType === 'seller'`), [[RequestTemplate]] (`subjectId` when `subjectType === 'template'`), [[PurchaseRequest]] (`purchaseRequestId`).
- **Referenced by**: none.
## State Transitions
```mermaid
stateDiagram-v2
[*] --> published : default
[*] --> pending : moderation required
pending --> published : approved
pending --> rejected : rejected
published --> rejected : flagged
rejected --> [*]
```
## Common Queries
```ts
// All reviews for a seller
Review.find({ subjectType: 'seller', subjectId: sellerUserId, status: 'published' })
.sort({ createdAt: -1 });
// Average rating per seller
Review.aggregate([
{ $match: { subjectType: 'seller', subjectId: sellerUserId, status: 'published' } },
{ $group: { _id: null, avg: { $avg: '$rating' }, count: { $sum: 1 } } }
]);
// Reviews written by a user
Review.find({ reviewerId: userId }).sort({ createdAt: -1 });
// Reviews for a template
Review.find({ subjectType: 'template', subjectId: templateId, status: 'published' });
```
> [!warning] Duplicate prevention
> Attempting to insert a second review for the same `(subjectType, subjectId, reviewerId)` will fail with a `E11000 duplicate key` error from MongoDB. Application code should treat that as "already reviewed".
Related: [[User]], [[RequestTemplate]], [[PurchaseRequest]].

View File

@@ -0,0 +1,96 @@
---
title: SellerOffer
tags: [data-model, mongoose]
aliases: [Seller Offer, Bid, ISellerOffer]
---
# SellerOffer
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
> `backend/src/models/SellerOffer.ts:24` — schema definition
> `backend/src/models/SellerOffer.ts:100` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `sellerId` | ObjectId → [[User]] | yes | — | — | yes | Seller submitting the bid. |
| `purchaseRequestId` | ObjectId → [[PurchaseRequest]] | yes | — | — | yes | Parent request. |
| `title` | String | yes | — | trim, maxlength 200 | — | Offer headline. |
| `description` | String | yes | — | trim, maxlength 1000 | — | Pitch and details. |
| `price.amount` | Number | yes | — | min 0 | — | Quoted amount. |
| `price.currency` | String | yes | `USDT` | enum: `USD` / `EUR` / `IRR` / `USDT` / `USDC` | — | Quote currency. |
| `deliveryTime.amount` | Number | yes | — | min 1 | — | Numeric ETA. |
| `deliveryTime.unit` | String | yes | — | enum: `hours` / `days` / `weeks` | — | ETA unit. |
| `status` | String | no | `pending` | enum: `pending` / `accepted` / `rejected` / `withdrawn` | yes | Offer status. |
| `attachments[]` | String[] | no | — | — | — | URLs of supporting files. |
| `notes` | String | no | — | trim | — | Internal/private notes. |
| `validUntil` | Date | no | — | — | — | Expiration. |
| `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
Defined at `backend/src/models/SellerOffer.ts:95-98`:
- `{ sellerId: 1 }`
- `{ purchaseRequestId: 1 }`
- `{ status: 1 }`
- `{ createdAt: -1 }`
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`sellerId`), [[PurchaseRequest]] (`purchaseRequestId`).
- **Referenced by**: [[PurchaseRequest]] (`offers[]`, `selectedOfferId`), [[Payment]] (`sellerOfferId`), [[Chat]] (`relatedTo.id` when `relatedTo.type === 'SellerOffer'`).
## State Transitions
```mermaid
stateDiagram-v2
[*] --> pending
pending --> accepted : buyer accepts
pending --> rejected : buyer rejects
pending --> withdrawn : seller cancels
accepted --> [*]
rejected --> [*]
withdrawn --> [*]
```
## Common Queries
```ts
// Offers for a request
SellerOffer.find({ purchaseRequestId }).sort({ createdAt: -1 });
// Seller's active offers
SellerOffer.find({ sellerId, status: 'pending' });
// Reject siblings on accept
SellerOffer.updateMany(
{ purchaseRequestId, _id: { $ne: acceptedId }, status: 'pending' },
{ status: 'rejected' }
);
// Cleanup expired offers
SellerOffer.find({ validUntil: { $lt: new Date() }, status: 'pending' });
```
Related: [[PurchaseRequest]], [[Payment]], [[User]].

View File

@@ -0,0 +1,90 @@
---
title: ShopSettings
tags: [data-model, mongoose]
aliases: [Shop, Storefront, IShopSettings]
---
# ShopSettings
One-to-one storefront configuration for a seller. Holds the shop name, description, avatar, cover image, public visibility flag, review toggles (`allowSellerReviews`, `allowTemplateReviews`), and social links. The unique constraint on `sellerId` enforces the one-shop-per-seller invariant.
> [!note] Source
> `backend/src/models/ShopSettings.ts:22` — schema definition
> `backend/src/models/ShopSettings.ts:86` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `sellerId` | ObjectId → [[User]] | yes | — | — | unique | Owning seller (one shop per seller). |
| `name` | String | yes | — | trim | — | Shop name. |
| `description` | String | no | `""` | trim | — | Shop description. |
| `avatar` | String | no | `""` | — | — | Avatar URL. |
| `coverImage` | String | no | `""` | — | — | Cover image URL. |
| `isPublic` | Boolean | no | `true` | — | — | Public visibility flag. |
| `allowSellerReviews` | Boolean | no | `true` | — | — | Whether buyers can review the seller. |
| `allowTemplateReviews` | Boolean | no | `true` | — | — | Whether buyers can review templates. |
| `socialLinks.facebook` | String | no | `""` | — | — | Facebook URL. |
| `socialLinks.instagram` | String | no | `""` | — | — | Instagram URL. |
| `socialLinks.linkedin` | String | no | `""` | — | — | LinkedIn URL. |
| `socialLinks.twitter` | String | no | `""` | — | — | Twitter / X URL. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
- Implicit unique index on `sellerId` (from `unique: true`). No additional indexes are declared (see comment at `backend/src/models/ShopSettings.ts:84`).
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: [[User]] (`sellerId`).
- **Referenced by**: none. [[Review]] toggles for the seller are read from here.
## State Transitions
No status field. The `isPublic` boolean is the only visibility control:
```mermaid
stateDiagram-v2
[*] --> public
public --> private : seller toggles off
private --> public : seller toggles on
```
## Common Queries
```ts
// Fetch the seller's shop
ShopSettings.findOne({ sellerId });
// Upsert on first save
ShopSettings.findOneAndUpdate(
{ sellerId },
{ $set: { name, description, ... } },
{ upsert: true, new: true }
);
// Public shop directory
ShopSettings.find({ isPublic: true }).sort({ createdAt: -1 });
```
> [!warning] Creating two shops will fail
> Inserting a second `ShopSettings` document with the same `sellerId` will fail with `E11000 duplicate key`. Application code should always use `findOneAndUpdate` with `upsert: true`.
Related: [[User]], [[Review]], [[RequestTemplate]].

View File

@@ -0,0 +1,97 @@
---
title: TempVerification
tags: [data-model, mongoose]
aliases: [Temp Verification, Pending Signup, ITempVerification]
---
# TempVerification
Short-lived holding collection for unverified signups. When a user begins registration the candidate data (email, hashed password, first name, last name, role, optional referral code) is saved here together with a verification OTP and its expiry. After the OTP is confirmed the row is promoted to a real [[User]] document and removed. Rows that are never confirmed self-destruct via a TTL index keyed on `emailVerificationCodeExpires`.
> [!note] Source
> `backend/src/models/TempVerification.ts:16` — schema definition
> `backend/src/models/TempVerification.ts:67` — model export
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `email` | String | yes | — | lowercase, trim | unique | Candidate email. |
| `password` | String | no | `""` | — | — | Hashed password (optional for passkey-only flows). |
| `firstName` | String | yes | — | trim | — | First name. |
| `lastName` | String | yes | — | trim | — | Last name. |
| `role` | String | no | `buyer` | enum: `buyer` / `seller` | — | Requested role. |
| `referralCode` | String | no | — | trim | — | Inviter's referral code. |
| `emailVerificationCode` | String | yes | — | — | — | OTP. |
| `emailVerificationCodeExpires` | Date | yes | — | — | TTL (`expireAfterSeconds: 0`) | OTP expiry. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
None defined.
## Indexes
- Implicit unique index on `email`.
- `{ emailVerificationCodeExpires: 1 }` with `expireAfterSeconds: 0``backend/src/models/TempVerification.ts:65`. MongoDB removes the document automatically once `emailVerificationCodeExpires` passes.
> [!note] TTL semantics
> `expireAfterSeconds: 0` together with the indexed date field means "delete this document as soon as the date in `emailVerificationCodeExpires` is reached".
## Pre/Post Hooks
None declared.
## Instance Methods
None defined.
## Static Methods
None defined.
## Relationships
- **References**: none.
- **Referenced by**: none. Promotes into a [[User]] document on successful verification.
## State Transitions
```mermaid
stateDiagram-v2
[*] --> pending : signup started
pending --> verified : code confirmed
pending --> expired : TTL purge
verified --> [*] : promoted to User
expired --> [*]
```
> [!warning] No persistent "verified" state
> A `TempVerification` row only ever exists in the `pending` state from the database's point of view. Once verified the row is deleted (and a [[User]] row is created); if not verified it is purged by the TTL index.
## Common Queries
```ts
// Look up by email during signup
TempVerification.findOne({ email: email.toLowerCase() });
// Validate OTP
TempVerification.findOne({
email: email.toLowerCase(),
emailVerificationCode: code,
emailVerificationCodeExpires: { $gt: new Date() },
});
// Replace stale row on a re-attempt
TempVerification.findOneAndUpdate(
{ email },
{ $set: { emailVerificationCode, emailVerificationCodeExpires, ... } },
{ upsert: true, new: true }
);
// Delete after promotion
TempVerification.deleteOne({ email });
```
Related: [[User]].

137
02 - Data Models/User.md Normal file
View File

@@ -0,0 +1,137 @@
---
title: User
tags: [data-model, mongoose]
aliases: [User Model, IUser, Account]
---
# User
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
## Schema
| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `email` | String | yes | — | lowercase, trim | unique | Primary login identifier. |
| `password` | String | no | — | minlength 6 | — | Hashed password. Optional to support passkey-only accounts. |
| `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. |
| `emailVerificationToken` | String | no | — | — | — | Legacy token-based email verification. |
| `emailVerificationCode` | String | no | — | — | — | OTP code for email verification. |
| `emailVerificationCodeExpires` | Date | no | — | — | — | Expiry for `emailVerificationCode`. |
| `passwordResetToken` | String | no | — | — | — | Token for reset link flow. |
| `passwordResetExpires` | Date | no | — | — | — | Expiry of `passwordResetToken`. |
| `passwordResetCode` | String | no | — | — | — | OTP reset code. |
| `passwordResetCodeExpires` | Date | no | — | — | — | Expiry for OTP reset code. |
| `passkeys[]` | Subdocument array | no | `[]` | — | — | WebAuthn credentials (see below). |
| `passkeys[].id` | String | yes | — | — | — | Credential ID. |
| `passkeys[].publicKey` | String | yes | — | — | — | Stored public key. |
| `passkeys[].counter` | Number | yes | `0` | — | — | Signature counter. |
| `passkeys[].deviceType` | String | yes | — | enum: `platform` / `cross-platform` | — | Authenticator class. |
| `passkeys[].deviceName` | String | no | — | — | — | Optional human label. |
| `passkeys[].createdAt` | Date | no | `Date.now` | — | — | Registration timestamp. |
| `profile.avatar` | String | no | — | — | — | Avatar URL. |
| `profile.photoURL` | String | no | — | — | — | Alternative photo URL. |
| `profile.phone` | String | no | — | — | — | Contact phone. |
| `profile.address.street` | String | no | — | — | — | Inline address (separate from [[Address]] book). |
| `profile.address.city` | String | no | — | — | — | — |
| `profile.address.state` | String | no | — | — | — | — |
| `profile.address.zipCode` | String | no | — | — | — | — |
| `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.isPublic` | Boolean | no | `false` | — | — | Whether the profile is publicly visible. |
| `preferences.language` | String | no | `"en"` | — | — | UI language. |
| `preferences.currency` | String | no | `"USD"` | — | — | Display currency. |
| `preferences.notifications.email` | Boolean | no | `true` | — | — | Opt-in for email notifications. |
| `preferences.notifications.sms` | Boolean | no | `false` | — | — | Opt-in for SMS notifications. |
| `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. |
| `referralCode` | String | no | — | — | unique, sparse | Personal invite code. |
| `referredBy` | ObjectId → User | no | — | — | yes | Who invited this user. |
| `points.total` | Number | no | `0` | — | — | Lifetime points earned. |
| `points.available` | Number | no | `0` | — | — | Currently spendable. |
| `points.used` | Number | no | `0` | — | — | Cumulative spent. |
| `points.level` | Number | no | `1` | — | yes (`points.level`) | Resolved against [[LevelConfig]]. |
| `referralStats.totalReferrals` | Number | no | `0` | — | — | Count of invited users. |
| `referralStats.activeReferrals` | Number | no | `0` | — | — | Subset that became active buyers. |
| `referralStats.totalEarned` | Number | no | `0` | — | — | Cumulative reward earnings. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
## Virtuals
| Virtual | Returns | Definition |
| --- | --- | --- |
| `fullName` | `${firstName} ${lastName}` | `backend/src/models/User.ts:238` |
## Indexes
Defined explicitly (in addition to the implicit `email` unique index):
- `{ role: 1 }``backend/src/models/User.ts:231`
- `{ status: 1 }``backend/src/models/User.ts:232`
- `{ referralCode: 1 }``backend/src/models/User.ts:233`
- `{ referredBy: 1 }``backend/src/models/User.ts:234`
- `{ 'points.level': 1 }``backend/src/models/User.ts:235`
## Pre/Post Hooks
None declared at the schema level.
## Instance Methods
| Signature | Purpose |
| --- | --- |
| `toJSON(): object` | Strips `password`, `refreshTokens`, all `emailVerification*` and `passwordReset*` fields before serialisation. Defined at `backend/src/models/User.ts:243`. |
## Static Methods
None defined on the schema.
## Relationships
- **References**: [[User]] (self, via `referredBy`).
- **Referenced by**: [[PurchaseRequest]] (`buyerId`, `preferredSellerIds`, `deliveryInfo.deliveryCodeUsedBy`, `deliveryInfo.deliveryAttempts[].sellerId`), [[SellerOffer]] (`sellerId`), [[Payment]] (`buyerId`, `sellerId`), [[Chat]] (`participants[].userId`, `messages[].senderId`, `metadata.createdBy`), [[Notification]] (`userId` as string), [[RequestTemplate]] (`sellerId`), [[Dispute]] (`buyerId`, `sellerId`, `adminId`), [[BlogPost]] (`author.id`), [[Address]] (`userId`), [[Review]] (`sellerId`, `reviewerId`), [[PointTransaction]] (`user`, `referredUser`), [[ShopSettings]] (`sellerId`).
## State Transitions
```mermaid
stateDiagram-v2
[*] --> active : signup verified
active --> suspended : admin action
suspended --> active : admin restore
active --> deleted : self-delete
suspended --> deleted : admin purge
deleted --> [*]
```
## Common Queries
```ts
// Find by email (login)
User.findOne({ email: email.toLowerCase() });
// Active sellers
User.find({ role: 'seller', status: 'active' });
// Validate referral
User.findOne({ referralCode: code });
// Leaderboard by points
User.find({ status: 'active' }).sort({ 'points.total': -1 }).limit(10);
// Promote level
User.updateOne({ _id: id }, { $set: { 'points.level': newLevel } });
```
Related: [[TempVerification]], [[LevelConfig]], [[PointTransaction]], [[ShopSettings]].