Initial commit: nick docs
This commit is contained in:
82
02 - Data Models/Address.md
Normal file
82
02 - Data Models/Address.md
Normal 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]].
|
||||
113
02 - Data Models/BlogPost.md
Normal file
113
02 - Data Models/BlogPost.md
Normal 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]].
|
||||
87
02 - Data Models/Category.md
Normal file
87
02 - Data Models/Category.md
Normal 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
144
02 - Data Models/Chat.md
Normal 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]].
|
||||
105
02 - Data Models/Data Model Overview.md
Normal file
105
02 - Data Models/Data Model Overview.md
Normal 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
127
02 - Data Models/Dispute.md
Normal 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]].
|
||||
90
02 - Data Models/LevelConfig.md
Normal file
90
02 - Data Models/LevelConfig.md
Normal 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]].
|
||||
99
02 - Data Models/Notification.md
Normal file
99
02 - Data Models/Notification.md
Normal 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
157
02 - Data Models/Payment.md
Normal 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]].
|
||||
93
02 - Data Models/PointTransaction.md
Normal file
93
02 - Data Models/PointTransaction.md
Normal 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]].
|
||||
172
02 - Data Models/PurchaseRequest.md
Normal file
172
02 - Data Models/PurchaseRequest.md
Normal 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]].
|
||||
134
02 - Data Models/RequestTemplate.md
Normal file
134
02 - Data Models/RequestTemplate.md
Normal 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]].
|
||||
95
02 - Data Models/Review.md
Normal file
95
02 - Data Models/Review.md
Normal 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]].
|
||||
96
02 - Data Models/SellerOffer.md
Normal file
96
02 - Data Models/SellerOffer.md
Normal 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]].
|
||||
90
02 - Data Models/ShopSettings.md
Normal file
90
02 - Data Models/ShopSettings.md
Normal 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]].
|
||||
97
02 - Data Models/TempVerification.md
Normal file
97
02 - Data Models/TempVerification.md
Normal 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
137
02 - Data Models/User.md
Normal 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]].
|
||||
Reference in New Issue
Block a user