From 0da235ae273a3d06e01cc9c2398c6e2b1f84a45f Mon Sep 17 00:00:00 2001 From: moojttaba Date: Sat, 23 May 2026 20:35:34 +0330 Subject: [PATCH] Initial commit: nick docs --- .gitignore | 8 + 00 - Overview/Glossary.md | 223 ++++++++ 00 - Overview/Introduction.md | 71 +++ 00 - Overview/Roles & Personas.md | 237 +++++++++ 00 - Overview/System Overview.md | 216 ++++++++ 00 - Overview/Tech Stack.md | 233 ++++++++ 01 - Architecture/Backend Architecture.md | 311 +++++++++++ 01 - Architecture/Frontend Architecture.md | 369 +++++++++++++ 01 - Architecture/Infrastructure.md | 224 ++++++++ 01 - Architecture/Real-time Layer.md | 220 ++++++++ 01 - Architecture/Security Architecture.md | 227 ++++++++ 01 - Architecture/System Architecture.md | 202 +++++++ 02 - Data Models/Address.md | 82 +++ 02 - Data Models/BlogPost.md | 113 ++++ 02 - Data Models/Category.md | 87 +++ 02 - Data Models/Chat.md | 144 +++++ 02 - Data Models/Data Model Overview.md | 105 ++++ 02 - Data Models/Dispute.md | 127 +++++ 02 - Data Models/LevelConfig.md | 90 ++++ 02 - Data Models/Notification.md | 99 ++++ 02 - Data Models/Payment.md | 157 ++++++ 02 - Data Models/PointTransaction.md | 93 ++++ 02 - Data Models/PurchaseRequest.md | 172 ++++++ 02 - Data Models/RequestTemplate.md | 134 +++++ 02 - Data Models/Review.md | 95 ++++ 02 - Data Models/SellerOffer.md | 96 ++++ 02 - Data Models/ShopSettings.md | 90 ++++ 02 - Data Models/TempVerification.md | 97 ++++ 02 - Data Models/User.md | 137 +++++ 03 - API Reference/AI API.md | 118 +++++ 03 - API Reference/API Overview.md | 173 ++++++ 03 - API Reference/Admin API.md | 143 +++++ 03 - API Reference/Authentication API.md | 305 +++++++++++ 03 - API Reference/Blog API.md | 125 +++++ 03 - API Reference/Chat API.md | 144 +++++ 03 - API Reference/Dispute API.md | 122 +++++ 03 - API Reference/Error Codes.md | 110 ++++ 03 - API Reference/File API.md | 111 ++++ 03 - API Reference/Marketplace API.md | 499 ++++++++++++++++++ 03 - API Reference/Notification API.md | 110 ++++ 03 - API Reference/Payment API.md | 387 ++++++++++++++ 03 - API Reference/Points API.md | 140 +++++ 03 - API Reference/Socket Events.md | 152 ++++++ 03 - API Reference/User API.md | 262 +++++++++ 04 - Flows/Authentication Flow.md | 187 +++++++ 04 - Flows/Chat Flow.md | 188 +++++++ 04 - Flows/Delivery Confirmation Flow.md | 127 +++++ 04 - Flows/Dispute Flow.md | 199 +++++++ 04 - Flows/Escrow Flow.md | 196 +++++++ 04 - Flows/Google OAuth Flow.md | 144 +++++ 04 - Flows/Negotiation Flow.md | 148 ++++++ 04 - Flows/Notification Flow.md | 155 ++++++ 04 - Flows/Passkey (WebAuthn) Flow.md | 162 ++++++ 04 - Flows/Password Reset Flow.md | 124 +++++ 04 - Flows/Payment Flow - DePay & Web3.md | 172 ++++++ 04 - Flows/Payment Flow - SHKeeper.md | 252 +++++++++ 04 - Flows/Payout Flow.md | 133 +++++ 04 - Flows/Purchase Request Flow.md | 202 +++++++ 04 - Flows/Rating Flow.md | 120 +++++ 04 - Flows/Referral Flow.md | 163 ++++++ 04 - Flows/Registration Flow.md | 195 +++++++ 04 - Flows/Seller Offer Flow.md | 197 +++++++ 05 - Design System/Colors.md | 207 ++++++++ 05 - Design System/Components.md | 222 ++++++++ 05 - Design System/Design System Overview.md | 179 +++++++ 05 - Design System/Iconography.md | 183 +++++++ .../Internationalization & RTL.md | 276 ++++++++++ 05 - Design System/Layouts.md | 217 ++++++++ 05 - Design System/Settings & Theming.md | 222 ++++++++ 05 - Design System/Theme Configuration.md | 250 +++++++++ 05 - Design System/Typography.md | 186 +++++++ 06 - Usage/Admin Guide.md | 435 +++++++++++++++ 06 - Usage/Seller Guide.md | 365 +++++++++++++ 06 - Usage/Support Guide.md | 331 ++++++++++++ 06 - Usage/User Guide.md | 410 ++++++++++++++ 07 - Development/Coding Standards.md | 380 +++++++++++++ 07 - Development/Environment Variables.md | 276 ++++++++++ 07 - Development/Git Workflow.md | 219 ++++++++ 07 - Development/Local Setup.md | 273 ++++++++++ 07 - Development/Project Structure.md | 200 +++++++ 07 - Development/Scripts.md | 365 +++++++++++++ 07 - Development/Testing.md | 262 +++++++++ 08 - Operations/Backup & Recovery.md | 315 +++++++++++ 08 - Operations/CI-CD Pipeline.md | 259 +++++++++ 08 - Operations/Database Operations.md | 301 +++++++++++ 08 - Operations/Deployment.md | 255 +++++++++ 08 - Operations/Docker Setup.md | 381 +++++++++++++ 08 - Operations/Incident Response.md | 393 ++++++++++++++ 08 - Operations/Monitoring.md | 253 +++++++++ README.md | 229 ++++++++ 90 files changed, 18268 insertions(+) create mode 100644 .gitignore create mode 100644 00 - Overview/Glossary.md create mode 100644 00 - Overview/Introduction.md create mode 100644 00 - Overview/Roles & Personas.md create mode 100644 00 - Overview/System Overview.md create mode 100644 00 - Overview/Tech Stack.md create mode 100644 01 - Architecture/Backend Architecture.md create mode 100644 01 - Architecture/Frontend Architecture.md create mode 100644 01 - Architecture/Infrastructure.md create mode 100644 01 - Architecture/Real-time Layer.md create mode 100644 01 - Architecture/Security Architecture.md create mode 100644 01 - Architecture/System Architecture.md create mode 100644 02 - Data Models/Address.md create mode 100644 02 - Data Models/BlogPost.md create mode 100644 02 - Data Models/Category.md create mode 100644 02 - Data Models/Chat.md create mode 100644 02 - Data Models/Data Model Overview.md create mode 100644 02 - Data Models/Dispute.md create mode 100644 02 - Data Models/LevelConfig.md create mode 100644 02 - Data Models/Notification.md create mode 100644 02 - Data Models/Payment.md create mode 100644 02 - Data Models/PointTransaction.md create mode 100644 02 - Data Models/PurchaseRequest.md create mode 100644 02 - Data Models/RequestTemplate.md create mode 100644 02 - Data Models/Review.md create mode 100644 02 - Data Models/SellerOffer.md create mode 100644 02 - Data Models/ShopSettings.md create mode 100644 02 - Data Models/TempVerification.md create mode 100644 02 - Data Models/User.md create mode 100644 03 - API Reference/AI API.md create mode 100644 03 - API Reference/API Overview.md create mode 100644 03 - API Reference/Admin API.md create mode 100644 03 - API Reference/Authentication API.md create mode 100644 03 - API Reference/Blog API.md create mode 100644 03 - API Reference/Chat API.md create mode 100644 03 - API Reference/Dispute API.md create mode 100644 03 - API Reference/Error Codes.md create mode 100644 03 - API Reference/File API.md create mode 100644 03 - API Reference/Marketplace API.md create mode 100644 03 - API Reference/Notification API.md create mode 100644 03 - API Reference/Payment API.md create mode 100644 03 - API Reference/Points API.md create mode 100644 03 - API Reference/Socket Events.md create mode 100644 03 - API Reference/User API.md create mode 100644 04 - Flows/Authentication Flow.md create mode 100644 04 - Flows/Chat Flow.md create mode 100644 04 - Flows/Delivery Confirmation Flow.md create mode 100644 04 - Flows/Dispute Flow.md create mode 100644 04 - Flows/Escrow Flow.md create mode 100644 04 - Flows/Google OAuth Flow.md create mode 100644 04 - Flows/Negotiation Flow.md create mode 100644 04 - Flows/Notification Flow.md create mode 100644 04 - Flows/Passkey (WebAuthn) Flow.md create mode 100644 04 - Flows/Password Reset Flow.md create mode 100644 04 - Flows/Payment Flow - DePay & Web3.md create mode 100644 04 - Flows/Payment Flow - SHKeeper.md create mode 100644 04 - Flows/Payout Flow.md create mode 100644 04 - Flows/Purchase Request Flow.md create mode 100644 04 - Flows/Rating Flow.md create mode 100644 04 - Flows/Referral Flow.md create mode 100644 04 - Flows/Registration Flow.md create mode 100644 04 - Flows/Seller Offer Flow.md create mode 100644 05 - Design System/Colors.md create mode 100644 05 - Design System/Components.md create mode 100644 05 - Design System/Design System Overview.md create mode 100644 05 - Design System/Iconography.md create mode 100644 05 - Design System/Internationalization & RTL.md create mode 100644 05 - Design System/Layouts.md create mode 100644 05 - Design System/Settings & Theming.md create mode 100644 05 - Design System/Theme Configuration.md create mode 100644 05 - Design System/Typography.md create mode 100644 06 - Usage/Admin Guide.md create mode 100644 06 - Usage/Seller Guide.md create mode 100644 06 - Usage/Support Guide.md create mode 100644 06 - Usage/User Guide.md create mode 100644 07 - Development/Coding Standards.md create mode 100644 07 - Development/Environment Variables.md create mode 100644 07 - Development/Git Workflow.md create mode 100644 07 - Development/Local Setup.md create mode 100644 07 - Development/Project Structure.md create mode 100644 07 - Development/Scripts.md create mode 100644 07 - Development/Testing.md create mode 100644 08 - Operations/Backup & Recovery.md create mode 100644 08 - Operations/CI-CD Pipeline.md create mode 100644 08 - Operations/Database Operations.md create mode 100644 08 - Operations/Deployment.md create mode 100644 08 - Operations/Docker Setup.md create mode 100644 08 - Operations/Incident Response.md create mode 100644 08 - Operations/Monitoring.md create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6d2bd5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# macOS +.DS_Store + +# Obsidian workspace/UI state (keep config, ignore per-machine state) +.obsidian/workspace.json +.obsidian/workspace-mobile.json +.obsidian/cache +.trash/ diff --git a/00 - Overview/Glossary.md b/00 - Overview/Glossary.md new file mode 100644 index 0000000..d9b9a39 --- /dev/null +++ b/00 - Overview/Glossary.md @@ -0,0 +1,223 @@ +--- +title: Glossary +tags: [overview, glossary, definitions, terminology] +created: 2026-05-23 +--- + +# Glossary + +> [!info] How to read this page +> Terms are listed alphabetically. Where a term is also the subject of its own doc, the heading links there. When a term references another glossary entry, it is shown as plain bold rather than a wikilink — flip back here as needed. + +--- + +### Admin + +> [!info] Definition +> A user with `role: "admin"` and full read/write access to every part of the platform — moderation, payments, disputes, content. Created at boot via `backend/src/infrastructure/database/init-admin.ts`. See [[Roles & Personas]]. + +### Buyer (User) + +> [!info] Definition +> A user with `role: "buyer"`. Posts **Purchase Requests** and accepts **Seller Offers**. The default role assigned at signup. UI label: "User". + +### Category + +> [!info] Definition +> A taxonomy node used to classify Purchase Requests and templates. Sellers can flag themselves as a preferred seller in a category. Backed by the `Category` model (`backend/src/models/Category.ts`); seeded via `seedCategories.ts`. + +### Chat + +> [!info] Definition +> A multi-party real-time conversation persisted in MongoDB (`Chat` model) and streamed over Socket.IO. Chats are scoped to a Purchase Request, a Dispute, or a direct support thread. Per-chat rate limiting is enforced by `chatRateLimiter.ts`. + +### DePay + +> [!info] Definition +> A drop-in Web3 payment widget (`@depay/widgets`) used on the frontend to let buyers pay directly from their own wallet on supported EVM chains. The transaction is signed in the wallet, broadcast to the chain, and verified server-side. See [[Payments Overview]]. + +### Delivery Code + +> [!info] Definition +> A six-digit one-time code generated when a physical order ships. The buyer reads it to the courier (or the seller types it into the app at handoff) to confirm receipt and release escrow. Stored on `PurchaseRequest.deliveryInfo.deliveryCode` with `deliveryCodeExpiresAt` and a usage audit trail (`backend/src/models/PurchaseRequest.ts`). + +### Dispute + +> [!info] Definition +> A formal complaint opened by either party when a deal goes wrong. Creates a three-way chat (buyer, seller, admin) and a `Dispute` document with a structured `timeline[]`, `evidence[]`, and `resolution`. Categories: `product_quality | delivery_delay | wrong_item | payment_issue | seller_behavior | other`. Outcomes: `refund | replacement | compensation | warning_seller | ban_seller | no_action`. See `backend/src/models/Dispute.ts`. + +### Escrow + +> [!info] Definition +> The custodial period during which buyer funds are held by the platform (SHKeeper or the smart contract layer) after payment but before release to the seller. Escrow guarantees the seller will be paid if they deliver, and guarantees the buyer can be refunded if they do not. The defining feature of Amn. + +### Idempotency + +> [!info] Definition +> The property that the same request (identified by an idempotency key) can safely be retried without performing the underlying operation more than once. Critical for payment webhooks — SHKeeper may deliver the same webhook several times if it does not receive a 200 quickly. Amn enforces idempotency in `PaymentCoordinator` and at the model level via unique constraints on transaction hashes. + +### JWT (Access / Refresh) + +> [!info] Definition +> JSON Web Tokens issued by the auth service. **Access tokens** are short-lived and carried on every authenticated request in the `Authorization: Bearer` header. **Refresh tokens** are longer-lived, stored on the `User` document, and exchanged for a new access token without re-prompting credentials. Rotated on every refresh. + +### Level + +> [!info] Definition +> A user tier derived from accumulated points. Configured in `LevelConfig` (`backend/src/models/LevelConfig.ts`). Higher levels unlock perks such as reduced fees or higher-priority placement. + +### Negotiation + +> [!info] Definition +> The back-and-forth between a buyer and a seller after an offer is submitted but before it is accepted. Implemented as standard chat messages in the request's chat room. A purchase request enters the `in_negotiation` status when at least one offer exists and discussion is active. + +### Notification + +> [!info] Definition +> A platform event delivered to a user, either in-app (via Socket.IO to the user's personal room) or by email (`nodemailer`). Persisted as `Notification` documents so they can be read on subsequent sessions. Categories include `offer_received`, `payment_confirmed`, `dispute_update`, and more. + +### Owner + +> [!info] Definition +> The UI label for a **Seller**. They "own" a shop. Synonym; same underlying `role: "seller"`. + +### Passkey + +> [!info] Definition +> A WebAuthn credential bound to a device (platform or cross-platform). Replaces passwords for fast biometric login. Multiple passkeys per user supported (`User.passkeys[]` in `backend/src/models/User.ts:125`). See [[Authentication Flow]]. + +### Pay-in + +> [!info] Definition +> Money flowing **into** escrow from the buyer. Recorded as `Payment.direction: "in"`. The buyer's choice of pay-in surface (SHKeeper invoice vs. Web3 wallet) is independent of how the payout will be sent. + +### Pay-in Intent + +> [!info] Definition +> The pre-authorisation record created when a buyer commits to paying but the funds have not yet arrived on-chain. Holds the chosen amount, currency, expected wallet address (SHKeeper) or counterparty (DePay), and an expiry. Becomes a confirmed `Payment` once the chain or webhook confirms settlement. + +### Payment + +> [!info] Definition +> A monetary transaction record. One `Payment` document covers a pay-in, payout, or refund. Carries `direction`, `provider`, `blockchain` metadata (network, tx hash, confirmations), buyer, seller, and the related purchase request and seller offer. See `backend/src/models/Payment.ts`. + +### Payment Coordinator + +> [!info] Definition +> A backend module (`backend/src/services/payment/paymentCoordinator.ts`) that serialises payment state transitions using Redis locks. Prevents the race condition where a webhook and a manual confirmation both try to flip the same payment to `paid`. + +### Payout + +> [!info] Definition +> Money flowing **out** of escrow to the seller's wallet. Recorded as `Payment.direction: "out"`. Triggered by admin action after delivery is confirmed; implemented via SHKeeper's payout API (`shkeeperPayoutService.ts`). + +### Points + +> [!info] Definition +> The loyalty currency. Earned for completed deals, verified referrals, and certain in-app actions. Spent on fee discounts or platform perks. Each grant or spend writes a `PointTransaction` row for full auditability (`backend/src/models/PointTransaction.ts`). + +### Point Transaction + +> [!info] Definition +> An immutable record of a single change to a user's point balance — credit or debit, with `reason`, `relatedEntity`, and timestamp. The audit trail for the points system. + +### Preferred Seller + +> [!info] Definition +> A seller a buyer can specify on a purchase request to direct the offer notification to a narrower audience. Listed in `PurchaseRequest.preferredSellerIds[]`. Other sellers may still see the request if it is public. + +### Purchase Request + +> [!info] Definition +> The buyer's brief — what they want to acquire, for how much, by when, and where to deliver. The originating document of every deal. Long status enum walking the entire lifecycle from `pending_payment` through `completed`. See `backend/src/models/PurchaseRequest.ts`. + +### Referral + +> [!info] Definition +> A signup attributed to an existing user via their unique `referralCode`. Triggered through the `/r/:code` short URL (`backend/src/app.ts:274`) which redirects to the signup page with the code pre-filled. Referrer earns points when the referred user completes qualifying actions. + +### Referral Code + +> [!info] Definition +> A short unique string on every user (`User.referralCode`) that identifies them as a referrer. Used in the `/r/:code` redirect. + +### Request Template + +> [!info] Definition +> A reusable, pre-configured purchase request that acts like an express checkout. Admin templates appear in a platform catalogue; seller templates are shop-scoped. Clicking "buy" on a template spawns a real `PurchaseRequest` with the template's defaults. See `backend/src/models/RequestTemplate.ts` and `frontend/template-request-feature.md`. + +### Review + +> [!info] Definition +> Post-completion feedback from one party about the other — star rating plus free-form text. Stored in the `Review` model. Aggregated onto the seller's shop profile. + +### Role + +> [!info] Definition +> The user's identity tier in the system: `"admin" | "buyer" | "seller"` (`backend/src/models/User.ts:94`). Drives middleware authorisation. Support is a special admin variant rather than a distinct role. + +### Seller (Owner) + +> [!info] Definition +> A user with `role: "seller"`. Submits offers on requests, runs a shop, receives payouts. Retains all buyer capabilities. UI label: "Owner". + +### Seller Offer + +> [!info] Definition +> A single bid attached to one Purchase Request — `price`, `deliveryTime`, `status` (`pending | accepted | rejected | withdrawn`), optional `validUntil`, optional attachments. See `backend/src/models/SellerOffer.ts`. + +### Shop + +> [!info] Definition +> A seller's public profile + storefront — name, banner, description, payout wallet, response-time SLA, working hours. Configured in `ShopSettings` and rendered at `/dashboard/shops/`. + +### SHKeeper + +> [!info] Definition +> A self-hosted crypto payment processor used as Amn's primary custodial pay-in / payout rail. Issues a fresh wallet address per invoice, watches the chain for incoming USDT, and emits a signed webhook on settlement. Lives at `https://pay.amn.gg` per `backend/TODO.md`. Integration code under `backend/src/services/payment/shkeeper/`. + +### Socket Room + +> [!info] Definition +> A logical channel inside the Socket.IO server that scopes events to a subset of connected clients. Rooms used by Amn include `user-`, `chat-`, `request-`, `seller-`, `buyer-`, `sellers`, `buyers`. See `backend/src/app.ts:79-179`. + +### Status (Order) + +> [!info] Definition +> The current point in a Purchase Request's lifecycle. Full enum: `pending_payment | pending | active | received_offers | in_negotiation | payment | processing | delivery | delivered | confirming | completed | cancelled | seller_paid`. Each transition emits a Socket.IO event and may trigger notifications. + +### Support + +> [!info] Definition +> A constrained admin persona dedicated to answering tickets and triaging disputes. Same role enum (`admin`) but with permission middleware that blocks destructive operations. Seeded as `support@amn.gg`. + +### USDT / USDC + +> [!info] Definition +> The two stablecoins Amn supports out of the box for pay-in and payout. USDT is the default for SHKeeper invoices; both are supported in offer pricing (`SellerOffer.price.currency` enum: `USD | EUR | IRR | USDT | USDC`). + +### Webhook + +> [!info] Definition +> An inbound HTTP POST from an external service notifying Amn of an event. SHKeeper webhooks (`/api/payment/shkeeper/webhook`) are the most important — they confirm pay-ins. All webhooks are HMAC-signed; verification uses `SHKEEPER_WEBHOOK_SECRET`. Failed verifications are dropped. + +### WalletConnect + +> [!info] Definition +> An open protocol for connecting wallets to web apps via a QR code or deep link. Configured but **currently disabled** in `frontend/src/web3/config.ts` pending an SSR-compatibility fix; MetaMask is the active connector. + +### Web3 Wallet + +> [!info] Definition +> A user-controlled key store (MetaMask, hardware wallet, mobile wallet) that signs blockchain transactions. The frontend talks to wallets through Wagmi + Viem and the user keeps custody of funds right up to the moment of payment. + +--- + +## See also + +- [[Introduction]] — high-level project framing. +- [[System Overview]] — how the pieces connect. +- [[Roles & Personas]] — who uses what. +- [[Tech Stack]] — exact tool versions. +- [[02 - Data Models]] — schemas for every term defined above. +- [[04 - Flows]] — the lifecycle diagrams those terms appear in. diff --git a/00 - Overview/Introduction.md b/00 - Overview/Introduction.md new file mode 100644 index 0000000..3d7e28e --- /dev/null +++ b/00 - Overview/Introduction.md @@ -0,0 +1,71 @@ +--- +title: Introduction +tags: [overview, introduction, mission, product] +created: 2026-05-23 +--- + +# Introduction + +> [!info] About this vault +> This is the technical documentation for **Amn** (internal code name: *nick app*), a crypto-native escrow marketplace. The vault is organised into numbered sections — start here in `00 - Overview`, then drill into [[01 - Architecture]], [[02 - Data Models]], and so on. + +## Mission + +**Amn exists to make peer-to-peer commerce trustworthy without forcing either side to trust the other.** + +The platform is built on a simple premise that has been hard to pull off in practice: a buyer should be able to describe what they want, receive competing offers from sellers, pay safely, and only release that payment once the goods or service have actually been delivered — all without exposing themselves to chargebacks, payment fraud, or a counterparty who simply disappears. Sellers, in turn, should know that the money they are quoting for is already locked in escrow before they invest time and inventory in fulfilling the order. + +Amn solves this with a fully integrated escrow flow, a real-time chat layer that keeps both parties accountable, and a human dispute system that catches the edge cases that automation cannot. + +## The problem we solve + +Traditional marketplaces tend to live at one of two extremes: + +1. **Fully custodial platforms** (Amazon, eBay, Fiverr) take a large cut, dictate every term of the transaction, and freeze funds on a whim. They work, but they are expensive and opaque. +2. **Free-form P2P channels** (Telegram groups, Discord servers, direct DMs) charge nothing but offer no protection at all. The first scam empties the wallet and there is no recourse. + +Amn sits between the two. It charges a thin escrow margin, holds funds for only as long as it takes to confirm delivery, and supports both fiat-style stablecoin escrow (via [[SHKeeper]]) and direct on-chain settlement (via [[DePay]] and the user's own wallet) — meaning the buyer can keep custody of their crypto until the literal moment of release. + +> [!tip] Why "crypto-native"? +> The escrow rails are built around stablecoins (USDT/USDC) on EVM chains rather than card networks. That means no chargebacks, no 3-day settlement, no geographic restrictions — and a transparent, auditable transaction trail for every step of the deal. See [[Tech Stack]] for the full Web3 surface. + +## Target users + +Amn serves four distinct personas, each with their own workflows and dashboard. The full breakdown lives in [[Roles & Personas]]; in summary: + +- **Buyers** (a.k.a. *Users*) post a **Purchase Request** describing what they want — a physical product, a digital good, a service, or a consultation — and receive bids from sellers. +- **Sellers** (a.k.a. *Owners*) browse open requests, submit **Seller Offers** with their price and delivery time, and run their shop with the help of a personalised dashboard, reviews, and a points-based loyalty programme. +- **Admins** moderate the marketplace, resolve disputes, manage categories and templates, configure payout wallets, and have full visibility into payments and users. +- **Support agents** are a lightweight subset of admin who can join chats, respond to user tickets, and triage disputes without having destructive permissions. + +Beyond the four roles, two ambient audiences read the platform: +- **Anonymous visitors** browse the [[Blog]], read public seller profiles, and convert through the referral programme. +- **Search engines** index the public blog and seller shops, which are server-rendered by Next.js for SEO. + +## What makes Amn distinctive + +A handful of design choices set Amn apart from generic marketplace software: + +1. **Dual payment rails.** Every order can be paid through SHKeeper (a self-hosted crypto payment processor that issues a fresh wallet per invoice) *or* through a Web3 wallet connect flow (DePay + Wagmi/Viem + MetaMask). The buyer picks; the escrow logic is identical downstream. See [[Payments Overview]]. +2. **Request-first marketplace.** Most platforms list *products*. Amn lists *needs*. Buyers describe what they want and let the market come to them — closer to a reverse auction than a catalogue. The unidirectional flow eliminates the "thousand-listings-with-no-stock" problem. +3. **Request Templates.** Power buyers (and admins) can publish reusable purchase request templates that act like express checkouts — a buyer clicks "I want this" and the order is opened pre-filled. Templates are the bridge between Amn and conventional ecommerce. +4. **First-class i18n with RTL.** The frontend ships with six locales out of the box (English, French, Vietnamese, Chinese, Arabic, Persian) and full right-to-left support — Persian is the default fallback. See `frontend/src/locales/locales-config.ts:36`. +5. **Real-time everything.** Chat, offer notifications, payment status, dispute updates, and presence indicators all flow over Socket.IO rooms. The UI is not just reactive to user input — it is reactive to other users. +6. **Points and referrals built in.** A loyalty system rewards completed deals, referrals, and verified actions. Points unlock fee tiers and are tracked in `PointTransaction` records for full auditability. +7. **AI assist.** OpenAI is wired into the backend to help draft purchase requests, summarise long chat threads, and surface intent classification for the admin dashboard. +8. **Human dispute resolution with a paper trail.** When a deal goes wrong, a three-way chat is opened (buyer, seller, admin) with a full timeline of every action and a structured resolution record. Disputes never disappear into a black box. + +## How to read this vault + +> [!note] Recommended reading order +> 1. **[[Introduction]]** — you are here. +> 2. **[[System Overview]]** — the 10,000-foot map. +> 3. **[[Tech Stack]]** — every dependency with versions. +> 4. **[[Roles & Personas]]** — who uses what. +> 5. **[[Glossary]]** — definitions of every domain term you will encounter. +> +> After the overview, jump to **[[01 - Architecture]]** for service boundaries, **[[02 - Data Models]]** for schemas, or **[[04 - Flows]]** for end-to-end user journeys. + +## Project status at a glance + +Amn is at version **2.6.x** across both repositories, on the `development` branch, and tagged "production-ready with minor enhancements" by the project leads. The core escrow loop, real-time chat, multi-language UI, dispute system, points programme, and blog are all live. Active work focuses on UX polish, admin analytics, and a more granular permissions matrix — see `backend/TODO.md` and `frontend/VERSION_0_PREPARATION_TODO.md` for the rolling task list, and [[Roadmap]] (forthcoming) for the strategic view. diff --git a/00 - Overview/Roles & Personas.md b/00 - Overview/Roles & Personas.md new file mode 100644 index 0000000..134c293 --- /dev/null +++ b/00 - Overview/Roles & Personas.md @@ -0,0 +1,237 @@ +--- +title: Roles & Personas +tags: [overview, roles, personas, permissions, rbac] +created: 2026-05-23 +--- + +# Roles & Personas + +> [!info] Where roles live in code +> The hard role enum is defined in `backend/src/models/User.ts:94` as `"admin" | "buyer" | "seller"`. Support is implemented as an admin variant (a dedicated `support@amn.gg` user is created at bootstrap — see `backend/TODO.md`) rather than as its own enum value. Permission checks live in route middleware and in service guards. + +Amn has four user personas. Three are first-class roles in the data model; the fourth (Support) is a special-cased admin with reduced privileges. + +```mermaid +flowchart LR + Visitor["Anonymous visitor
(blog, public shops)"] + Buyer["Buyer
(User)"] + Seller["Seller
(Owner)"] + Support["Support
(admin variant)"] + Admin["Admin"] + + Visitor -->|signs up| Buyer + Buyer -->|requests seller mode
+ admin approval| Seller + Buyer & Seller -->|opens ticket| Support + Support -->|escalates| Admin +``` + +--- + +## Buyer (User) + +> [!example] Who they are +> Default new user. Anyone who signs up is a buyer until they request seller status. The persona name in the UI is **"User"** but the model role is `buyer`. + +### Primary workflows + +- **Browse and search** the public marketplace and request templates. +- **Create a [[Purchase Request]]** describing what they want — product type (physical / digital / service / consultation), budget, urgency, delivery info, attachments. See `backend/src/models/PurchaseRequest.ts`. +- **Review incoming [[Seller Offer]]s**, negotiate over chat, accept the best one. +- **Pay** via [[SHKeeper]] (custodial crypto invoice) or Web3 wallet ([[DePay]] + MetaMask through Wagmi). +- **Track the order** through `processing → delivery → delivered → confirming → completed` states. +- **Confirm receipt** (or let the SLA auto-confirm), leave a review, accrue points. +- **Open a [[Dispute]]** if delivery never lands, item is wrong, or quality is poor. +- **Refer friends** via their personal referral code; earn points per signup that converts. +- **Manage profile**: avatar, addresses, wallet address, passkeys, notification preferences, language. + +### Key permissions + +- Create, edit, cancel **own** purchase requests. +- Accept / reject offers on **own** requests. +- Initiate payments on **own** requests. +- Open disputes on **own** orders. +- Chat with sellers on **own** open requests and disputes. +- Read public sellers, public requests, public blog. +- **Cannot**: see other users' private data, moderate content, issue payouts, change another user's role. + +### Dashboard sections (frontend routes) + +The buyer dashboard lives under `/dashboard` (`frontend/src/app/dashboard/`). Notable areas: + +- `/dashboard` — overview (recent requests, recent offers received, points balance) +- `/dashboard/request` — list, detail, and create flow for purchase requests +- `/dashboard/request-template` — saved templates and the catalogue of admin templates +- `/dashboard/chat` — all conversations +- `/dashboard/payment` — payment history (in + out) and active invoices +- `/dashboard/disputes` — open and resolved disputes +- `/dashboard/points` — points, levels, referrals +- `/dashboard/account` — profile, security (passkeys), addresses, language +- `/dashboard/post` — read-only blog reader (also public) + +> [!tip] See also +> Full buyer playbook in [[User Guide]]. Auth specifics in [[Authentication Flow]]. + +--- + +## Seller (Owner) + +> [!example] Who they are +> A buyer who has been promoted to `role: "seller"`. The UI sometimes calls them **"Owner"** because they own a shop. Sellers retain all buyer capabilities — they can still post requests of their own. + +### Primary workflows + +- **Configure shop**: shop name, banner, description, response time SLA, accepted payment methods, payout wallet address. See `backend/src/models/ShopSettings.ts` and `frontend/src/sections/shop-settings/`. +- **Discover requests** through the seller feed (filtered by category and preferred-seller status). Receive live notifications when a relevant request is posted via the `sellers` / `seller-` Socket.IO rooms (`backend/src/app.ts:101-112`). +- **Submit offers** with price, currency (USDT default, USDC, USD, EUR, IRR supported), delivery time, optional attachments and notes. +- **Negotiate** in the per-request chat — bilateral with the buyer until an offer is accepted. +- **Fulfil** the order: ship physical goods (with optional tracking number), or upload/email digital deliverables. +- **Use the [[delivery code]]** for physical handoffs: a six-digit one-time code the buyer reads to the courier to confirm receipt. +- **Receive payout** automatically via SHKeeper to the configured wallet once the order is finalised (admin-triggered batch or per-order based on shop policy). +- **Manage [[Request Templates]]** scoped to their shop — publish "off-the-shelf" offerings buyers can purchase in one click. +- **Engage with reviews and disputes**: respond to reviews, contest disputes, provide evidence. + +### Key permissions + +- All buyer permissions. +- Create / edit / withdraw **own** seller offers. +- Read all public purchase requests + read-only access to requests where they have an active offer. +- Manage **own** shop settings, payout wallet, business hours. +- Publish [[Request Templates]] scoped to their shop. +- Mark orders as shipped, upload digital deliverables, request delivery codes. +- Respond in disputes they are a party to. +- **Cannot**: see another seller's analytics, edit another seller's shop, moderate the platform. + +### Dashboard sections + +Seller dashboard reuses the same `/dashboard` shell with extra modules: + +- `/dashboard/shops` — public-facing shop browser (also visible to buyers) +- `/dashboard/shop-settings` — edit own shop, payout wallet, working hours +- `/dashboard/request-template` — create / edit shop-scoped templates +- `/dashboard/payment` — receivables, payout history, pending releases +- `/dashboard/disputes` — disputes where the seller is the respondent + +> [!tip] See also +> [[Seller Guide]] walks through onboarding, first listing, and payout setup end-to-end. [[Payments Overview]] explains the escrow + payout state machine. + +--- + +## Admin + +> [!example] Who they are +> Platform operators with full read/write access. Admins are seeded at startup via `backend/src/infrastructure/database/init-admin.ts`. There is typically one root admin (`admin@amn.gg`) and additional admins promoted through database operations or a dedicated admin-creation endpoint. + +### Primary workflows + +- **Moderate users**: suspend / unsuspend accounts (`User.status: "active" | "suspended" | "deleted"`, see `backend/src/models/User.ts`), promote buyers to sellers, ban repeat offenders. +- **Moderate marketplace content**: categories (`Category` model), request templates (the canonical platform-wide ones), blog posts. +- **Resolve disputes**: get assigned to disputes, drive them to resolution, choose an outcome (`refund | replacement | compensation | warning_seller | ban_seller | no_action`). See `backend/src/services/dispute/DisputeService.ts`. +- **Operate payments**: trigger payouts, fetch on-chain transactions, manually confirm stuck payments (the manual transaction-hash flow described in `backend/TODO.md`), audit the SHKeeper webhook history (`services/payment/shkeeper/webhookStats.ts`). +- **Configure the platform**: levels (`LevelConfig`), points multipliers, blog seed content, default templates. +- **Run data cleanup**: `/api/admin/cleanup` exposes destructive maintenance utilities (`services/admin/`). +- **Author blog posts** via the TipTap rich-text editor. +- **Monitor health**: SHKeeper status (background health monitor in `app.ts:433`), Redis, MongoDB. + +### Key permissions + +- **All** read and write across the platform — no row-level scoping. +- Promote users, change roles, suspend accounts. +- Approve / reject seller applications. +- Resolve any dispute, override any payment state. +- Trigger payouts and refunds. +- Edit / delete any blog post, template, or category. +- Run admin cleanup endpoints (irreversible — use with care). +- Access the full Sentry stream and webhook logs. + +### Dashboard sections + +Admins see the buyer/seller surfaces plus dedicated admin modules (typically under `/dashboard` with admin-only guards, growing into a dedicated `/admin` area in later versions): + +- User management (search, suspend, role change) +- Dispute queue with assignment and resolution +- Payment console (manual confirmation, payout dispatch, webhook log) +- Category and template management +- Blog editor (publish / unpublish / featured) +- Platform analytics (TODO — see `backend/TODO.md`) +- Data cleanup utilities + +> [!warning] Destructive operations +> `/api/admin/cleanup/*` and direct role mutations are not undoable. Admin actions should be logged to an audit trail. Audit logging is on the roadmap (`backend/TODO.md` → Security Enhancements → Audit Logging). + +> [!tip] See also +> [[Admin Guide]] gives the step-by-step for moderation, dispute resolution, and payment operations. + +--- + +## Support + +> [!example] Who they are +> A dedicated support persona — the seeded `support@amn.gg` account — that exists to handle user tickets and triage disputes without granting full destructive access. Implemented as an `admin` role with a restricted permission profile applied in middleware (see `backend/TODO.md` → "Support user creation"). + +### Primary workflows + +- **Join existing chats** to assist a buyer or seller in real time. +- **Triage disputes**: read incoming disputes, attach notes, assign to the right admin, set priority. +- **Answer user queries** about payments and orders without performing the payment action itself. +- **Escalate** anything that requires destructive action (refunds, bans, manual payouts) to a full admin. + +### Key permissions + +- Read access to all user accounts (no PII edits). +- Read access to all disputes and chats. +- Comment / reply in disputes and chats (write access scoped to messages, not resolution). +- **Cannot**: change roles, issue payouts, suspend users, delete content, resolve a dispute, edit settings. + +### Dashboard sections + +Support sees a stripped-down admin view focused on the inbox: + +- Support chat queue +- Dispute triage list +- User lookup (read-only) +- Order lookup (read-only) + +--- + +## Cross-cutting concerns + +### Role transitions + +| From | To | How | Audit | +|---|---|---|---| +| Anonymous | Buyer | Self-service signup | `User` created | +| Buyer | Seller | Application → admin approval | `User.role` change | +| Buyer / Seller | Admin | Manual DB / boot-time seed | High-risk, manual | +| Admin | Support | Permission profile applied at middleware | Role stays `admin` | + +### Permission model + +Permissions are enforced at three layers: + +1. **Route middleware** (e.g. `requireAuth`, `requireRole('admin')`) — coarse gating on the HTTP layer. +2. **Service guards** — finer ownership checks (e.g. "you can only edit a request whose `buyerId` matches your `userId`"). +3. **Frontend route guards** — `frontend/src/auth/guard/` hides UI from users who would be rejected anyway, preventing dead-ends. + +### Identity sources + +- **Email + password** — primary, with bcrypt hashing. +- **Passkey (WebAuthn)** — multi-device, stored in `User.passkeys[]`. +- **Google OAuth** — server-verified, links by email. + +See [[Authentication Flow]] for the full sequence diagram. + +### Internationalisation per persona + +All four personas see the same six-language UI (en, fr, vi, cn, ar, fa) with the default fallback being Persian (`fa`) — see `frontend/src/locales/locales-config.ts:39`. Date formatting switches to the Jalali calendar in Persian. + +--- + +## See also + +- [[Introduction]] — project mission and target audience framing. +- [[System Overview]] — where each persona fits in the architecture. +- [[Authentication Flow]] — how identity becomes a role. +- [[Admin Guide]] — operational handbook for admins and support. +- [[User Guide]] — buyer journey end-to-end. +- [[Seller Guide]] — seller onboarding and shop ops. +- [[Glossary]] — definitions of every domain term referenced above. diff --git a/00 - Overview/System Overview.md b/00 - Overview/System Overview.md new file mode 100644 index 0000000..50161d1 --- /dev/null +++ b/00 - Overview/System Overview.md @@ -0,0 +1,216 @@ +--- +title: System Overview +tags: [overview, architecture, system-map, mermaid] +created: 2026-05-23 +--- + +# System Overview + +> [!info] Scope +> This document gives you a single-page map of Amn from browser to blockchain. It is intentionally broad — the goal is for a new developer to walk away knowing what each major box does and how the boxes talk to each other. For deep dives into any subsystem, follow the wikilinks into [[01 - Architecture]]. + +## The 10,000-foot view + +Amn is a **two-repo system**: + +- **Frontend** (`/Users/mojtabaheidari/code/frontend`) — a Next.js 16 App Router application that serves the marketplace UI, the admin dashboard, the public blog, and the user-facing Web3 wallet flow. +- **Backend** (`/Users/mojtabaheidari/code/backend`) — an Express 5 + TypeScript API server that owns all business logic, persists to MongoDB, caches in Redis, and brokers all external integrations. + +The two repos are deployable independently. They communicate over **HTTPS (REST)** for stateful actions and over **WebSocket (Socket.IO)** for live updates. The frontend never talks directly to MongoDB, Redis, SHKeeper, or OpenAI — every external interaction is mediated by the backend so that secrets stay on the server. + +## System map + +```mermaid +flowchart TB + subgraph Client["Client tier"] + Browser["Browser
(Chrome / Safari / mobile)"] + Wallet["Wallet extension
(MetaMask / WalletConnect)"] + end + + subgraph FE["Frontend tier — Next.js 16"] + SSR["Next.js SSR / RSC
App Router"] + ClientJS["Client JS
MUI v7 + React 19"] + Wagmi["Wagmi + Viem
Web3 client"] + SocketC["socket.io-client"] + I18n["i18next
6 locales + RTL"] + end + + subgraph BE["Backend tier — Node.js / Express 5"] + REST["REST API
/api/*"] + SocketS["Socket.IO server
rooms per user / chat / request"] + Auth["Auth service
JWT + Passkey + Google"] + Market["Marketplace service
Requests, Offers, Templates"] + ChatSvc["Chat service"] + PaySvc["Payment service
+ PaymentCoordinator"] + Disp["Dispute service"] + Points["Points / Referrals"] + BlogSvc["Blog service"] + AISvc["AI service"] + Notif["Notification service"] + Files["File upload
(multer + sharp)"] + end + + subgraph Data["Data tier"] + Mongo[("MongoDB
via Mongoose")] + RedisDB[("Redis
cache + locks")] + Disk[("Local disk
/uploads")] + end + + subgraph External["External services"] + SHK["SHKeeper
crypto invoicing"] + DePay["DePay widget"] + Chain["EVM chains
BSC / ETH / Polygon"] + SMTP["SMTP
(nodemailer)"] + OpenAI["OpenAI API"] + Google["Google OAuth"] + Sentry["Sentry"] + Alchemy["Alchemy RPC"] + end + + Browser --> SSR + Browser <--> ClientJS + ClientJS <--> SocketC + ClientJS --> Wagmi + Wagmi <--> Wallet + Wallet <--> Chain + + SSR --> REST + ClientJS --> REST + SocketC <--> SocketS + + REST --> Auth & Market & ChatSvc & PaySvc & Disp & Points & BlogSvc & AISvc & Notif & Files + SocketS --> ChatSvc & Notif & Market + + Auth & Market & ChatSvc & PaySvc & Disp & Points & BlogSvc --> Mongo + Auth & PaySvc & Notif --> RedisDB + Files --> Disk + + PaySvc <--> SHK + SHK -.webhook.-> PaySvc + PaySvc --> Chain + Wagmi --> DePay + DePay --> Chain + PaySvc -.tx fetch.-> Alchemy + + Notif --> SMTP + Auth --> Google + AISvc --> OpenAI + BE --> Sentry + FE --> Sentry +``` + +## Walk-through of each subsystem + +### Authentication & identity — [[Authentication Flow]] + +Auth is the gate to every authenticated route. Amn supports three login methods in parallel: + +- **Email + password (JWT).** Standard `bcrypt`-hashed credentials, access + refresh token pair, six-digit email verification codes, and password reset codes. Source: `backend/src/services/auth/authService.ts`. +- **Passkey (WebAuthn).** Platform and cross-platform authenticators are registered against the user account; multiple devices per user are stored in `User.passkeys[]` (`backend/src/models/User.ts:125`). +- **Google OAuth.** Server-side verification via `google-auth-library`. See `backend/src/services/auth/googleOAuthService.ts`. + +Roles are `admin | buyer | seller` (`backend/src/models/User.ts:94`). Role checks happen in route middleware. **Refresh tokens** are stored on the `User` document and rotated. + +### Marketplace — [[Marketplace Domain]] + +The heart of the platform. Three first-class models drive it: + +- **PurchaseRequest** (`backend/src/models/PurchaseRequest.ts`) — the buyer's brief, with productType (`physical_product | digital_product | service | consultation`), budget range, urgency, delivery info (physical or online), and a long status enum that walks the entire deal: `pending_payment → pending → active → received_offers → in_negotiation → payment → processing → delivery → delivered → confirming → completed`. +- **SellerOffer** (`backend/src/models/SellerOffer.ts`) — a single bid attached to a request with `price`, `deliveryTime`, `status`, `validUntil`, and free-form `notes`. +- **RequestTemplate** (`backend/src/models/RequestTemplate.ts`) — a reusable "express checkout" version of a purchase request that can spawn real `PurchaseRequest` instances at click time. + +Services live in `backend/src/services/marketplace/` and are exposed through `/api/marketplace/*`. The frontend uses a mix of React Query (`@tanstack/react-query`) and SWR for data fetching, with mutations gated through the actions layer in `frontend/src/actions/`. + +### Payments — [[Payments Overview]] / [[SHKeeper Integration]] + +Payments are where Amn is most distinctive. The backend supports **three payment surfaces** routed through a common `Payment` model (`backend/src/models/Payment.ts`): + +- **SHKeeper** — `/api/payment/shkeeper`. Mounted at `backend/src/app.ts:327`. Issues a fresh wallet address per invoice, polls / webhooks for payment confirmation, and runs through `PaymentCoordinator` to avoid race conditions where a payment status is updated twice. Health is monitored in the background (`shkeeperHealthCheck.ts`, started in `app.ts:433`). +- **Decentralized (Wagmi + DePay)** — `/api/payment/decentralized`. The user signs and sends the transfer from their own wallet; the backend then verifies the transaction on-chain via `blockchainTxFetcher.ts` and the Alchemy SDK. +- **Payout** — `/api/payment/shkeeper/payout`. Admin-triggered release of escrow funds to the seller's wallet once delivery is confirmed. + +All three surfaces converge on the same `Payment` record (with `direction: 'in' | 'out' | 'refund'`) and trigger the same downstream events: order status update, notification, points award. **Pending payments are auto-cleaned** by a background timer started in `app.ts:374`. + +### Real-time chat — [[Chat System]] + +Chat is built on Socket.IO rooms. Every entity that needs live updates gets its own room (see `backend/src/app.ts:79-178`): + +- `user-` — personal notifications +- `chat-` — chat room messages, typing indicators, presence +- `request-` — purchase request lifecycle events +- `buyer-` / `seller-` — marketplace-wide updates +- `sellers` / `buyers` — global broadcast pools + +Messages persist to MongoDB through the `Chat` model and are rate-limited per chat (`chatRateLimiter.ts`). The frontend's `socket/` directory wraps `socket.io-client` and exposes typed event hooks to React components. + +### Notifications — [[Notifications]] + +Two notification channels: + +- **In-app** — `Notification` documents pushed over Socket.IO to `user-` rooms; rendered in the frontend's bell-icon drawer. +- **Email** — `nodemailer` + SMTP for verification codes, password resets, and high-importance events. See `backend/src/services/email/`. + +Push and SMS are tracked as **planned** in `backend/TODO.md`. + +### Disputes — [[Dispute System]] + +When a deal goes wrong (see [[Glossary#Dispute]]), either party can open a dispute. The backend (`backend/src/services/dispute/DisputeService.ts`) creates a **three-way chat** between buyer, seller, and admin, opens a `Dispute` document with a structured `timeline[]` and `evidence[]`, and assigns the dispute to an admin via `assignAdmin()`. Resolution can be `refund | replacement | compensation | warning_seller | ban_seller | no_action` and is recorded on the dispute itself. + +### Points & referrals — [[Points System]] + +Each user has an embedded `points` object (`total | available | used | level`) and `referralStats` (`backend/src/models/User.ts`). Every grant or spend writes a `PointTransaction` record for auditability. The points module supplies referral codes (the `/r/:code` short URL in `app.ts:274` redirects to signup), tracks level progression against `LevelConfig`, and exposes the user-facing dashboard through `frontend/src/sections/points/`. + +### Blog — [[Blog System]] + +A simple admin-authored CMS. `BlogPost` documents support categories, tags, and embedded video via TipTap's rich-text editor on the frontend. Public reads are unauthenticated (`/api/blog/posts`); writes require admin role. Seed data lives in `backend/src/seeds/seedBlogPosts.ts` and runs on dev startup. + +### AI — [[AI Assist]] + +OpenAI (model configurable per call) is exposed through `/api/ai/*`. The current surface includes purchase-request drafting, chat summarisation, and admin-facing intent classification. Requests are queued from the frontend's `actions/ai*` modules and use streaming where appropriate. + +### File uploads + +`multer` accepts multipart uploads (max 10 MB body, `app.ts:232`), `sharp` resizes images on the fly, and files land in `/uploads` (mounted as a static directory at `app.ts:260`). In production the path is configurable via `UPLOAD_PATH`. + +### Caching, locks & background jobs + +**Redis** (`backend/src/services/redis/`) is used for: +- short-lived caches (sessions, marketplace listings) +- locks used by `PaymentCoordinator` to serialise status transitions +- rate-limit counters (currently disabled in code but plumbed in) + +**Background workers** run inside the Express process for now — no separate worker tier. Notable timers: +- `startPendingPaymentsCleanup()` — sweeps stale unpaid invoices +- `startShkeeperHealthMonitor()` — pings the SHKeeper instance and surfaces alerts +- Auto-seed logic on startup (gated by `NODE_ENV` and `AUTO_SEED_ON_START`) + +## Request lifecycle (the happy path) + +> [!example] End-to-end deal walk-through +> 1. Buyer signs in (JWT). UI joins `user-` over Socket.IO. +> 2. Buyer creates a [[Purchase Request]] → `POST /api/marketplace/requests`. The request lands in `pending`/`active`. Sellers in the matching category receive a Socket.IO notification. +> 3. Seller views the request, opens [[Seller Offer]] modal, submits price + delivery time → `POST /api/marketplace/offers`. Buyer sees the offer arrive live. +> 4. Buyer accepts an offer → request moves to `payment`. UI opens the payment selector. +> 5. Buyer picks **SHKeeper** → backend creates a SHKeeper invoice, returns a wallet address + QR code. Buyer pays. SHKeeper webhook hits `/api/payment/shkeeper/webhook`; `PaymentCoordinator` flips `Payment.status = paid` and `PurchaseRequest.status = processing`. +> 6. Seller ships. Buyer confirms delivery (or it auto-confirms after the SLA window). Admin triggers (or schedules) a **payout** → SHKeeper releases USDT to the seller's wallet. +> 7. Both parties leave reviews. Points are awarded. The deal is closed. +> +> If the buyer disputes the delivery, jump to step 7 of the [[Dispute Flow]] instead. + +## Cross-cutting concerns + +- **Observability** — Sentry is initialised at the very top of `app.ts` (line 2-3) and on the frontend in `sentry.{client,edge,server}.config.ts`. Logs flow through `backend/src/utils/logger.ts`. +- **Security** — `helmet`, CORS scoped to `FRONTEND_URL`, JWT with bcrypt-hashed passwords, role-gated middleware. Rate limiting is plumbed but currently disabled (see `app.ts:227`). +- **Internationalisation** — Six locales (en, fr, vi, cn, ar, fa), with `fa` as default. RTL via `stylis-plugin-rtl`. See `frontend/src/locales/`. +- **Theming** — MUI v7 with a custom theme in `frontend/src/theme/`. Dark mode is on the roadmap. +- **Containerisation** — Docker Compose stacks for dev and prod live in both repos (`docker-compose.dev.yml`, `docker-compose.production.yml`). + +## Where to go next + +- [[Tech Stack]] — exact versions of every dependency. +- [[Roles & Personas]] — who does what in the system. +- [[Glossary]] — a domain dictionary you will want open in another pane. +- [[01 - Architecture]] — service boundaries, module layout, and deployment topology. +- [[02 - Data Models]] — MongoDB collections and field-by-field schemas. +- [[03 - API Reference]] — every endpoint, its payload, and its auth requirements. +- [[04 - Flows]] — diagrammed user journeys for every major use case. diff --git a/00 - Overview/Tech Stack.md b/00 - Overview/Tech Stack.md new file mode 100644 index 0000000..1b040e9 --- /dev/null +++ b/00 - Overview/Tech Stack.md @@ -0,0 +1,233 @@ +--- +title: Tech Stack +tags: [overview, tech-stack, dependencies, versions] +created: 2026-05-23 +--- + +# Tech Stack + +> [!info] Versions +> Versions below are pulled directly from `frontend/package.json` and `backend/package.json` on the `development` branch. Where a `^` range is declared in package.json, the **declared minimum** is shown — the lockfile may have resolved a newer patch. When in doubt, check `yarn.lock` in each repo. + +## Frontend stack + +The frontend is a Next.js 16 App Router application written in TypeScript. The build is deliberately heavy on best-in-class libraries rather than home-grown solutions: MUI for components, Wagmi for Web3, React Query / SWR for data, Zod for validation, Sentry for errors. The package is `amn-frontend@2.6.5-beta` and requires Node `>=20`. + +### Core framework & language + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| Next.js | ^16.1.1 | App Router framework, SSR, RSC, image opt, file-based routing | `frontend/src/app/**` | +| React | ^19.1.0 | UI runtime (concurrent, transitions) | Everywhere | +| react-dom | ^19.1.0 | DOM renderer | Entry / layout | +| TypeScript | ^5.8.3 | Strict typing across the codebase | `tsconfig.json` | +| Node.js | >=20 | Runtime for `next dev`, `next build`, `next start` | `package.json` `engines` | + +### UI, theming & icons + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| @mui/material | ^7.1.0 | Component library (buttons, inputs, dialogs) | `frontend/src/components/**` | +| @mui/lab | ^7.0.0-beta.12 | Pre-release MUI components (Timeline, Masonry) | Dispute timeline, layouts | +| @mui/material-nextjs | ^7.1.0 | App Router cache + SSR integration | `frontend/src/app/layout.tsx` | +| @mui/x-data-grid | ^8.4.0 | Admin grids with sorting / filtering | Admin dashboard tables | +| @mui/x-date-pickers | ^8.4.0 | Date + time inputs | Request forms, delivery | +| @mui/x-tree-view | ^8.4.0 | Category and admin trees | Category management | +| @emotion/react | ^11.14.0 | CSS-in-JS engine for MUI | Theme + sx prop | +| @emotion/styled | ^11.14.0 | Styled-component sugar | Custom MUI wrappers | +| @emotion/cache | ^11.14.0 | RTL-aware emotion cache | RTL plugin chain | +| stylis | ^4.3.6 | Emotion's CSS preprocessor | Style pipeline | +| stylis-plugin-rtl | ^2.1.1 | Right-to-left CSS transformation | Persian/Arabic layouts | +| @iconify/react | ^6.0.0 | Iconify icon component (100k+ icons) | Icons everywhere | +| framer-motion | ^12.13.0 | Animations and transitions | Modals, page transitions | +| @fontsource-variable/{inter,dm-sans,nunito-sans,public-sans} | ^5.2.5 | Self-hosted variable fonts | Theme typography | +| @fontsource/barlow | ^5.2.5 | Display font for headlines | Marketing pages | + +### Data, state & forms + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| @tanstack/react-query | ^5.83.0 | Server state, mutations, caching | API hooks under `actions/` | +| swr | ^2.3.3 | Lightweight data fetching | Lists, polling endpoints | +| react-hook-form | ^7.56.4 | Form state and validation glue | Every form | +| @hookform/resolvers | ^5.0.1 | Bridges RHF with Zod schemas | Form schemas | +| zod | ^4.0.10 | Runtime + compile-time schema validation | `actions/**/schema.ts` | +| es-toolkit | ^1.38.0 | Modern Lodash alternative | Utility helpers | +| dayjs | ^1.11.13 | Date manipulation | Formatting throughout | +| date-fns-jalali | ^4.1.0-0 | Jalali (Persian) date formatting | Persian locale | +| axios | ^1.11.0 | HTTP client | Backend calls in `actions/` | + +### Web3 & crypto + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| wagmi | ^2.15.6 | React hooks for EVM (connect, signer, tx) | `frontend/src/web3/` | +| viem | ^2.31.7 | Low-level EVM client | RPC calls, encoding | +| ethers | ^6.15.0 | Alternative EVM client (legacy paths) | Tx signing helpers | +| @depay/widgets | ^13.0.36 | DePay drop-in payment widget | Wallet checkout | +| alchemy-sdk | ^3.6.1 | Enriched RPC + transaction history | Tx verification | + +### Realtime, i18n, rich text + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| socket.io-client | ^4.8.1 | WebSocket transport | Chat, notifications | +| i18next | ^25.2.1 | i18n engine | All translations | +| react-i18next | ^15.5.2 | React bindings for i18next | `useTranslation` hooks | +| i18next-browser-languagedetector | ^8.1.0 | Browser locale detection | Initial language pick | +| i18next-resources-to-backend | ^1.2.1 | Lazy locale loading | `frontend/src/locales/` | +| accept-language | ^3.0.20 | Server-side language negotiation | SSR locale detection | +| @tiptap/{core,react,starter-kit,...} | ^2.12.0 | Rich-text editor (blog, descriptions) | Blog editor | +| lowlight | ^3.3.0 | Code syntax highlighting in TipTap | Blog code blocks | +| react-markdown | 10.1.0 | Markdown rendering | Blog views, chat | +| remark-gfm / rehype-raw / rehype-highlight | latest | Markdown extensions | Blog pipeline | +| turndown | 7.2.0 | HTML → Markdown converter | Blog migration | + +### Maps, media, misc UI + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| mapbox-gl | ^3.12.0 | Map rendering | Address picker | +| react-map-gl | ^8.0.4 | React wrapper for Mapbox | Address picker | +| react-apexcharts | ^1.7.0 | Charts and graphs | Admin analytics | +| react-dropzone | ^14.3.8 | Drag-and-drop file upload | Attachments | +| react-phone-number-input | ^3.4.12 | International phone input | Profile, address | +| mui-one-time-password-input | ^5.0.0 | OTP input cells | Email verification | +| embla-carousel + autoplay/auto-scroll | 8.6.0 | Carousels | Marketing, blog hero | +| notistack | ^3.0.2 | Snackbar / toast queue | App-wide notifications | +| sonner | ^2.0.3 | Modern toast library | Newer surfaces | +| nprogress | ^0.2.0 | Top-of-page progress bar | Page transitions | +| qrcode | ^1.5.4 | QR code generation | Wallet QR for invoices | +| simplebar-react | ^3.3.0 | Custom scrollbars | Sidebars, drawers | +| yet-another-react-lightbox | ^3.23.2 | Image lightbox | Gallery viewers | +| autosuggest-highlight | ^3.3.4 | Search match highlighting | Combobox results | +| minimal-shared | ^1.0.11 | Shared theme helpers (from Minimal template) | Theme tokens | + +### Observability & tooling + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| @sentry/nextjs | ^10.22.0 | Error + perf monitoring | `sentry.*.config.ts` | +| @playwright/test | ^1.56.1 | E2E test runner | `e2e/` | +| jest | ^29.7.0 | Unit test runner | `__tests__/` | +| @testing-library/react | ^16.3.0 | React component testing | Component tests | +| eslint + perfectionist/import/react plugins | ^9.27.0 | Linting | `eslint.config.mjs` | +| prettier | ^3.5.3 | Formatting | `prettier.config.mjs` | +| @svgr/webpack | ^8.1.0 | SVG → React component loader | Build pipeline | + +## Backend stack + +The backend is `amn-backend@2.6.3-beta`, an Express 5 server in TypeScript backed by MongoDB (Mongoose), Redis, and Socket.IO. It owns all integrations with SHKeeper, the EVM chains, OpenAI, Google OAuth, and SMTP. + +### Core runtime & framework + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| Node.js | (per Dockerfile) | Runtime | `Dockerfile.prod` | +| TypeScript | ^5.8.3 | Strict typing | `tsconfig.json` | +| express | ^5.2.1 | HTTP framework | `backend/src/app.ts` | +| body-parser | ^2.2.0 | Body parsing (legacy fallback) | Body middleware | +| helmet | ^8.1.0 | HTTP security headers | `app.ts:189` | +| cors | ^2.8.5 | Cross-origin policy | `app.ts:194` | +| express-rate-limit | ^8.0.1 | Rate-limit middleware (currently off) | Plumbed in | +| express-validator | ^7.2.1 | Request validation | Auth, marketplace | +| multer | ^2.0.2 | Multipart file uploads | `services/file/` | +| sharp | ^0.34.3 | Image resizing / format conversion | Upload pipeline | +| dotenv | ^17.2.0 | Env var loader | Bootstrap | +| uuid | ^11.1.0 | ID generation | Tokens, ephemeral IDs | +| axios | ^1.11.0 | Outbound HTTP (SHKeeper, blockchain) | Integration calls | +| @babel/runtime | ^7.27.6 | Babel runtime helpers | Compiled output | + +> [!warning] React in backend dependencies +> `react` and `react-dom` are listed in `backend/package.json:86-87`. These are vestigial — they slipped in via a tool that shared types and are not used at runtime. Safe to remove during cleanup; see `backend/TODO.md`. + +### Database & cache + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| mongoose | ^8.16.4 | MongoDB ODM | `backend/src/models/**` | +| redis | ^5.6.0 | Cache, locks, rate-limit store | `services/redis/`, `app.ts:362` | +| mongodb-memory-server | ^10.2.0 (dev) | In-memory Mongo for tests | `__tests__/` | + +### Auth, crypto & validation + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| jsonwebtoken | ^9.0.2 | JWT issue + verify | `services/auth/` | +| bcrypt | ^6.0.0 | Password hashing (native) | Auth service | +| bcryptjs | ^3.0.2 | Pure-JS fallback for bcrypt | Auth fallback | +| google-auth-library | ^10.3.0 | Google OAuth token verification | `googleOAuthService.ts` | +| crypto | ^1.0.1 | Node `crypto` polyfill (legacy package) | Webhook signing | + +### Realtime, AI, email + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| socket.io | ^4.8.1 | WebSocket server with rooms | `app.ts:70-179` | +| openai | ^5.10.1 | OpenAI SDK | `services/ai/` | +| nodemailer | ^7.0.5 | SMTP email | `services/email/` | +| @types/nodemailer | ^6.4.17 | Types | dev | + +### Blockchain + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| ethers | ^6.15.0 | EVM client | `services/blockchain/` | +| web3 | ^4.16.0 | Alternative EVM client | Legacy paths | + +### Observability + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| @sentry/node | ^10.22.0 | Error + perf monitoring | `config/sentry.ts` | +| @sentry/profiling-node | ^10.22.0 | CPU profiling | Sentry init | + +### Build & test + +| Tool | Version | Purpose | Where used | +|---|---|---|---| +| ts-node | ^10.9.2 | TS execution in dev / seeds | `npm run dev` | +| nodemon | ^3.1.10 | Auto-restart on change | `dev:start` script | +| jest | ^29.7.0 | Unit / integration tests | `__tests__/` | +| ts-jest | ^29.4.0 | TS transform for Jest | jest config | +| supertest | ^7.1.4 | HTTP-level API tests | `__tests__/` | +| eslint | ^9.31.0 | Linting | `eslint.config.js` | +| prettier | ^3.6.2 | Formatting | scripts | + +## Infrastructure + +| Component | Tool / version | Purpose | Notes | +|---|---|---|---| +| Container engine | Docker + Docker Compose | Dev & prod deployment | `docker-compose.dev.yml`, `docker-compose.production.yml` in each repo | +| Reverse proxy | Nginx (external) | TLS termination, routing | `TRUST_PROXY=true` recognised in `app.ts:64` | +| Database | MongoDB | Primary store | Connection string via env | +| Cache | Redis | Sessions, locks, ephemeral data | Optional — backend boots without it | +| Object storage | Local disk `/uploads` | User uploads | `UPLOAD_PATH` env override | +| Process manager | Docker `restart: unless-stopped` (typical) | Crash recovery | Per compose file | +| CI/CD | Manual + `scripts/auto-version.sh` | Semver bumps + tags | `npm run release:*` | + +## External services + +| Service | Purpose | Touchpoint in code | +|---|---|---| +| **SHKeeper** | Self-hosted crypto payment processor — issues wallets, watches for incoming USDT, pays out | `backend/src/services/payment/shkeeper/` | +| **DePay** | Drop-in Web3 widget for wallet-to-wallet payment | `@depay/widgets` on frontend | +| **EVM chains** (BSC, Ethereum mainnet, Sepolia, Polygon) | Settlement layer for stablecoin transfers | `frontend/src/web3/config.ts`, backend `blockchain/` | +| **Alchemy RPC** | Hosted EVM RPC + transaction lookup | Frontend `alchemy-sdk`, backend `blockchainTxFetcher.ts` | +| **MetaMask / WalletConnect** | Wallet connectors via Wagmi | `web3/config.ts` (WalletConnect commented out pending SSR fix) | +| **OpenAI** | LLM for drafting / summarising | `backend/src/services/ai/` | +| **Google OAuth** | Federated login | `googleOAuthService.ts` | +| **SMTP** (provider configured per env) | Transactional email | `services/email/` | +| **Sentry** | Error + performance monitoring | Both repos | +| **BSCScan / explorers** | Link-out for tx visualisation | Payment UI | +| **Mapbox** | Maps for address selection | `frontend/src/sections/address/` | + +> [!tip] How to upgrade +> Always upgrade frontend and backend together when they share a wire protocol — chat events, Socket.IO room schemas, and the `Payment` model are the three areas most sensitive to drift. After bumping a major version, run both `yarn typecheck` and the full test suites (`yarn test` in backend, `yarn test` and `yarn test:e2e` in frontend) before tagging a release. + +## See also + +- [[System Overview]] — how all these pieces talk to each other. +- [[Roles & Personas]] — who consumes each part of the stack. +- [[01 - Architecture/Deployment Topology]] — production layout. +- [[07 - Development/Local Setup]] — getting the stack running on your machine. diff --git a/01 - Architecture/Backend Architecture.md b/01 - Architecture/Backend Architecture.md new file mode 100644 index 0000000..62d9941 --- /dev/null +++ b/01 - Architecture/Backend Architecture.md @@ -0,0 +1,311 @@ +--- +title: Backend Architecture +tags: [architecture, backend] +created: 2026-05-23 +--- + +# Backend Architecture + +Module-level architecture of the Express 5 + TypeScript + Mongoose backend at `/Users/mojtabaheidari/code/backend` (development branch). + +> [!info] +> Repo: `git@git.manko.yoga:222/nick/backend.git` · Branch: `development` · Version: 2.6.3-beta (`package.json:4`) + +--- + +## 1. Folder tree + +``` +backend/src/ +├── app.ts # Express bootstrap, middleware chain, route registration +├── config/ # Per-feature config (legacy — most moved to shared/config) +├── controllers/ # HTTP request handlers (slim — delegate to services) +├── infrastructure/ +│ ├── database/ # Mongoose connection, retries, graceful shutdown +│ └── socket/socketService.ts # Socket.IO server, rooms, emit helpers +├── models/ # Mongoose models — see 02 - Data Models/ +├── routes/ # Express Router definitions (mounted in app.ts) +├── scripts/ # CLI utilities (seed:users, seed:categories, ...) +├── seeds/ # Seed data fixtures +├── services/ +│ ├── ai/ # OpenAI integration (descriptions, moderation) +│ ├── auth/ # JWT, OAuth, Passkey, password reset +│ ├── blockchain/ # Web3 read/verify helpers +│ ├── blog/ # Posts, categories, comments +│ ├── chat/ # Conversations, messages, attachments +│ ├── dispute/ # Dispute lifecycle, evidence, mediator +│ ├── file/ # Multer uploads, MIME validation +│ ├── marketplace/ # PurchaseRequest, SellerOffer, Template, Shop +│ ├── notification/ # Templates, delivery, mark-as-read +│ ├── payment/ # Payment orchestration + shkeeper/ subdir +│ │ └── shkeeper/ # SHKeeper API, webhook, payout +│ ├── points/ # Loyalty points, levels, redemption +│ ├── redis/ # Redis client, cache helpers +│ ├── user/ # Profile, preferences, addresses +│ ├── admin/ # Admin-only operations +│ └── email/ # Nodemailer transport + templates +├── shared/ +│ ├── config/index.ts # Centralised env-var loader (typed) +│ ├── middleware/ # auth, errorHandler, validators +│ ├── types/ # Cross-cutting TypeScript types +│ └── utils/response-handler.ts # Standard success/error response envelope +└── utils/ # Pure utility fns (logger, currencyUtils, etc.) +``` + +> [!tip] +> Service folders are self-contained: each typically has `Service.ts`, `Controller.ts`, `Routes.ts`, `Validation.ts`. This makes each service movable to a microservice later with minimal coupling. + +--- + +## 2. Bootstrap — `src/app.ts` + +The bootstrap is intentionally linear and easy to audit. Execution order: + +1. **Imports & env load** — `dotenv` (if used), then `import { config } from './shared/config'`. +2. **Express app construction** — `const app = express();` +3. **Trust proxy** — `app.set('trust proxy', config.trustProxy)` so X-Forwarded-For works behind Nginx. +4. **Security headers** — `app.use(helmet({ ... }))`. +5. **CORS** — `cors({ origin: config.frontendUrl, credentials: true, methods: [...] })`. +6. **Body parsers** — `express.json({ limit: '10mb' })`, `express.urlencoded({ extended: true })`. +7. **Static uploads** — `app.use('/uploads', express.static(uploadDir))`. +8. **Health endpoint** — `GET /health` for Docker healthcheck and external monitors. +9. **Route mounting** — every `/api/*` route registered before the error handler. +10. **404 handler** — catches unmatched `/api/*`. +11. **Error handler** — central `errorHandler` middleware formats responses via `response-handler.ts`. +12. **HTTP server creation** — `const server = http.createServer(app)`. +13. **Socket.IO attach** — `initSocket(server, corsOptions)` (see [[Real-time Layer]]). +14. **DB connect** — `await connectDatabase()`. +15. **Redis connect** — `await connectRedis()`. +16. **Listen** — `server.listen(config.port, ...)`. +17. **Graceful shutdown** — SIGTERM/SIGINT handlers close server, drain sockets, close Mongoose, close Redis. +18. **Optional dev seeding** — when `NODE_ENV === 'development'` and `SEED_USERS !== 'false'`, the bootstrap calls the seed scripts to provision default test users. + +--- + +## 3. Middleware chain + +| Order | Middleware | Where | Purpose | +|---|---|---|---| +| 1 | `helmet` | global | Sets security headers (CSP, X-Frame-Options, ...). | +| 2 | `cors` | global | Origin allow-list = `config.frontendUrl`, credentials enabled. | +| 3 | `express.json` / `express.urlencoded` | global | Body parsers (10MB limit). | +| 4 | `morgan` (dev only) | global | HTTP request log to stdout. | +| 5 | `requestId` | global | Adds `X-Request-Id` for log correlation. | +| 6 | `authMiddleware` | per-route | Verifies JWT, attaches `req.user`. Mounted only on protected routes. | +| 7 | `roleGuard('admin'|'seller'|...)` | per-route | RBAC check after auth. | +| 8 | `validate(schema)` | per-route | express-validator + zod inputs. | +| 9 | `controllerFn` | per-route | Delegates to service layer. | +| 10 | `notFound` | tail | Returns 404 envelope for unmatched routes. | +| 11 | `errorHandler` | tail | Catches thrown errors, formats response. | + +> [!warning] +> Rate-limit middleware is **disabled by default for personal use** (see `app.ts:227` cited in the architecture review). Enable before any real public traffic — `express-rate-limit` is already a dependency. + +--- + +## 4. Route registration + +The full route table mounted by `app.ts`: + +| Mount path | Module | Auth | Notes | +|---|---|---|---| +| `/api/auth` | `services/auth/authRoutes.ts` | mixed | login, register, refresh, OAuth, passkey | +| `/api/user` | `services/user/userRoutes.ts` | JWT | profile, preferences | +| `/api/address` | `services/user/addressRoutes.ts` | JWT | CRUD addresses | +| `/api/marketplace/requests` | `services/marketplace/controllerRoutes.ts` | JWT | PurchaseRequest CRUD | +| `/api/marketplace/offers` | `services/marketplace/controllerRoutes.ts` | JWT (seller) | SellerOffer CRUD | +| `/api/marketplace/templates` | `services/marketplace/controllerRoutes.ts` | JWT (seller) | RequestTemplate CRUD | +| `/api/marketplace/categories` | `services/marketplace/controllerRoutes.ts` | public read | Category list | +| `/api/marketplace/shop-settings` | `services/marketplace/shopSettingsController.ts` | JWT (seller) | Shop profile | +| `/api/payment` | `services/payment/paymentRoutes.ts` | JWT | Payment intent, status | +| `/api/payment/shkeeper/webhook` | `services/payment/shkeeper/shkeeperWebhook.ts` | HMAC | Inbound from gateway | +| `/api/payment/payout` | `services/payment/shkeeper/shkeeperPayoutService.ts` | JWT (seller/admin) | Withdraw to wallet | +| `/api/chat` | `services/chat/chatRoutes.ts` | JWT | Conversations, messages | +| `/api/notification` | `services/notification/notificationRoutes.ts` | JWT | List, mark read | +| `/api/dispute` | `services/dispute/disputeRoutes.ts` | JWT | Open, evidence, resolve | +| `/api/blog` | `services/blog/blogRoutes.ts` | mixed | Public read, admin write | +| `/api/admin` | `services/admin/adminRoutes.ts` | JWT (admin) | Mod operations | +| `/api/points` | `services/points/pointsRoutes.ts` | JWT | Balance, redemption | +| `/api/ai` | `services/ai/aiRoutes.ts` | JWT | OpenAI-backed helpers | +| `/api/file` | `services/file/fileRoutes.ts` | JWT | Multipart upload | + +Full per-endpoint details → [[03 - API Reference/API Overview]] and the service-specific reference docs. + +--- + +## 5. Service layer pattern + +Every service module follows this contract: + +```ts +// services//Service.ts +export class FeatureService { + static async createX(input, ctx): Promise { /* business logic */ } + static async getX(id, ctx): Promise { /* ... */ } + static async listX(filter, ctx): Promise { /* ... */ } + static async updateX(id, patch, ctx): Promise { /* ... */ } +} +``` + +- Controllers are **thin** — they validate request shape, call the service, format the response. +- Services own **business logic**, side effects (DB writes, socket emits, email sends). +- Models are **pure schema** — only Mongoose definitions + virtuals/hooks. + +Cross-service calls are direct imports — no event bus yet. When the system grows, the seam between services is a natural place to introduce a message queue. + +--- + +## 6. Dependency map (simplified) + +```mermaid +flowchart TB + auth[auth] + user[user] + market[marketplace] + pay[payment] + chat[chat] + notify[notification] + dispute[dispute] + points[points] + file[file] + email[email] + socket[socket] + + auth --> user + auth --> notify + market --> notify + market --> chat + market --> file + pay --> market + pay --> notify + pay --> socket + dispute --> market + dispute --> chat + dispute --> notify + points --> notify + notify --> socket + notify --> email +``` + +> [!note] +> `socket` and `email` are leaf services — every notification path funnels through them. Mocking these two in tests covers most side-effect verification. + +--- + +## 7. Error handling + +All thrown errors are caught by the central error handler. The expected shape: + +```ts +class AppError extends Error { + statusCode: number; // HTTP 4xx/5xx + code: string; // app-specific code, e.g. "PAYMENT_ALREADY_REFUNDED" + details?: unknown; // optional debug payload +} +``` + +Response envelope (success path is `{success:true,data:...}`): + +```json +{ + "success": false, + "error": { + "code": "VALIDATION_FAILED", + "message": "email is required", + "details": [{ "path": "email", "msg": "required" }] + } +} +``` + +See `backend/src/shared/utils/response-handler.ts` and `backend/src/shared/middleware/errorHandler.ts`. + +--- + +## 8. Configuration + +Single source of truth for env vars: `src/shared/config/index.ts`. It exports a typed `config` object — anywhere you would write `process.env.X`, instead import `config.x`. + +Full table in [[Environment Variables]]. Critical ones: + +| Key | Default | Notes | +|---|---|---| +| `PORT` | `5001` | Listen port | +| `MONGODB_URI` | `mongodb://localhost:27017/nickapp` | Includes db name | +| `REDIS_URI` | `redis://localhost:6379` | + `REDIS_PASSWORD` | +| `JWT_SECRET` | required | ≥32 chars | +| `JWT_EXPIRES_IN` | `7d` | | +| `REFRESH_TOKEN_EXPIRES_IN` | `30d` | | +| `FRONTEND_URL` | `http://localhost:3000` | CORS origin | +| `SHKEEPER_API_URL` | `https://pay.amn.gg` | | +| `SHKEEPER_API_KEY` | required | | +| `SHKEEPER_WEBHOOK_SECRET` | required | HMAC key | +| `SMTP_*` | required | Nodemailer | +| `OPENAI_API_KEY` | required | | + +--- + +## 9. Database & connection management + +- **Mongoose** is the ODM. Connection in `src/infrastructure/database/`. +- Connection options enable retryable writes, exponential backoff on reconnect. +- Indexes are defined on each model and auto-created on connect (Mongoose `autoIndex: true` in dev, recommend `false` in prod with explicit migration). +- See [[Data Model Overview]] for the relational map and per-model docs. + +Redis client (in `src/services/redis/`) provides: +- Session caching (login attempts, lockout counters) +- Rate-limit counters (when middleware is enabled) +- Hot-path caches (category list, level configs) + +--- + +## 10. Background work + +The codebase has no dedicated queue runner — scheduled / async work is triggered inline from request handlers and uses `setTimeout` / `setInterval` patterns where needed (e.g., delayed retries). Consider introducing Bull / BullMQ if you grow: + +- Payment status reconciliation (polling SHKeeper for stragglers) +- Notification email digests +- Auto-release escrow timers +- Token / refresh-token cleanup + +--- + +## 11. Testing + +Jest test suites in `backend/__tests__/`: + +| File | Covers | +|---|---| +| `models.test.ts` | Schema validation, virtuals, hooks | +| `payment-services.test.ts` | Payment orchestration logic | +| `complete-backend.test.ts` | Cross-service integration | +| `shkeeper-backend.test.ts` | SHKeeper service + webhook | + +Run with `npm run test:all`. CI runs the same. Reach for `npm run test:models`, `npm run test:payment`, etc. when iterating on a slice. + +--- + +## 12. Notable files for orientation + +| File | Why it matters | +|---|---| +| `src/app.ts` | Bootstrap — read once to understand wiring | +| `src/shared/config/index.ts` | All env vars, typed | +| `src/shared/utils/response-handler.ts` | Standard response shape | +| `src/shared/middleware/auth.ts` | JWT verify + RBAC | +| `src/infrastructure/socket/socketService.ts` | All socket plumbing | +| `src/services/payment/shkeeper/shkeeperWebhook.ts` | Webhook signature scheme | +| `src/services/marketplace/PurchaseRequestService.ts` | Core marketplace state machine | +| `src/services/auth/authService.ts` | Auth flows, lockout, hashing | +| `src/models/User.ts` | Central entity with role/preferences | +| `openapi.json` | Generated API spec — definitive endpoint list | + +--- + +## Related + +- [[System Architecture]] — full system topology +- [[Frontend Architecture]] — how the FE talks to this BE +- [[Real-time Layer]] — Socket.IO room model +- [[Security Architecture]] — JWT, passkeys, webhook HMAC +- [[Data Model Overview]] — entity-relationship map +- [[Authentication Flow]] · [[Payment Flow - SHKeeper]] · [[Dispute Flow]] diff --git a/01 - Architecture/Frontend Architecture.md b/01 - Architecture/Frontend Architecture.md new file mode 100644 index 0000000..0a5fcc0 --- /dev/null +++ b/01 - Architecture/Frontend Architecture.md @@ -0,0 +1,369 @@ +--- +title: Frontend Architecture +tags: [architecture, frontend, nextjs] +created: 2026-05-23 +--- + +# Frontend Architecture + +Module-level architecture of the Next.js 16 (App Router) + TypeScript + MUI v7 frontend at `/Users/mojtabaheidari/code/frontend` (development branch). + +> [!info] +> Repo: `git@git.manko.yoga:222/nick/frontend.git` · Branch: `development` · Version: 1.9.6 (`package.json:4`) · Dev port `3000`, Docker port `8083`. + +--- + +## 1. Folder tree + +``` +frontend/src/ +├── app/ # Next.js 16 App Router (server + client islands) +│ ├── layout.tsx # Root layout — providers, fonts, Sentry +│ ├── (public)/ # Unauthenticated routes +│ │ ├── post/[slug]/ # Blog reader +│ │ └── shop/[seller]/[id] # Public seller / item view +│ ├── auth/jwt/ # sign-in, sign-up, verify, reset, update +│ ├── dashboard/ # AuthGuard + EmailVerificationGuard +│ │ ├── overview/ # KPI / home tiles +│ │ ├── chat/ # Real-time chat +│ │ ├── account/ # Profile, address, notifications, wallet, passkey +│ │ ├── request/ # Buyer purchase requests +│ │ ├── request-template/ # Seller request templates +│ │ ├── payment/ # Payment history / detail +│ │ ├── points/ # Loyalty hub (transactions, referrals, levels) +│ │ ├── disputes/ # Dispute hub +│ │ ├── user/ # Admin user management +│ │ ├── post/ # Admin blog editor +│ │ ├── shop-settings/ # Seller shop config +│ │ └── shops/ # Browse / checkout (dashboard scope) +│ ├── error/ # Global error page +│ └── not-found.tsx # 404 +├── sections/ # Page-specific composition modules (one folder per feature) +│ └── (chat|payment|request|request-template|dispute|user|points|...) +├── components/ # Reusable UI primitives (hook-form, table, upload, editor, ...) +├── layouts/ # Page-template wrappers (auth-centered, auth-split, dashboard, main) +├── theme/ # MUI theme creation, palette, typography, overrides +├── settings/ # Settings drawer (mode, layout, direction, color, font) +├── contexts/ # React Context providers (socket-context) +├── hooks/ # 50+ custom hooks (use-boolean, use-socket, use-web3-wagmi, …) +├── lib/ # Cross-cutting libs (axios.ts with interceptors) +├── locales/ # i18next config + langs/{en,fa,ar,fr,cn,vi}/*.json +└── types/ # Cross-app TypeScript types +``` + +--- + +## 2. Rendering strategy + +- **App Router** with `output: 'standalone'` in `next.config.ts` for production single-binary serving. +- **Server components by default**, **client components** opted into with `"use client"`. Heavy interactive pages (chat, editor, dashboard) are mostly client-side; SEO routes (`shop/`, `post/[slug]`) leverage server rendering. +- **Turbopack** in dev (`next dev --turbopack`). +- **Streaming** via `loading.tsx` and Suspense boundaries. +- **MUI cache** is wired up via `@mui/material-nextjs` in the root layout to avoid FOUC across server/client boundary. + +--- + +## 3. Provider tree (root layout) + +```mermaid +flowchart TB + A[RootLayout] + A --> B[AppRouterCacheProvider
MUI emotion cache] + B --> C[ThemeProvider
theme + RTL stylis] + C --> D[LocalizationProvider
dayjs adapter] + D --> E[I18nProvider
i18next] + E --> F[QueryClientProvider
React Query] + F --> G[SocketProvider
Socket.IO context] + G --> H[SnackbarProvider
notistack] + H --> I[Children — routes] +``` + +Order matters: theme must wrap query (because mutations show snackbars styled by theme); socket wraps snackbar (so socket-driven notifications can fire snackbars). + +--- + +## 4. Route layout & guards + +| Route group | Layout | Guard chain | +|---|---|---| +| `(public)` | `layouts/main` | none | +| `auth/jwt/*` | `layouts/auth-centered` or `auth-split` | redirect if already authed | +| `dashboard/*` | `layouts/dashboard` (sidebar + topbar + breadcrumbs) | `AuthGuard` → `EmailVerificationGuard` | +| `dashboard/user/*` | dashboard | + `role: admin` | +| `dashboard/post/*` (editor) | dashboard | + `role: admin` | +| `dashboard/shop-settings/*` | dashboard | + `role: seller` | + +Guards live in `frontend/src/auth/` (HOC + hook). They consult the JWT-derived user context and redirect unauthenticated to `/auth/jwt/sign-in?returnTo=...`. + +--- + +## 5. Sections vs components vs hooks + +The codebase enforces a three-layer split: + +| Layer | Lives in | Purpose | Example | +|---|---|---|---| +| **Section** | `src/sections//` | Page-specific composition; orchestrates components | `sections/chat/view/ChatView.tsx` | +| **Component** | `src/components//` | Reusable across sections | `components/hook-form/RHFTextField.tsx` | +| **Hook** | `src/hooks/.ts` | Encapsulated stateful behavior | `hooks/use-chat-socket.ts` | + +> [!tip] +> Per the cursor rules (`backend/.cursor/rules/ui-development-standards.mdc`), every component folder has `index.ts` (barrel export), `.tsx` (component), `classes.ts` (styled-component class names), `types.ts` (interface). Following this layout keeps each component refactorable. + +--- + +## 6. State management + +The frontend deliberately uses **three** state mechanisms, each for one concern: + +| Concern | Tool | Where | +|---|---|---| +| Server data | **React Query** | every `useXxxQuery` / `useXxxMutation` hook | +| Cross-page shared state | **React Context** | `contexts/socket-context.tsx`, settings context, auth context | +| Per-component / local UI | `useState`, `useReducer`, `use-boolean`, `use-set-state` | inside components | + +No Redux, no MobX, no Recoil. The cursor rules also mention **Zustand** as the preferred client store if one is added, but the dev branch currently relies on React Query + Context. + +### React Query setup + +- `QueryClient` created once at the root layout with `defaultOptions`: + - `queries: { staleTime: 30_000, retry: 1, refetchOnWindowFocus: false }` + - `mutations: { retry: 0 }` +- Query keys are tuples — convention: `['', ]` e.g. `['requests', { status: 'open' }]`. +- Mutations invalidate related keys in `onSuccess`. + +--- + +## 7. API client (`src/lib/axios.ts`) + +A single axios instance underpins every HTTP call: + +```ts +const api = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + withCredentials: true, +}); + +// Request interceptor — attach token +api.interceptors.request.use((cfg) => { + const token = getStoredToken(); + if (token) cfg.headers.Authorization = `Bearer ${token}`; + cfg.headers['X-Request-Id'] = randomUUID(); + return cfg; +}); + +// Response interceptor — handle 401, normalize errors +api.interceptors.response.use( + (res) => res.data, + async (err) => { + if (err.response?.status === 401 && !retried) { + // attempt refresh-token flow + await refresh(); + return api.request(err.config); + } + throw normalizeError(err); + } +); +``` + +Endpoint constants live alongside the hook that uses them — no central `api/endpoints.ts`. + +--- + +## 8. Real-time integration + +The `SocketProvider` in `contexts/socket-context.tsx`: + +1. Opens the connection after authentication (token from storage). +2. Auto-joins user-specific rooms (`user-{id}`, `seller-{id}` or `buyer-{id}` based on role). +3. Exposes a `useSocket()` hook returning `socket`, `connected`, helper emitters. + +Higher-level hooks build on this: + +| Hook | Subscribes to | +|---|---| +| `use-chat-socket` | `chat:new-message`, typing indicators, read receipts | +| `use-conversations` | conversation list updates | +| `use-notifications` | `notification:received` | +| `use-purchase-requests` | `request:offer-received`, status changes | +| `use-marketplace-socket` | broad market events | +| `use-unified-real-time` | multi-event aggregator | + +See [[Real-time Layer]] for the full event catalog. + +--- + +## 9. Web3 integration + +```ts +// wagmi config (approx — confirm in src/web3/ or src/lib/wagmi.ts) +const config = createConfig({ + chains: [bsc, polygon, mainnet, sepolia], + transports: { + [bsc.id]: http(`https://...alchemy.com/${KEY}`), + [polygon.id]: http(`https://polygon-mainnet.g.alchemy.com/v2/${KEY}`), + ... + }, + connectors: [ + injected(), // MetaMask + walletConnect({ projectId: NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID }), + ], +}); +``` + +Wallet UI: connect / disconnect / show address / show balance via `use-web3-wagmi`, `use-web3-context`. The DePay widget (`@depay/widgets`) is loaded for the assisted-pay flow. + +--- + +## 10. Internationalization + +- `i18next` + `react-i18next` initialized in `src/locales/locales-config.ts` with 6 langs (en, fa, ar, fr, cn, vi). +- Translation files in `langs//*.json` (e.g., `common.json`). +- Direction (`ltr`|`rtl`) lives in settings; `stylis-plugin-rtl` is wired into the MUI cache when `dir === 'rtl'`. +- Date formatting via `dayjs` with locale auto-loaded. +- Number formatting via `Intl.NumberFormat` + helper at `locales/utils/number-format-locale.ts`. +- DataGrid has Persian-specific locale at `custom-fa-data-grid-locale.ts`. + +See [[Internationalization & RTL]] for full detail. + +--- + +## 11. Forms & validation + +`react-hook-form` + `zod` schema via `@hookform/resolvers/zod`. Custom field wrappers in `components/hook-form/`: + +| Wrapper | Wraps | +|---|---| +| `RHFTextField` | MUI `TextField` | +| `RHFSelect` | MUI `Select` | +| `RHFAutocomplete` | MUI `Autocomplete` | +| `RHFCheckbox` | MUI `Checkbox` | +| `RHFRadioGroup` | MUI `RadioGroup` | +| `RHFSwitch` | MUI `Switch` | +| `RHFUpload` | custom Dropzone (`components/upload`) | +| `RHFEditor` | TipTap editor wrapper | +| `RHFDatePicker` | `@mui/x-date-pickers` | +| `RHFPhoneInput` | `react-phone-number-input` | +| `RHFCountrySelect` | `components/country-select` | + +A typical form section: + +```tsx +const schema = z.object({ email: z.string().email(), password: z.string().min(8) }); +const methods = useForm({ resolver: zodResolver(schema) }); +return ( + + + + Sign in + +); +``` + +--- + +## 12. Theming + +`src/theme/index.ts` creates the theme; `src/theme/options/` contains palette / typography / overrides. Light & dark variants share tokens; the active mode is read from the **Settings** drawer. + +- Primary: `Public Sans Variable` (per cursor rules). +- Secondary: `Barlow`. +- Breakpoints: xs=600, sm=600, md=900, lg=1200, xl=1536. +- Shape radius: 8 (default), customizable in settings. + +See [[Theme Configuration]] and [[Design System Overview]]. + +--- + +## 13. Settings drawer + +`src/settings/` provides a side-drawer that lets a user toggle: + +- Mode (light / dark / system) +- Contrast (default / high) +- Layout (vertical / mini / horizontal nav) +- Direction (ltr / rtl) +- Color preset (one of N curated palettes) +- Font family override + +State persists in `localStorage` under `settings-key`. + +--- + +## 14. Editor (TipTap) + +`components/editor/` wraps `@tiptap/react` with these extensions enabled: + +- `StarterKit` (paragraph, headings, bold/italic, lists, blockquote) +- `Link` (URL parsing) +- `Image` (upload via `components/upload`) +- `Underline`, `TextAlign` +- `CodeBlock` + `CodeBlockLowlight` (syntax highlighting via `lowlight`) +- `Placeholder` + +Used in: +- Blog post editor (`dashboard/post/new`, `dashboard/post/[id]/edit`) +- Long-form description fields in Purchase Request & Request Template forms +- Dispute evidence narrative + +Content is stored as HTML in MongoDB. `react-markdown` + `remark-gfm` are available where Markdown rendering is preferred (e.g., chat messages). + +--- + +## 15. File uploads + +`components/upload/` provides a dropzone with: +- Multi-file selection +- Drag-and-drop +- Per-file progress +- Preview (`components/file-thumbnail/` shows by MIME) +- Removal +- Total-size enforcement (5 MB default, matches `MAX_FILE_SIZE`) + +Backed by `/api/file/*` (multipart upload). + +--- + +## 16. Sentry + +`@sentry/nextjs` is initialised at app boot. Errors include user context (role, userId) and breadcrumbs (route changes, API failures). Source maps uploaded at build time. + +--- + +## 17. Build & deploy + +`package.json`: +- `dev` → `next dev -p 8083 --turbopack` +- `build` → `next build && cp .next/static .next/standalone/.next/ && cp -r public .next/standalone/` +- `start` → `PORT=8083 node .next/standalone/server.js` + +`Dockerfile` is a 2-stage multi-stage build that produces `.next/standalone/` and copies into a small `node:22-alpine` runner image with non-root user `nextjs`. Healthcheck via `curl http://localhost:8083`. + +CI: `.gitea/workflows/deploy.yml` runs `scripts/deploy.sh` on push to `main` / `master`. + +See [[Docker Setup]], [[CI-CD Pipeline]], and [[Deployment]]. + +--- + +## 18. Notable files for orientation + +| File | Why it matters | +|---|---| +| `src/app/layout.tsx` | Provider tree | +| `src/lib/axios.ts` | Every HTTP call goes through this | +| `src/contexts/socket-context.tsx` | Realtime plumbing | +| `src/theme/index.ts` | Theme creation entry | +| `src/locales/locales-config.ts` | i18next setup | +| `src/settings/context/` | Settings context (persistence) | +| `next.config.ts` | Standalone build, COOP/COEP headers for Web3 popups | + +--- + +## Related + +- [[System Architecture]] — bird's-eye topology +- [[Backend Architecture]] — server peer +- [[Real-time Layer]] — Socket.IO plumbing +- [[Design System Overview]] · [[Theme Configuration]] · [[Components]] +- [[Tech Stack]] — versions +- [[Coding Standards]] — UI cursor-rules summary diff --git a/01 - Architecture/Infrastructure.md b/01 - Architecture/Infrastructure.md new file mode 100644 index 0000000..4fc01b7 --- /dev/null +++ b/01 - Architecture/Infrastructure.md @@ -0,0 +1,224 @@ +--- +title: Infrastructure +tags: [architecture, infrastructure, docker, devops] +created: 2026-05-23 +--- + +# Infrastructure + +How the system is packaged, deployed and run in production. Read alongside [[Docker Setup]] (operations runbook) and [[CI-CD Pipeline]]. + +--- + +## 1. Runtime topology + +```mermaid +flowchart LR + DNS[DNS amn.gg] --> CF[CloudFlare / Edge SSL] + CF --> Nginx + subgraph Host[Single Docker host] + Nginx[nginx:alpine
nickapp-nginx
:8083] + FE[nickapp-frontend:latest
Next.js standalone
:8083] + BE[nickapp-backend:latest
Express + Socket.IO
:5001] + Mongo[(mongo:8.0
mongodb_data volume)] + Redis[(redis:8-alpine
redis_data volume)] + Up[/uploads volume/] + end + Nginx --> FE + Nginx --> BE + BE --> Mongo + BE --> Redis + BE --- Up + FE --- Up + Watchtower>Watchtower
auto-update] -.-> BE + Watchtower -.-> FE +``` + +> [!info] +> Single-host today. Horizontal scaling requires Redis pub/sub adapter for Socket.IO and externalizing the `uploads/` volume (S3 / R2). + +--- + +## 2. Docker images + +| Image | Source Dockerfile | Stages | Final size target | User | +|---|---|---|---|---| +| `nickapp-backend:latest` | `backend/Dockerfile.prod` | builder + runner | ~200 MB | `marketplace` (uid 1001) | +| `nickapp-backend:dev` | `backend/Dockerfile.dev` | single | ~400 MB | `marketplace` (uid 1001) | +| `nickapp-frontend:latest` | `frontend/Dockerfile` | builder + runner | ~250 MB | `nextjs` (uid 1001) | +| `nickapp-frontend:dev` | `frontend/Dockerfile.dev` | single | ~600 MB | root | + +All built on `node:22-alpine`. Backend images include Python+make+g++ in the builder stage for native deps (bcrypt). Frontend runner only ships the `.next/standalone` output + `public/`. + +--- + +## 3. Compose stacks + +### 3.1 `docker-compose.dev.yml` (backend repo) + +Used by developers running `npm run docker:dev`. Three services: + +| Service | Image | Ports | Volumes | Depends on | +|---|---|---|---|---| +| `nickdev-backend` | built from `Dockerfile.dev` | `5001:5001` | `./src:/app/src`, `./uploads:/app/uploads` | mongodb, redis | +| `nickdev-mongodb` | `mongo:8.0` | `27017:27017` | `mongodb_data:/data/db` | — | +| `nickdev-redis` | `redis:8-alpine` | (internal) | `redis_data:/data` | — | + +Network: `nickapp-network` (bridge). Env: `.env.local`. + +Frontend dev runs OUTSIDE compose — developer launches `yarn dev` on host so React Fast Refresh works. + +### 3.2 `docker-compose.production.yml` (backend repo) + +Used in production. Five services: + +| Service | Image | Ports | Healthcheck | Watchtower | +|---|---|---|---|---| +| `nickapp-nginx` | `nginx:alpine` | `8083:80` | implicit | — | +| `nickapp-backend` | `nickapp-backend:latest` (pulled) | 5001 internal | `node healthcheck.js` 30s | ✓ | +| `nickapp-frontend` | `nickapp-frontend:latest` (pulled) | 8083 internal | `curl http://localhost:8083` 30s | ✓ | +| `nickapp-mongodb` | `mongo:8.0` | internal | `mongosh ping` 30s | — | +| `nickapp-redis` | `redis:8-alpine` (with `--requirepass`) | internal | `redis-cli ping` 30s | — | + +Volumes: `mongodb_data`, `redis_data`, `./uploads`, `./nginx/nginx.conf`, `./nginx/logs`. Env: single root `.env`. + +> [!warning] +> `REDIS_PASSWORD` MUST be set in the production `.env` before starting — otherwise the Redis container fails its healthcheck and dependents won't start. + +--- + +## 4. Nginx configuration + +The Nginx proxy at `./nginx/nginx.conf` (mounted read-only) is responsible for: + +- Terminating internal HTTP (SSL handled by an external edge — CloudFlare / nginx-proxy / Caddy) +- Routing `/api/*` and `/socket.io/*` to `nickapp-backend:5001` +- Routing everything else to `nickapp-frontend:8083` +- Serving `/uploads/*` directly from the shared volume (bypasses the Node process) +- Standard gzip / compression / client_max_body_size (≥10 MB to match backend body limit) + +> [!tip] +> Put the Nginx access log path on a tmpfs or rotate it aggressively — the container exposes `./nginx/logs` so the host can manage retention. + +--- + +## 5. Watchtower (auto-update) + +Both `nickapp-backend` and `nickapp-frontend` carry the `watchtower.enable=true` label. Watchtower polls the container registry on its configured interval and re-pulls when the `latest` tag moves. + +Release cycle: +1. Developer pushes commits to a feature branch → merged into `development`. +2. Manual Gitea workflow `docker-build-simple.yml` builds & pushes `nickapp-backend:latest` (and a versioned tag) to `git.manko.yoga/manawenuz/escrow-backend`. +3. Within the next poll interval (default 5 min) Watchtower restarts the affected service. + +> [!warning] +> Because Watchtower restarts only on tag change, sequential pushes are safe — but a broken build pushed to `latest` will roll out automatically. Keep `dev` and `production` tags separated, or pin production to a versioned tag. + +--- + +## 6. Persistent storage + +| Volume | What it stores | Backup priority | +|---|---|---| +| `mongodb_data` | All business data (users, requests, payments, chats, disputes...) | **Critical** — daily dump | +| `redis_data` | Cache, session, rate counters | Low — losing it logs everyone out but no data loss | +| `./uploads` (host bind) | Avatars, product images, dispute evidence, documents | **High** — daily rsync | +| `./nginx/logs` | Access / error logs | Medium | + +See [[Backup & Recovery]] for the runbook. + +--- + +## 7. CI / CD + +Located at `backend/.gitea/workflows/`: + +| Workflow | Trigger | Output | +|---|---|---| +| `docker-build-simple.yml` | manual | Build → push backend image to registry (`latest` + version) | +| `docker-build-dev.yml` | manual | Dev image variant | +| `docker-build-no-cache.yml` | manual | Clean build (no GH-Actions cache) | + +Frontend at `frontend/.gitea/workflows/`: + +| Workflow | Trigger | Output | +|---|---|---| +| `deploy.yml` | push to `main`/`master`, manual | Runs `scripts/deploy.sh` (deploy via SSH) | +| `devDeploy.yml` | manual | Deploy to dev env | + +Single secret required: `GITEATOKEN` (Gitea PAT with `write:package`). + +Full breakdown → [[CI-CD Pipeline]]. + +--- + +## 8. Secrets + +Never committed. Required in production `.env`: + +- `JWT_SECRET`, `REFRESH_TOKEN_SECRET` +- `MONGODB_URI`, `REDIS_PASSWORD` +- `SHKEEPER_API_KEY`, `SHKEEPER_WEBHOOK_SECRET` +- `SMTP_USER`, `SMTP_PASS` +- `OPENAI_API_KEY` +- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` +- `NEXT_PUBLIC_ALCHEMY_API_KEY_*` (frontend public envs — embedded at build) + +Recommend storing host-side in an `.env` outside the repo, mounted via Compose. For multi-host: Vault / AWS SSM / GCP Secret Manager. + +--- + +## 9. Observability + +| Signal | Where | +|---|---| +| App logs | container `stdout` → `docker logs nickapp-backend` | +| Access logs | `./nginx/logs` | +| Frontend errors | Sentry (`@sentry/nextjs`) | +| Backend errors | Sentry (optional — add `@sentry/node`) | +| Healthchecks | `GET /health` (backend) · `GET /` (frontend) · `mongosh ping` · `redis-cli ping` | + +See [[Monitoring]] for the full table of metrics & recommended alerts. + +--- + +## 10. Networking + +| Direction | Port | Protocol | Notes | +|---|---|---|---| +| Internet → Nginx | 8083 | HTTP | SSL terminated upstream | +| Browser → Backend | 5001 | HTTP + WS | via Nginx `/api`, `/socket.io` | +| Backend → MongoDB | 27017 | TCP | Docker network | +| Backend → Redis | 6379 | TCP | Docker network | +| Backend → SHKeeper | 443 | HTTPS | External | +| Backend → SMTP | 587 | TLS | External | +| Backend → OpenAI | 443 | HTTPS | External | +| Browser → Blockchain RPC | 443 | HTTPS | Alchemy URLs | +| Browser → WalletConnect | 443 | HTTPS | Relay server | + +--- + +## 11. Hardening checklist + +- [ ] Non-root user inside every container (`marketplace` / `nextjs`) ✓ in Dockerfiles +- [ ] Read-only Nginx config mount ✓ +- [ ] Redis requires password ✓ +- [ ] MongoDB has root credentials + bind to internal network only ✓ +- [ ] Rate limit enabled before public traffic ⚠ currently disabled +- [ ] Refresh tokens rotated on use ⚠ partial +- [ ] All public env values reviewed (no `IS_DEVELOPMENT=true` in prod build) ⚠ +- [ ] Watchtower scoped to specific repos +- [ ] Sentry source-map upload at build time ✓ (frontend) + +See [[Security Architecture]] for the deeper review. + +--- + +## Related + +- [[System Architecture]] · [[Backend Architecture]] · [[Frontend Architecture]] +- [[Docker Setup]] — operational walk-through with commands +- [[CI-CD Pipeline]] — workflow breakdown +- [[Deployment]] — first-deploy runbook +- [[Backup & Recovery]] · [[Monitoring]] · [[Incident Response]] +- [[Environment Variables]] — full env-var catalog diff --git a/01 - Architecture/Real-time Layer.md b/01 - Architecture/Real-time Layer.md new file mode 100644 index 0000000..192e6e0 --- /dev/null +++ b/01 - Architecture/Real-time Layer.md @@ -0,0 +1,220 @@ +--- +title: Real-time Layer +tags: [architecture, realtime, websocket, socket.io] +created: 2026-05-23 +--- + +# Real-time Layer + +Socket.IO 4.8 is the single transport for all server-pushed updates: chat messages, notifications, payment status, offer arrivals, dispute updates. + +> [!info] +> Backend handler: `backend/src/infrastructure/socket/socketService.ts`. Frontend context: `frontend/src/contexts/socket-context.tsx` consumed via `useSocket()`. + +--- + +## 1. Connection lifecycle + +```mermaid +sequenceDiagram + actor U as Browser + participant FE as React (SocketProvider) + participant BE as Socket.IO Server + U->>FE: Login completes (JWT stored) + FE->>BE: io.connect(url, { auth: { token } }) + BE->>BE: verifyJwt(token) → req.user + BE-->>FE: "connect" + socket.id + FE->>BE: emit "join-user-room", userId + BE-->>FE: ack + Note over BE,FE: Long-lived connection. Auto-reconnect handles drops. + BE-->>FE: emit "notification:received" (when triggered) + FE->>FE: update React Query cache / show snackbar +``` + +On the client: +- Socket is **created lazily** after auth state is known. +- Connection auto-recovers on disconnect (Socket.IO default exponential backoff). +- The provider exposes `connected: boolean` so UI can show a "reconnecting…" indicator. + +On the server: +- The `connection` handler verifies the JWT from `socket.handshake.auth.token`. +- If invalid → `socket.disconnect(true)`. +- If valid → `socket.data.user = decoded` is set for use in subsequent handlers. + +--- + +## 2. Room model + +Rooms are joined explicitly by the client via emitter events. Names use a `prefix-{id}` convention so socket addressing matches the canonical entity ID. + +| Room | Joined by | Purpose | +|---|---|---| +| `user-{userId}` | every authenticated socket | User-targeted events (notifications, payment updates for them) | +| `seller-{userId}` | sellers on login | Seller-targeted events (new offer requests in their category) | +| `sellers` | all sellers | Broadcast to all sellers (e.g., new public request) | +| `buyer-{userId}` | buyers on login | Buyer-targeted events | +| `buyers` | all buyers | Broadcast to all buyers | +| `request-{requestId}` | participants of a request (buyer + accepted seller) | Per-request updates (status, offer arrivals) | +| `chat-{chatId}` | conversation participants | Live chat messages, typing, read receipts | +| `admin` | admin users | Mod-only events (new dispute opened, suspicious activity) | + +Client-side join/leave emitters (defined in `socketService.ts`): + +| Emit | Args | Effect | +|---|---|---| +| `join-user-room` | `userId` | Adds socket to `user-{userId}` | +| `join-request-room` | `requestId` | Adds to `request-{requestId}` | +| `leave-request-room` | `requestId` | Removes from `request-{requestId}` | +| `join-seller-room` | `sellerId` | Adds to `seller-{sellerId}` and `sellers` | +| `leave-seller-room` | `sellerId` | Removes from both | +| `join-buyer-room` | `buyerId` | Adds to `buyer-{buyerId}` and `buyers` | +| `leave-buyer-room` | `buyerId` | Removes from both | +| `join-chat-room` | `chatId` | Adds to `chat-{chatId}` | + +--- + +## 3. Emit helpers (server-side) + +The socket service exposes typed emitters used across all services so individual modules never touch `io` directly: + +```ts +// src/infrastructure/socket/socketService.ts +export function emitToRoom(room: string, event: string, payload: unknown): void; +export function emitToUser(userId: string, event: string, payload: unknown): void; +export function emitToSellers(event: string, payload: unknown): void; +export function emitToBuyers(event: string, payload: unknown): void; +export function emitGlobalEvent(event: string, payload: unknown): void; +``` + +This indirection makes it trivial to: +- Add per-event logging +- Throttle / batch in the future +- Swap to a Redis pub/sub adapter when scaling out + +--- + +## 4. Event catalog + +### 4.1 Notifications + +| Event | Payload | Emitted by | Rooms | +|---|---|---|---| +| `notification:received` | `Notification` doc | NotificationService | `user-{recipientId}` | +| `notification:read` | `{ id, readAt }` | NotificationService.markRead | `user-{id}` | + +### 4.2 Marketplace + +| Event | Payload | Emitted by | Rooms | +|---|---|---|---| +| `request:created` | `PurchaseRequest` | PurchaseRequestService.create | `sellers` (or category-scoped sellers in future) | +| `request:offer-received` | `{ requestId, offer: SellerOffer }` | SellerOfferService.create | `user-{buyerId}`, `request-{requestId}` | +| `request:status-updated` | `{ requestId, status }` | various | `request-{requestId}` | +| `offer:negotiation-update` | `{ offerId, message }` | SellerOfferService.counter | `request-{requestId}` | +| `offer:accepted` | `{ offerId }` | SellerOfferService.accept | `seller-{sellerId}`, `user-{buyerId}` | + +### 4.3 Payment + +| Event | Payload | Emitted by | +|---|---|---| +| `payment:status-updated` | `{ paymentId, status, providerPaymentId }` | shkeeperWebhook → PaymentService | +| `payment:created` | `Payment` | PaymentService.createPayInIntent | +| `payment:completed` | `{ paymentId }` | PaymentService on webhook completion | +| `payout:created` | `Payout` | shkeeperPayoutService | +| `payout:completed` | `{ payoutId, txHash }` | payout polling / webhook | + +### 4.4 Chat + +| Event | Payload | Rooms | +|---|---|---| +| `chat:new-message` | `Message` | `chat-{chatId}` and `user-{participantId}` (for badge) | +| `chat:message-read` | `{ chatId, messageId, userId, readAt }` | `chat-{chatId}` | +| `chat:typing` | `{ chatId, userId }` | `chat-{chatId}` | +| `chat:stopped-typing` | `{ chatId, userId }` | `chat-{chatId}` | + +### 4.5 Dispute + +| Event | Payload | Rooms | +|---|---|---| +| `dispute:opened` | `Dispute` | `request-{requestId}`, `admin` | +| `dispute:evidence-added` | `{ disputeId, evidence }` | `request-{requestId}` | +| `dispute:resolved` | `{ disputeId, resolution }` | `request-{requestId}`, `user-{buyerId}`, `user-{sellerId}` | + +### 4.6 System + +| Event | Payload | Rooms | +|---|---|---| +| `system:maintenance` | `{ message, startsAt }` | global broadcast | +| `system:announcement` | `{ message }` | global broadcast | + +--- + +## 5. Client-side hooks + +Higher-level React hooks consume the socket and integrate with React Query so UI stays consistent: + +| Hook | Listens for | Effect | +|---|---|---| +| `useChatSocket(chatId)` | `chat:new-message`, `chat:typing` | Appends to chat query cache; updates typing state | +| `useConversations()` | `chat:new-message` (any chat) | Bumps conversation in list; updates unread count | +| `useNotifications()` | `notification:received` | Prepends to notification list; shows snackbar | +| `usePurchaseRequests()` | `request:status-updated`, `request:offer-received` | Invalidates `['requests']` cache | +| `useMarketplaceSocket()` | `request:created` (seller side) | Bumps seller feed | +| `useUnifiedRealTime()` | multi | Aggregates the above for the dashboard overview | + +Pattern: each hook subscribes inside a `useEffect` and unsubscribes in cleanup. Use `socket.off(event, handler)` to avoid handler leaks on re-render. + +--- + +## 6. Authentication & authorization on sockets + +- **Connection-time auth** — JWT verified in handshake; invalid token → disconnect. +- **Per-event auth** — Critical handlers re-check role from `socket.data.user.role`. Example: `admin` room membership only added for admins. +- **Tampering** — `userId` arguments in `join-*-room` events are **NOT trusted blindly**; the server must verify `socket.data.user.id === userId` before adding to the room (cite: needs verification in `socketService.ts`). + +> [!warning] +> If `join-user-room` accepts any userId from the client, malicious users could subscribe to other users' notifications. Always cross-check with `socket.data.user.id`. + +--- + +## 7. Reconnection & buffering + +Socket.IO buffers up to N events per disconnected client by default — but **the server does not replay missed events after a long disconnect**. Frontend strategy: + +1. On reconnect, the SocketProvider re-fires `join-*-room` for the user's current state. +2. Critical hooks (notifications, chat) call `refetch()` on reconnect to backfill from REST. +3. Last-event-id pattern is **not implemented**; consider adding for chat reliability. + +--- + +## 8. Scaling to multiple backend nodes + +Socket.IO requires a shared bus when running >1 backend instance. Recommended adapter: + +```ts +import { createAdapter } from '@socket.io/redis-adapter'; +import { createClient } from 'redis'; +const pub = createClient({ url: config.redisUri, password: config.redisPassword }); +const sub = pub.duplicate(); +await Promise.all([pub.connect(), sub.connect()]); +io.adapter(createAdapter(pub, sub)); +``` + +Without this adapter, emits from node A never reach sockets connected to node B. + +Sticky sessions on the load balancer are also required so a given client always lands on the same node (otherwise the handshake / connection upgrade fails). + +--- + +## 9. Testing + +- Backend tests (`backend/__tests__/`) mock the socket emitter via the indirection layer — no real socket server in unit tests. +- Manual smoke test: open two browser tabs as buyer + seller, accept an offer, watch both receive `offer:accepted` instantly. + +--- + +## Related + +- [[Backend Architecture]] · [[Frontend Architecture]] +- [[Chat Flow]] · [[Notification Flow]] · [[Payment Flow - SHKeeper]] · [[Dispute Flow]] +- [[Security Architecture]] — socket auth concerns +- [[Socket Events]] — full event reference (developer-facing API doc) diff --git a/01 - Architecture/Security Architecture.md b/01 - Architecture/Security Architecture.md new file mode 100644 index 0000000..d35a2d6 --- /dev/null +++ b/01 - Architecture/Security Architecture.md @@ -0,0 +1,227 @@ +--- +title: Security Architecture +tags: [architecture, security, authentication, rbac] +created: 2026-05-23 +--- + +# Security Architecture + +How identity, authorization, transport, and integrity are handled across the platform. + +> [!important] +> Read alongside [[Authentication Flow]] (user-facing), [[Passkey (WebAuthn) Flow]], and [[Payment Flow - SHKeeper]] (webhook HMAC). + +--- + +## 1. Threat model — at a glance + +| Threat | Mitigation | +|---|---| +| Credential stuffing | bcrypt 12-round hashing + account lockout + rate-limit (when enabled) | +| Session hijacking | Short-lived JWTs (7d), opaque refresh tokens (30d), token rotation | +| CSRF | JWT in `Authorization` header (not cookie), CORS allow-list | +| XSS | Helmet CSP, React auto-escaping, sanitize HTML before storage | +| SQL/NoSQL injection | Mongoose parameterized queries, no `$where` strings, schema validation | +| Webhook spoofing | HMAC SHA-256 over body + secret, constant-time compare | +| File upload abuse | Multer MIME validation, 5 MB cap, non-executable storage, served by Nginx not Node | +| Replay attacks | Per-payment idempotency on `providerPaymentId`, per-request `X-Request-Id` | +| Account takeover | Email verification required, password reset code expiry (1h), passkey support | +| Phishing | Passkey origin binding (`NEXT_PUBLIC_PASSKEY_ORIGIN`), email domain pinning | +| Data leakage | Role-gated endpoints, field-level projection (`select: false` on password), redacted logs | + +--- + +## 2. Authentication layers + +### 2.1 Email + password (primary) + +```mermaid +sequenceDiagram + actor U as User + participant FE as Frontend + participant BE as Backend + participant DB as MongoDB + U->>FE: enters credentials + FE->>BE: POST /api/auth/login { email, password } + BE->>DB: User.findOne({ email }) + DB-->>BE: user doc (incl. hashed password) + BE->>BE: bcrypt.compare(password, hash) + alt invalid + BE->>DB: increment loginAttempts; lock at N + BE-->>FE: 401 / 423 locked + else valid + BE->>BE: sign JWT (7d), refresh (30d) + BE->>DB: store refresh-token id; clear attempts + BE-->>FE: 200 { user, token, refreshToken } + end +``` + +- Password rules enforced by `authValidation.ts`: ≥8 chars, mixed case + digit recommended (cite the validator for exact rules). +- bcrypt rounds = 12 (`authService.ts`). +- Lockout: after N failed attempts within window, account locked for cooldown — see `authService.ts:113-145`. +- Reset code emailed on `passwordResetCode` request; valid 1h. + +### 2.2 Google OAuth 2.0 + +- Frontend uses `NEXT_PUBLIC_GOOGLE_CLIENT_ID` for the Sign-In with Google button. +- ID token sent to backend → `googleOAuthService.ts` verifies via Google's public keys → either links to existing User by email or creates a new one. +- See [[Google OAuth Flow]]. + +### 2.3 WebAuthn / Passkey + +- Standards-based passwordless. +- Backend: `passkeyService.ts` orchestrates registration and assertion challenges. +- Frontend env: `NEXT_PUBLIC_PASSKEY_RP_NAME=Amn`, `NEXT_PUBLIC_PASSKEY_RP_ID=`, `NEXT_PUBLIC_PASSKEY_ORIGIN=`. +- See [[Passkey (WebAuthn) Flow]]. + +> [!warning] +> Dev env files ship `NEXT_PUBLIC_PASSKEY_RP_ID=localhost`. In production this MUST be the actual eTLD+1 domain (e.g., `amn.gg`) — passkeys are scoped to the RP ID and can't be transferred. + +### 2.4 Refresh-token rotation + +- On `POST /api/auth/refresh`, the backend: + - Verifies the supplied refresh token. + - Issues a NEW access token + a NEW refresh token. + - Invalidates the old refresh token id in MongoDB. +- If the same refresh token is presented twice → all sessions for that user are invalidated (token reuse detection). + +--- + +## 3. Authorization (RBAC) + +### 3.1 Roles + +| Role | Source | Capabilities | +|---|---|---| +| `buyer` (user) | default on signup | Create requests, pay, chat, dispute, rate | +| `seller` (owner) | chosen at signup OR upgraded | Make offers, build templates, run a shop, withdraw | +| `admin` | seed / manual | Moderate, mediate disputes, manage users/blogs/levels | +| `support` | seed / manual | Read-only on most data, can reset passwords, escalate | + +A single User may be `buyer` and `seller` simultaneously (combined role). + +### 3.2 Enforcement points + +- **Middleware** — `authMiddleware` (verifies JWT) followed by `roleGuard(role)` on every route that requires elevation. +- **Service layer** — defensive `assertRole(ctx, 'admin')` calls inside critical service methods so even mis-mounted routes can't bypass. +- **UI** — `AuthGuard` + `EmailVerificationGuard` + role-aware nav (`components/nav-section`) hide admin/seller menus for users without permission. This is convenience only — never the security boundary. + +--- + +## 4. Transport security + +- **HTTPS** terminated upstream (CloudFlare / external Nginx). Internal cluster is HTTP. +- **HSTS** header set by upstream proxy (recommended `max-age=31536000; includeSubDomains; preload`). +- **CORS** — exactly one origin allowed: `config.frontendUrl`. `credentials: true`. +- **CSP** — Helmet default, currently permissive for Web3 popup compatibility (see `frontend/next.config.ts` setting COOP=`same-origin-allow-popups`, COEP=`unsafe-none`). + +--- + +## 5. Webhook integrity (SHKeeper) + +```mermaid +sequenceDiagram + participant SHK + participant BE + SHK->>BE: POST /api/payment/shkeeper/webhook
X-Signature: sha256= + BE->>BE: hmac = HMAC_SHA256(SHKEEPER_WEBHOOK_SECRET, body) + BE->>BE: crypto.timingSafeEqual(hmac, providedSig) + alt mismatch + BE-->>SHK: 401 Unauthorized + else match + BE->>BE: process payment update + BE-->>SHK: 200 OK + end +``` + +- Body must be the raw bytes used for HMAC — apply `express.raw({ type: 'application/json' })` on the webhook route ONLY (the rest of the app uses parsed JSON). +- In dev (`NODE_ENV === 'development'`) signature verification can be bypassed for local testing — confirm this is gated and never reachable in prod. +- Idempotency: identical webhook delivered twice should be no-op. Check by `(providerPaymentId, status)` tuple before mutating. + +See [[Payment Flow - SHKeeper]] for the full flow. + +--- + +## 6. Input validation + +- **Backend** — `express-validator` per route (e.g., `authValidation.ts`), centralised `validate` middleware that 422s on failure with `{ details: [...] }`. +- **Frontend** — `zod` schemas via `@hookform/resolvers/zod`. Same schema can be re-exported to a `shared/` package for true single-source-of-truth (not yet wired). +- **Mongoose** — schema-level `type`, `required`, `enum`, `min`/`max`, custom `validate` functions as a last line of defence. + +--- + +## 7. File upload safety + +- Stored under `uploads/{avatars|documents|products|temp}/` — non-executable, served by Nginx (no Node interpretation). +- MIME allow-list in `fileService.ts`: images for avatars/products, PDFs/docs for evidence. +- 5 MB hard cap (`MAX_FILE_SIZE=5242880`). +- Original filenames hashed → no path traversal, no clobber. +- Recommended: virus scan via ClamAV before exposing to other users (dispute evidence, chat attachments). + +--- + +## 8. Secrets management + +- Production secrets injected via host `.env`, mounted into compose `env_file`. +- Never log secrets — logger redaction recommended (winston/pino formatter). +- `.env*` files in `.gitignore`. Repo includes only `.env.development` / `.env.production` templates with **public** values (NEXT_PUBLIC_*). +- Rotate `JWT_SECRET` invalidates all existing JWTs — schedule a maintenance window. +- Rotate `SHKEEPER_WEBHOOK_SECRET` coordinated with SHKeeper dashboard (set new → verify → remove old). + +See [[Environment Variables]] for the catalog. + +--- + +## 9. Rate limiting & abuse + +- Backend has `express-rate-limit` ready but currently disabled (`app.ts:227`). +- Recommended pre-launch settings: + - `/api/auth/*` — 10 req / 5 min / IP + - `/api/auth/login` — 5 req / 5 min / IP **and** /email + - global API — 100 req / 15 min / IP (current default constants) +- Counters stored in Redis when enabled. +- For chat and notifications, debounce at the client to avoid spamming legitimate emits. + +--- + +## 10. Audit logging + +The codebase currently uses `morgan` (HTTP access logs) and ad-hoc `logger.info/warn/error`. For PCI-adjacent operations (payments) consider: + +- Append-only audit log of every payment / payout / refund / role change. +- Include actor (userId), target, action, before/after diff, request id. +- Persist in a separate Mongo collection or external log sink with retention ≥1y. + +--- + +## 11. Frontend session storage + +- JWT and refresh token stored in `localStorage` (per current implementation — cite to verify in `frontend/src/lib/`). +- Risk: XSS = total takeover. Mitigations: strict CSP, no `dangerouslySetInnerHTML` on untrusted content, audit dependencies (`yarn audit`). +- Alternative: store refresh token in `httpOnly` cookie and keep only short-lived access token in memory — recommended for production hardening. + +--- + +## 12. Hardening checklist (pre-launch) + +- [ ] Enable rate-limit middleware +- [ ] Promote refresh tokens to `httpOnly` cookies +- [ ] Replace `localhost` passkey RP ID with production domain +- [ ] Disable `NEXT_PUBLIC_IS_DEVELOPMENT=true` and `ENABLE_DEBUG=true` in prod build +- [ ] Verify `NODE_ENV=production` in backend prod env +- [ ] Pin production Watchtower to versioned tag (not `latest`) +- [ ] Add backend Sentry SDK + source maps +- [ ] Rotate all dev-seeded credentials before public launch +- [ ] Run `yarn audit` / `npm audit` and triage CVEs +- [ ] Pentest the payment + dispute flows specifically +- [ ] Review every `> [!warning]` callout in this vault + +--- + +## Related + +- [[Authentication Flow]] · [[Google OAuth Flow]] · [[Passkey (WebAuthn) Flow]] · [[Password Reset Flow]] +- [[Backend Architecture]] · [[Frontend Architecture]] · [[Real-time Layer]] +- [[Payment Flow - SHKeeper]] — webhook HMAC details +- [[Environment Variables]] — secret catalog +- [[Incident Response]] — what to do when something goes wrong diff --git a/01 - Architecture/System Architecture.md b/01 - Architecture/System Architecture.md new file mode 100644 index 0000000..eb447b1 --- /dev/null +++ b/01 - Architecture/System Architecture.md @@ -0,0 +1,202 @@ +--- +title: System Architecture +tags: [architecture, system, overview] +created: 2026-05-23 +--- + +# System Architecture + +End-to-end architecture for the **Amn** escrow marketplace platform — a two-repo system (frontend + backend) that brokers crypto-escrowed transactions between buyers and sellers. + +> [!info] +> Read [[Backend Architecture]] and [[Frontend Architecture]] for component-level detail. Read [[Infrastructure]] for deployment topology. + +--- + +## 1. High-level topology + +```mermaid +flowchart LR + Browser[Browser / PWA] + CDN[Static CDN] + Nginx[Nginx Reverse Proxy
:80/:443] + FE[Next.js Frontend
standalone server
:8083] + BE[Express Backend
+ Socket.IO
:5001] + Mongo[(MongoDB 8)] + Redis[(Redis 8)] + SHK[SHKeeper
Crypto Gateway] + SMTP[SMTP
Nodemailer] + OAI[OpenAI API] + BC[Blockchain RPC
Alchemy / WalletConnect] + + Browser -->|HTTPS + WSS| CDN + Browser -->|HTTPS| Nginx + Nginx --> FE + Nginx --> BE + FE -->|REST /api/*| BE + FE -.->|Socket.IO| BE + BE --> Mongo + BE --> Redis + BE -->|Pay-in / Pay-out| SHK + SHK -.->|Webhook HMAC| BE + BE --> SMTP + BE --> OAI + FE -->|Wallet Connect| BC + BE -->|Tx verify| BC +``` + +--- + +## 2. Request lifecycle (typical authenticated REST call) + +```mermaid +sequenceDiagram + actor U as User + participant FE as Frontend (Next.js) + participant AX as Axios Client + participant BE as Backend (Express) + participant MW as Middleware Chain + participant SVC as Service Layer + participant DB as MongoDB + participant SK as Socket.IO + + U->>FE: Interact with UI + FE->>AX: useMutation(...).mutate(payload) + AX->>AX: Attach Authorization: Bearer + AX->>BE: POST /api/marketplace/requests + BE->>MW: helmet, cors, body-parser + MW->>MW: authMiddleware → verify JWT + MW->>MW: validation middleware + MW->>SVC: controller invokes service + SVC->>DB: Mongoose create / update + DB-->>SVC: document + SVC->>SK: emitToRoom("user-{id}", "request:created", payload) + SVC-->>BE: response payload + BE->>AX: 200 { success, data } + AX->>FE: React Query cache update + FE-->>U: UI re-render +``` + +Concurrent realtime path: + +```mermaid +sequenceDiagram + participant FE as Frontend + participant BE as Backend + FE->>BE: socket.connect() w/ auth token + BE-->>FE: socket connected + FE->>BE: join-user-room + BE-->>FE: joined user-{userId} + Note over BE,FE: long-lived connection + BE-->>FE: emit "notification:received" + FE->>FE: update notification badge +``` + +--- + +## 3. Deployment topology + +Production runs as a single Docker Compose stack (`backend/docker-compose.production.yml`) behind an internal Nginx that proxies to both the frontend and backend containers. Watchtower watches the container registry (`git.manko.yoga/manawenuz/escrow-backend`) and auto-pulls new images tagged `latest`. + +| Layer | Service | Image | Port | Purpose | +|---|---|---|---|---| +| Edge | Nginx | `nginx:alpine` | 8083 | Reverse proxy + static assets | +| App | Frontend | `nickapp-frontend:latest` | 8083 (internal) | Next.js standalone | +| App | Backend | `nickapp-backend:latest` | 5001 (internal) | Express + Socket.IO | +| Data | MongoDB | `mongo:8.0` | 27017 (internal) | Primary store | +| Data | Redis | `redis:8-alpine` | 6379 (internal) | Cache + sessions + rate-limit counters | + +External SSL termination, DNS, and CDN are assumed to live in front of Nginx (CloudFlare / nginx-proxy / similar). + +> [!note] +> Dev uses `backend/docker-compose.dev.yml` (no Nginx, no frontend container — developer runs FE locally with `yarn dev`). See [[Docker Setup]] for full compose breakdown. + +--- + +## 4. Network ports + +| Port | Process | Visibility | Notes | +|---|---|---|---| +| 8083 | Frontend (Docker) | Public via Nginx | `next start` standalone | +| 3000 | Frontend (local dev) | localhost | `yarn dev` — hot reload | +| 5001 | Backend (Express + Socket.IO) | Public via Nginx `/api` & `/socket.io` | One process, two protocols | +| 27017 | MongoDB | Internal | Mapped to host only in dev | +| 6379 | Redis | Internal | Mapped to host only in dev | +| 222 | Gitea SSH | Public | Used for `git clone` | + +--- + +## 5. Data flow patterns + +### 5.1 Read path + +REST `GET` requests are cache-able. React Query on the frontend de-duplicates concurrent requests, retries on failure (exponential backoff), and treats data as stale after `staleTime` (default 60s, varies per query). Backend reads use Mongoose lean queries where possible for performance. + +### 5.2 Write path + +Mutations follow optimistic-then-confirm: +1. Frontend `useMutation` issues the request, often with `onMutate` updating cache optimistically. +2. Backend validates → writes to MongoDB → emits affected socket rooms → returns canonical response. +3. Frontend invalidates related query keys; React Query refetches. +4. Other connected clients receive the socket event and refetch on their side. + +### 5.3 Webhook path (inbound) + +External services (SHKeeper) POST to `/api/payment/shkeeper/webhook`. The backend verifies HMAC signature, updates the `Payment` document, advances any linked `PurchaseRequest`/`SellerOffer` state, and emits Socket.IO events to both buyer and seller rooms. + +```mermaid +sequenceDiagram + participant SHK as SHKeeper + participant BE as Backend + participant DB as MongoDB + participant Buyer + participant Seller + SHK->>BE: POST /api/payment/shkeeper/webhook
X-Signature: HMAC-SHA256 + BE->>BE: verifySignature(body, header, SHKEEPER_WEBHOOK_SECRET) + BE->>DB: Payment.updateOne({providerPaymentId}, {status:"completed"}) + BE->>DB: PurchaseRequest.updateOne(..., {status:"funded"}) + BE-->>Buyer: socket emit "payment:status-updated" + BE-->>Seller: socket emit "request:funded" + BE-->>SHK: 200 OK +``` + +See [[Payment Flow - SHKeeper]] for the full sequence. + +--- + +## 6. Scaling considerations + +| Concern | Current state | Scale-out path | +|---|---|---| +| Backend stateless? | Yes — JWT-only auth, no in-memory session | Run N replicas behind LB; use Redis pub/sub adapter for Socket.IO | +| MongoDB | Single-node | Replica set → sharding by `buyerId` | +| Redis | Single-node | Cluster mode; separate cache vs session DBs | +| Socket.IO | Single process | `@socket.io/redis-adapter` for multi-node fan-out | +| File uploads | Local `uploads/` mount | S3 / R2; multer-s3 adapter | +| Logs | Container stdout | Loki / ELK | +| Errors | Sentry (`@sentry/nextjs` on FE) | Add Sentry backend SDK | + +> [!warning] +> Before horizontal-scaling backend, switch Socket.IO to the Redis adapter — otherwise users on different nodes will not receive each other's events. + +--- + +## 7. Cross-cutting concerns + +- **Auth** — Bearer JWT on REST, same token also passed via Socket.IO `auth` payload on connect. See [[Security Architecture]] & [[Authentication Flow]]. +- **i18n** — Frontend supports en/fa/ar/fr/cn/vi via `i18next`. RTL handled by `stylis-plugin-rtl` per direction. See [[Internationalization & RTL]]. +- **Realtime** — All notification/chat/payment state changes broadcast over Socket.IO. See [[Real-time Layer]]. +- **Errors** — Standardized envelope `{ success: false, error: { code, message, details } }` via `backend/src/shared/utils/response-handler.ts`. Sentry on frontend. +- **Idempotency** — Payment webhooks idempotent by `providerPaymentId` + status. Pay-out uses a client-supplied `reference` string. + +--- + +## 8. Related documents + +- [[Backend Architecture]] — module-level walk-through of the Express app +- [[Frontend Architecture]] — Next.js App Router & section organization +- [[Infrastructure]] — Docker, compose, registry, Watchtower +- [[Real-time Layer]] — Socket.IO setup, rooms, events +- [[Security Architecture]] — auth, hashing, rate-limit, webhook HMAC +- [[Tech Stack]] — exact versions & purpose of every dependency +- [[Payment Flow - SHKeeper]] — end-to-end crypto pay-in flow diff --git a/02 - Data Models/Address.md b/02 - Data Models/Address.md new file mode 100644 index 0000000..d4ec3dc --- /dev/null +++ b/02 - Data Models/Address.md @@ -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]]. diff --git a/02 - Data Models/BlogPost.md b/02 - Data Models/BlogPost.md new file mode 100644 index 0000000..372d72d --- /dev/null +++ b/02 - Data Models/BlogPost.md @@ -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-` 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]]. diff --git a/02 - Data Models/Category.md b/02 - Data Models/Category.md new file mode 100644 index 0000000..db89d28 --- /dev/null +++ b/02 - Data Models/Category.md @@ -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]]. diff --git a/02 - Data Models/Chat.md b/02 - Data Models/Chat.md new file mode 100644 index 0000000..fb84783 --- /dev/null +++ b/02 - Data Models/Chat.md @@ -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` | 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]]. diff --git a/02 - Data Models/Data Model Overview.md b/02 - Data Models/Data Model Overview.md new file mode 100644 index 0000000..a6cd555 --- /dev/null +++ b/02 - Data Models/Data Model Overview.md @@ -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` 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/.ts:`. diff --git a/02 - Data Models/Dispute.md b/02 - Data Models/Dispute.md new file mode 100644 index 0000000..6c671f5 --- /dev/null +++ b/02 - Data Models/Dispute.md @@ -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]]. diff --git a/02 - Data Models/LevelConfig.md b/02 - Data Models/LevelConfig.md new file mode 100644 index 0000000..e636a97 --- /dev/null +++ b/02 - Data Models/LevelConfig.md @@ -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]]. diff --git a/02 - Data Models/Notification.md b/02 - Data Models/Notification.md new file mode 100644 index 0000000..6c2fa42 --- /dev/null +++ b/02 - Data Models/Notification.md @@ -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]]. diff --git a/02 - Data Models/Payment.md b/02 - Data Models/Payment.md new file mode 100644 index 0000000..2c3da4c --- /dev/null +++ b/02 - Data Models/Payment.md @@ -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-` | `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]]. diff --git a/02 - Data Models/PointTransaction.md b/02 - Data Models/PointTransaction.md new file mode 100644 index 0000000..9b6b9ca --- /dev/null +++ b/02 - Data Models/PointTransaction.md @@ -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]]. diff --git a/02 - Data Models/PurchaseRequest.md b/02 - Data Models/PurchaseRequest.md new file mode 100644 index 0000000..29f46a7 --- /dev/null +++ b/02 - Data Models/PurchaseRequest.md @@ -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]]. diff --git a/02 - Data Models/RequestTemplate.md b/02 - Data Models/RequestTemplate.md new file mode 100644 index 0000000..f1d96e8 --- /dev/null +++ b/02 - Data Models/RequestTemplate.md @@ -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]]. diff --git a/02 - Data Models/Review.md b/02 - Data Models/Review.md new file mode 100644 index 0000000..f5d405f --- /dev/null +++ b/02 - Data Models/Review.md @@ -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]]. diff --git a/02 - Data Models/SellerOffer.md b/02 - Data Models/SellerOffer.md new file mode 100644 index 0000000..f900bdd --- /dev/null +++ b/02 - Data Models/SellerOffer.md @@ -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]]. diff --git a/02 - Data Models/ShopSettings.md b/02 - Data Models/ShopSettings.md new file mode 100644 index 0000000..1f4b3aa --- /dev/null +++ b/02 - Data Models/ShopSettings.md @@ -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]]. diff --git a/02 - Data Models/TempVerification.md b/02 - Data Models/TempVerification.md new file mode 100644 index 0000000..a2e3d0b --- /dev/null +++ b/02 - Data Models/TempVerification.md @@ -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]]. diff --git a/02 - Data Models/User.md b/02 - Data Models/User.md new file mode 100644 index 0000000..f3fd79c --- /dev/null +++ b/02 - Data Models/User.md @@ -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]]. diff --git a/03 - API Reference/AI API.md b/03 - API Reference/AI API.md new file mode 100644 index 0000000..93c4bf9 --- /dev/null +++ b/03 - API Reference/AI API.md @@ -0,0 +1,118 @@ +--- +title: AI API +tags: [api, ai, reference] +--- + +# AI API + +Endpoints live under `/api/ai/*`. The router is [`backend/src/services/ai/aiRoutes.ts`](../../backend/src/services/ai/aiRoutes.ts) and delegates to [`aiController`](../../backend/src/services/ai/aiController.ts), which wraps the OpenAI SDK. + +The AI service is intentionally generic — there are no resource-specific endpoints (no "generate product description" route). Instead, four general-purpose endpoints (`generate`, `analyze`, `translate`, `assist`) accept a `purpose` field that the controller maps to a system prompt template. The frontend uses these for: + +- Product description generation (purpose: `description`) +- Content moderation (purpose: `moderation` on `/analyze`) +- Product recommendations (purpose: `recommend` on `/generate`) +- In-app helper (`/assist`) + +> Note: the router currently exposes these endpoints **without** `authenticateToken`. Front-end usage assumes the user is logged in. If you need to add auth, do it in the router. OpenAI cost-protection and rate-limiting are handled inside `aiController` via the Redis-backed `rateLimitService`. + +## Endpoints + +### POST /api/ai/generate + +**Description:** Generate free-form text. Used for product descriptions, marketing copy, recommendation rationales. +**Auth required:** No (intended for authenticated frontends) +**Request body:** +```ts +{ + prompt: string; + purpose?: "description" | "recommend" | "summary" | "generic"; // default "generic" + context?: Record; // injected into the system prompt + maxTokens?: number; // default 512, capped server-side + temperature?: number; // 0..2, default 0.7 + language?: "en" | "fa" | "ar"; // default "en" +} +``` +**Response 200:** +```json +{ + "success": true, + "data": { + "text": "...", + "model": "gpt-4o-mini", + "tokensUsed": { "prompt": 120, "completion": 340, "total": 460 } + } +} +``` +**Errors:** `400` invalid params, `429` quota exceeded, `502` upstream OpenAI error. + +### POST /api/ai/analyze + +**Description:** Analyze a piece of text. Used for moderation (flag offensive content), sentiment, keyword extraction. +**Auth required:** No +**Request body:** +```ts +{ + text: string; + purpose?: "moderation" | "sentiment" | "keywords" | "summary"; // default "moderation" + language?: "en" | "fa" | "ar"; +} +``` +**Response 200 (moderation example):** +```json +{ + "success": true, + "data": { + "purpose": "moderation", + "flagged": false, + "categories": { "hate": 0.001, "harassment": 0.0 }, + "suggestion": null + } +} +``` +**Response 200 (sentiment example):** +```json +{ "success": true, "data": { "purpose": "sentiment", "score": 0.62, "label": "positive" } } +``` + +### POST /api/ai/translate + +**Description:** Translate `text` between languages. +**Auth required:** No +**Request body:** +```ts +{ + text: string; + from?: "en" | "fa" | "ar" | "auto"; // default "auto" + to: "en" | "fa" | "ar"; +} +``` +**Response 200:** `{ success, data: { translated: "...", detectedFrom: "en" } }` + +### POST /api/ai/assist + +**Description:** In-app conversational assistant. Maintains short-term context per call (the client passes prior turns explicitly). +**Auth required:** No +**Request body:** +```ts +{ + messages: Array<{ role: "user" | "assistant" | "system"; content: string }>; + purpose?: "help" | "search" | "navigate"; // tunes the system prompt + context?: { userId?: string; route?: string; locale?: string }; +} +``` +**Response 200:** `{ success, data: { reply: { role: "assistant", content: "..." }, tokensUsed } }` + +## Configuration + +The OpenAI client is configured via env: + +- `OPENAI_API_KEY` (required) +- `OPENAI_MODEL` (default `gpt-4o-mini`) +- `OPENAI_BASE_URL` (override for self-hosted proxies) + +## Related + +- [[AI Integration]] +- [[Moderation Pipeline]] +- [[Rate Limiting]] diff --git a/03 - API Reference/API Overview.md b/03 - API Reference/API Overview.md new file mode 100644 index 0000000..5d573f1 --- /dev/null +++ b/03 - API Reference/API Overview.md @@ -0,0 +1,173 @@ +--- +title: API Overview +tags: [api, reference, overview] +--- + +# API Overview + +The AMN backend is an Express.js + TypeScript service that exposes a REST API plus a Socket.IO real-time channel. Every HTTP endpoint is mounted under `/api//...` in [`backend/src/app.ts`](../../backend/src/app.ts) and follows a consistent response envelope, authentication scheme, and error model. + +This page is the entry point for the API. See the individual service pages for endpoint details: + +- [[Authentication API]] - register/login/passkeys/Google OAuth +- [[User API]] - profile, wallet, admin user management +- [[Marketplace API]] - purchase requests, seller offers, templates, shop, reviews +- [[Payment API]] - SHKeeper, Web3, DePay, payouts +- [[Chat API]] - conversations and messages +- [[Notification API]] - in-app notifications +- [[Dispute API]] - dispute resolution +- [[Blog API]] - blog posts (admin-managed) +- [[Admin API]] - user management, data cleanup, manual ops +- [[Points API]] - loyalty points, levels, referrals +- [[AI API]] - OpenAI-backed text endpoints +- [[File API]] - upload, delete, serve +- [[Socket Events]] - real-time events +- [[Error Codes]] - status mapping and error shape + +## Base URLs + +| Environment | Base URL | +| --- | --- | +| Local development | `http://localhost:5001/api` | +| Staging / dev | `https://dev.amn.gg/api` | +| Production | `https://amn.gg/api` | + +The base port is set via `PORT` env var; in `development` it defaults to `5001`. CORS is restricted to `process.env.FRONTEND_URL` and credentials are allowed (`cors({ origin, credentials: true })` in `app.ts`). + +Health check (not under `/api`): `GET /health` → `{ success, message, timestamp, environment, version }`. + +API discovery endpoint: `GET /api` → returns a map of available service prefixes. + +## Versioning + +The API is currently un-versioned (there is no `/v1` in paths). Breaking changes are communicated via release notes in the repository. The version string is exposed on `/health` and is sourced from `package.json` (`process.env.npm_package_version`). At the time of writing the deployed version is in the `4.x` line. + +## Authentication + +All protected endpoints use a stateless JWT in the standard HTTP Authorization header: + +``` +Authorization: Bearer +``` + +The token is verified by `authenticateToken` in [`backend/src/shared/middleware/auth.ts`](../../backend/src/shared/middleware/auth.ts) using `config.jwtSecret`. The decoded payload is normalised into `req.user = { id, email, role }` regardless of which key carried it (`id`, `userId`, `_id`, `sub`). + +Tokens are issued by `POST /api/auth/login`, `POST /api/auth/google/signin`, `POST /api/auth/google/signup`, `POST /api/auth/passkey/authenticate`, and `POST /api/auth/refresh-token`. Refresh tokens are stored on the [[User]] document (`refreshTokens` array) so an admin password change wipes them and forces re-login. + +Role-based access uses `authorizeRoles('admin', ...)` after `authenticateToken`. Three roles exist: `buyer`, `seller`, `admin`. + +WebAuthn / Passkey flows live under `/api/auth/passkey/*` and exchange a challenge for an assertion; on success a regular JWT pair is returned (`tokens.accessToken`, `tokens.refreshToken`). See [[Authentication Flow]] for the full lifecycle. + +## Standard response envelope + +The canonical helper is `ResponseHandler` in [`backend/src/shared/utils/response-handler.ts`](../../backend/src/shared/utils/response-handler.ts). Successful responses look like: + +```json +{ + "success": true, + "message": "Success", + "data": { /* payload */ }, + "statusCode": 200, + "timestamp": "2026-05-23T10:00:00.000Z", + "path": "/api/marketplace/purchase-requests", + "method": "GET", + "meta": { /* optional */ } +} +``` + +Paginated responses add a `pagination` block: + +```json +{ + "success": true, + "data": [ /* items */ ], + "pagination": { + "page": 1, + "limit": 20, + "total": 137, + "totalPages": 7, + "hasMore": true + } +} +``` + +Error responses use the same envelope with `success: false`: + +```json +{ + "success": false, + "message": "Validation failed", + "error": "Validation Error", + "statusCode": 400, + "timestamp": "2026-05-23T10:00:00.000Z", + "path": "/api/auth/register", + "method": "POST", + "data": [ /* validation details */ ] +} +``` + +Caveat: not every endpoint uses `ResponseHandler`. Legacy routes (mainly under `/api/users`, `/api/marketplace` legacy, `/api/payment/decentralized`, parts of `/api/payment/shkeeper`) return ad-hoc shapes such as `{ error: string }` on failure or bare objects on success. When in doubt consult the per-endpoint response section in the service pages. The error middleware in [`shared/middleware/errorHandler.ts`](../../backend/src/shared/middleware/errorHandler.ts) covers uncaught exceptions and emits `{ success: false, error, statusCode, stack? }`. + +## Pagination conventions + +Most list endpoints accept `?page=&limit=` query params: + +- `page` defaults to `1`, 1-based. +- `limit` defaults vary (20 for notifications/marketplace, 50 for user lists) and is capped (e.g. 100 for templates). +- Server computes `total`, `totalPages`, and `hasMore` and returns them under `pagination`. +- Some endpoints also accept `offset` (e.g. payment lists) as a raw skip count. + +Sort parameters: list endpoints commonly accept `sortBy` (default `createdAt`) and `sortOrder` (`asc` | `desc`, default `desc`). + +## Common query params + +| Parameter | Used by | Meaning | +| --- | --- | --- | +| `search` | users, templates, posts | Case-insensitive regex over title/name/email fields | +| `status` | payments, requests, disputes, notifications | Filter by entity state | +| `role` | users, contacts | Filter by `buyer`/`seller`/`admin` | +| `isActive` | users, templates | Boolean filter | +| `categoryId` | templates, requests | MongoId category filter | +| `unreadOnly` | notifications | Only unread items | +| `format` | payment export | `json` (default) or `csv` | + +## Rate limiting + +Rate limiting is currently **disabled** in the deployed code (`app.ts` logs `🔓 Rate limiting COMPLETELY DISABLED for personal use`). The intended policy when re-enabled is **100 requests per 15-minute window per IP**, applied per `/api/*` route. The Express `trust proxy` setting is enabled in production so the real client IP is read from `X-Forwarded-For` (Nginx terminator). + +Redis-backed rate limiting helpers exist in `src/services/redis/rateLimitService.ts` and are used by sensitive auth flows (password reset, email verification) even with the global limiter off. + +## CORS + +CORS is configured globally in `app.ts`: + +```ts +cors({ + origin: process.env.FRONTEND_URL, + credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"], +}) +``` + +Only the configured `FRONTEND_URL` may make cross-origin requests with credentials. The SHKeeper configuration endpoint (`GET /api/payment/shkeeper/config`) overrides this with `Access-Control-Allow-Origin: *` because it is consumed by the SHKeeper payment widget hosted on another domain. + +Uploaded files served from `/uploads/*` use `helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })` so they can be embedded from the frontend domain. + +## Real-time channel + +Socket.IO runs on the same HTTP server. The frontend should connect to the API origin with credentials. Clients join rooms via `join-user-room`, `join-request-room`, `join-seller-room`, `join-buyer-room`, `join-chat-room`. Full catalog: [[Socket Events]]. + +## Standard HTTP status codes + +- `200 OK` - success +- `201 Created` - resource created +- `400 Bad Request` - validation error +- `401 Unauthorized` - missing/invalid token +- `403 Forbidden` - role/ownership failure +- `404 Not Found` - resource or route missing +- `409 Conflict` - duplicate (email, review) +- `423 Locked` - account locked (auth flows) +- `500 Internal Server Error` - unhandled exception + +See [[Error Codes]] for the app-specific codes returned in the `error` field. diff --git a/03 - API Reference/Admin API.md b/03 - API Reference/Admin API.md new file mode 100644 index 0000000..b1b0316 --- /dev/null +++ b/03 - API Reference/Admin API.md @@ -0,0 +1,143 @@ +--- +title: Admin API +tags: [api, admin, reference] +--- + +# Admin API + +There is no single `/api/admin` namespace — admin-only endpoints are scattered across the service routers. This page catalogs them in one place. All require `Bearer JWT` with `req.user.role === 'admin'`. The two enforcement patterns are: + +- Middleware: `authorizeRoles('admin')` after `authenticateToken` (used by the dispute, data-cleanup, blog routers). +- Inline check inside the handler: `if (req.user.role !== 'admin') return 403` (used by user, points, payment routes). + +## User management + +See full descriptions in [[User API]]. + +| Endpoint | Action | +| --- | --- | +| `POST /api/user/admin/create` | Create user with role/status | +| `DELETE /api/user/admin/:userId` | Delete user (admins cannot delete each other) | +| `PATCH /api/user/admin/:userId/status` | Activate / suspend | +| `PATCH /api/user/admin/:userId/toggle-status` | Flip active flag | +| `PATCH /api/user/admin/:userId/role` | Change role | +| `GET /api/user/admin/list` | Paginated directory + stats | +| `GET /api/user/admin/:userId/dependencies` | Pre-delete dependency check | +| `GET /api/users/admin/stats` | Aggregate user analytics | +| `GET /api/users/admin/:userId` | Full user detail (admin view) | +| `PUT /api/users/admin/:userId` | Mass update user | +| `PUT /api/users/admin/update/:email` | Mass update by email | +| `PATCH /api/users/admin/:userId/password` | Force password reset (wipes refresh tokens) | +| `POST /api/users/admin/:userId/resend-verification` | Resend verification email | + +## Listing / marketplace moderation + +See [[Marketplace API]]. Admins can use most marketplace endpoints with elevated privileges (e.g. delete any purchase request, override offer status). Specific admin-only actions: + +| Endpoint | Action | +| --- | --- | +| `PUT /api/marketplace/offers/:id/status` | Direct status mutation including admin overrides | +| `POST /api/marketplace/purchase-requests/:id/release-payment` | Force escrow release | +| `PATCH /api/marketplace/purchase-requests/:id/status` (any → any) | Override request state machine | + +Template approval is implicit: admins use the same template CRUD endpoints with override privileges. + +## Dispute mediation + +See [[Dispute API]]. + +| Endpoint | Action | +| --- | --- | +| `POST /api/disputes/:id/assign` | Assign moderator | +| `PATCH /api/disputes/:id/status` | Update status | +| `POST /api/disputes/:id/resolve` | Final decision (buyer / seller / split) | +| `GET /api/disputes/statistics` | Admin dashboard data | + +## Manual payment operations + +See [[Payment API]]. + +| Endpoint | Action | +| --- | --- | +| `POST /api/payment/payments/cleanup-pending` | Delete stale pending payments | +| `POST /api/payment/payments/:id/fetch-tx` | Re-query chain for missing tx hash | +| `POST /api/payment/payments/auto-fetch-missing` | Batch tx-hash backfill | +| `POST /api/payment/shkeeper/:id/release` | Build escrow-release tx | +| `POST /api/payment/shkeeper/:id/release/confirm` | Confirm release tx hash | +| `POST /api/payment/shkeeper/:id/refund` | Build refund tx | +| `POST /api/payment/shkeeper/:id/refund/confirm` | Confirm refund tx hash | +| `POST /api/payment/shkeeper/payout` | Create payout task | +| `GET /api/payment/shkeeper/webhook-stats` | Webhook telemetry | +| `POST /api/payment/decentralized/admin-payout` | Direct admin-wallet payout | + +## Points (admin) + +See [[Points API]]. + +| Endpoint | Action | +| --- | --- | +| `POST /api/points/admin/add` | Manually grant / deduct points for a user | + +## Data cleanup + +Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/services/admin/dataCleanupRoutes.ts). Mounted under `/api/admin/cleanup/*`. The router applies `authenticateToken` + `authorizeRoles('admin')` to every endpoint. + +### GET /api/admin/cleanup/stats + +**Description:** Per-collection document counts and sizes. +**Response 200:** `{ success, data: { collections: [{ name, count, sizeBytes }] } }` + +### GET /api/admin/cleanup/collections + +**Description:** List collections that can be cleaned and the supported flags. +**Response 200:** `{ success, data: { collections, options } }` + +### POST /api/admin/cleanup/clean + +**Description:** Bulk delete records. Defaults to `dryRun: true` and `keepAdmins: true`. +**Request body:** +```ts +{ + collections?: string[]; // default ["all"] + dryRun?: boolean; // default true + keepAdmins?: boolean; // default true + olderThanDays?: number; // optional age filter + confirm?: "DELETE_ALL_DATA"; // required for actual deletion +} +``` +**Response 200:** `{ success, data: { deletedCounts, dryRun } }` + +### DELETE /api/admin/cleanup/user/:userId + +**Description:** Cascade-delete all data for a specific user (GDPR). Requires `?confirm=DELETE_USER_DATA` for real execution. +**Query params:** `dryRun=true|false`, `confirm=DELETE_USER_DATA` + +### POST /api/admin/cleanup/temp + +**Description:** Purge temporary data older than N hours (verification codes, file temp uploads). +**Request body:** `{ olderThanHours?: number }` (default 24) + +### POST /api/admin/cleanup/seed-templates + +**Description:** Re-runs the request templates seeder (production safe; idempotent). + +### POST /api/admin/cleanup/seed-all + +**Description:** Seeds users, addresses, and templates in dependency order. Used to bootstrap a fresh staging environment. + +## Analytics + +There is no dedicated analytics router. Admin dashboards stitch together: + +- `GET /api/users/admin/stats` (user metrics) +- `GET /api/payment/stats` (payment aggregates) +- `GET /api/disputes/statistics` (dispute KPIs) +- `GET /api/admin/cleanup/stats` (collection sizes) +- `GET /api/payment/shkeeper/webhook-stats` (provider health) +- `GET /api/payment/shkeeper/wallet-monitor/status` (chain monitor) + +## Related + +- [[Admin Console Architecture]] +- [[Authorization Model]] +- [[Error Codes]] diff --git a/03 - API Reference/Authentication API.md b/03 - API Reference/Authentication API.md new file mode 100644 index 0000000..a4d1629 --- /dev/null +++ b/03 - API Reference/Authentication API.md @@ -0,0 +1,305 @@ +--- +title: Authentication API +tags: [api, auth, reference] +--- + +# Authentication API + +All endpoints are mounted under `/api/auth/*` in `backend/src/app.ts`. The routes file is [`backend/src/services/auth/authRoutes.ts`](../../backend/src/services/auth/authRoutes.ts) and the WebAuthn sub-routes are in [`passkeyRoutes.ts`](../../backend/src/services/auth/passkeyRoutes.ts). Controller logic lives in [`authController.ts`](../../backend/src/services/auth/authController.ts) and [`authService.ts`](../../backend/src/services/auth/authService.ts). + +Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[TempVerification]] document that holds pending registration data until the email code is confirmed. Tokens are signed JWTs (access + refresh) created in `authService`. See [[Authentication Flow]] for the high-level lifecycle diagram. + +## Registration + +### POST /api/auth/register + +**Description:** Start a new registration. Creates a [[TempVerification]] document and emails an 8-digit verification code. The actual [[User]] is only created once the code is verified. +**Auth required:** No +**Request body:** +```ts +{ + email: string; + firstName?: string; // default "کاربر" + lastName?: string; // default "جدید" + role?: "buyer" | "seller"; // default "buyer" + password?: string; // accepted now or at verify-email-code time + referralCode?: string; // optional, links to referrer for [[Points API]] +} +``` +**Response 200:** +```json +{ "success": true, "message": "Verification code sent ...", "data": { "email": "x@y.z" } } +``` +**Errors:** `400` validation, `409 USER_EXISTS` if email already registered. +**Side effects:** +- Upserts a `TempVerification` row. +- Sends `emailService.sendVerificationCodeEmail`. +- No socket emission yet (user does not exist). +**Source:** `authController.register` + +### POST /api/auth/verify-email-code + +**Description:** Confirms the registration code. Creates the [[User]], deletes the `TempVerification`, processes any referral, issues JWT tokens, and starts the session in Redis. +**Auth required:** No +**Request body:** +```ts +{ + email: string; + code: string; // 8 digits + password?: string; // required if not provided at register +} +``` +**Response 200:** +```json +{ + "success": true, + "message": "Email verified", + "data": { + "user": { "_id": "...", "email": "...", "role": "buyer", ... }, + "tokens": { "accessToken": "...", "refreshToken": "..." } + } +} +``` +**Errors:** `400` invalid/expired code, `404` no pending verification, `409` email already taken. +**Side effects:** +- Creates [[User]], deletes `TempVerification`. +- Calls [[Points API]] referral hook when `referralCode` was supplied (emits `referral-signup` to `user-`). +- Stores refresh token in `user.refreshTokens`. +- Welcome email via `emailService.sendWelcomeEmail`. + +### GET /api/auth/verify-email/:token + +**Description:** Legacy URL-based verification (link in email). Marks `isEmailVerified=true`. +**Auth required:** No +**Response 200:** `{ "success": true, "message": "Email verified successfully" }` +**Errors:** `400` invalid/expired token. + +### POST /api/auth/resend-verification + +**Description:** Re-issues the 8-digit code for a pending or unverified user. +**Auth required:** No +**Request body:** `{ email: string }` +**Response 200:** `{ "success": true, "message": "Verification code resent" }` +**Errors:** `400` invalid email, `404` no user/temp record, `429` rate-limited (Redis). + +### POST /api/auth/force-verify-user + +**Description:** Development-only helper to mark a user verified without going through email. +**Auth required:** No (intended for dev only — gate with env in prod) + +## Login & sessions + +### POST /api/auth/login + +**Description:** Email/password login. Validates credentials with bcrypt, signs JWT pair, stores refresh token on user, returns sanitized user object. +**Auth required:** No +**Request body:** +```ts +{ + email: string; + password: string; +} +``` +**Response 200:** +```json +{ + "success": true, + "message": "Login successful", + "data": { + "user": { "_id": "...", "email": "...", "role": "buyer", "firstName": "..." }, + "tokens": { "accessToken": "...", "refreshToken": "..." } + } +} +``` +**Errors:** +- `400` validation +- `401` invalid credentials +- `403` email not verified +- `423` account locked (after repeated failures, tracked in Redis via `rateLimitService`) +**Side effects:** +- Updates `user.lastLoginAt`. +- Pushes refresh token onto `user.refreshTokens`. +- Redis session start via `sessionService`. + +### POST /api/auth/refresh-token + +**Description:** Exchanges a refresh token for a new access token. Rotates the refresh token. +**Auth required:** No (refresh token in body) +**Request body:** `{ refreshToken: string }` +**Response 200:** `{ "success": true, "data": { "tokens": { "accessToken": "...", "refreshToken": "..." } } }` +**Errors:** `401` token expired / not present in user record, `403` user disabled. + +### POST /api/auth/logout + +**Description:** Removes the current refresh token from the user record and clears the Redis session. +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "message": "Logged out" }` +**Side effects:** `redisService` session removed. + +## Google OAuth + +### POST /api/auth/google/signup + +**Description:** Verifies a Google ID token, creates a new [[User]] (no password), optionally links a referral, returns JWT tokens. +**Auth required:** No +**Request body:** +```ts +{ + googleToken: string; + role?: "buyer" | "seller"; + referralCode?: string; +} +``` +**Response 200:** Same shape as `verify-email-code` response. +**Errors:** `400` invalid Google token, `409` email already registered (suggest sign-in instead). + +### POST /api/auth/google/signin + +**Description:** Verifies a Google ID token and signs in an existing user. Will not create a new account. +**Auth required:** No +**Request body:** `{ googleToken: string }` +**Response 200:** `{ success, data: { user, tokens } }` +**Errors:** `400` invalid token, `404` no user with that Google email. + +## Passkey / WebAuthn + +Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyService.ts`. + +### POST /api/auth/passkey/authenticate/challenge + +**Description:** Generates a sign-in challenge that the browser/authenticator will sign. No `userId` required — the assertion will identify the user. +**Auth required:** No +**Response 200:** `{ "success": true, "challenge": { /* PublicKeyCredentialRequestOptions */ } }` + +### POST /api/auth/passkey/authenticate + +**Description:** Verifies the WebAuthn assertion and, on success, returns a JWT pair. +**Auth required:** No +**Request body:** `{ challenge, assertion }` (assertion is the browser's `navigator.credentials.get()` output). +**Response 200:** `{ "success": true, "userId": "...", "user": { ... }, "tokens": { ... } }` +**Errors:** `400` missing fields, `404` `Passkey not found`, `500` verification error. + +### POST /api/auth/passkey/register/challenge + +**Description:** Generates a registration challenge for the authenticated user. +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "challenge": { /* PublicKeyCredentialCreationOptions */ } }` + +### POST /api/auth/passkey/register + +**Description:** Verifies a new passkey registration and stores the credential on the user. +**Auth required:** Bearer JWT +**Request body:** `{ challenge, credential }` +**Response 200:** `{ "success": true, "message": "Passkey registered successfully" }` + +### GET /api/auth/passkey/list + +**Description:** Returns the calling user's registered passkeys (id, label, created date). +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "passkeys": [...] }` + +### DELETE /api/auth/passkey/:passkeyId + +**Description:** Removes a passkey by id. +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "message": "Passkey removed successfully" }` + +## Password management + +### POST /api/auth/request-password-reset + +**Description:** Generates a reset token, stores it on the user, and emails a reset link plus a numeric code. +**Auth required:** No +**Request body:** `{ email: string }` +**Response 200:** `{ "success": true, "message": "Password reset email sent" }` (always returns success to avoid email enumeration). +**Side effects:** `emailService.sendPasswordResetEmail`; rate-limited per IP via Redis. + +### POST /api/auth/reset-password + +**Description:** Sets a new password using a token from the reset email. Wipes refresh tokens. +**Auth required:** No +**Request body:** +```ts +{ + token: string; + password: string; // 6+ chars, mixed case + digit +} +``` +**Response 200:** `{ "success": true, "message": "Password updated" }` +**Errors:** `400` invalid/expired token or weak password. + +### POST /api/auth/reset-password-with-code + +**Description:** Alternative reset flow using a numeric code instead of a tokenised URL. +**Auth required:** No +**Request body:** `{ email, code, password }` +**Response 200:** `{ "success": true }` + +### POST /api/auth/change-password + +**Description:** Authenticated password change. +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + currentPassword: string; + newPassword: string; // 6+ chars, mixed case + digit +} +``` +**Response 200:** `{ "success": true, "message": "Password updated" }` +**Errors:** `400` validation, `401` wrong current password. +**Side effects:** Clears `user.refreshTokens` (forces re-login on other devices). + +## Current user / profile + +### GET /api/auth/profile + +**Description:** Returns the full sanitized [[User]] document for the caller. +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "data": { /* User */ } }` + +### PUT /api/auth/profile (and POST /api/auth/update-profile) + +**Description:** Updates the caller's profile (first/last name, phone, bio, website, language/currency preferences). +**Auth required:** Bearer JWT +**Request body:** Partial profile, see `updateProfileValidation`: +```ts +{ + firstName?: string; // 2-50 + lastName?: string; // 2-50 + profile?: { + phone?: string; // E.164-ish + bio?: string; // <=500 + website?: string; // URL + }; + preferences?: { + language?: "en" | "fa" | "ar"; + currency?: "USD" | "EUR" | "IRR" | "AED"; + }; +} +``` +**Response 200:** Updated user. +**Errors:** `400` validation. + +## Account deletion + +### DELETE /api/auth/account + +**Description:** Permanently deletes the caller's account after re-authenticating with password. +**Auth required:** Bearer JWT +**Request body:** `{ password: string }` +**Response 200:** `{ "success": true, "message": "Account deleted" }` +**Errors:** `401` bad password. +**Side effects:** Removes [[User]] document, clears Redis session, cascades configured by `dataCleanupService`. + +## Error codes summary + +| HTTP | App code | Meaning | +| --- | --- | --- | +| 400 | `Validation Error` | `express-validator` rejected the body | +| 401 | — | Bad credentials / missing token | +| 403 | — | Email not verified or insufficient role | +| 409 | `USER_EXISTS` | Email already in use | +| 423 | — | Account temporarily locked after failed logins | + +See [[Error Codes]] for the global error shape. diff --git a/03 - API Reference/Blog API.md b/03 - API Reference/Blog API.md new file mode 100644 index 0000000..c9b82cf --- /dev/null +++ b/03 - API Reference/Blog API.md @@ -0,0 +1,125 @@ +--- +title: Blog API +tags: [api, blog, reference] +--- + +# Blog API + +Endpoints live under `/api/blog/*`. The router is [`backend/src/routes/blogRoutes.ts`](../../backend/src/routes/blogRoutes.ts) and delegates to [`blogController`](../../backend/src/services/blog/blogController.ts). + +Public endpoints (post listing and reads) are open. Mutating endpoints (create/update/delete) require **admin** role — enforced via `authenticateToken` + `authorizeRoles('admin')`. + +Model: [[BlogPost]]. Image uploads use the [[File API]] (`POST /api/files/upload/blog-images`). + +## Public reads + +### GET /api/blog/posts + +**Description:** Paginated list of published blog posts. +**Auth required:** No +**Query params:** +- `page` (default 1) +- `limit` (default 10) +- `category` +- `tag` +- `search` (matches title/excerpt/content) +- `sortBy` (default `publishedAt`) +- `sortOrder` (`asc` | `desc`, default `desc`) +**Response 200:** +```json +{ + "success": true, + "data": { + "posts": [BlogPost, ...], + "pagination": { "page": 1, "limit": 10, "total": 25, "totalPages": 3, "hasMore": true } + } +} +``` + +### GET /api/blog/posts/featured + +**Description:** Posts with `isFeatured: true`. Used by the home page hero strip. +**Auth required:** No +**Response 200:** `{ success, data: { posts } }` + +### GET /api/blog/posts/recent + +**Description:** Most recent N published posts. Used by the footer / sidebar. +**Auth required:** No +**Query params:** `limit` (default 5) + +### GET /api/blog/posts/search + +**Description:** Full-text search across title/excerpt/content/tags. +**Auth required:** No +**Query params:** `q` (required), `page`, `limit` +**Errors:** `400` if `q` is missing or shorter than 2 characters. + +### GET /api/blog/posts/:slug + +**Description:** Get a single published post by URL slug. +**Auth required:** No +**Response 200:** `{ success, data: { post: BlogPost } }` +**Errors:** `404` not found or not published. +**Side effects:** Increments `viewsCount`. + +## Admin (mutating) + +All endpoints below require `Bearer JWT` with `role: "admin"`. + +### GET /api/blog/admin/posts + +**Description:** Admin list including drafts and unpublished posts. +**Auth required:** Bearer JWT (admin) +**Query params:** `page`, `limit`, `status` (`draft` | `published` | `archived`), `search` +**Response 200:** `{ success, data: { posts, pagination } }` + +### GET /api/blog/admin/posts/:id + +**Description:** Get a post by id (admin view, includes draft fields). +**Auth required:** Bearer JWT (admin) + +### POST /api/blog/posts + +**Description:** Create a post. +**Auth required:** Bearer JWT (admin) +**Request body:** +```ts +{ + title: string; + slug?: string; // auto-generated from title when omitted + excerpt?: string; + content: string; // Markdown / rich text + coverImage?: string; // URL from [[File API]] + category?: string; + tags?: string[]; + status?: "draft" | "published"; // default "draft" + isFeatured?: boolean; + publishedAt?: string; // ISO date, used when status === "published" + videoUrl?: string; + metadata?: Record; +} +``` +**Response 201:** `{ success, data: { post } }` + +### PUT /api/blog/posts/:id + +**Description:** Update a post. Partial; the body can contain any of the create fields. +**Auth required:** Bearer JWT (admin) + +### DELETE /api/blog/posts/:id + +**Description:** Hard-delete a post. +**Auth required:** Bearer JWT (admin) +**Response 200:** `{ success: true, message: "Post deleted" }` + +## Comments + +The codebase does not currently expose a comments API — comments are intentionally disabled. If/when added, they will appear under `/api/blog/posts/:id/comments`. + +## Related + +- [[BlogPost]] +- [[File API]] (for cover and inline image uploads) +- `backend/BLOG_API_ENDPOINTS.md` (legacy in-repo notes) +- `backend/BLOG_VIDEO_EXAMPLES.md` diff --git a/03 - API Reference/Chat API.md b/03 - API Reference/Chat API.md new file mode 100644 index 0000000..6e6146c --- /dev/null +++ b/03 - API Reference/Chat API.md @@ -0,0 +1,144 @@ +--- +title: Chat API +tags: [api, chat, reference] +--- + +# Chat API + +All chat endpoints live under `/api/chat/*`. The router is [`backend/src/services/chat/chatRoutes.ts`](../../backend/src/services/chat/chatRoutes.ts), controller is `chatController`, service is `ChatService`. Every endpoint requires `Bearer JWT` — the router applies `authenticateToken` globally. + +Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat-`. Clients must call `join-chat-room` after connecting. See [[Socket Events]] for `new-message`, `messages-read`, `message-edited`, `message-deleted`, `participants-added`, `participant-removed`, and `user-typing` payloads. + +## Conversations + +### POST /api/chat + +**Description:** Create a generic chat between two or more participants. +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + participantIds: string[]; // 2+ user ids (caller is added automatically) + type?: "direct" | "group"; // default "direct" + title?: string; // group only + metadata?: Record; +} +``` +**Response 201:** `{ success: true, data: { chat: { _id, participants, type, ... } } }` +**Errors:** `400` need 2+ participants, `404` participant not found. + +### POST /api/chat/purchase-request + +**Description:** Create (or fetch the existing) chat tied to a specific [[PurchaseRequest]] between buyer and seller. Idempotent. +**Auth required:** Bearer JWT +**Request body:** `{ purchaseRequestId: string; sellerId: string }` +**Response 200/201:** `{ success: true, data: { chat } }` + +### POST /api/chat/support + +**Description:** Open a support chat. Adds the caller and the support/admin pool. Used by the in-app help widget. +**Auth required:** Bearer JWT +**Request body:** `{ subject?: string; initialMessage?: string }` +**Response 201:** `{ success: true, data: { chat } }` + +### GET /api/chat + +**Description:** List the caller's chats, ordered by latest message. Includes unread counts. +**Auth required:** Bearer JWT +**Query params:** `page`, `limit`, `type` (`direct` | `group` | `support`) +**Response 200:** `{ success, data: { chats: [...], pagination } }` + +### GET /api/chat/stats + +**Description:** Aggregate counts (total chats, total messages, total unread). +**Auth required:** Bearer JWT + +### GET /api/chat/:id/info + +**Description:** Full chat metadata: participants (populated), title, type, created-at, last message, unread per user. +**Auth required:** Bearer JWT (participant) +**Errors:** `403` not a participant, `404` not found. + +### PATCH /api/chat/:id/archive + +**Description:** Toggle archived state for the caller (per-user flag). +**Auth required:** Bearer JWT (participant) + +### POST /api/chat/:id/participants + +**Description:** Add a participant to a group chat. +**Auth required:** Bearer JWT (creator/admin) +**Request body:** `{ userId: string }` +**Side effects:** Emits `participants-added` on `chat-`. + +### DELETE /api/chat/:id/participants/:participantId + +**Description:** Remove a participant from a group chat. +**Auth required:** Bearer JWT (creator/admin) +**Side effects:** Emits `participant-removed` on `chat-`. + +## Messages + +### GET /api/chat/:id/messages + +**Description:** Paginated message history (newest first by default). +**Auth required:** Bearer JWT (participant) +**Query params:** `page`, `limit`, `before` (cursor by createdAt) + +### POST /api/chat/:id/messages + +**Description:** Send a text message. +**Auth required:** Bearer JWT (participant) +**Request body:** +```ts +{ + content: string; + type?: "text" | "system"; // default "text" + replyToMessageId?: string; + metadata?: Record; +} +``` +**Response 201:** `{ success, data: { message } }` +**Side effects:** Emits `new-message` on `chat-`; increments unread for other participants. + +### POST /api/chat/:id/messages/file + +**Description:** Send a message with a binary attachment. Multipart form (`file` field). Maxes follow the [[File API]] limits. +**Auth required:** Bearer JWT (participant) +**Form fields:** +- `file` - the file blob +- `content?` - optional caption +- `replyToMessageId?` - optional + +**Response 201:** `{ success, data: { message: { attachments: [{ url, filename, mimeType, size }] } } }` + +### PATCH /api/chat/:id/messages/read + +**Description:** Mark all unread messages up to the latest as read for the caller. +**Auth required:** Bearer JWT (participant) +**Response 200:** `{ success, data: { modifiedCount } }` +**Side effects:** Emits `messages-read` on `chat-`. + +### PUT /api/chat/:id/messages/:messageId + +**Description:** Edit an existing message (author only, within edit window). +**Auth required:** Bearer JWT (message author) +**Request body:** `{ content: string }` +**Side effects:** Emits `message-edited` on `chat-`. + +### DELETE /api/chat/:id/messages/:messageId + +**Description:** Soft-delete a message (author or admin). The record stays but `deletedAt` is set and `content` is cleared. +**Auth required:** Bearer JWT (author or admin) +**Side effects:** Emits `message-deleted` on `chat-`. + +## Typing indicator + +Typing indicators are not REST endpoints — clients emit `typing-start` and `typing-stop` over Socket.IO. The server broadcasts `user-typing` to the chat room. Full details in [[Socket Events]]. + +## Related + +- [[Chat]] +- [[Chat Flow]] +- [[File API]] (for attachment uploads) +- [[Notification API]] (for out-of-band notifications when a chat message is received and the user is offline) diff --git a/03 - API Reference/Dispute API.md b/03 - API Reference/Dispute API.md new file mode 100644 index 0000000..2b6bd5a --- /dev/null +++ b/03 - API Reference/Dispute API.md @@ -0,0 +1,122 @@ +--- +title: Dispute API +tags: [api, dispute, reference] +--- + +# Dispute API + +Endpoints live under `/api/disputes/*`. The router is [`backend/src/routes/disputeRoutes.ts`](../../backend/src/routes/disputeRoutes.ts) and delegates to `DisputeController` ([`backend/src/controllers/disputeController.ts`](../../backend/src/controllers/disputeController.ts)). The router applies `authenticateToken` globally — every endpoint requires `Bearer JWT`. + +Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[Payment]] and is the input to the mediation workflow that ends in either a `resolved_buyer` or `resolved_seller` decision and triggers an escrow release or refund via the [[Payment API]]. + +## Create + +### POST /api/disputes + +**Description:** Open a dispute against a purchase request. +**Auth required:** Bearer JWT (buyer or seller participant in the request) +**Request body:** +```ts +{ + purchaseRequestId: string; + reason: "not_delivered" | "wrong_item" | "damaged" | "quality" | "other"; + description: string; + evidence?: string[]; // URLs from [[File API]] + paymentId?: string; +} +``` +**Response 201:** `{ success: true, data: { dispute } }` +**Errors:** `400` validation, `403` not a participant of the request, `409` dispute already open for this request. +**Side effects:** +- Notifies the counter-party via `POST /api/notifications` (`new-notification` socket event). +- Pauses any in-flight payout (sets a hold flag on the related [[Payment]]). + +## Read + +### GET /api/disputes + +**Description:** List disputes the caller can see (their own as buyer/seller, all for admins). +**Auth required:** Bearer JWT +**Query params:** +- `status` (`open` | `under_review` | `resolved_buyer` | `resolved_seller` | `closed`) +- `purchaseRequestId` +- `page`, `limit`, `sortBy`, `sortOrder` +**Response 200:** `{ success, data: { disputes, pagination } }` + +### GET /api/disputes/statistics + +**Description:** Aggregated counts (open, by reason, average resolution time) for admin dashboards. +**Auth required:** Bearer JWT (admin) +**Response 200:** `{ success, data: { open, byReason, avgResolutionHours, ... } }` + +### GET /api/disputes/:id + +**Description:** Full dispute including evidence list, messages, assigned admin, decision (if any). +**Auth required:** Bearer JWT (participant or admin) +**Errors:** `403` not allowed, `404` not found. + +## Admin operations + +### POST /api/disputes/:id/assign + +**Description:** Assign an admin moderator to the dispute. Sets `assignedAdminId` and transitions status to `under_review`. +**Auth required:** Bearer JWT (admin) +**Request body:** `{ adminId: string }` +**Side effects:** Notifies all participants. + +### PATCH /api/disputes/:id/status + +**Description:** Generic status update (e.g. close without resolution). +**Auth required:** Bearer JWT (admin) +**Request body:** `{ status: string; note?: string }` + +### POST /api/disputes/:id/resolve + +**Description:** Final adjudication. Records the decision and triggers the appropriate escrow action. +**Auth required:** Bearer JWT (admin) +**Request body:** +```ts +{ + decision: "buyer" | "seller" | "split"; + refundAmount?: number; // required when "split" + releaseAmount?: number; // required when "split" + reasoning: string; +} +``` +**Response 200:** `{ success, data: { dispute, paymentAction } }` +**Side effects:** +- `decision === "buyer"` → triggers `POST /api/payment/shkeeper/:id/refund` flow. +- `decision === "seller"` → triggers `POST /api/payment/shkeeper/:id/release` flow. +- `decision === "split"` → admin executes both partial release and partial refund manually. +- Notifies both participants and updates [[PurchaseRequest]] status to `disputed_resolved`. + +## Evidence and messages + +### POST /api/disputes/:id/evidence + +**Description:** Attach additional evidence (image / document URLs from the [[File API]]) to the dispute. Either party can call. +**Auth required:** Bearer JWT (participant or admin) +**Request body:** +```ts +{ + url: string; + description?: string; + type?: "image" | "document" | "video"; +} +``` +**Response 200:** `{ success, data: { dispute } }` with the evidence appended. + +### Messages + +Direct messages between disputants and the admin moderator are handled via a dedicated [[Chat]] created automatically when the dispute is opened (`type: "dispute"`). Use the [[Chat API]] endpoints (`POST /api/chat/:id/messages`, `GET /api/chat/:id/messages`) once you have the `chatId` from the dispute payload. + +## Real-time + +Dispute mutations emit notifications via `POST /api/notifications` which delivers `new-notification` socket events to each participant's `user-` room. See [[Socket Events]] for payload shape. + +## Related + +- [[Dispute]] +- [[Dispute Resolution Flow]] +- [[Payment API]] +- [[Chat API]] diff --git a/03 - API Reference/Error Codes.md b/03 - API Reference/Error Codes.md new file mode 100644 index 0000000..e6b6c52 --- /dev/null +++ b/03 - API Reference/Error Codes.md @@ -0,0 +1,110 @@ +--- +title: Error Codes +tags: [api, errors, reference] +--- + +# Error Codes + +This page documents the error shape returned by the AMN backend and the HTTP status mapping used across services. + +## Standard error shape + +The canonical helper is `ResponseHandler.error` in [`backend/src/shared/utils/response-handler.ts`](../../backend/src/shared/utils/response-handler.ts). Modern routes return: + +```json +{ + "success": false, + "message": "Validation failed", + "error": "Validation Error", + "statusCode": 400, + "timestamp": "2026-05-23T10:00:00.000Z", + "path": "/api/auth/register", + "method": "POST", + "data": [ /* field-level details for validation errors */ ] +} +``` + +Uncaught errors are formatted by [`shared/middleware/errorHandler.ts`](../../backend/src/shared/middleware/errorHandler.ts): + +```json +{ + "success": false, + "error": "Internal Server Error", + "statusCode": 500, + "stack": "..." // only in NODE_ENV=development +} +``` + +Legacy routes (chiefly `/api/users` legacy paths, `/api/marketplace` legacy paths, `/api/payment/decentralized/*`, parts of `/api/payment/shkeeper/*`) return ad-hoc shapes such as `{ "error": "..." }` or `{ "success": false, "message": "..." }`. Treat any non-`2xx` response as an error and read whichever of `error` / `message` is present. + +## HTTP status mapping + +| Status | When | Examples | +| --- | --- | --- | +| `200 OK` | Successful read or mutation | Most `GET`s, idempotent `PUT`s/`PATCH`s | +| `201 Created` | Resource created | `POST /api/marketplace/purchase-requests`, `POST /api/auth/register` (when user created), `POST /api/marketplace/reviews` | +| `202 Accepted` | Async accepted (provider webhooks) | SHKeeper webhook acknowledgement | +| `204 No Content` | Mutations with no body to return | Rare — most endpoints return the updated object | +| `400 Bad Request` | Validation failure, malformed input | `express-validator` errors, bad MongoIds, missing fields | +| `401 Unauthorized` | Missing or invalid JWT | `Access token required`, `Invalid or expired token` | +| `403 Forbidden` | Authenticated but not allowed | Role check failed, email not verified, ownership check failed | +| `404 Not Found` | Resource (or route) missing | `notFoundHandler`, `Resource not found` from `ResponseHandler.notFound` | +| `409 Conflict` | Duplicate / state collision | `USER_EXISTS`, duplicate review, dispute already open | +| `423 Locked` | Account temporarily locked | After repeated failed logins (Redis-tracked) | +| `429 Too Many Requests` | Rate limit hit | Currently issued only by per-feature Redis limits (auth / AI); global limiter is disabled | +| `500 Internal Server Error` | Unhandled exception | Caught by `errorHandler`; included stack trace in dev | +| `502 Bad Gateway` | Upstream provider failure | OpenAI / SHKeeper unreachable | + +## Application error codes + +The `error` field of the response envelope contains a human-readable category. Currently used codes: + +| Code | Meaning | Returned by | +| --- | --- | --- | +| `Validation Error` | `express-validator` rejected the body | All `*Validation` middlewares | +| `Not Found` | Generic resource lookup miss | `ResponseHandler.notFound` | +| `Unauthorized` | No token / bad token | `authenticateToken`, `ResponseHandler.unauthorized` | +| `Forbidden` | Role/ownership check failed | `authorizeRoles`, `ResponseHandler.forbidden` | +| `Internal Server Error` | Catch-all 500 | `ResponseHandler.internalError`, `errorHandler` | +| `USER_EXISTS` | Email already registered | `POST /api/auth/register` | + +For auth-specific 4xx responses the body's `message` carries the user-facing text (often Persian/Farsi for legacy reasons): + +- `"کاربری با این ایمیل قبلاً ثبت‌نام کرده است"` - email already in use (409) +- `"کد تحویل نادرست است"` - wrong delivery code (400) +- `"شما مجاز به ایجاد کد تحویل برای این درخواست نیستید"` - not the buyer (403) + +## Mongoose-specific mappings + +Handled in `errorHandler`: + +| Mongoose error | Mapped status | Body `message` | +| --- | --- | --- | +| `ValidationError` | 400 | `Validation Error` | +| `MongoError` code `11000` | 409 | `Duplicate resource` | +| `JsonWebTokenError` | 401 | `Invalid token` | +| `TokenExpiredError` | 401 | `Token expired` | + +## Webhook-specific + +| Provider | Endpoint | Status on success | Status on signature mismatch | +| --- | --- | --- | --- | +| SHKeeper pay-in | `POST /api/payment/shkeeper/webhook` | 200 `{ success: true }` | 401 `{ success: false }` (then ignored) | +| SHKeeper payout | `POST /api/payment/shkeeper/payout/webhook` | 200 / 400 with `{ success, message, data }` | 400 | +| Generic payment callback | `POST /api/payment/callback` | 200 `{ success: true, message }` | 400 | + +If a webhook is acknowledged with non-2xx, the provider re-delivers (SHKeeper retries every 60 seconds). + +## Client guidance + +1. Always parse `response.json()` even on non-2xx — both `message` and `error` are useful for UX surface text. +2. For optimistic UI, treat `409` as "your action raced — refresh". +3. For `401`, attempt one silent refresh-token call before redirecting to sign-in. +4. For `403`, do not retry — the user lacks permission. +5. For `423`, surface the lockout window from the `message`/`data` and direct the user to password reset. + +## Related + +- [[API Overview]] +- [[Authentication API]] +- [[Rate Limiting]] diff --git a/03 - API Reference/File API.md b/03 - API Reference/File API.md new file mode 100644 index 0000000..59c94d5 --- /dev/null +++ b/03 - API Reference/File API.md @@ -0,0 +1,111 @@ +--- +title: File API +tags: [api, file, reference] +--- + +# File API + +Endpoints live under `/api/files/*`. The router is [`backend/src/services/file/fileRoutes.ts`](../../backend/src/services/file/fileRoutes.ts), delegating to `fileController` and `fileService`. Multer is used for multipart parsing and uploaded files are written under `uploads/`. + +All endpoints require `Bearer JWT`. Static serving is wired in `app.ts` at `/uploads/*` with `helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })` so files can be embedded from the frontend domain. + +## Multer configuration + +`fileService.getUploadMiddleware(options)` produces a Multer instance per route. Options: + +- `subfolder` - where the file lands under `uploads/` +- `fieldName` - form field name +- `fileTypes` - allowed MIME types (default: any) +- `maxFiles` - default 1 +- `maxFileSizeMB` - default 10 MB + +Global body limits in `app.ts` are `10mb` for `express.json` and `express.urlencoded`. + +## Upload + +### POST /api/files/upload/avatar + +**Description:** Upload an avatar image. Lands in `uploads/temp/` and is then moved when the user persists it on their profile. +**Auth required:** Bearer JWT +**Form fields:** `avatar` (image; JPEG / PNG / GIF / WebP) +**Response 200:** +```json +{ + "success": true, + "data": { + "url": "/uploads/temp/avatar-1716459200000.jpg", + "filename": "avatar-1716459200000.jpg", + "mimeType": "image/jpeg", + "size": 51234 + } +} +``` +**Errors:** `400` bad type / too large, `401` not authenticated. +**Side effects:** None — caller is responsible for `PUT /api/user/profile` to persist the URL. + +### POST /api/files/upload/file + +**Description:** Generic single-file upload (any MIME), lands in `uploads/temp/`. +**Auth required:** Bearer JWT +**Form fields:** `file` +**Response 200:** `{ success, data: { url, filename, mimeType, size } }` + +### POST /api/files/upload/files + +**Description:** Multi-file upload (up to 5). +**Auth required:** Bearer JWT +**Form fields:** `files` (repeated) +**Response 200:** `{ success, data: { files: [{ url, filename, ... }] } }` + +### POST /api/files/upload/request-template-images + +**Description:** Up to 10 images for a [[RequestTemplate]]. Lands in `uploads/request-templates/`. +**Auth required:** Bearer JWT +**Form fields:** `images` (repeated; JPEG / PNG / GIF / WebP) +**Response 200:** `{ success, data: { files: [...] } }` + +### POST /api/files/upload/blog-images + +**Description:** Up to 10 images for a [[BlogPost]]. Lands in `uploads/blog/`. +**Auth required:** Bearer JWT +**Form fields:** `images` (repeated; JPEG / PNG / GIF / WebP) +**Response 200:** `{ success, data: { files: [...] } }` + +## Delete + +### DELETE /api/files/delete + +**Description:** Delete a file by relative path. +**Auth required:** Bearer JWT +**Request body:** `{ filePath: string }` (e.g. `"temp/avatar-1716459200000.jpg"`) +**Response 200:** `{ success, message: "File deleted" }` +**Errors:** `400` invalid path (must stay inside `uploads/`), `404` file missing. + +## Inspect + +### GET /api/files/info/:filePath + +**Description:** Returns metadata for a file (URL-encoded `filePath` segment). +**Auth required:** Bearer JWT +**Response 200:** `{ success, data: { url, size, mimeType, createdAt } }` + +### GET /api/files/stats + +**Description:** Aggregate upload statistics (counts and sizes per subfolder). +**Auth required:** Bearer JWT (admin gating planned per TODO in source) +**Response 200:** `{ success, data: { subfolders: [{ name, count, sizeBytes }], total: { count, sizeBytes } } }` + +## Serving + +- `GET /uploads/` - static file delivery (no auth, public read). Suitable for embedding avatars and blog/template images. +- The server logs `❌ File not found:` for missing paths and `✅ Serving file:` on hits — useful when debugging frontend image refs. + +The on-disk root resolves from `config.uploadPath`. In production this defaults to `/app/uploads`; in development to `/uploads`. Both are normalised to an absolute path before being passed to `express.static`. + +## Related + +- [[File Storage Architecture]] +- [[User API]] (avatar consumer) +- [[Marketplace API]] (template image consumer) +- [[Blog API]] (blog image consumer) +- [[Chat API]] (message attachments use a separate multipart route under chat) diff --git a/03 - API Reference/Marketplace API.md b/03 - API Reference/Marketplace API.md new file mode 100644 index 0000000..8223c6c --- /dev/null +++ b/03 - API Reference/Marketplace API.md @@ -0,0 +1,499 @@ +--- +title: Marketplace API +tags: [api, marketplace, reference] +--- + +# Marketplace API + +All marketplace endpoints live under `/api/marketplace/*`. The router is composed of several files mounted from `app.ts`: + +- New controller-pattern routes: [`backend/src/services/marketplace/controllerRoutes.ts`](../../backend/src/services/marketplace/controllerRoutes.ts) (`marketplaceControllerRouter`) +- Legacy + delivery-code routes: [`backend/src/services/marketplace/routes.ts`](../../backend/src/services/marketplace/routes.ts) (`marketplaceRouter`) +- Request templates: [`requestTemplateRoutes.ts`](../../backend/src/services/marketplace/requestTemplateRoutes.ts) (`/api/marketplace/request-templates/*`) +- Shop settings: [`shopSettingsRoutes.ts`](../../backend/src/services/marketplace/shopSettingsRoutes.ts) (`/api/marketplace/shop/*`) +- Reviews: [`reviewRoutes.ts`](../../backend/src/services/marketplace/reviewRoutes.ts) (`/api/marketplace/reviews/*`) + +Core models: [[PurchaseRequest]], [[SellerOffer]], [[Category]], [[RequestTemplate]], [[ShopSettings]], [[Review]]. Real-time updates are emitted on the `request-`, `seller-`, `buyer-`, and `sellers`/`buyers` rooms — see [[Socket Events]]. + +## Categories + +### GET /api/marketplace/categories + +**Description:** Flat list of all categories. +**Auth required:** No +**Response 200:** `{ success: true, data: [Category, ...] }` + +### GET /api/marketplace/categories/tree + +**Description:** Nested tree based on `parentId`. +**Auth required:** No + +### GET /api/marketplace/categories/:id + +**Description:** Single category by id. +**Auth required:** No +**Errors:** `404` not found. + +## Sellers + +### GET /api/marketplace/sellers + +**Description:** Public seller directory (users with `role: 'seller'`). Includes aggregated stats (completed orders, ratings). +**Auth required:** No + +### GET /api/marketplace/request-templates/sellers + +**Description:** Sellers who currently expose at least one active [[RequestTemplate]] (used by the shop discovery page). +**Auth required:** No + +### GET /api/marketplace/request-templates/sellers/:sellerId + +**Description:** Public shop profile for a seller, including their templates and shop settings. +**Auth required:** No + +## Purchase Requests + +The buyer-facing CRUD plus seller-side workflow endpoints. Model: [[PurchaseRequest]]. + +### POST /api/marketplace/purchase-requests + +**Description:** Create a purchase request (new controller path). +**Auth required:** Bearer JWT (buyer) +**Request body:** +```ts +{ + title: string; + description: string; + categoryId: string; + productLink?: string; + size?: string; + color?: string; + quantity?: number; // default 1 + budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" }; + urgency?: "low" | "medium" | "high"; + deliveryInfo?: { + deliveryType: "physical" | "online"; + addressId?: string; // when physical + email?: string; // when online + }; + preferredSellerIds?: string[]; + attachments?: string[]; // URLs from [[File API]] +} +``` +**Response 201:** `{ success, data: { purchaseRequest } }` +**Side effects:** Emits `new-purchase-request` to the `sellers` room (broadcast). +**Source:** `marketplaceController.createPurchaseRequest` + +### POST /api/marketplace/purchase-requests/bulk + +**Description:** Bulk creation (used by the template checkout flow). +**Auth required:** Bearer JWT +**Request body:** `{ items: Array }` + +### GET /api/marketplace/purchase-requests + +**Description:** List requests with filters. Buyers see their own; sellers see ones routed to them; admins see all. +**Auth required:** Bearer JWT +**Query params:** `status`, `categoryId`, `urgency`, `search`, `page`, `limit`, `sortBy`, `sortOrder` + +### GET /api/marketplace/purchase-requests/my + +**Description:** Shortcut for the caller's own purchase requests. +**Auth required:** Bearer JWT + +### GET /api/marketplace/purchase-requests/:id + +**Description:** Single request with populated category and selected offer. +**Auth required:** No (public read for shareable links) +**Errors:** `404` not found. + +### PATCH /api/marketplace/purchase-requests/:id + +**Description:** Buyer edits draft / pending request fields. +**Auth required:** Bearer JWT (owner) + +### PATCH /api/marketplace/purchase-requests/:id/status + +**Description:** Transition the request status (`draft` → `pending` → `payment` → `processing` → `delivery` → `delivered` → `seller_paid` → `completed`, or `cancelled`). +**Auth required:** Bearer JWT (owner or admin) +**Request body:** `{ status: string }` +**Side effects:** Emits `purchase-request-update` to `request-`. + +### DELETE /api/marketplace/purchase-requests/:id + +**Description:** Cancel/delete a request that has no committed payment. +**Auth required:** Bearer JWT (owner) + +### GET /api/marketplace/purchase-requests/:id/workflow-steps + +**Description:** Returns the ordered workflow steps + current pointer used by the frontend stepper. +**Auth required:** No + +### GET /api/marketplace/purchase-requests/:id/payment-status + +**Description:** Returns the latest payment + escrow state for the request. +**Auth required:** Bearer JWT + +### POST /api/marketplace/purchase-requests/:id/sync-payment-status + +**Description:** Force a re-check against the payment provider ([[Payment API]]). +**Auth required:** Bearer JWT + +### POST /api/marketplace/purchase-requests/:id/confirm-payment + +**Description:** Legacy buyer-confirmation endpoint kept for old clients. +**Auth required:** Bearer JWT + +### POST /api/marketplace/purchase-requests/:id/release-payment + +**Description:** Triggers admin escrow release (mirror of `POST /api/payment/shkeeper/:id/release`). +**Auth required:** Bearer JWT (admin) + +### PUT /api/marketplace/purchase-requests/:id/delivery (controller route) + +**Description:** Seller submits shipping details (carrier, tracking number, expected date). +**Auth required:** Bearer JWT (selected seller) + +### POST /api/marketplace/purchase-requests/:id/update-delivery (legacy) + +**Description:** Older equivalent retained for compatibility. + +### POST /api/marketplace/requests/:id/start-delivery + +**Description:** Marks the request as `delivery` and notifies the buyer. +**Auth required:** Bearer JWT (seller) +**Side effects:** Emits `purchase-request-update` event. + +### PATCH /api/marketplace/purchase-requests/:id/confirm-delivery + +**Description:** Buyer confirms goods received (transitions to `delivered`). +**Auth required:** Bearer JWT (buyer) + +### POST /api/marketplace/purchase-requests/:id/final-approval + +**Description:** Buyer's final approval that releases escrow to the seller. +**Auth required:** Bearer JWT (buyer) + +### GET /api/marketplace/buyers/:buyerId/purchase-requests + +**Description:** Admin/seller view of a buyer's request history. +**Auth required:** Bearer JWT + +## Delivery codes + +Six-digit codes the buyer hands to the seller at handover. Backed by `deliveryService`. + +### POST /api/marketplace/purchase-requests/:id/delivery-code/generate + +**Description:** Buyer generates a delivery code. Request must be in `delivery` status. +**Auth required:** Bearer JWT (buyer) +**Response 200:** `{ success: true, data: { deliveryCode: "123456" } }` +**Errors:** `400` wrong status, `403` not buyer, `404` request not found. +**Side effects:** Emits `delivery-code-generated` on `request-`. + +### POST /api/marketplace/purchase-requests/:id/delivery-code/verify + +**Description:** Seller verifies the code. On success the status moves to `delivered`. +**Auth required:** Bearer JWT (selected seller) +**Request body:** `{ code: string }` (exactly 6 chars) +**Errors:** `400` bad code, `403` not the selected seller. +**Side effects:** Emits `delivery-confirmed` and `buyer-confirmed-delivery`. + +### GET /api/marketplace/purchase-requests/:id/delivery-code + +**Description:** Buyer or seller fetches the current code metadata. +**Auth required:** Bearer JWT + +### GET /api/marketplace/purchase-requests/:id/delivery-code/status + +**Description:** Returns `{ isValid, hasDeliveryCode, ... }` so the UI can decide whether to show "regenerate". +**Auth required:** Bearer JWT + +## Seller Offers + +Model: [[SellerOffer]]. + +### POST /api/marketplace/purchase-requests/:id/offers + +**Description:** Submit an offer against a purchase request. +**Auth required:** Bearer JWT (seller) +**Request body:** +```ts +{ + price: { amount: number; currency: "USD" | "EUR" | "IRR" }; + deliveryEstimate: { days: number; note?: string }; + notes?: string; + attachments?: string[]; +} +``` +**Response 201:** `{ success, data: { offer } }` +**Side effects:** Emits `new-offer` to `buyer-` and `seller-offer-update` to `seller-`. + +### PUT /api/marketplace/purchase-requests/:id/offers (legacy) + +**Description:** Older offer-update endpoint kept for compatibility. + +### GET /api/marketplace/purchase-requests/:id/offers + +**Description:** List all offers for a request. +**Auth required:** No (buyers and prospective sellers compare offers) + +### GET /api/marketplace/purchase-requests/:id/has-offer + +**Description:** Returns `{ hasOffer: boolean }` for the caller (seller-side helper). +**Auth required:** Bearer JWT (seller) + +### GET /api/marketplace/purchase-requests/:id/offers/:sellerId + +**Description:** Fetch a specific seller's offer on a request. +**Auth required:** No + +### PATCH /api/marketplace/offers/:id + +**Description:** Seller edits their pending offer (price, delivery estimate, notes). +**Auth required:** Bearer JWT (offer owner) + +### DELETE /api/marketplace/offers/:id + +**Description:** Seller withdraws their offer. +**Auth required:** Bearer JWT (offer owner) + +### PUT /api/marketplace/offers/:id/status + +**Description:** Direct status mutation (admin override / counter-offer states). +**Auth required:** Bearer JWT +**Request body:** `{ status: "pending" | "accepted" | "rejected" | "withdrawn" | "countered" }` + +### POST /api/marketplace/purchase-requests/:id/select-offer + +**Description:** Buyer selects/accepts an offer; this triggers payment intent creation in [[Payment API]] and rejects all other offers automatically once payment lands. +**Auth required:** Bearer JWT (buyer) +**Request body:** `{ offerId: string }` +**Side effects:** +- Updates [[PurchaseRequest]] `selectedOfferId`, status moves toward `payment`. +- Emits `seller-offer-update` to all sellers for the request. + +### POST /api/marketplace/offers/:id/accept (legacy) + +**Description:** Older synonym retained for backward compatibility with old clients. + +### DELETE /api/marketplace/offers/:id (controller route) + +**Description:** Controller-pattern delete that also notifies the buyer. + +## Request Templates + +A [[RequestTemplate]] is a re-usable "shop product" a seller can publish. Buyers convert templates into actual purchase requests via the shareable link, individually or in bulk (cart checkout). + +### POST /api/marketplace/request-templates + +**Description:** Create a new template. +**Auth required:** Bearer JWT (seller) +**Request body:** +```ts +{ + title: string; // 1-200 chars + description: string; // 1-2000 + categoryId: string; // MongoId + productLink?: string; // valid URL + size?: string; // <=100 + color?: string; // <=100 + quantity?: number; // 1-10000 + budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" }; + urgency?: "low" | "medium" | "high"; + deliveryInfo?: { deliveryType: "physical" | "online"; email?: string }; + maxUsage?: number | null; // 0/null = unlimited + expiresAt?: string | null; // ISO date + images?: string[]; // URLs from [[File API]] +} +``` +**Response 201:** `{ data: { template } }` with a generated `shareableLink`. + +### GET /api/marketplace/request-templates + +**Description:** Paginated list of the caller's templates. +**Auth required:** Bearer JWT (seller) +**Query params:** `page`, `limit` (1-100), `isActive`, `categoryId`, `search` + +### GET /api/marketplace/request-templates/stats + +**Description:** Aggregate counts of the caller's templates (active, expired, usage). +**Auth required:** Bearer JWT (seller) + +### GET /api/marketplace/request-templates/:id + +**Description:** Full template by id (owner view). +**Auth required:** Bearer JWT (owner) + +### PUT /api/marketplace/request-templates/:id + +**Description:** Update the template. Same body as create. +**Auth required:** Bearer JWT (owner) + +### DELETE /api/marketplace/request-templates/:id + +**Description:** Delete a template. +**Auth required:** Bearer JWT (owner) + +### PATCH /api/marketplace/request-templates/:id/toggle-status + +**Description:** Toggle `isActive`. +**Auth required:** Bearer JWT (owner) + +### GET /api/marketplace/request-templates/public/:shareableLink + +**Description:** Public read of a template via its shareable slug. Used by the shop preview page. +**Auth required:** No +**Errors:** `404` link not found or template inactive/expired. + +### POST /api/marketplace/request-templates/:shareableLink/convert + +**Description:** Buyer converts the template into a real [[PurchaseRequest]]. +**Auth required:** Bearer JWT (buyer) +**Request body:** Overrides (quantity, address, etc.) +**Response 201:** `{ data: { purchaseRequest } }` + +### POST /api/marketplace/request-templates/batch-convert + +**Description:** Convert several templates at once (cart checkout). +**Auth required:** Bearer JWT (buyer) +**Request body:** +```ts +{ + items: Array<{ + shareableLink: string; + quantity: number; // 1-100 + sellerId: string; // MongoId + }>; + status?: "pending" | "pending_payment" | "active"; +} +``` + +### POST /api/marketplace/request-templates/complete-payment + +**Description:** Marks the requests created by `batch-convert` as paid (called after a successful checkout). +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + requestIds: string[]; // 1+ MongoIds + newStatus?: "pending" | "active" | "processing"; + paymentData?: Record; +} +``` + +## Shop Settings + +Per-seller storefront preferences. Model: [[ShopSettings]]. + +### GET /api/marketplace/shop/settings/:sellerId + +**Description:** Public shop settings for the given seller (used by the shop landing page). +**Auth required:** No +**Response 200:** `{ data: ShopSettings }` + +### GET /api/marketplace/shop/settings + +**Description:** The authenticated seller's own settings. +**Auth required:** Bearer JWT (seller) + +### PUT /api/marketplace/shop/settings + +**Description:** Update shop settings (banner, bio, policies, `allowSellerReviews`, `allowTemplateReviews`, ...). +**Auth required:** Bearer JWT (seller) + +## Reviews + +Model: [[Review]]. Reviews can target a seller or a template. Subject must be `seller` or `template`. + +### GET /api/marketplace/reviews/:subjectType/:subjectId + +**Description:** Published reviews + aggregate stats. Honours `allowSellerReviews` / `allowTemplateReviews` from [[ShopSettings]]. +**Auth required:** No +**Query params:** `page` (default 1), `limit` (default 10) +**Response 200:** +```json +{ + "data": [Review, ...], + "pagination": { "page": 1, "limit": 10, "total": 42, "pages": 5 }, + "stats": { "count": 42, "avg": 4.6, "one": 1, "two": 0, "three": 3, "four": 10, "five": 28 } +} +``` +**Errors:** `400` bad subjectType / invalid id, `403` reviews disabled by seller. + +### GET /api/marketplace/reviews/:subjectType/:subjectId/summary + +**Description:** Stats only (no review list). +**Auth required:** No + +### POST /api/marketplace/reviews + +**Description:** Submit a review. The server computes `isVerifiedBuyer` when `purchaseRequestId` is given and the request is in a terminal state (`delivery`, `delivered`, `seller_paid`, `completed`). +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + subjectType: "seller" | "template"; + subjectId: string; + rating: 1 | 2 | 3 | 4 | 5; + comment?: string; + purchaseRequestId?: string; +} +``` +**Errors:** `400` validation, `403` reviews disabled, `404` template not found, `409` duplicate review. +**Response 201:** `{ data: Review, stats: { ... } }` + +## Payments (legacy under marketplace) + +These routes are duplicates of the main [[Payment API]] kept under `/api/marketplace/payments/*` for backward compatibility with the early frontend. Prefer the canonical endpoints. + +### POST /api/marketplace/payments +### GET /api/marketplace/payments +### GET /api/marketplace/payments/:paymentId +### PATCH /api/marketplace/payments/:paymentId + +See [[Payment API]] for the canonical descriptions. + +## Verify Web3 payment (legacy) + +### POST /api/marketplace/payments/verify + +**Description:** Legacy Web3 verification endpoint that records a transaction and moves the purchase request to `processing`. Modern flows use `POST /api/payment/shkeeper/confirm-transaction` instead. +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + purchaseRequestId: string; + sellerOfferId: string; + buyerId: string; + sellerId: string; + amount: number; + currency: "USDT" | string; + paymentHash: string; + paymentMethod?: string; + token?: string; + network?: string; + escrowWalletAddress?: string; + metadata?: Record; +} +``` +**Side effects:** Creates a [[Payment]] record, updates [[PurchaseRequest]] status, emits `payment-received` to `user-`. + +## Real-time events + +Most marketplace mutations fan out via `global.io` to the rooms below — see [[Socket Events]] for payloads: + +- `purchase-request-update` → `request-` +- `new-purchase-request` → `sellers` +- `new-offer` → `buyer-` +- `seller-offer-update` → `seller-` (and global on payment confirm) +- `delivery-code-generated` / `delivery-confirmed` / `delivery-update` → `request-` +- `request-cancelled` → `user-`, `user-` +- `transaction-completed` → `user-`, `user-` + +## Related + +- [[Purchase Request Flow]] +- [[Seller Offer Flow]] +- [[Template Checkout Flow]] +- [[Delivery Code Flow]] diff --git a/03 - API Reference/Notification API.md b/03 - API Reference/Notification API.md new file mode 100644 index 0000000..cea56d4 --- /dev/null +++ b/03 - API Reference/Notification API.md @@ -0,0 +1,110 @@ +--- +title: Notification API +tags: [api, notification, reference] +--- + +# Notification API + +Endpoints live under `/api/notifications/*`. Two routers are mounted: + +- New controller pattern: [`notificationControllerRoutes.ts`](../../backend/src/services/notification/notificationControllerRoutes.ts) (controller-backed, requires auth) +- Legacy: [`notification/routes.ts`](../../backend/src/services/notification/routes.ts) (no auth gate — userId is passed in query/body) + +Both routers are mounted at `/api`, so the paths collide; the controller router wins for the shared paths (it is mounted first). The legacy router is still used by background scripts and admin tools that have no JWT context. + +Model: [[Notification]]. Real-time delivery is via `new-notification` and `unread-count-update` Socket.IO events on `user-`. See [[Socket Events]]. + +## List + +### GET /api/notifications + +**Description:** Paginated notifications for the caller. +**Auth required:** Bearer JWT (controller route); legacy variant takes `?userId=...`. +**Query params:** +- `page` (default 1) +- `limit` (default 20) +- `unreadOnly` (`true` | `false`, default `false`) +**Response 200:** +```json +{ + "success": true, + "data": { + "notifications": [Notification, ...], + "pagination": { "page": 1, "limit": 20, "total": 42, "hasMore": true } + } +} +``` + +### GET /api/notifications/unread-count + +**Description:** Just the integer unread count. +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "data": { "unreadCount": 5 } }` + +### GET /api/notifications/:id + +**Description:** Single notification by id. Available on the controller router. +**Auth required:** Bearer JWT +**Errors:** `404` not found, `403` not owner. + +## Mutations + +### PATCH /api/notifications/:id/read + +**Description:** Mark one notification as read. +**Auth required:** Bearer JWT (controller); legacy variant requires `{ userId }` in body. +**Response 200:** `{ success: true, data: { /* updated notification */ } }` +**Side effects:** Emits `unread-count-update` to `user-`. + +### PATCH /api/notifications/mark-all-read + +**Description:** Mark every notification for the caller as read. +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "data": { "modifiedCount": 12 } }` + +### PATCH /api/notifications/bulk/mark-read + +**Description:** Mark a list of notifications as read. +**Auth required:** Bearer JWT +**Request body:** `{ ids: string[] }` +**Response 200:** `{ success, data: { modifiedCount } }` + +### DELETE /api/notifications/:id + +**Description:** Delete a notification. +**Auth required:** Bearer JWT +**Errors:** `404` not found. + +### DELETE /api/notifications/bulk/delete + +**Description:** Bulk delete. +**Auth required:** Bearer JWT +**Request body:** `{ ids: string[] }` + +### POST /api/notifications + +**Description:** Create a notification. Primarily used by other services and admin tools. +**Auth required:** Bearer JWT (controller); legacy variant is open. +**Request body:** +```ts +{ + userId: string; + type: string; // e.g. "order_update", "chat_message", "payment_received" + title: string; + body?: string; + data?: Record; + channel?: "in_app" | "email" | "push"; +} +``` +**Response 201:** `{ success, data: { notification } }` +**Side effects:** Emits `new-notification` to `user-`; also increments unread count via `unread-count-update`. + +## Preferences + +Notification preferences live on [[User]] (`preferences.notifications.email | sms | push`). They are read and written through the [[User API]] (`GET /api/user/profile`, `PUT /api/user/profile`). + +## Related + +- [[Notification]] +- [[Notification Flow]] +- [[Socket Events]] diff --git a/03 - API Reference/Payment API.md b/03 - API Reference/Payment API.md new file mode 100644 index 0000000..2cc7f6b --- /dev/null +++ b/03 - API Reference/Payment API.md @@ -0,0 +1,387 @@ +--- +title: Payment API +tags: [api, payment, reference, shkeeper] +--- + +# Payment API + +The payment surface is split across four routers, all mounted under `/api/payment/*`: + +| Path prefix | File | Purpose | +| --- | --- | --- | +| `/api/payment/*` | [`paymentControllerRoutes.ts`](../../backend/src/services/payment/paymentControllerRoutes.ts) | New controller pattern (CRUD + configuration) | +| `/api/payment/*` | [`paymentRoutes.ts`](../../backend/src/services/payment/paymentRoutes.ts) | Additional legacy endpoints (tx fetch, exports) | +| `/api/payment/decentralized/*` | [`decentralizedPaymentRoutes.ts`](../../backend/src/services/payment/decentralizedPaymentRoutes.ts) | DePay / Web3 confirmations | +| `/api/payment/shkeeper/*` | [`shkeeper/shkeeperRoutes.ts`](../../backend/src/services/payment/shkeeper/shkeeperRoutes.ts) | SHKeeper pay-in, webhook, release/refund | +| `/api/payment/shkeeper/payout*` | [`shkeeper/shkeeperPayoutRoutes.ts`](../../backend/src/services/payment/shkeeper/shkeeperPayoutRoutes.ts) | SHKeeper payouts to sellers | + +Core model: [[Payment]]. Coordination logic to avoid race conditions when multiple sources update the same payment is in `paymentCoordinator.ts`. + +## Configuration / health + +### POST /api/payment/configuration + +**Description:** Returns the payment provider configuration the SHKeeper widget needs (accepted blockchains, escrow receiver address, redirect URLs, webhook URL). +**Auth required:** No +**Request body:** `{ amount?, currency?, purchaseRequestId? }` (used to scope returned config) +**Response 200:** +```json +{ + "accept": [{ "blockchain": "bsc", "token": "0x55d3...", "receiver": "0xa30..." }], + "redirect": { "success": "...", "cancel": "..." }, + "webhook": "https://.../api/payment/shkeeper/webhook" +} +``` + +### GET /api/payment/health + +**Description:** Lightweight health probe. +**Auth required:** No +**Response 200:** `{ success, message, endpoints: { shkeeper, decentralized, health } }` + +### GET /api/payment/shkeeper/config + +**Description:** Same payload as `/configuration` but tailored for the SHKeeper-hosted widget; includes explicit CORS `*` headers. +**Auth required:** No + +## Payment records (CRUD) + +### POST /api/payment + +**Description:** Create a payment record (manual entry — usually the SHKeeper intent path is preferred). +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + purchaseRequestId: string; + sellerOfferId: string; + buyerId: string; + sellerId: string; + amount: { amount: number; currency: string }; + blockchain?: { network: string; token: string }; + metadata?: Record; +} +``` +**Response 201:** `{ /* Payment document */ }` + +### PUT /api/payment/:id + +**Description:** Update a payment record (status, transactionHash, metadata). +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + status?: "pending" | "processing" | "completed" | "failed" | "cancelled"; + transactionHash?: string; + blockchain?: { ... }; + metadata?: Record; +} +``` + +### GET /api/payment + +**Description:** List the caller's payments (defaults to `completed,success` status). +**Auth required:** Bearer JWT +**Query params:** `status`, `limit` (default 50), `offset` (default 0) + +### GET /api/payment/:id + +**Description:** Fetch a payment by id. +**Auth required:** Bearer JWT +**Errors:** `404` not found. + +### GET /api/payment/:id/debug + +**Description:** Debug bundle including the raw payment, blockchain metadata, and wallet-monitor status. +**Auth required:** Bearer JWT +**Notes:** Intended for admin / development. + +### GET /api/payment/user/:userId + +**Description:** Payments for a specific user (admin or self). +**Auth required:** Bearer JWT +**Query params:** `status`, `limit`, `offset` + +### GET /api/payment/stats / GET /api/payment/stats/:userId + +**Description:** Aggregated counts and sums per status. +**Auth required:** Bearer JWT + +### GET /api/payment/export / GET /api/payment/export/:userId + +**Description:** Export payments as `json` or `csv`. +**Auth required:** Bearer JWT +**Query params:** `format=json|csv` + +### POST /api/payment/payments/cleanup-pending + +**Description:** Admin cleanup of stale `pending` payments. +**Auth required:** Bearer JWT (admin) +**Response 200:** `{ success, deletedCount, message }` + +### POST /api/payment/payments/:id/fetch-tx + +**Description:** Re-queries the blockchain to fetch the missing `transactionHash` for a completed payment. +**Auth required:** Bearer JWT +**Response 200:** `{ success, transactionHash, network, source, message }` + +### POST /api/payment/payments/auto-fetch-missing + +**Description:** Batch tx-hash backfill across the database. +**Auth required:** Bearer JWT +**Request body:** `{ limit?: number }` (default 10) + +### POST /api/payment/callback + +**Description:** Generic payment callback (called by the older client SDK). +**Auth required:** No (verified by `paymentRef` matching) +**Request body:** `{ paymentId, transactionHash, status, data }` + +### POST /api/payment/verify + +**Description:** Frontend verification endpoint used by the Web3 flow. Confirms a payment and updates the related [[PurchaseRequest]]. +**Auth required:** Bearer JWT + +## SHKeeper - Pay-in + +### POST /api/payment/shkeeper/intents + +**Description:** Creates a SHKeeper pay-in intent. The server provisions an invoice on SHKeeper, stores a [[Payment]] with `provider: "shkeeper"`, `direction: "in"`, returns the hosted-widget URL. +**Auth required:** Bearer JWT (buyer) +**Request body:** +```ts +{ + purchaseRequestId: string; + sellerOfferId: string; + amount: number; + sellerId: string; + token?: string; // default "USDT" + network?: string; // default "bsc" + metadata?: Record; +} +``` +**Response 200:** +```json +{ + "success": true, + "data": { + "paymentId": "...", + "paymentUrl": "https://pay.amn.gg/invoice/...", + "externalId": "AMN_...", + "expiresAt": "2026-05-23T11:00:00.000Z" + } +} +``` +**Errors:** `400` missing fields, `401` not authenticated, `500` SHKeeper error. +**Side effects:** Emits `payment-created` globally and to the request room. + +### POST /api/payment/shkeeper/webhook + +**Description:** SHKeeper posts here when an invoice changes state. Handles both raw-string and JSON bodies and verifies the HMAC signature (`x-shkeeper-signature` against the raw body using `SHKEEPER_WEBHOOK_SECRET`). +**Auth required:** No (signature-protected) +**Body:** The SHKeeper callback envelope (`external_id`, `crypto`, `addr`, `fiat`, `balance_fiat`, `balance_crypto`, `paid`, `status`, `transactions[]`). +**Response 200:** `{ success: true }` +**Side effects:** +- Updates the matching [[Payment]] to `completed` (`OVERPAID` and `PAID` both count). +- Releases or rejects [[SellerOffer]] siblings (the chosen offer becomes `accepted`, others `rejected`). +- Updates [[PurchaseRequest]] status to `payment` / `processing`. +- Emits `seller-offer-update` to each affected seller room and `purchase-request-update` to the request room. + +### POST /api/payment/shkeeper/confirm-transaction + +**Description:** Manual fallback when the webhook misses — the frontend calls this after the buyer signs the EVM transaction directly. Coordinated through `PaymentCoordinator` to avoid double updates. +**Auth required:** Bearer JWT +**Request body:** `{ paymentId, transactionHash, network? }` +**Response 200:** `{ success, message, data: { paymentId, transactionHash, status } }` +**Side effects:** Closes the SHKeeper invoice session, then runs the same offer/request updates as the webhook. + +### POST /api/payment/shkeeper/test + +**Description:** Smoke-tests the real SHKeeper API. Development only. +**Auth required:** No + +### POST /api/payment/shkeeper/callback-test (and GET equivalent) + +**Description:** Echo-style endpoints used during webhook configuration. +**Auth required:** No + +### POST /api/payment/shkeeper/create-test-payment + +**Description:** Inserts a sample [[Payment]] row to exercise the webhook handler. +**Auth required:** No + +### POST /api/payment/shkeeper/trigger-webhook + +**Description:** Sends a fake webhook payload to the local webhook endpoint for end-to-end testing. +**Auth required:** Bearer JWT + +### GET /api/payment/shkeeper/wallet-monitor/status + +**Description:** Returns the wallet-monitor state (`isMonitoring`, watched wallet addresses). +**Auth required:** No + +### GET /api/payment/shkeeper/auto-webhook/status + +**Description:** Returns the auto-webhook fallback monitor state. +**Auth required:** No + +### GET /api/payment/shkeeper/webhook-stats + +**Description:** Counters for webhook deliveries (success / failure / duplicates). +**Auth required:** Bearer JWT (admin) + +## SHKeeper - Release / Refund (escrow) + +These build an admin-signed transaction off-chain and require a follow-up confirm with the broadcast tx hash. Source: `shkeeperService.buildAdminSignedTxPayload` and `confirmAdminTx`. + +### POST /api/payment/shkeeper/:id/release + +**Description:** Prepares the admin-signed payload to release escrow to the seller. Returns the raw payload — the admin client signs and broadcasts. +**Auth required:** Bearer JWT (admin) +**Response 200:** `{ success: true, data: { /* tx payload */ } }` + +### POST /api/payment/shkeeper/:id/release/confirm + +**Description:** Records the broadcast transaction hash for the release; marks the payment as released, updates [[PurchaseRequest]] to `seller_paid` and emits `purchase-request-update` (`type: payment_released`). +**Auth required:** Bearer JWT (admin) +**Request body:** `{ txHash: string }` +**Errors:** `400` missing `txHash`. + +### POST /api/payment/shkeeper/:id/refund + +**Description:** Mirror of release, but returns the escrow to the buyer. +**Auth required:** Bearer JWT (admin) + +### POST /api/payment/shkeeper/:id/refund/confirm + +**Description:** Records the refund tx hash; emits `purchase-request-update` (`type: payment_refunded`). +**Auth required:** Bearer JWT (admin) +**Request body:** `{ txHash: string }` + +## SHKeeper - Payouts + +Payouts are SHKeeper-side outbound transfers (admin pays the seller from a hot wallet). + +### POST /api/payment/shkeeper/payout + +**Description:** Create a payout task. The server creates a [[Payment]] row with `direction: "out"` and provider task id, then returns the SHKeeper task descriptor. +**Auth required:** Bearer JWT (admin) +**Request body:** +```ts +{ + purchaseRequestId: string; + sellerOfferId: string; + buyerId: string; + sellerId: string; + amount: number; + recipientAddress: string; + token?: string; // default "USDT" + network?: string; // default "bsc" + metadata?: Record; +} +``` +**Response 200:** `{ success, data: { payoutId, taskId, status }, message }` +**Side effects:** `emitGlobalEvent('payout-created', { ... })`. +**Errors:** `400` for each missing required field, `500` upstream error. + +### GET /api/payment/shkeeper/payout/status/:taskId + +**Description:** Polls SHKeeper for the current task status. +**Auth required:** Bearer JWT +**Response 200:** `{ success, data: { /* status payload */ } }` + +### POST /api/payment/shkeeper/payout/webhook + +**Description:** SHKeeper webhook for payout state changes. Handled by `processPayoutWebhookEvent`. Emits `payout-completed` (or `payout-updated`) global socket events on success. +**Auth required:** No (signature checked) +**Response 200/400:** `{ success, message, data }` + +## DePay / Web3 (decentralized) + +### POST /api/payment/decentralized/save + +**Description:** Persists a Web3-initiated payment record. +**Auth required:** No +**Request body:** +```ts +{ + purchaseRequestId: string; + buyerId: string; + sellerId?: string; + amount: number; + currency?: string; + transactionHash?: string; + network?: string; + token?: string; + walletAddress?: string; + metadata?: Record; +} +``` + +### GET /api/payment/decentralized/status/:paymentId + +**Description:** Returns the latest status for a decentralized payment. +**Auth required:** No + +### PUT /api/payment/decentralized/update + +**Description:** Update a decentralized payment's status / confirmations. +**Auth required:** No +**Request body:** `{ paymentId, status, confirmations? }` + +### GET /api/payment/decentralized/receiver + +**Description:** Returns the configured escrow receiver wallet address. +**Auth required:** No + +### GET /api/payment/decentralized/history/:userId + +**Description:** Decentralized payment history for a user. +**Auth required:** No + +### POST /api/payment/decentralized/verify/:paymentId + +**Description:** Re-verifies a single decentralized payment against the chain. +**Auth required:** No + +### POST /api/payment/decentralized/verify-all-pending + +**Description:** Iterates all `pending` decentralized payments and re-verifies them. +**Auth required:** No (typically called by a cron) + +### POST /api/payment/decentralized/admin-payout + +**Description:** Pay a seller directly from an admin hot wallet (no SHKeeper). +**Auth required:** Bearer JWT (admin) +**Request body:** +```ts +{ + purchaseRequestId: string; + receiverWalletAddress: string; + amount: number; + currency?: string; // default "USDT" + network?: string; // default "BSC" +} +``` +**Response 200:** `{ success, data: { /* payout receipt */ } }` + +## Status model + +[[Payment]] uses the statuses below across all providers: + +- `pending` - intent created, awaiting on-chain settlement +- `processing` - settlement seen, awaiting confirmations +- `completed` - confirmed, escrow funded +- `failed` - intentionally failed (expired, declined, refused) +- `cancelled` - cancelled by user/admin +- `released` - escrow released to seller (`shkeeper` flow) +- `refunded` - escrow returned to buyer + +Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded`. + +## Related + +- [[Payment Flow]] +- [[Escrow Flow]] +- [[SHKeeper Webhook Flow]] +- [[Socket Events]] diff --git a/03 - API Reference/Points API.md b/03 - API Reference/Points API.md new file mode 100644 index 0000000..8087f57 --- /dev/null +++ b/03 - API Reference/Points API.md @@ -0,0 +1,140 @@ +--- +title: Points API +tags: [api, points, reference] +--- + +# Points API + +Endpoints live under `/api/points/*`. The router is [`backend/src/routes/pointsRoutes.ts`](../../backend/src/routes/pointsRoutes.ts), delegating to [`PointsController`](../../backend/src/controllers/pointsController.ts) and `PointsService` ([`backend/src/services/points/PointsService.ts`](../../backend/src/services/points/PointsService.ts)). The router applies `authenticateToken` globally — every endpoint requires `Bearer JWT`. + +Models: [[PointTransaction]], [[LevelConfig]]. Points are minted automatically by the platform (referral signup, successful purchases, reviews) and can be redeemed for discounts or marketplace credits. Levels progress as the user's lifetime points cross configured thresholds. + +## Balance and history + +### GET /api/points/my-points + +**Description:** Caller's current balance, lifetime totals, current level, referral code, and progress to next level. +**Auth required:** Bearer JWT +**Response 200:** +```json +{ + "success": true, + "data": { + "balance": 320, + "lifetimePoints": 1280, + "level": { "name": "silver", "tier": 2, "minPoints": 1000, "perks": [...] }, + "nextLevel": { "name": "gold", "tier": 3, "minPoints": 2500, "pointsToGo": 1220 }, + "referralCode": "ABCD1234", + "referralsCount": 4 + } +} +``` + +### GET /api/points/transactions + +**Description:** Paginated point ledger. +**Auth required:** Bearer JWT +**Query params:** +- `page` (default 1), `limit` (default 20) +- `type` (`earn` | `redeem` | `referral` | `purchase` | `review` | `admin_grant` | `admin_deduct`) +- `from` / `to` (ISO dates) +**Response 200:** +```json +{ + "success": true, + "data": { + "transactions": [PointTransaction, ...], + "pagination": { "page": 1, "limit": 20, "total": 17, "hasMore": false } + } +} +``` + +### GET /api/points/referrals + +**Description:** Users referred by the caller plus the points earned from each. +**Auth required:** Bearer JWT +**Response 200:** `{ success, data: { referrals: [{ userId, name, joinedAt, pointsEarned, status }] } }` + +### GET /api/points/levels + +**Description:** Public list of every configured level (from [[LevelConfig]]). Used by the marketing / levels page. +**Auth required:** Bearer JWT (but data is non-sensitive) +**Response 200:** `{ success, data: { levels: [LevelConfig, ...] } }` + +### GET /api/points/leaderboard + +**Description:** Top referrers by referral count and points earned. Used for community displays. +**Auth required:** Bearer JWT +**Query params:** `limit` (default 10), `period` (`all` | `month` | `week`) +**Response 200:** `{ success, data: { entries: [{ userId, name, avatar, referrals, pointsEarned }] } }` + +## Mutations + +### POST /api/points/redeem + +**Description:** Redeem points for a marketplace credit / discount. Server validates available balance and configured redemption rate. +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + amount: number; // points to redeem + purpose?: "wallet_credit" | "discount_code"; + purchaseRequestId?: string; // when applying to an in-progress purchase +} +``` +**Response 200:** +```json +{ + "success": true, + "data": { + "transaction": { /* PointTransaction */ }, + "redemption": { "creditAmount": 3.20, "currency": "USD", "code": "DISC-..." }, + "newBalance": 0 + } +} +``` +**Errors:** `400` insufficient balance, validation; `409` redemption already claimed. + +### POST /api/points/generate-referral-code + +**Description:** Generates (or rotates) the caller's unique referral code. Idempotent if the user already has one and `force` is not set. +**Auth required:** Bearer JWT +**Request body:** `{ force?: boolean }` +**Response 200:** `{ success, data: { referralCode: "ABCD1234", link: "https://amn.gg/r/ABCD1234" } }` + +The short link redirect (`GET /r/:code`) is mounted at the app root in `app.ts` and forwards to `${FRONTEND_URL}/auth/jwt/sign-up?ref=`. + +## Admin + +### POST /api/points/admin/add + +**Description:** Manually grant (or deduct, with negative amount) points to a user. Logged as a `PointTransaction` with `type: "admin_grant"` / `"admin_deduct"`. +**Auth required:** Bearer JWT (admin) +**Request body:** +```ts +{ + userId: string; + amount: number; // positive = grant; negative = deduct + reason: string; // mandatory free-text reason + metadata?: Record; +} +``` +**Response 200:** `{ success, data: { transaction, newBalance } }` +**Errors:** `403` non-admin, `404` target user not found, `400` would go below zero. + +## Side effects and real-time + +`PointsService` emits Socket.IO events on level-up and referral rewards: + +- `level-up` on `user-` when a transaction crosses a level threshold. +- `referral-reward` on `user-` when a referred user triggers a reward. + +See [[Socket Events]] for payload shape. + +## Related + +- [[PointTransaction]] +- [[LevelConfig]] +- [[Points and Levels Flow]] +- [[Referral Flow]] +- `backend/POINTS_MIGRATION.md` (legacy migration notes) diff --git a/03 - API Reference/Socket Events.md b/03 - API Reference/Socket Events.md new file mode 100644 index 0000000..4ecd40f --- /dev/null +++ b/03 - API Reference/Socket Events.md @@ -0,0 +1,152 @@ +--- +title: Socket Events +tags: [api, socket, realtime, reference] +--- + +# Socket Events + +The backend runs a Socket.IO server on the same HTTP port as the REST API. It is initialised in [`backend/src/app.ts`](../../backend/src/app.ts) and exposed globally as `global.io`. Helper functions for emitting events from services live in [`backend/src/infrastructure/socket/socketService.ts`](../../backend/src/infrastructure/socket/socketService.ts): + +```ts +emitGlobalEvent(event, data); // io.emit +emitToRoom(room, event, data); // io.to(room).emit +setSocketServer(server); +getSocketServer(); +``` + +## Connection + +```ts +import { io } from "socket.io-client"; + +const socket = io(process.env.NEXT_PUBLIC_API_URL!, { + withCredentials: true, + transports: ["websocket"], +}); +``` + +The CORS policy mirrors the REST API: only `FRONTEND_URL` is allowed, and credentials are enabled. There is no token-based handshake — the server identifies users by the `join-*` events the client emits after connecting. (A future improvement would authenticate via the JWT on handshake; for now, ownership is checked at the REST layer when a mutation triggers an event.) + +The server logs `🔌 User connected: ` on connect and `🔌 User disconnected: ` on disconnect. There is no broadcast `user-offline` on disconnect — see "Online status" below. + +## Rooms + +Rooms are the targeting mechanism. The client joins/leaves rooms with these events: + +| Client → Server | Room joined | +| --- | --- | +| `join-user-room` (userId) | `user-` | +| `join-request-room` (requestId) | `request-` | +| `leave-request-room` (requestId) | leaves `request-` | +| `join-seller-room` (sellerId) | `seller-` + `sellers` (global) | +| `leave-seller-room` (sellerId) | leaves both | +| `join-buyer-room` (buyerId) | `buyer-` + `buyers` (global) | +| `leave-buyer-room` (buyerId) | leaves both | +| `join-chat-room` (chatId) | `chat-` | +| `leave-chat-room` (chatId) | leaves `chat-` | +| `user-online` (userId) | joins `user-`, broadcasts `user-status-change` | +| `typing-start` ({ chatId, userId, userName }) | broadcasts `user-typing` to `chat-` | +| `typing-stop` ({ chatId, userId }) | broadcasts `user-typing` (isTyping=false) | + +Joining a room is unauthenticated — clients are expected to only join their own rooms. Sensitive data is filtered at the REST layer that emits the event. + +## Server → Client events + +Grouped by the service that emits them. + +### Marketplace + +| Event | Room | Payload | Source | +| --- | --- | --- | --- | +| `new-purchase-request` | `sellers` | `PurchaseRequest` document | `marketplaceController.createPurchaseRequest` | +| `new-offer` | `buyer-` | `{ requestId, offer, sellerId }` | `marketplaceController.createSellerOffer` | +| `seller-offer-update` | `seller-` (and global on payment confirm) | `{ sellerId, requestId, eventType: "payment-completed" \| "offer-rejected" \| "offer-accepted", data: { offerId, isSelected, paymentId?, transactionHash?, reason? } }` | `marketplaceController`, `shkeeperRoutes`, `shkeeperWebhook`, `SellerOfferService` | +| `purchase-request-update` | `request-` | `{ type, requestId, status?, paymentId?, txHash?, provider? }` | `marketplaceController`, `PurchaseRequestService`, `shkeeperRoutes`, `paymentCoordinator` | +| `request-cancelled` | `user-`, `user-` | `{ requestId, reason }` | `PurchaseRequestService` | +| `transaction-completed` | `user-`, `user-` | `{ requestId, paymentId, amount, currency }` | `marketplaceController` | +| `delivery-code-generated` | `request-` | `{ requestId, deliveryCode }` (only the seller UI uses this) | `DeliveryService` | +| `delivery-update` | `request-` | `{ requestId, status, carrier?, trackingNumber? }` | `DeliveryService` | +| `delivery-confirmed` | `request-` | `{ requestId }` | `DeliveryService` | +| `buyer-confirmed-delivery` | `user-` | `{ requestId, buyerId }` | `DeliveryService` | +| `template-checkout-payment-confirmed` | global + `template-checkout-` | `{ checkoutId, requestIds, paymentId }` | `templateCheckoutWebhook`, `paymentCoordinator` | +| `template-checkout-payment-pending` | global | `{ checkoutId }` | `templateCheckoutWebhook` | +| `template-checkout-payment-failed` | global | `{ checkoutId, reason }` | `templateCheckoutWebhook` | + +### Payment + +| Event | Room | Payload | Source | +| --- | --- | --- | --- | +| `payment-created` | global | `{ paymentId, provider, requestId, buyerId, sellerId, amount, currency }` | `shkeeperService`, `decentralizedPaymentService` | +| `payment-received` | `user-` | `{ purchaseRequestId, amount, currency, buyerId }` | `paymentRoutes`, `marketplace/routes` | +| `payment-update` | global + room-specific | `{ paymentId, status, escrowState?, txHash? }` | `paymentCoordinator` | +| `payout-created` | global | `{ payoutId, taskId, sellerId, amount, currency }` | `shkeeperPayoutService` | +| `payout-completed` | global, `user-` | `{ payoutId, taskId, txHash }` | `shkeeperPayoutService`, `decentralizedPaymentService` | +| `payout-updated` | global | `{ payoutId, status }` | `shkeeperPayoutService` | + +### Chat + +| Event | Room | Payload | +| --- | --- | --- | +| `new-message` | `chat-` | `{ chatId, message: { _id, content, senderId, createdAt, attachments? }, senderId }` | +| `messages-read` | `chat-` | `{ chatId, userId, upToMessageId, modifiedCount }` | +| `message-edited` | `chat-` | `{ chatId, messageId, content, editedAt }` | +| `message-deleted` | `chat-` | `{ chatId, messageId, deletedAt }` | +| `participants-added` | `chat-` | `{ chatId, addedUserIds }` | +| `participant-removed` | `chat-` | `{ chatId, removedUserId }` | +| `user-typing` | `chat-` | `{ userId, userName?, isTyping }` | +| `user-status-change` | broadcast | `{ userId, status: "online", lastSeen }` | + +Sources: [`ChatService.ts`](../../backend/src/services/chat/ChatService.ts), [`chatController.ts`](../../backend/src/services/chat/chatController.ts), and `app.ts` socket handlers. + +### Notification + +| Event | Room | Payload | +| --- | --- | --- | +| `new-notification` | `user-` | `{ notification: { _id, type, title, body, data, createdAt } }` | +| `unread-count-update` | `user-` | `{ unreadCount }` | + +Source: [`NotificationService.ts`](../../backend/src/services/notification/NotificationService.ts). + +### Points + +| Event | Room | Payload | +| --- | --- | --- | +| `level-up` | `user-` | `{ oldLevel, newLevel, lifetimePoints, perks }` | +| `referral-reward` | `user-` | `{ referredUserId, points, transactionId }` | +| `referral-signup` | `user-` | `{ referredUserId, name, joinedAt }` | + +Sources: [`PointsService.ts`](../../backend/src/services/points/PointsService.ts), [`authController.ts`](../../backend/src/services/auth/authController.ts). + +## Online status + +A client emits `user-online` after connecting; the server broadcasts `user-status-change` (status `"online"`). There is currently **no** matching offline emit on disconnect because the server does not track which userId belongs to which socketId. To implement presence, store the mapping on connect and emit `user-status-change` (status `"offline"`) in the disconnect handler. + +## Reconnection + +Socket.IO defaults are used: exponential backoff starting at 1s, capped at 5s, with jitter. The recommended client policy is: + +1. On reconnect, re-emit every `join-*` for the rooms the user cares about. +2. Re-fetch any state that may have moved while disconnected (`GET /api/notifications/unread-count`, the active purchase request, chat history with `?before=`). +3. Treat the first 5 seconds after reconnect as "catching up" in the UI. + +The server does not buffer missed events; if delivery guarantees matter, fall back to REST. + +## Server-side helpers (for service authors) + +```ts +import { emitGlobalEvent, emitToRoom } from "@/infrastructure/socket/socketService"; + +emitGlobalEvent("new-purchase-request", request); +emitToRoom(`request-${requestId}`, "purchase-request-update", { type: "status_changed", status }); +``` + +Use `emitToRoom` whenever you can — it limits the broadcast surface area. Reserve `emitGlobalEvent` for events that genuinely need every connected client (rare). + +## Related + +- [[Authentication API]] (token issuance) +- [[Chat API]] +- [[Notification API]] +- [[Marketplace API]] +- [[Payment API]] +- [[Real-time Architecture]] diff --git a/03 - API Reference/User API.md b/03 - API Reference/User API.md new file mode 100644 index 0000000..2b25337 --- /dev/null +++ b/03 - API Reference/User API.md @@ -0,0 +1,262 @@ +--- +title: User API +tags: [api, user, reference] +--- + +# User API + +Two routers are mounted for users: + +- `/api/user/*` - the new controller pattern in [`backend/src/services/user/userControllerRoutes.ts`](../../backend/src/services/user/userControllerRoutes.ts) wired to `userController`. +- `/api/users/*` - the legacy router in [`backend/src/services/user/userRoutes.ts`](../../backend/src/services/user/userRoutes.ts) (kept for backward compatibility, primarily used by the admin console). + +Address-book CRUD lives on its own service: [`/api/addresses/*`](../../backend/src/services/address/addressRoutes.ts). All endpoints require `Bearer JWT` unless noted. Source of truth model: [[User]]. + +## Profile (current user) + +### GET /api/user/profile + +**Description:** Returns the caller's profile. +**Auth required:** Bearer JWT +**Response 200:** +```json +{ "success": true, "data": { /* User without password / verification tokens */ } } +``` +**Source:** `userController.getCurrentUserProfile` + +### PUT /api/user/profile + +**Description:** Updates the caller's profile. +**Auth required:** Bearer JWT +**Request body (whitelisted fields):** +```ts +{ + firstName?: string; + lastName?: string; + name?: string; // alias for profile.name + phone?: string; + bio?: string; + website?: string; + photoURL?: string; // also mirrored to profile.avatar + isPublic?: boolean; + address?: { + street?: string; + city?: string; + state?: string; + country?: string; + postalCode?: string; + }; + preferences?: { + language?: "en" | "fa" | "ar"; + currency?: "USD" | "EUR" | "IRR" | "AED"; + notifications?: { email?: boolean; sms?: boolean; push?: boolean }; + }; +} +``` +**Response 200:** Updated user. + +### GET /api/users/profile + +**Description:** Legacy equivalent of the above. Returns the full sanitized user document. +**Auth required:** Bearer JWT + +### GET /api/users/profile/:userId + +**Description:** Public profile by id. If `profile.isPublic === false` and caller is not the owner or admin, only `firstName`, `lastName`, `avatar`, `role` are returned. +**Auth required:** Bearer JWT + +## Avatar upload + +Avatar upload is handled by the [[File API]]: + +`POST /api/files/upload/avatar` (multipart `avatar`) returns a URL that the caller then writes to `profile.avatar` via `PUT /api/user/profile`. + +## Wallet address + +### GET /api/user/wallet-address + +**Description:** Returns the caller's stored EVM wallet address (or `null`). +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "data": { "walletAddress": "0x..." | null } }` + +### PATCH /api/user/wallet-address + +**Description:** Verifies an EIP-191 signed message and stores `profile.walletAddress`. The server uses `ethers.verifyMessage(message, signature)` and rejects if the recovered address does not match. +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + walletAddress: string; // 0x-prefixed 40-hex + signature: string; // signed `message` + message: string; // human-readable challenge text +} +``` +**Response 200:** `{ "success": true, "data": { "walletAddress": "0x..." } }` +**Errors:** +- `400` missing fields, malformed address, signature mismatch +- `404` user not found + +The legacy alias `PATCH /api/users/wallet-address` performs the same logic. + +## Contacts and search + +### GET /api/users/contacts + +**Description:** Returns the users the caller is allowed to chat with based on role: +- `buyer` → sees `seller` + `admin` +- `seller` → sees `buyer` + `admin` +- `admin` → sees everyone +**Auth required:** Bearer JWT +**Response 200:** `{ "success": true, "data": { "contacts": [...], "count": N } }` + +### GET /api/users/search?q=&role= + +**Description:** Case-insensitive search across `firstName`/`lastName`/`email`. Returns up to 20 active users. +**Auth required:** Bearer JWT +**Errors:** `400` if `q.length < 2`. + +### GET /api/users?role=...&isActive=true&search=...&page=1&limit=50 + +**Description:** Paginated user directory (legacy, no admin gate). See pagination conventions in [[API Overview]]. +**Auth required:** Bearer JWT + +## Admin: user management + +These are duplicated across the two routers. The newer controller variants live under `/api/user/admin/*`; the legacy bodies live under `/api/users/admin/*`. All require `req.user.role === 'admin'` (the legacy routes check inline; the controller routes only check `authenticateToken` and the controller enforces the role). + +### POST /api/user/admin/create + +**Description:** Admin creates a user with a chosen role and verification state. +**Auth required:** Bearer JWT (admin) +**Request body:** +```ts +{ + email: string; + password: string; + firstName: string; + lastName: string; + role?: "buyer" | "seller" | "admin"; // default "buyer" + isActive?: boolean; // default true + isVerified?: boolean; // default false + profile?: { /* free-form */ }; +} +``` +**Response 201:** `{ success, data: { user } }` +**Errors:** `400` missing fields, `403` non-admin, `409` email exists. + +### DELETE /api/user/admin/:userId + +**Description:** Hard-delete a user. Prevents self-deletion and deleting other admins. +**Auth required:** Bearer JWT (admin) +**Response 200:** `{ success, data: { deletedUserId } }` +**Errors:** `400` self-delete, `403` admin-on-admin, `404` not found. + +### PATCH /api/user/admin/:userId/status + +**Description:** Activate / suspend a user. +**Auth required:** Bearer JWT (admin) +**Request body:** `{ isActive: boolean; reason?: string }` +**Response 200:** `{ success, data: { user: { _id, isActive, statusUpdatedAt } } }` + +### PATCH /api/user/admin/:userId/toggle-status + +**Description:** Flip active/suspended without explicit body. +**Auth required:** Bearer JWT (admin) + +### PATCH /api/user/admin/:userId/role + +**Description:** Change a user's role. +**Auth required:** Bearer JWT (admin) +**Request body:** `{ role: "buyer" | "seller" | "admin"; reason?: string }` +**Errors:** `400` invalid role. + +### GET /api/user/admin/list + +**Description:** Paginated admin user list with filters. +**Auth required:** Bearer JWT (admin) +**Query params:** `role`, `isActive`, `isVerified`, `search`, `page`, `limit`, `sortBy`, `sortOrder` +**Response 200:** `{ success, data: { users, pagination, stats: { totalUsers, activeUsers, verifiedUsers, buyers, sellers, admins } } }` + +### GET /api/user/admin/:userId/dependencies + +**Description:** Returns a count of related entities (purchase requests, offers, payments) before a destructive admin operation. +**Auth required:** Bearer JWT (admin) + +### GET /api/users/admin/stats + +**Description:** Aggregated user stats — total/active/verified counts, role distribution, activity buckets (24h / 7d / 30d). +**Auth required:** Bearer JWT (admin) + +### GET /api/users/admin/:userId + +**Description:** Fetch a single user by id (admin view, includes preferences and last-login). +**Auth required:** Bearer JWT (admin) + +### PUT /api/users/admin/:userId + +**Description:** Mass update a user document (admin override). +**Auth required:** Bearer JWT (admin) + +### PUT /api/users/admin/update/:email + +**Description:** Same as above but keyed on email. +**Auth required:** Bearer JWT (admin) + +### PATCH /api/users/admin/:userId/password + +**Description:** Admin forces a new password. Wipes `refreshTokens` so all sessions are invalidated. +**Auth required:** Bearer JWT (admin) +**Request body:** `{ newPassword: string; reason?: string }` + +### POST /api/users/admin/:userId/resend-verification + +**Description:** Regenerate the 8-digit email verification code and re-send the verification email. +**Auth required:** Bearer JWT (admin) +**Errors:** `400` user already verified. + +## Address book + +Source: [`backend/src/services/address/addressRoutes.ts`](../../backend/src/services/address/addressRoutes.ts), model: [[Address]]. + +### GET /api/addresses + +**Description:** List the caller's addresses. +**Auth required:** Bearer JWT +**Response 200:** `{ success: true, data: [Address, ...] }` + +### POST /api/addresses + +**Description:** Create a new address. First address auto-becomes primary. +**Auth required:** Bearer JWT +**Request body:** +```ts +{ + fullName: string; + phone: string; + street: string; + city: string; + state?: string; + country: string; + postalCode?: string; + isPrimary?: boolean; + notes?: string; +} +``` + +### PUT /api/addresses/:addressId + +**Description:** Update an address. +**Auth required:** Bearer JWT +**Errors:** `404` not owned by user. + +### DELETE /api/addresses/:addressId + +**Description:** Delete an address. If it was the primary, another address is promoted. +**Auth required:** Bearer JWT + +### PATCH /api/addresses/:addressId/primary + +**Description:** Promote an address to primary; demotes the previous primary. +**Auth required:** Bearer JWT + +See [[Address]] for the schema, and [[Marketplace API]] for how purchase requests reference an address. diff --git a/04 - Flows/Authentication Flow.md b/04 - Flows/Authentication Flow.md new file mode 100644 index 0000000..6871d53 --- /dev/null +++ b/04 - Flows/Authentication Flow.md @@ -0,0 +1,187 @@ +--- +title: Authentication Flow +tags: [flow, auth, jwt, login, security] +related_models: ["[[User]]", "[[TempVerification]]"] +related_apis: ["[[Auth API]]", "POST /api/auth/login", "POST /api/auth/refresh-token", "POST /api/auth/logout"] +--- + +# Authentication Flow + +End-to-end specification for **email + password** authentication, JWT issuance, token lifecycle, refresh-token rotation, and logout cleanup. This is the most-used auth path in the marketplace and underpins every protected API call and Socket.IO room subscription. + +## Actors + +- **User (Buyer / Seller / Admin)** – submits credentials via the frontend. +- **Frontend (Next.js)** – the JWT sign-in view in `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx`, which delegates to `signInWithPassword()` in `frontend/src/auth/context/jwt/action.ts`. +- **Backend (Express)** – `AuthController.login` in `backend/src/services/auth/authController.ts`. +- **MongoDB** – `User` collection (refresh tokens are appended to `user.refreshTokens[]`). +- **Redis** – session store (`sessionService.createSession`) and login-attempt rate-limiter (`rateLimitService.checkLoginAttempts`). +- **Socket.IO** – not directly involved in login, but the issued JWT is later used to join `user-${userId}` rooms (see [[Notification Flow]] and [[Chat Flow]]). + +## Preconditions + +- The user has already completed [[Registration Flow]] and their `User.isEmailVerified === true`. +- The user's `User.status === "active"` (soft-deleted accounts have status `"deleted"`). +- Network connectivity is available (the frontend uses `NetworkUtils.isOnline()` from `frontend/src/auth/utils/error-handler.ts`). +- `localStorage` is available — the frontend rejects the request early via `StorageUtils.isAvailable()` if storage is blocked. +- Backend env vars `JWT_SECRET`, `JWT_EXPIRES_IN`, `REFRESH_TOKEN_EXPIRES_IN` are set (`backend/src/services/auth/authService.ts:19-21`). + +## Step-by-step narrative + +1. **User fills the sign-in form** at `/auth/jwt/sign-in` and clicks "Sign in". The form is implemented in `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx`. +2. **Client-side guards**: `signInWithPassword()` (`action.ts:32-116`) verifies the browser is online and `localStorage` is writable; otherwise it throws a typed `AuthErrorHandler` error. +3. **HTTP request**: The frontend POSTs `{ email, password }` to `POST /api/auth/login` (resolved by `endpoints.auth.login` in `frontend/src/lib/axios.ts`). An `AbortController` is armed with a 60-second timeout. +4. **Validation middleware** runs `loginValidation` (`backend/src/services/auth/authValidation.ts`) — wires into Express via `authRoutes.ts:22`. +5. **Rate limiting**: `rateLimitService.checkLoginAttempts(email, 5, 15*60)` is called (`authController.ts:173`). Five failures within 15 minutes returns `429 TOO_MANY_ATTEMPTS`. Counters live in Redis so they survive restarts. +6. **User lookup**: `User.findOne({ email, status: "active" }).select("+password")` — `password` is `select: false` by default in the schema and must be explicitly projected. +7. **Password comparison**: `authService.comparePassword()` invokes `bcrypt.compare()` (cost factor 12 — see `authService.ts:102-105`). Constant-time per bcrypt's design. +8. **Email-verification gate**: If `!user.isEmailVerified`, returns `403 EMAIL_NOT_VERIFIED` with `needsVerification: true`. The frontend intercepts this in `action.ts:104-111` and redirects to `/auth/jwt/verify?email=...`. +9. **Reset attempts**: On success, `rateLimitService.resetLoginAttempts(email)` wipes the Redis counter. +10. **Last-login stamp**: `user.lastLoginAt = new Date()` is saved. +11. **Token issuance**: + - **Access token** — `authService.generateToken()` builds a JWT with `{ id, email, role, isEmailVerified, iat }`, signed with `HS256`, `issuer: 'marketplace-backend'`, `audience: 'marketplace-users'`, default `expiresIn` from config. + - **Refresh token** — `authService.generateRefreshToken()` issues a separate JWT with `{ id, type: 'refresh' }` and a longer TTL. +12. **Refresh-token persistence**: The new refresh token is appended to the `User.refreshTokens` array (`authController.ts:230-231`). This array is the server-side allow-list — only tokens present here can be used to mint new access tokens. +13. **Redis session**: `sessionService.createSession(accessToken, userId, email, role, ip, userAgent, 86400)` stores a 24h session record keyed by the access token (`authController.ts:235-247`). Failures are logged but do **not** block login. +14. **Response**: `200 OK` with `{ user: user.toJSON(), tokens: { accessToken, refreshToken } }`. `toJSON()` strips `password`, `refreshTokens`, and verification codes (see User model `toJSON` transform). +15. **Client-side storage**: The frontend writes both tokens to `localStorage` via `StorageUtils.safeSet()` — keys `accessToken` and `refreshToken` (`action.ts:77-82`). + +> [!warning] Token storage is `localStorage`, not cookies +> Tokens are persisted in `window.localStorage`. This is intentional (the API is a separate origin and the app is fully SPA-like), but it means tokens are reachable from any script running on the page. Mitigations in place: strict CSP via `helmet`, no third-party scripts in the auth views, and short access-token TTL with refresh rotation. There are **no httpOnly auth cookies**. + +16. **Axios interceptor** (`frontend/src/lib/axios.ts`) attaches `Authorization: Bearer ${accessToken}` to every subsequent request and, on `401/403`, automatically calls the refresh flow described below. +17. **Socket.IO bootstrap**: After login, the dashboard layout connects to Socket.IO and emits `join-user-room`, `join-buyer-room`/`join-seller-room` based on `user.role`. See `backend/src/app.ts:83-126`. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor U as User + participant FE as Frontend (Next.js) + participant BE as Backend (Express) + participant DB as MongoDB + participant R as Redis + participant IO as Socket.IO + + U->>FE: Enter email + password, click Sign in + FE->>FE: NetworkUtils.isOnline() / StorageUtils.isAvailable() + FE->>BE: POST /api/auth/login { email, password } + BE->>R: rateLimitService.checkLoginAttempts(email) + R-->>BE: { allowed: true, remaining } + BE->>DB: User.findOne({ email, status: "active" }).select("+password") + DB-->>BE: user document + BE->>BE: bcrypt.compare(password, user.password) + alt password invalid + BE-->>FE: 401 Invalid credentials + else email not verified + BE-->>FE: 403 EMAIL_NOT_VERIFIED + FE-->>U: Redirect /auth/jwt/verify + else success + BE->>R: rateLimitService.resetLoginAttempts(email) + BE->>DB: user.lastLoginAt = now; user.refreshTokens.push(refresh) + BE->>BE: generateToken(authUser) / generateRefreshToken(authUser) + BE->>R: sessionService.createSession(accessToken, ...) + BE-->>FE: 200 { user, tokens: { accessToken, refreshToken } } + FE->>FE: localStorage.setItem('accessToken' / 'refreshToken') + FE->>IO: socket.emit('join-user-room', userId) + FE-->>U: Redirect to /dashboard + end +``` + +## API calls + +| Method | Endpoint | Source | +|---|---|---| +| `POST` | `/api/auth/login` | `backend/src/services/auth/authRoutes.ts:22` → `authController.login` | +| `POST` | `/api/auth/refresh-token` | `authRoutes.ts:24-27` → `authController.refreshToken` | +| `POST` | `/api/auth/logout` | `authRoutes.ts:68` → `authController.logout` (protected) | +| `GET` | `/api/auth/profile` | `authRoutes.ts:69` → `authController.getProfile` | + +## Database writes + +- **`users` collection**: `lastLoginAt` updated; `refreshTokens` array gains one entry per successful login or refresh. +- No write at all on a failed attempt (only Redis counter increments). +- On `changePassword` / `resetPassword`: `refreshTokens` is reset to `[]` (forces re-login on every device — `authController.ts:600` and `:685`). + +## Socket events emitted + +- Login itself emits nothing. After the response, the frontend emits the **client-side** events `join-user-room`, `join-buyer-room` or `join-seller-room` to subscribe to targeted notifications. + +## Side effects + +- **Redis session**: 24-hour key holding `{ userId, email, role, ip, userAgent }`. Used for forced logout (admin can call `sessionService.deleteSession(token)`). +- **Redis rate-limit counter**: TTL 15 min, reset on success. +- **No email** is sent on a normal login (no "new sign-in" notification today — opportunity for future enhancement). +- **Sentry**: any unexpected exception bubbles to `Sentry.setupExpressErrorHandler` (`app.ts:351`). + +## Refresh-token flow + +The access token is short-lived. When a protected request returns `401 TOKEN_INVALID` or `403`, the axios interceptor calls: + +1. `POST /api/auth/refresh-token` with `{ refreshToken }` from `localStorage`. +2. Backend `authController.refreshToken` (`:263-313`) verifies the token via `verifyRefreshToken`, checks it is **still present in `user.refreshTokens[]`**, then issues a brand-new access **and** refresh token. +3. The old refresh token is **removed** from the array and the new one is pushed — implementing **refresh-token rotation**. A leaked-but-stale token therefore becomes invalid the moment the legitimate user refreshes. +4. The new pair is written back to `localStorage` and the original failed request is retried. + +```mermaid +sequenceDiagram + autonumber + participant FE as Frontend axios + participant BE as Backend + participant DB as MongoDB + + FE->>BE: GET /api/marketplace/... (Bearer access) + BE-->>FE: 401 TOKEN_INVALID + FE->>BE: POST /api/auth/refresh-token { refreshToken } + BE->>BE: verifyRefreshToken(refreshToken) + BE->>DB: User.findById(decoded.id); ensure refresh ∈ user.refreshTokens + BE->>BE: Generate new access + refresh tokens + BE->>DB: user.refreshTokens = [...minus old, new] + BE-->>FE: 200 { tokens: { accessToken, refreshToken } } + FE->>BE: GET /api/marketplace/... (Bearer new access) — retry +``` + +## Logout flow + +1. Frontend `signOut()` (`action.ts:146-176`) reads `refreshToken` from `localStorage` and POSTs `/api/auth/logout` with a 10-second timeout. +2. Backend `authController.logout` (`:316-344`) removes the refresh token from `user.refreshTokens[]` and calls `sessionService.deleteSession(accessToken)`. +3. **Always-clear**: the frontend's `finally` block removes both `accessToken` and `refreshToken` from `localStorage` regardless of network success — meaning even an offline logout effectively signs the user out locally. + +> [!tip] Force-logout an entire user +> Setting `user.refreshTokens = []` in MongoDB instantly invalidates all sessions on next refresh. `changePassword`, `resetPassword`, and `deleteAccount` all do this. + +## Error / edge cases + +- **Wrong password** → `401 Invalid credentials` (intentionally vague — no distinction between "unknown email" and "wrong password"). +- **Email unverified** → `403 EMAIL_NOT_VERIFIED`; frontend auto-redirects to verify page. +- **5+ failures in 15 min** → `429 TOO_MANY_ATTEMPTS`; only an admin can manually clear via Redis. +- **Network timeout** → axios `AbortController` cancels at 60s; frontend shows a typed error and the user can retry. +- **Redis down** → login still succeeds (session creation is best-effort, wrapped in try/catch at `authController.ts:234-247`). Rate limiting falls back to the in-memory map in `authService.ts:113-145` if `rateLimitService` itself throws. +- **Stale refresh token** (rotated by another device) → `403 Invalid refresh token`. Frontend signs out and redirects to sign-in. +- **JWT signature mismatch** (secret rotated) → all sessions invalidated server-side; clients clear tokens on first 401. +- **Token issued for another audience/issuer** → `verifyToken` returns `null` (`authService.ts:60-79`), middleware returns `403 TOKEN_INVALID`. +- **Refresh token used as access token** → blocked by the `if (decoded.type === 'refresh') return null` check in `verifyToken` (`authService.ts:67`). This is critical: a leaked refresh token alone cannot read protected data. +- **Soft-deleted account** → `User.findOne({ status: "active" })` filter excludes deleted accounts; login fails as if the email did not exist. + +> [!warning] Constant-time response is approximate +> Today we return `401` immediately when the user is missing, before running bcrypt. This is a timing oracle that lets an attacker enumerate registered emails by response-time analysis. Mitigation tracked separately — the recommendation is to always run a dummy bcrypt compare on missing users. + +## Linked flows + +- [[Registration Flow]] — produces the `User` document this flow consumes. +- [[Password Reset Flow]] — alternate entry into the account if credentials are lost. +- [[Google OAuth Flow]] — parallel auth path that produces equivalent tokens. +- [[Passkey (WebAuthn) Flow]] — passwordless alternative. +- [[Chat Flow]], [[Notification Flow]] — both consume the access token to authorise Socket.IO rooms. + +## Source files + +- Backend: `backend/src/services/auth/authController.ts:161-260` +- Backend: `backend/src/services/auth/authService.ts:24-99` +- Backend: `backend/src/services/auth/authRoutes.ts:22` +- Backend: `backend/src/services/redis/sessionService.ts` +- Backend: `backend/src/services/redis/rateLimitService.ts` +- Frontend: `frontend/src/auth/context/jwt/action.ts:32-176` +- Frontend: `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx` +- Frontend: `frontend/src/lib/axios.ts` (interceptor + endpoints) diff --git a/04 - Flows/Chat Flow.md b/04 - Flows/Chat Flow.md new file mode 100644 index 0000000..6861356 --- /dev/null +++ b/04 - Flows/Chat Flow.md @@ -0,0 +1,188 @@ +--- +title: Chat Flow +tags: [flow, chat, socket-io, messaging] +related_models: ["[[Chat]]", "[[Message]]", "[[User]]"] +related_apis: ["POST /api/chat", "POST /api/chat/:chatId/messages", "GET /api/chat/:chatId/messages", "POST /api/chat/:chatId/read"] +--- + +# Chat Flow + +Real-time messaging between buyer & seller (direct), three-way dispute mediation (group), and user-to-support (support). Backed by `Chat` documents with embedded `messages[]` and a Socket.IO room per chat for live updates. + +## Actors + +- **User A (initiator)** — typically a buyer. +- **User B (recipient)** — typically a seller. +- **Support agent** — for `type: 'support'` chats (user is `support@amn.gg`). +- **Admin** — added as a third participant in dispute chats. +- **Frontend** — `frontend/src/sections/chat/` (chat list, conversation view, message composer). +- **Backend** — `ChatService` (`backend/src/services/chat/ChatService.ts`), routes under `/api/chat`. +- **MongoDB** — `chats` collection with embedded `messages`, `participants`, `unreadCounts`, `settings`, `metadata`. +- **Socket.IO** — events `new-message`, `chat-notification`, `messages-read`, `user-typing`, `user-status-change`. + +## Preconditions + +- Both parties authenticated. +- For `direct` chats tied to a purchase request, the request exists and both users are participants in it. + +## Chat lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Created: ChatService.createChat\n(or auto on first contact) + Created --> Active: messages flowing + Active --> Active: send / read / typing + Active --> Archived: settings.isArchived=true + Archived --> Active: unarchive + Active --> [*]: chat deleted (rare) +``` + +## Step-by-step narrative + +### Creation + +1. **Direct chat (find-or-create)** — when buyer clicks "Chat with seller" on a request detail, frontend POSTs `POST /api/chat` with `{ type: 'direct', participantIds: [buyer, seller], relatedTo: { type: 'PurchaseRequest', id } }`. +2. `ChatService.createChat` (`ChatService.ts:90-192`): + - For `direct` with exactly 2 participants, runs `Chat.findOne({ type: 'direct', 'participants.userId': { $all: [...] }, 'participants.isActive': true })` and returns the existing chat if found. + - Otherwise creates a new `Chat` with `participants` (each with `role:'member'`, `joinedAt`, `isActive:true`), zeroed `unreadCounts`, default `settings`, `metadata.createdBy`. + - Appends a system welcome message (`messageType: 'system'`). + - If `relatedTo.type === 'PurchaseRequest'`, also writes `"چت برای درخواست خرید \"{title}\" ایجاد شد"` system line. + - Re-loads with `populate('participants.userId', 'firstName lastName profile.avatar email')` for the response. +3. **Group chat (dispute)** — same pattern, but `type: 'group'`, all three participants (buyer, seller, admin) added (admin is added later by `DisputeService.assignAdmin`). +4. **Support chat** — `ChatService.createSupportChat(userId)` (`:41-88`) auto-discovers `User.findOne({ email: 'support@amn.gg' })` and creates a `type: 'support'` chat with a welcome message. Idempotent. +5. **Post-payment auto-chat** — when SHKeeper confirms payment, `shkeeperWebhook.ts:606-618` calls `chatService.createChat` to ensure a direct chat exists between buyer and winning seller. + +### Joining the room (real-time) + +6. On chat page mount, the frontend emits `socket.emit('join-chat-room', chatId)` (`backend/src/app.ts:130-133`). The socket joins room `chat-{chatId}`. +7. Optionally `socket.emit('user-online', userId)` so other clients see green status (`app.ts:161-169`). + +### Sending a message + +8. User types and hits send. Frontend POSTs `POST /api/chat/:chatId/messages` with `{ content, messageType?, fileUrl?, fileName?, fileSize?, replyTo? }`. +9. `ChatService.sendMessage` (`:195-260`): + - Loads chat, verifies the sender is in `participants[]` and `isActive`. + - Builds `Message`: `{ senderId, content, messageType: 'text', fileUrl?, fileName?, fileSize?, replyTo?, timestamp, isRead: false, isEdited: false }`. + - `chat.addMessage(messageData)` — schema method that pushes the message, updates `metadata.lastActivity`, increments `unreadCounts` for non-senders, and updates the cached `lastMessage` summary. + - Persists. + - Emits **`new-message`** to `chat-{chatId}` (everyone in the room sees the message immediately). + - Emits **`chat-notification`** to each non-sender's `user-{userId}` room (drives the chat-list unread badge and the toast/notification bell if the user is not currently viewing the chat). +10. Frontend reconciles its own message list (the sender either appends optimistically and then matches the server echo or waits for the round-trip). + +### Attachments + +11. To attach a file, the user picks a file → frontend calls `chatService.uploadChatFile(chatId, file)` (or the equivalent `POST /api/chat/:chatId/upload`) — backend persists the upload via `fileService` (returns `{ fileUrl, fileName, fileSize }`). +12. The send-message call then includes `messageType: 'image' | 'file'` and the file metadata. Files are served from `/uploads`. + +### Read receipts + +13. When the user opens a chat, frontend POSTs `POST /api/chat/:chatId/read` (optionally with `messageIds: string[]`). +14. `ChatService.markMessagesAsRead` (`:438-483`): + - Calls `chat.markAsRead(userId, messageObjectIds)` (schema method that flips `isRead` on the relevant messages and zeros the user's `unreadCounts` entry). + - Emits **`messages-read`** to `chat-{chatId}` so the sender sees the double-tick. + +### Typing indicator + +15. On `input` events, frontend emits `socket.emit('typing-start', { chatId, userId, userName })`; on idle/blur emits `typing-stop`. +16. Backend `app.ts:142-158` relays to `chat-{chatId}` as `user-typing` (excluding the sender). No DB persistence. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor A as User A + actor B as User B + participant FE_A as Frontend A + participant FE_B as Frontend B + participant BE as Backend + participant DB as MongoDB + participant IO as Socket.IO + + A->>FE_A: Open conversation + FE_A->>BE: POST /api/chat {type:direct, participantIds, relatedTo} + BE->>DB: find-or-create Chat + BE-->>FE_A: { chat } + FE_A->>IO: emit 'join-chat-room' chatId + FE_B->>IO: emit 'join-chat-room' chatId (when B opens too) + + A->>FE_A: type & send + FE_A->>BE: POST /api/chat/{id}/messages {content} + BE->>DB: chat.addMessage; metadata.lastActivity=now + BE->>IO: emit chat-{id} 'new-message' + IO-->>FE_A: 'new-message' (echo) + IO-->>FE_B: 'new-message' (live) + BE->>IO: emit user-{B} 'chat-notification' (badge) + + B->>FE_B: opens chat + FE_B->>BE: POST /api/chat/{id}/read + BE->>DB: chat.markAsRead(B) + BE->>IO: emit chat-{id} 'messages-read' + IO-->>FE_A: 'messages-read' (double-tick) + + A->>IO: emit 'typing-start' + IO-->>FE_B: 'user-typing' {isTyping:true} +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `POST` | `/api/chat` | Find-or-create chat | +| `GET` | `/api/chat` | List user's chats | +| `GET` | `/api/chat/:chatId/messages` | Paginated message history | +| `POST` | `/api/chat/:chatId/messages` | Send message | +| `POST` | `/api/chat/:chatId/upload` | Upload attachment | +| `POST` | `/api/chat/:chatId/read` | Mark read | +| `POST` | `/api/chat/support` | Create/get support chat | + +## Database writes + +- **`chats`**: insert on create; `$push` into `messages[]`; `$set` `metadata.lastActivity`; `unreadCounts.$.count` increment per recipient; `settings.isArchived` toggled; `participants.$.isActive` flipped on leave. + +## Socket events emitted + +- **`new-message`** → `chat-{chatId}` (every message). +- **`chat-notification`** → `user-{recipientId}` for non-senders (badge). +- **`messages-read`** → `chat-{chatId}` after read mark. +- **`user-typing`** → `chat-{chatId}` (relayed by `app.ts`). +- **`user-status-change`** → broadcast when `user-online` is emitted. +- **`new-message`** (system) for system welcome lines on chat creation. + +## Side effects + +- **`metadata.lastActivity`** drives the chat-list sort order. +- **`lastMessage`** cache lets the chat-list render previews without loading the entire `messages[]` array. +- **`unreadCounts`** is the source-of-truth for badge counts; resetting on read also drives global unread totals. +- **Embedded messages array** can grow large; consider migrating to a separate `messages` collection if conversations exceed several thousand messages. + +## Error / edge cases + +- **Sender not a participant** → `403 "User is not a participant in this chat"` (`:209-211`). +- **Chat not found** → `404` on `getChatMessages`. +- **Direct duplicate** → idempotent — `createChat` returns existing chat. +- **Empty content** — currently allowed (system messages are typically non-empty though); add a min-length validator if needed. +- **Files served from `/uploads`** — anonymous access is allowed. Sensitive attachments (KYC docs, dispute evidence) should be protected by signed URLs or per-user authorisation; currently any user with the URL can fetch. +- **Long conversations** — `getChatMessages` slices an in-memory copy of the messages array. For >10k messages this is inefficient. Use aggregation `$slice` or a separate collection. +- **Race on `markAsRead`** — two parallel reads may double-zero the counter, which is harmless. +- **Typing indicator spam** — clients should debounce `typing-start` and emit `typing-stop` on a 2s idle. + +> [!warning] Notification message uses placeholder sender name +> `ChatService.sendMessage` posts `chat-notification` with `senderName: "کاربر"` (`:248`) — the literal Persian word for "user". Resolve `senderName` from `participant.userId.firstName` for a better UX. + +## Linked flows + +- [[Notification Flow]] — `chat-notification` is one of the inputs. +- [[Negotiation Flow]] — uses chats heavily. +- [[Dispute Flow]] — three-way group chat. +- [[Authentication Flow]] — supplies the `user-${userId}` rooms via `join-user-room`. + +## Source files + +- Backend: `backend/src/services/chat/ChatService.ts` +- Backend: `backend/src/services/chat/chatController.ts` +- Backend: `backend/src/services/chat/routes.ts` (under `/api/chat`) +- Backend: `backend/src/models/Chat.ts` +- Backend: `backend/src/app.ts:130-179` (Socket.IO chat handlers) +- Frontend: `frontend/src/sections/chat/` +- Frontend: `frontend/src/contexts/socket-context.tsx` (or equivalent socket provider) diff --git a/04 - Flows/Delivery Confirmation Flow.md b/04 - Flows/Delivery Confirmation Flow.md new file mode 100644 index 0000000..bdb779e --- /dev/null +++ b/04 - Flows/Delivery Confirmation Flow.md @@ -0,0 +1,127 @@ +--- +title: Delivery Confirmation Flow +tags: [flow, delivery, escrow-release, code] +related_models: ["[[PurchaseRequest]]", "[[Payment]]"] +related_apis: ["POST /api/marketplace/purchase-requests/:id/delivery-code", "POST /api/marketplace/purchase-requests/:id/verify-delivery"] +--- + +# Delivery Confirmation Flow + +After the escrow is funded ([[Payment Flow - SHKeeper]] / [[Payment Flow - DePay & Web3]]) and the seller has prepared the item, the seller **marks shipped**, the buyer **enters a delivery code** to confirm receipt, and the escrow becomes eligible for release ([[Payout Flow]]). + +## Actors + +- **Seller** — marks the order shipped and presents the delivery code to the buyer at hand-off. +- **Buyer** — confirms by entering the code in the dashboard. +- **Backend** — `DeliveryService` (`backend/src/services/delivery/DeliveryService.ts`), exposed through the marketplace routes (`backend/src/services/marketplace/routes.ts`). +- **MongoDB** — `purchaserequests.deliveryInfo` subdocument fields. +- **Socket.IO** — `delivery-code-generated`, `delivery-update`. + +## Preconditions + +- `PurchaseRequest.status` is `payment`, `processing`, or `delivery`. +- `Payment.escrowState === 'funded'`. + +## Step-by-step narrative + +1. **Seller marks shipped** — from the seller step `frontend/src/sections/request/components/seller-steps/step-3-ship-goods.tsx`, clicks "Mark as shipped". This patches the request status to `delivery`. +2. **Delivery code generation** — when the order transitions to `delivery`, `DeliveryService.generateDeliveryCode(requestId)` is invoked. It: + - Generates a 6-digit code (`Math.floor(100000 + Math.random()*900000)`). + - Sets `deliveryInfo.deliveryCode`, `deliveryCodeGeneratedAt = now`, `deliveryCodeExpiresAt = now + 7d`, `deliveryCodeUsed = false`. + - Emits `delivery-code-generated` and `delivery-update` to `request-{requestId}`. + - Sends a notification to the buyer with the code (in-app, and via email if configured). +3. **Buyer entry** — buyer meets the courier / picks up the item, enters the code in `frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx` (also surfaced on the buyer side via `step-5-receive-goods.tsx`). +4. **Verification** — `POST /api/marketplace/purchase-requests/:id/verify-delivery` with `{ code }`: + - Matches `code` against `deliveryInfo.deliveryCode`. + - Checks `deliveryCodeExpiresAt > now` and `deliveryCodeUsed === false`. + - On success: `deliveryInfo.deliveryCodeUsed = true; deliveryCodeUsedAt = now`. Status flips `delivery → delivered`. + - Emits `purchase-request-update` `status-changed`. + - Triggers buyer/seller notifications via `notifyDeliveryConfirmed` (see `PurchaseRequestService.ts:631-641`). +5. **Optional auto-release timer** — once `status === 'delivered'`, a scheduled job can flip the request to `confirming` and then to `seller_paid` after a grace period (e.g. 48h). The auto-release worker is not yet implemented; today an admin completes the chain via [[Payout Flow]]. +6. **Manual fast-track** — the buyer can also tap "Confirm I received it" to skip the code (used when the code path fails — e.g. lost in transit) which patches `status` to `delivered`. This relies on admin trust. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor S as Seller + actor B as Buyer + participant FE as Frontend + participant BE as Backend + participant DB as MongoDB + participant IO as Socket.IO + + S->>FE: Click "Mark as shipped" + FE->>BE: PATCH /api/marketplace/purchase-requests/{id} {status:"delivery"} + BE->>DB: PurchaseRequest.status="delivery" + BE->>BE: DeliveryService.generateDeliveryCode + BE->>DB: deliveryInfo.deliveryCode=XXXXXX\nexpires=+7d + BE->>IO: emit request-{id} 'delivery-code-generated' + BE->>B: notification w/ code (in-app/email) + + S->>B: At hand-off, share the 6-digit code (verbally) + B->>FE: Enter code in dashboard + FE->>BE: POST /api/marketplace/purchase-requests/{id}/verify-delivery {code} + BE->>DB: match code, expires>now, !used + BE->>DB: deliveryCodeUsed=true; status="delivered" + BE->>IO: emit request-{id} 'purchase-request-update' status-changed + BE->>B,S: notifyDeliveryConfirmed + Note over BE: Auto-release timer (planned) → seller_paid → payout +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `PATCH` | `/api/marketplace/purchase-requests/:id` `{status:"delivery"}` | Seller marks shipped | +| `POST` | `/api/marketplace/purchase-requests/:id/delivery-code` | Manual code regeneration (admin) | +| `POST` | `/api/marketplace/purchase-requests/:id/verify-delivery` | Buyer confirms with code | +| `PATCH` | `/api/marketplace/purchase-requests/:id/confirm-delivery` | Buyer fast-track confirm (no code) | + +## Database writes + +- **`purchaserequests.deliveryInfo`** — `deliveryCode`, `deliveryCodeGeneratedAt`, `deliveryCodeExpiresAt`, `deliveryCodeUsed`, `deliveryCodeUsedAt`. +- **`purchaserequests.status`** — `delivery` → `delivered` → (eventually `seller_paid` → `completed`). +- **`notifications`** — generated for both parties. + +## Socket events emitted + +- **`delivery-code-generated`** → `request-{id}` (with code, expiresAt). +- **`delivery-update`** → `request-{id}` (`type: 'code-generated'`). +- **`purchase-request-update`** `status-changed` on `delivery → delivered`. +- **`new-notification`** → `user-{buyerId}` with the code. + +## Side effects + +- Code is **emitted via socket and in-app notification**. If a malicious actor has access to the buyer's notifications, they could intercept and confirm delivery prematurely. Treat the code as confidential at the UI layer. +- Triggers the path that eventually frees up the escrow (manual today via [[Payout Flow]], auto in the future). + +## Error / edge cases + +- **Wrong code** → `400 Invalid delivery code`. +- **Expired code** (>7 days) → `400 Code expired`. Admin can regenerate via the manual endpoint. +- **Already used code** → `400 Code already used`. +- **Buyer never confirms** → status remains `delivery`. Auto-release timer (not yet built) should trigger `delivered` after N days. Until then, admin intervention. +- **Seller delivers but never marks shipped** → buyer can dispute via [[Dispute Flow]]; the dispute resolution will release the escrow regardless. +- **Lost code** → `POST /:id/delivery-code` regenerates a new 6-digit value, invalidates the old one, and re-notifies. Restrict to admin/seller to avoid abuse. + +> [!tip] Use the code as proof-of-handover +> The seller should ask the courier or the buyer at the door for the code before leaving the item. If the buyer disputes "never received", an unused code is strong circumstantial evidence; a used code = buyer confirmed. + +## Linked flows + +- [[Payment Flow - SHKeeper]] / [[Payment Flow - DePay & Web3]] — funding precondition. +- [[Escrow Flow]] — state transitions triggered by confirmation. +- [[Payout Flow]] — fires after confirmation (manual today). +- [[Dispute Flow]] — escape hatch. +- [[Notification Flow]] — channel for the code. + +## Source files + +- Backend: `backend/src/services/delivery/DeliveryService.ts` +- Backend: `backend/src/services/marketplace/routes.ts` (delivery endpoints) +- Backend: `backend/src/services/marketplace/PurchaseRequestService.ts:631-641` (confirmation notifications) +- Frontend: `frontend/src/sections/request/components/seller-steps/step-3-ship-goods.tsx` +- Frontend: `frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx` +- Frontend: `frontend/src/sections/request/components/buyer-steps/step-5-receive-goods.tsx` diff --git a/04 - Flows/Dispute Flow.md b/04 - Flows/Dispute Flow.md new file mode 100644 index 0000000..f3cac42 --- /dev/null +++ b/04 - Flows/Dispute Flow.md @@ -0,0 +1,199 @@ +--- +title: Dispute Flow +tags: [flow, dispute, mediator, evidence, chat, state-machine] +related_models: ["[[Dispute]]", "[[Chat]]", "[[PurchaseRequest]]", "[[Payment]]"] +related_apis: ["POST /api/disputes", "POST /api/disputes/:id/assign", "POST /api/disputes/:id/resolve", "POST /api/disputes/:id/evidence", "PATCH /api/disputes/:id/status"] +--- + +# Dispute Flow + +When something goes wrong (item not delivered, wrong item, fraud), either party can open a **dispute**. A three-way chat (buyer, seller, admin mediator) is created automatically. After evidence is gathered, the admin **resolves** the dispute — releasing the escrow to the seller, refunding the buyer, splitting the funds, or rejecting the claim. + +## Actors + +- **Buyer** — typical initiator. +- **Seller** — party against whom the dispute is raised (or in rarer cases, initiator). +- **Admin / Mediator** — assigned to investigate. +- **Frontend** — buyer/seller "Report issue" buttons in the request detail view; admin dispute dashboard. +- **Backend** — `DisputeService` (`backend/src/services/dispute/DisputeService.ts`), `DisputeController` (`backend/src/controllers/disputeController.ts`), routes at `backend/src/routes/disputeRoutes.ts`. +- **MongoDB** — `disputes`, `chats`, `purchaserequests`, `payments`. +- **Socket.IO** — `new-notification`, `new-message`, `dispute-updated` (planned). + +## Preconditions + +- The related `PurchaseRequest` exists. +- The initiator is the request's buyer or the related seller. +- Funds are typically held in escrow (`Payment.escrowState = 'funded'`) — disputes on unfunded orders are accepted but have no monetary impact. + +## Dispute state machine (`Dispute.status`) + +```mermaid +stateDiagram-v2 + [*] --> pending: createDispute()\nresponseDeadline=+48h\ndeadline=+7d + pending --> in_progress: admin assigned\nassignAdmin() + in_progress --> resolved: admin resolves\naction ∈ {refund, partial, release, reject} + in_progress --> closed: admin closes without resolution\n(e.g. duplicate/spam) + pending --> closed: same + resolved --> [*] + closed --> [*] +``` + +Resolution actions (from `Dispute.resolution.action` enum, see `Dispute.ts`): `refund`, `partial`, `release`, `reject` (the wording differs slightly in the model — verify with `backend/src/models/Dispute.ts`). + +## Step-by-step narrative + +### Phase 1 — Opening + +1. Buyer or seller opens the request detail and clicks **"Report problem"** (`frontend/src/sections/request/components/report-problem-to-admin.tsx`). +2. They select a `category` (delivery, payment, quality, fraud, other), a `priority` (`low | medium | high | urgent`), write a `description`, and optionally upload `evidence` (images, screenshots, video, document) via `POST /api/files/upload`. +3. Frontend POSTs `POST /api/disputes` with `{ purchaseRequestId, reason, description, priority, category, evidence: [...] }`. +4. Backend `DisputeService.createDispute` (`:12-119`): + - Loads the purchase request with `populate('selectedOfferId')`. + - Resolves the **counter-party `sellerId`** by priority: explicit `data.sellerId` → `selectedOffer.sellerId` → first of `preferredSellerIds`. This means once an offer is accepted, the dispute targets the actual seller, not the entire preferred list. + - Creates the `Dispute` with `status: 'pending'`, `responseDeadline = now + 48h`, `deadline = now + 7 days`, and an empty `timeline[]`. + - Creates a **`Chat` of type `group`** with the buyer and the resolved seller as participants. The opening message is a system-typed line `"اختلاف جدید ایجاد شد: {reason}"`. The chat's `relatedTo = { type: 'PurchaseRequest', id }`. + - Persists `dispute.chatId = chat._id`. +5. Notifications (currently a `TODO` in the service — `:107-116`) should fire `new-notification` to the seller. Today the chat creation alone provides real-time presence via the `new-message` socket emit inside `Chat.create`'s lifecycle. + +> [!warning] Dispute does not auto-pause escrow +> Today, opening a dispute does **not** flip `Payment.escrowState` away from `funded`. An admin could theoretically still release the escrow before resolving the dispute. Until a `disputed` flag is added to Payment, admins must check the dispute table before any release/refund action. + +### Phase 2 — Admin assignment + +6. The admin dispute dashboard lists pending disputes (sorted by `priority: -1, createdAt: -1`). +7. Admin clicks "Pick up" → `POST /api/disputes/:id/assign` with `{ adminId }` (currently the admin's own id). +8. `DisputeService.assignAdmin` (`:184-223`): + - `dispute.adminId = adminId; dispute.status = 'in_progress'`. + - Appends `timeline` entry `{ action: 'admin_assigned', performedBy: adminId, ... }`. + - Adds the admin to the dispute `chat.participants[]` (role `admin`). + - Saves. + +### Phase 3 — Investigation + +9. All three parties chat in the dispute chat room (same socket mechanics as [[Chat Flow]]). Each party can upload more evidence via `POST /api/disputes/:id/evidence` — `DisputeService.addEvidence` (`:305-337`) appends to `dispute.evidence[]` and writes a `timeline` entry `evidence_added`. +10. The admin may also `PATCH /api/disputes/:id/status` with intermediate states or notes; this updates `dispute.status` and writes a `timeline` entry `status_changed`. + +### Phase 4 — Resolution + +11. Once the admin has enough information, they call `POST /api/disputes/:id/resolve` with `{ action, amount?, currency?, notes? }`. +12. `DisputeService.resolveDispute` (`:262-300`): + - `dispute.status = 'resolved'` + - `dispute.resolution = { action, amount, currency, notes, resolvedBy: adminId, resolvedAt: now }` + - `dispute.closedAt = now` + - Appends `timeline` entry `dispute_resolved`. + - Saves. +13. **Financial side-effect (manual today):** depending on the action, the admin then triggers either the **payout** ([[Payout Flow]] with `kind: 'release'`) or the **refund** (`kind: 'refund'`, see [[Escrow Flow]]). The dispute service does not automatically dispatch the on-chain action. +14. Both parties are notified (TODOs in code — planned: `notifyDisputeResolved`). + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor B as Buyer + actor S as Seller + actor A as Admin + participant FE as Frontend + participant BE as Backend + participant DB as MongoDB + participant IO as Socket.IO + + B->>FE: "Report problem" on request + B->>FE: Choose category, priority, evidence + FE->>BE: POST /api/disputes + BE->>DB: Dispute.create({status:"pending"}) + BE->>DB: Chat.create({type:"group", participants:[buyer, seller], system message}) + BE->>DB: dispute.chatId = chat._id + BE-->>FE: { dispute } + FE-->>B,S: chat opens (real-time via existing chat join) + + A->>FE: Admin dashboard, click "Pick up" + FE->>BE: POST /api/disputes/{id}/assign + BE->>DB: dispute.adminId, status="in_progress", timeline.push + BE->>DB: chat.participants.push(admin) + BE-->>FE: { dispute } + + loop investigation + A->>FE: Chat with B & S + B-->>BE: POST /api/disputes/{id}/evidence (image) + BE->>DB: dispute.evidence.push, timeline.push + end + + A->>FE: Click "Resolve" choose action + FE->>BE: POST /api/disputes/{id}/resolve { action, amount, notes } + BE->>DB: dispute.status="resolved", resolution={...} + alt action="refund" + A->>BE: trigger refund payout to buyer\n[[Escrow Flow]] / [[Payout Flow]] + else action="release" + A->>BE: trigger payout to seller\n[[Payout Flow]] + else action="partial" + A->>BE: split — refund X to buyer, release Y to seller + end + BE-->>FE: { dispute } + IO-->>B,S: 'new-notification' dispute resolved (planned) +``` + +## API calls + +| Method | Endpoint | Source | +|---|---|---| +| `POST` | `/api/disputes` | `disputeRoutes.ts:12` → `DisputeController.createDispute` | +| `GET` | `/api/disputes` | `disputeRoutes.ts:15` (filters: status, priority, category, adminId, buyer/seller) | +| `GET` | `/api/disputes/statistics` | `disputeRoutes.ts:18` | +| `GET` | `/api/disputes/:id` | `disputeRoutes.ts:21` | +| `POST` | `/api/disputes/:id/assign` | `disputeRoutes.ts:24` | +| `PATCH` | `/api/disputes/:id/status` | `disputeRoutes.ts:27` | +| `POST` | `/api/disputes/:id/resolve` | `disputeRoutes.ts:30` | +| `POST` | `/api/disputes/:id/evidence` | `disputeRoutes.ts:33` | + +All require `authenticateToken` (router-level middleware). + +## Database writes + +- **`disputes`** — insert on open; updates `adminId`, `status`, `timeline[]`, `evidence[]`, `resolution`, `closedAt` over the lifecycle. +- **`chats`** — new `group` chat on open; admin appended to `participants[]` on assignment; messages appended throughout. +- **`purchaserequests`** — not directly mutated by the dispute service; the resolution side-effect (release/refund) updates the request via [[Escrow Flow]]. +- **`payments`** — touched indirectly when the admin performs the financial resolution. +- **`notifications`** — `TODO` markers in code; planned addition. + +## Socket events emitted + +- **`new-message`** → `chat-{disputeChatId}` for each chat line (via the standard `ChatService.sendMessage` and the system message created in `DisputeService.createDispute`). +- **`new-notification`** (planned) → `user-{buyerId}` and `user-{sellerId}` on creation, assignment, evidence-added, resolution. + +## Side effects + +- **Three-way chat creation** is the most visible side effect — pulls the buyer and seller into a controlled conversation room. +- **Timeline append-only log** is the audit trail. Surface it in the admin UI for compliance. +- **Response deadline = 48h** — used by reminders / SLA dashboards (no automated enforcement today). Past-deadline disputes could auto-escalate priority. +- **Hard deadline = 7d** — same intent: a watchdog could mark long-unresolved disputes for admin attention. + +## Error / edge cases + +- **Purchase request missing** → `400 Purchase request not found`. +- **No seller identifiable** (orphan request) → dispute still created but with `sellerId: undefined`; the chat becomes 2-party (buyer + admin only). Recommended: reject creation in this case to avoid mediator-less situations. +- **Initiator is neither buyer nor seller** → not enforced at service level — should be validated in `DisputeController` (recommended hardening). +- **Same user opens multiple disputes for the same request** → no uniqueness constraint today. Consider adding `unique on (purchaseRequestId, status:'pending'|'in_progress')` to prevent duplicates. +- **Evidence URL is hot-linked** → frontend uploads through `POST /api/files/upload` and the URL is served from `/uploads`. Ensure auth on the upload endpoint to prevent random users from polluting evidence. +- **Dispute resolved without financial follow-up** → the dispute is "resolved" in record only; the escrow stays in its previous state. Add automation that auto-fires the payout/refund when the admin selects `release` or `refund`. +- **Admin resigns mid-dispute** → no transfer-of-mediator endpoint today. Add `POST /api/disputes/:id/reassign`. + +> [!tip] Sort disputes by priority + age +> The query `Dispute.find().sort({ priority: -1, createdAt: -1 })` already used in `getDisputes` ensures `urgent` ones bubble to the top. Make sure the admin dashboard uses this default sort. + +## Linked flows + +- [[Chat Flow]] — message-level mechanics inside the dispute chat. +- [[Escrow Flow]] — the financial state being contested. +- [[Payout Flow]] — executed on `release` resolutions. +- [[Notification Flow]] — channels for dispute alerts. +- [[Delivery Confirmation Flow]] — disputes often arise from failed delivery. + +## Source files + +- Backend: `backend/src/services/dispute/DisputeService.ts` +- Backend: `backend/src/controllers/disputeController.ts` +- Backend: `backend/src/routes/disputeRoutes.ts` +- Backend: `backend/src/models/Dispute.ts` +- Frontend: `frontend/src/sections/request/components/report-problem-to-admin.tsx` +- Frontend: admin dispute dashboard under `frontend/src/sections/admin/` (subject to organisation) diff --git a/04 - Flows/Escrow Flow.md b/04 - Flows/Escrow Flow.md new file mode 100644 index 0000000..e1600d0 --- /dev/null +++ b/04 - Flows/Escrow Flow.md @@ -0,0 +1,196 @@ +--- +title: Escrow Flow +tags: [flow, escrow, payment, state-machine] +related_models: ["[[Payment]]", "[[PurchaseRequest]]"] +related_apis: ["POST /api/payment/release/:paymentId", "POST /api/payment/refund/:paymentId"] +--- + +# Escrow Flow + +The escrow is not a separate smart contract — it is a **state machine on the `Payment` document** combined with a **custodial wallet** (the platform-controlled BSC address `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS`). Funds sit at that wallet once SHKeeper / Web3 verification completes, and are released to the seller or refunded to the buyer based on order outcome. + +## Actors + +- **System** — the backend, on receiving pay-in confirmation. +- **Buyer** — confirms delivery to authorise release; can open a dispute to block release. +- **Seller** — recipient of release. +- **Admin** — resolves disputes and signs payout transactions when manual control is required. +- **MongoDB** — `payments` document holds the canonical `escrowState`. + +## Escrow state machine (`Payment.escrowState`) + +Enum from `Payment.ts:112-115`: `funded | releasable | released | refunded | releasing | failed`. + +```mermaid +stateDiagram-v2 + [*] --> Pending: Payment.status="pending"\nescrowState=undefined + Pending --> Partial: webhook PARTIAL\nescrowState="partial" + Pending --> Funded: webhook PAID/OVERPAID\nor on-chain verify success\nescrowState="funded" + Partial --> Funded: top-up reaches threshold + Funded --> Releasable: buyer confirms delivery\n(or auto-release timer) + Releasable --> Releasing: admin/system initiates payout\n[[Payout Flow]] + Releasing --> Released: payout tx confirmed\nescrowState="released" + Releasing --> Failed: payout tx reverted\nescrowState="failed" + Funded --> Refunded: dispute resolution = refund\nescrowState="refunded" + Funded --> Refunded: order cancelled\npre-shipment + Failed --> Releasing: admin retries + Released --> [*] + Refunded --> [*] +``` + +`Payment.status` mirrors a coarser business state: +- `pending` → invoice issued, awaiting funds. +- `processing` → SHKeeper sees partial / confirmations in progress. +- `confirmed` → fully credited (intermediate; sometimes skipped). +- `completed` → escrow `funded` and onward. +- `failed`, `cancelled`, `refunded` → terminal. + +## Step-by-step narrative + +### 1. Funding + +- Triggered by either [[Payment Flow - SHKeeper]] (webhook `PAID`/`OVERPAID`) or [[Payment Flow - DePay & Web3]] (verified `eth_getTransactionReceipt`). +- Backend sets `Payment.status = "completed"` and `Payment.escrowState = "funded"` (`shkeeperWebhook.ts:388-391`, `shkeeperService.ts:600-602`). +- Cascade: `PurchaseRequest.status` → `payment`, then `processing` once the seller acknowledges; `SellerOffer.status` → `accepted`; chat created. +- Funds physically sit at the **custodial wallet** — SHKeeper's per-invoice deposit address (auto-swept to the merchant wallet) or directly at the escrow wallet in the Web3 path. + +### 2. Holding + +- While `escrowState === "funded"` and the order is in `processing` / `delivery`, the funds are inert. No interest accrues; no on-chain action happens. +- The buyer cannot withdraw; the seller cannot collect. Only an admin/system action moves it forward. +- Visible in admin dashboard: `GET /api/payment/admin/funded?status=funded` (or similar — see admin payment view in `frontend/src/sections/payment/view/payment-list-admin-view.tsx`). + +### 3. Releasing (happy path) + +- Trigger options: + - **Buyer confirms delivery** via the delivery-code flow ([[Delivery Confirmation Flow]]). + - **Auto-release timer** elapses (configurable; today a manual or scheduled job — `PurchaseRequestService` exposes status transitions through to `completed`). + - **Admin manual release** from the admin payment detail view. +- The system marks `Payment.escrowState = "releasable"` (intermediate). +- `shkeeperPayoutService.createPayoutTask` (or a manual EVM admin signature via `admin-wallet-payout.tsx`) starts the on-chain transfer to the seller's verified wallet address. State flips to `releasing`. +- On confirmation: `confirmAdminTx(paymentId, txHash, 'release')` (`shkeeperService.ts:628-647`) sets: + - `Payment.status = 'completed'` + - `Payment.escrowState = 'released'` + - `Payment.blockchain.transactionHash = ` +- Cascade: `PurchaseRequest.status` → `seller_paid` then `completed`. + +### 4. Refunding (dispute / cancellation) + +- Trigger: dispute resolution with `action: 'refund'` or pre-shipment cancellation. +- Backend builds the refund tx via `buildAdminSignedTxPayload(paymentId, 'refund')` (`shkeeperService.ts:614-626`) — destination is `payment.blockchain.sender` (the buyer's verified wallet). +- Admin signs and broadcasts (currently a manual step in the admin UI). +- On confirmation: `confirmAdminTx(paymentId, txHash, 'refund')` sets: + - `Payment.status = 'refunded'` + - `Payment.escrowState = 'refunded'` +- Cascade: `PurchaseRequest.status` → `cancelled` (or remains in dispute-resolved state). + +### 5. Failed payout + +- If the payout tx reverts (insufficient gas, contract pause, wrong address), `escrowState = 'failed'`. Admin can retry by initiating a fresh payout. + +## Sequence diagram (release path) + +```mermaid +sequenceDiagram + autonumber + actor B as Buyer + actor A as Admin + participant FE as Frontend + participant BE as Backend + participant DB as MongoDB + participant SK as SHKeeper Payout API + participant BC as BSC + + B->>FE: Enter delivery code (or auto-timer fires) + FE->>BE: POST /api/marketplace/purchase-requests/:id/confirm-delivery + BE->>DB: PurchaseRequest.status="delivered"\nPayment.escrowState="releasable" + BE-->>FE: ok + A->>FE: Click "Release" in admin + FE->>BE: POST /api/payment/shkeeper/payout + BE->>DB: Payment.escrowState="releasing" + BE->>SK: createPayoutTask({recipient, amount}) + SK->>BC: signed payout tx + BC-->>SK: confirmed + SK->>BE: payout webhook / poll + BE->>BE: confirmAdminTx(paymentId, txHash, "release") + BE->>DB: Payment.escrowState="released"\nPurchaseRequest.status="completed" +``` + +## Sequence diagram (refund path) + +```mermaid +sequenceDiagram + autonumber + actor A as Admin + participant BE as Backend + participant DB as MongoDB + participant BC as BSC + actor B as Buyer + + A->>BE: Dispute resolved with action="refund" + BE->>BE: buildAdminSignedTxPayload(paymentId, "refund") + BE-->>A: { to:buyerWallet, amount, token, network } + A->>BC: sign + broadcast tx + BC-->>A: txHash + A->>BE: confirmAdminTx(paymentId, txHash, "refund") + BE->>DB: Payment.status="refunded"\nescrowState="refunded" + BE->>B: notifyRefundCompleted +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `POST` | `/api/payment/admin/release/:paymentId` | Initiate release | +| `POST` | `/api/payment/admin/refund/:paymentId` | Initiate refund | +| `POST` | `/api/payment/admin/confirm-tx/:paymentId` | Admin marks the signed tx confirmed | +| `GET` | `/api/payment/:paymentId/status` | Polled by both parties | + +## Database writes + +- **`payments`**: `status`, `escrowState`, `blockchain.transactionHash`, `completedAt`, `metadata.*` are mutated as the state progresses. +- **`purchaserequests`**: `status` cascades (`payment → processing → delivery → delivered → confirming → seller_paid → completed`). +- **`notifications`**: created on each terminal state. + +## Socket events emitted + +- **`purchase-request-update`** `status-changed` on every cascading status flip. +- **`payment-status`** (planned/admin) — admin dashboard real-time feed. + +## Side effects + +- **Custodial risk** — the escrow wallet's private key sits with the platform. Lose it → lose all in-flight escrows. Operational controls: hardware wallet, multi-sig, cold storage of the recovery seed. +- **No on-chain escrow contract** — there is no Solidity escrow today. Migration toward a smart-contract escrow (e.g. OpenZeppelin's `Escrow.sol` pattern) would remove custodial trust at the cost of higher complexity and gas. + +## Error / edge cases + +- **Buyer never confirms delivery** → today requires admin intervention. An auto-release timer (e.g. 7 days after `delivered`) is a recommended addition. +- **Seller's wallet address invalid** → payout tx fails or sends to a black hole. Validate `recipientAddress` shape (`^0x[0-9a-fA-F]{40}$`) before signing (`shkeeperPayoutService.ts:62-64` checks `.startsWith('0x')`). +- **Partial payment** (`PARTIAL`) → escrow remains in `pending/partial`; release blocked until full payment arrives. +- **Overpaid** → currently treated as `completed/funded`; the surplus is not auto-refunded. +- **Concurrent release + refund** → blocked by `PaymentCoordinator` serialisation; whichever fires first wins, the other is rejected. +- **Payout fails on chain** → state stays in `releasing` until admin re-runs; consider auto-retry with exponential backoff. +- **Disputed payment** → `escrowState` is **not** auto-changed when a dispute is opened. Admin must explicitly resolve to refund/release. Add a `disputed` boolean or `escrowState='disputed'` to make this more obvious. + +> [!warning] Single custodial wallet = single point of failure +> Centralising all in-flight escrow at one BSC address is the platform's largest operational risk. Use a multi-sig (Gnosis Safe) for the escrow wallet, store one key in HSM, and require two admin signatures for any payout > a threshold. + +> [!tip] Recovering inconsistent state +> If `Payment.escrowState` looks stale (e.g. `released` but no on-chain tx hash), inspect with `Payment.find({ escrowState: 'released', 'blockchain.transactionHash': { $exists: false } })` and reconcile via the SHKeeper invoice or the `fix-transaction-hashes.js` script. + +## Linked flows + +- [[Payment Flow - SHKeeper]] — funds the escrow. +- [[Payment Flow - DePay & Web3]] — alternative funding path. +- [[Delivery Confirmation Flow]] — triggers release. +- [[Dispute Flow]] — can divert to refund. +- [[Payout Flow]] — executes the release transfer. + +## Source files + +- Backend: `backend/src/models/Payment.ts:96-145` (status + escrowState enums) +- Backend: `backend/src/services/payment/shkeeper/shkeeperService.ts:600-647` +- Backend: `backend/src/services/payment/shkeeper/shkeeperWebhook.ts:387-411` +- Backend: `backend/src/services/payment/paymentCoordinator.ts` +- Frontend: `frontend/src/sections/payment/view/payment-list-admin-view.tsx` +- Frontend: `frontend/src/sections/request/components/admin-steps/admin-wallet-payout.tsx` diff --git a/04 - Flows/Google OAuth Flow.md b/04 - Flows/Google OAuth Flow.md new file mode 100644 index 0000000..9ae9547 --- /dev/null +++ b/04 - Flows/Google OAuth Flow.md @@ -0,0 +1,144 @@ +--- +title: Google OAuth Flow +tags: [flow, auth, oauth, google] +related_models: ["[[User]]"] +related_apis: ["POST /api/auth/google/signup", "POST /api/auth/google/signin"] +--- + +# Google OAuth Flow + +Google sign-in/up integration using **Google Identity Services** (`accounts.google.com/gsi/client`). The flow short-circuits email verification because Google accounts are pre-verified. + +## Actors + +- **User** with a Google account. +- **Google Identity Services (GSI)** — JS SDK loaded on demand by the frontend service. +- **Frontend** — `frontend/src/auth/services/google-oauth.ts`, consumed by `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx` and `jwt-sign-up-view.tsx`. +- **Backend** — `AuthController.googleSignUp` and `googleSignIn` in `backend/src/services/auth/authController.ts`, backed by `googleOAuthService.verifyGoogleToken()` in `backend/src/services/auth/googleOAuthService.ts`. +- **MongoDB** — `User` collection (account linking by email). + +## Preconditions + +- `NEXT_PUBLIC_GOOGLE_CLIENT_ID` is set on the frontend (`frontend/src/auth/services/google-oauth.ts` line 2 — there is a hard-coded fallback for the dev project ID). +- Same client ID is whitelisted on the backend `googleOAuthService`. +- The current origin is registered under "Authorized JavaScript origins" in Google Cloud Console (see `frontend/GOOGLE_OAUTH_SETUP.md`). + +## Step-by-step narrative + +### Sign-up + +1. User clicks the Google icon on `/auth/jwt/sign-up`. The form is configured with the chosen role (buyer/seller) and an optional referral code. +2. The frontend lazy-loads `https://accounts.google.com/gsi/client` if it is not yet on `window.google`. +3. `google.accounts.oauth2.initTokenClient({ client_id, scope: 'openid email profile' })` is initialised, and `.requestAccessToken()` opens the Google popup. +4. On success the popup returns an **ID token** (a Google-signed JWT containing `email`, `email_verified`, `name`, `given_name`, `family_name`, `picture`, `sub`). +5. Frontend calls `signUpWithGoogle({ googleToken, role, referralCode })` (`frontend/src/auth/context/jwt/action.ts:281-304`), which POSTs `POST /api/auth/google/signup`. +6. Backend `authController.googleSignUp` (`:781-872`) calls `googleOAuthService.verifyGoogleToken(googleToken)`. The verifier uses `google-auth-library` to validate the JWT signature, expiry, audience (`client_id`), and issuer. +7. **Duplicate check**: `User.findOne({ email: googleUser.email })` — if found returns `409 USER_EXISTS` so the user can use *sign-in* instead. +8. **New user creation** with `password` omitted, `isEmailVerified: true`, `status: "active"`, `profile.avatar = googleUser.picture`, role from the request. +9. **Referral attribution** (`authController.ts:817-838`): same logic as the email path — increment `referrer.referralStats.totalReferrals`, emit `referral-signup` on `user-${referrer._id}`. +10. Generate access + refresh tokens, push refresh into `user.refreshTokens[]`, respond with `{ user, tokens }`. +11. Frontend stores tokens in `localStorage` and redirects to the dashboard. + +### Sign-in + +1. User clicks the Google icon on `/auth/jwt/sign-in`. +2. Same GSI flow as sign-up — Google returns an ID token. +3. Frontend calls `signInWithGoogle(googleToken)` → `POST /api/auth/google/signin`. +4. Backend verifies the token, looks up `User.findOne({ email: googleUser.email })`. If no user, returns `404 USER_NOT_FOUND` ("please sign up first"). The frontend surfaces a localized prompt. +5. On hit: `existingUser.lastLoginAt = now`; if `profile.avatar` is empty and Google has a picture, it is back-filled (`authController.ts:905-907`). +6. Tokens issued and returned identically to email login. + +> [!tip] Account linking is implicit by email +> A user who originally signed up via email + password can sign in with Google as long as the email matches — no extra "link account" step. The backend simply reuses the existing user document. There is **no** separate `googleId` field stored today, so this is a one-way trust on `googleUser.email`. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor U as User + participant FE as Frontend + participant G as Google GSI + participant BE as Backend + participant GA as google-auth-library + participant DB as MongoDB + + U->>FE: Click Google icon + FE->>G: load gsi/client, initTokenClient(client_id) + FE->>G: requestAccessToken() + G-->>U: Popup → consent + U-->>G: Approve + G-->>FE: ID token (signed JWT) + alt Sign-up + FE->>BE: POST /api/auth/google/signup { googleToken, role, referralCode } + else Sign-in + FE->>BE: POST /api/auth/google/signin { googleToken } + end + BE->>GA: verifyGoogleToken(googleToken) + GA-->>BE: { email, name, picture, ... } or null + BE->>DB: User.findOne({ email }) + alt Sign-up: user exists + BE-->>FE: 409 USER_EXISTS + else Sign-up: new + BE->>DB: User.create({ email, role, isEmailVerified:true, profile.avatar }) + opt referral + BE->>DB: increment referrer.referralStats + end + else Sign-in: user missing + BE-->>FE: 404 USER_NOT_FOUND + else Sign-in: ok + BE->>DB: user.lastLoginAt = now; back-fill avatar if blank + end + BE->>BE: generate access + refresh; push refresh + BE-->>FE: 200 { user, tokens } + FE->>FE: localStorage.setItem(accessToken/refreshToken) + FE-->>U: Redirect /dashboard/{role} +``` + +## API calls + +| Method | Endpoint | Source | +|---|---|---| +| `POST` | `/api/auth/google/signup` | `authRoutes.ts:30` → `authController.googleSignUp` | +| `POST` | `/api/auth/google/signin` | `authRoutes.ts:31` → `authController.googleSignIn` | + +## Database writes + +- **`users` collection**: on sign-up, full insert (no password). On sign-in, only `lastLoginAt`, possibly `profile.avatar`, and a new refresh token appended. + +## Socket events emitted + +- **`referral-signup`** → `user-${referrerId}` when sign-up includes a valid `referralCode`. + +## Side effects + +- **No email** is sent (Google handles trust). No `TempVerification` is created. +- The avatar URL is stored from Google's CDN; consider proxying or rehosting if Google's privacy rules change for `googleusercontent.com`. + +## Error / edge cases + +- **Invalid Google token** (bad signature, wrong audience, expired) → `googleOAuthService` returns `null` → `401 INVALID_GOOGLE_TOKEN`. +- **User already exists during sign-up** → `409`; frontend prompts to use sign-in instead. +- **User missing during sign-in** → `404`; frontend redirects to sign-up. +- **Popup blocker** → GSI throws a client-side error caught in the view and surfaced as a toast. +- **Network failure to `accounts.google.com`** → GSI rejects; frontend retries on next click. +- **`email_verified === false` on the Google token** → currently not enforced; the backend trusts any successful Google response. For an extra-strict mode, gate on `googleUser.email_verified === true` in `googleOAuthService`. + +> [!warning] Single backup file +> `frontend/src/auth/services/google-oauth.ts.backup` is checked in. Delete or convert to a documentation note — it leaks a hard-coded client ID that should only live in `.env.*`. + +## Linked flows + +- [[Authentication Flow]] — token issuance and storage are identical from step 9 onward. +- [[Registration Flow]] — alternative path that requires email verification. +- [[Referral Flow]] — works identically for Google-signup referrals. + +## Source files + +- Frontend: `frontend/src/auth/services/google-oauth.ts` +- Frontend: `frontend/src/auth/context/jwt/action.ts:281-331` +- Frontend: `frontend/src/auth/view/jwt/jwt-sign-up-view.tsx`, `jwt-sign-in-view.tsx` +- Frontend: `frontend/GOOGLE_OAUTH_SETUP.md` +- Backend: `backend/src/services/auth/authController.ts:781-941` +- Backend: `backend/src/services/auth/googleOAuthService.ts` +- Backend: `backend/src/services/auth/authRoutes.ts:30-31` diff --git a/04 - Flows/Negotiation Flow.md b/04 - Flows/Negotiation Flow.md new file mode 100644 index 0000000..63d84c2 --- /dev/null +++ b/04 - Flows/Negotiation Flow.md @@ -0,0 +1,148 @@ +--- +title: Negotiation Flow +tags: [flow, marketplace, negotiation, counter-offer, chat] +related_models: ["[[SellerOffer]]", "[[PurchaseRequest]]", "[[Chat]]"] +related_apis: ["PATCH /api/marketplace/offers/:id", "POST /api/chat", "POST /api/chat/:chatId/messages"] +--- + +# Negotiation Flow + +After an offer is submitted ([[Seller Offer Flow]]), the buyer and seller can negotiate the price/ETA via **counter-offers** exchanged through the chat. The request status moves to `in_negotiation`, and either party can finalise with accept/reject. + +## Actors + +- **Buyer** — initiates negotiation by replying to an offer or opening chat from the request detail. +- **Seller** — receives counter, can accept or counter back. +- **Frontend** — chat component (`frontend/src/sections/chat/`) overlaid on the request view; offer-edit modal under `frontend/src/sections/request/components/buyer-steps/step-3-components/`. +- **Backend** — `ChatService.sendMessage` for chat lines, `SellerOfferService.updateOffer` for price/ETA edits, `PurchaseRequestService.updatePurchaseRequest` for the status flip. +- **MongoDB** — `chats`, `selleroffers`, `purchaserequests`. +- **Socket.IO** — `new-message`, `seller-offer-update`, `purchase-request-update`. + +## Preconditions + +- A `SellerOffer` exists on the purchase request (status `pending`). +- The purchase request is `received_offers` or `in_negotiation`. +- Both parties are still active users. + +## Step-by-step narrative + +1. **Open negotiation chat** — when a buyer first clicks "Chat with seller" on an offer card, the frontend calls `POST /api/chat` to find-or-create a `direct` chat tied to the purchase request (`ChatService.createChat`, `chat.ts:90-192`). The chat's `relatedTo = { type: 'PurchaseRequest', id }` makes it discoverable from the request view. + +> [!tip] Pre-payment chats vs. post-payment chats +> A negotiation chat may exist **before** the SHKeeper webhook auto-creates the post-payment chat. The `ChatService.createChat` `direct` find-or-create logic (`ChatService.ts:95-108`) prevents duplicates — the same chat object is reused. + +2. **Status flip to `in_negotiation`** — the first message in the negotiation chat triggers a backend hook (or a manual frontend PATCH) that calls `PurchaseRequestService.updatePurchaseRequest` with `{ status: 'in_negotiation' }`. The status-progression guard allows this (`received_offers → in_negotiation`). + +3. **Buyer proposes a counter** — the buyer types a message like "Can you do $80 instead of $100?". Two patterns are used: + - **Free-form** — just a chat message; the seller eyeballs the offer-edit screen and updates the price. + - **Structured counter** — the buyer opens an "edit offer" modal that PATCHes `/api/marketplace/offers/{id}` with the new desired terms. This is currently a seller-only edit endpoint; structured buyer counters typically come as a system message in the chat (`messageType: 'system'`) referencing the new price. + +4. **Seller updates the offer** — `SellerOfferService.updateOffer` (`:271-295`): + - `SellerOffer.findByIdAndUpdate(id, { ...updateData, updatedAt: now }, { new: true })`. + - Emits `purchase-request-update` with `eventType: 'offer-updated'` to `request-{requestId}` (`SellerOfferService.ts:284-288`) — both parties' open tabs refresh. + +5. **Buyer accepts** — clicks "Accept this offer", which kicks off [[Payment Flow - SHKeeper]] with the (now-updated) `sellerOfferId`. The webhook flips offer → `accepted` and request → `payment`. + +6. **Buyer rejects** — calls `PATCH /api/marketplace/offers/{id}` with `{ status: 'rejected' }`. `SellerOfferService.updateOfferStatus` (`:306-353`) sends `notifyOfferRejected` to the seller and stamps `rejectedAt` + `rejectionReason`. + +7. **Seller withdraws** — `withdrawOffer` (`:428-443`) only works while `status === 'pending'`. After rejection/acceptance, withdrawal is impossible. + +8. **Chat continues** — even after status flips, the chat remains open for clarifications (delivery details, dispute prep). See [[Chat Flow]] for message-level semantics. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor B as Buyer + actor S as Seller + participant FE_B as Frontend (buyer) + participant FE_S as Frontend (seller) + participant BE as Backend + participant DB as MongoDB + participant IO as Socket.IO + + B->>FE_B: Click "Chat with seller" on offer + FE_B->>BE: POST /api/chat (type:direct, relatedTo:PR) + BE->>DB: find-or-create Chat + BE-->>FE_B: { chat } + B->>FE_B: Send "Can you do $80?" + FE_B->>BE: POST /api/chat/{id}/messages + BE->>DB: Chat.addMessage(...) + BE->>IO: emit chat-{id} 'new-message' + IO-->>FE_S: 'new-message' (seller sees in real time) + BE->>BE: optionally trigger request → in_negotiation + BE->>DB: PurchaseRequest.status = "in_negotiation" + BE->>IO: emit request-{id} 'purchase-request-update' (status-changed) + S->>FE_S: Open edit-offer modal, set new price + FE_S->>BE: PATCH /api/marketplace/offers/{id} {price:{amount:80}} + BE->>DB: SellerOffer update + BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated) + IO-->>FE_B: refresh offer card + alt Buyer accepts + B->>FE_B: Click "Pay" → [[Payment Flow - SHKeeper]] + Note over BE: Webhook PAID flips offer→accepted, request→payment + else Buyer rejects + B->>FE_B: Click "Reject" + FE_B->>BE: PATCH /api/marketplace/offers/{id} {status:"rejected"} + BE->>DB: offer.status = "rejected" + BE->>BE: notifyOfferRejected(seller) + IO-->>FE_S: 'new-notification' + end +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `POST` | `/api/chat` | Find-or-create negotiation chat | +| `POST` | `/api/chat/:chatId/messages` | Send chat message | +| `PATCH` | `/api/marketplace/offers/:id` | Seller updates price / ETA / notes (counter) | +| `PATCH` | `/api/marketplace/purchase-requests/:id` | Status transition to `in_negotiation` | +| `POST` | `/api/marketplace/offers/:id/withdraw` | Seller pulls the offer | + +## Database writes + +- **`chats`**: messages appended via `chat.addMessage`; `metadata.lastActivity` bumped; `unreadCounts` incremented for non-sender participants. +- **`selleroffers`**: counter changes update `price`, `deliveryTime`, `notes`, `updatedAt`. +- **`purchaserequests`**: status flips when first counter arrives. +- **`notifications`**: created per status change (accept/reject), not per chat message (chat has its own real-time channel). + +## Socket events emitted + +- **`new-message`** → `chat-{chatId}` (the canonical chat event). +- **`chat-notification`** → `user-{participantId}` for non-senders (badge increment). +- **`purchase-request-update`** with `eventType: 'offer-updated'` → `request-{id}` whenever the offer is edited. +- **`purchase-request-update`** with `eventType: 'status-changed'` → `request-{id}` on the `received_offers → in_negotiation` flip. + +## Side effects + +- The chat's unread badge grows in the recipient's chat list (`Chat.unreadCounts` array). Resets when they open the chat and `POST /api/chat/{id}/read`. +- Typing indicators are emitted via `typing-start` / `typing-stop` socket events (see `backend/src/app.ts:142-158`) — purely client-driven. + +## Error / edge cases + +- **Sender not a chat participant** → `403 "User is not a participant in this chat"` (`ChatService.sendMessage:209-211`). +- **Counter on an offer the buyer doesn't own a request for** → blocked by the controller (the buyer must be the request's owner). +- **Counter on an `accepted` offer** → `updateOffer` does not enforce status; the schema/controller should reject. Current code allows the price change, which is dangerous post-payment. Recommended hardening: guard with `if (offer.status !== 'pending') throw`. +- **Status regression attempt** (`in_negotiation → received_offers`) → blocked by `isValidStatusProgression` (`PurchaseRequestService.ts:31-50`). +- **Two simultaneous edits** — last-write-wins on `findByIdAndUpdate`; consider optimistic concurrency via `__v` if conflicts become an issue. +- **Chat created in negotiation but buyer never pays** → orphan chat remains; the post-payment chat (in [[Chat Flow]]) reuses it because the find-or-create logic matches by participants + relatedTo. + +> [!warning] No structured "counter-offer object" +> Today, counter-offer negotiations are mostly free-form chat plus an `updateOffer` edit. There is no `CounterOffer` collection with provenance. If audit/regulatory needs emerge, capture each counter as a snapshot (`{ oldPrice, newPrice, byUserId, atTime }`) on the offer. + +## Linked flows + +- [[Seller Offer Flow]] — the prior step. +- [[Payment Flow - SHKeeper]] — closes the negotiation with an on-chain payment. +- [[Chat Flow]] — message-level mechanics, attachments, read receipts. +- [[Notification Flow]] — accept/reject notifications. + +## Source files + +- Backend: `backend/src/services/marketplace/SellerOfferService.ts:271-353` +- Backend: `backend/src/services/marketplace/PurchaseRequestService.ts:408-495` +- Backend: `backend/src/services/chat/ChatService.ts:90-260` +- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-components/` +- Frontend: `frontend/src/sections/chat/` (chat UI) diff --git a/04 - Flows/Notification Flow.md b/04 - Flows/Notification Flow.md new file mode 100644 index 0000000..6a5d158 --- /dev/null +++ b/04 - Flows/Notification Flow.md @@ -0,0 +1,155 @@ +--- +title: Notification Flow +tags: [flow, notification, socket-io, email] +related_models: ["[[Notification]]", "[[User]]"] +related_apis: ["GET /api/notifications", "PATCH /api/notifications/:id/read", "POST /api/notifications/read-all", "DELETE /api/notifications/:id"] +--- + +# Notification Flow + +Cross-cutting flow that powers the bell icon, toast pop-ups, badge counters, and (optionally) email digests. Notifications are created by many services and travel to the user via both **MongoDB persistence** and **Socket.IO push**. + +## Trigger sources + +- **Purchase request lifecycle** — created, status changed, cancelled (`PurchaseRequestService`). +- **Offer lifecycle** — new offer, accepted, rejected, withdrawn (`SellerOfferService`). +- **Payment lifecycle** — confirmed, refunded, payout sent (`shkeeperWebhook`, `PurchaseRequestService.notifyPaymentConfirmed`). +- **Chat** — `chat-notification` events (see [[Chat Flow]]). +- **Dispute** — created, assigned, resolved (TODO in `DisputeService`; the chat itself notifies). +- **Delivery** — code generated, delivery confirmed (`DeliveryService`). +- **Referral** — sign-up via referral code (`AuthController.verifyEmailWithCode`, `googleSignUp`). +- **Points / levels** — `level-up` event when crossing a tier (`PointsService.addPoints:91-99`). +- **Admin actions** — e.g. dispute resolution, manual payouts. + +## Actors + +- **System** — the various services calling `NotificationService.createNotification`. +- **User** — the recipient. +- **Frontend** — bell-icon dropdown and toast subscribers in `frontend/src/layouts/components/notifications-drawer/` and the global socket provider. +- **Backend** — `NotificationService` (`backend/src/services/notification/NotificationService.ts`), routes at `/api/notifications`. +- **MongoDB** — `notifications` collection (one document per notification). +- **Socket.IO** — emits `new-notification` to `user-{userId}`. +- **Email** (optional) — periodic digest worker (not implemented today; planned). + +## Step-by-step narrative + +### Creating a notification + +1. Any service builds a `NotificationCreateData` object: + ``` + { + userId, title, message, + type: 'info' | 'success' | 'warning' | 'error', + category: 'purchase_request' | 'offer' | 'payment' | 'delivery' | 'system', + relatedId?, metadata?, actionUrl? + } + ``` +2. Calls `await notificationService.createNotification(data)`. +3. `NotificationService.createNotification` (`NotificationService.ts:18-37`): + - `Notification.create({ ...data, isRead: false, createdAt: now })`. + - Calls `this.emitRealTimeNotification(userId, saved)` which `global.io.to('user-${userId}').emit('new-notification', payload)`. +4. The notification is persisted and pushed simultaneously. + +### Frontend reception + +5. The frontend's global Socket.IO provider listens for `new-notification` events on its `user-{me}` room (joined on app mount via `socket.emit('join-user-room', userId)`). +6. On event: increment the bell-icon badge, optionally show a toast (`notistack`), and prepend the entry into the cached notifications list (React Query cache). +7. The bell-icon dropdown also fetches `GET /api/notifications?page=1&limit=20` for paginated history. + +### Reading + +8. User opens the bell-icon dropdown — frontend calls `POST /api/notifications/mark-read` for each viewed entry (or `POST /api/notifications/read-all`). +9. `NotificationService.markAsRead(notificationId, userId)` (`NotificationService.ts:74-90`): + - `Notification.findOneAndUpdate({ _id, userId }, { isRead: true, readAt: now })`. + - Emits `notification-read` (or recomputes unread count) so other open tabs sync. + +### Preferences + +- `User.preferences.notifications` (in the User schema) can hold per-category opt-outs (`emailNotifications`, `pushNotifications`, etc.). The current implementation does not enforce preferences at send-time — all enabled notifications fire. Add a check in `createNotification` to short-circuit when the user has opted out of a category. + +### Email digest (planned) + +- A scheduled worker should `Notification.find({ userId, emailDigested: false, createdAt: { $gte: yesterday } })`, batch by user, render a digest email via `emailService`, mark `emailDigested: true`. Not implemented today. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + participant Svc as Originating Service
(SellerOfferService / Webhook / ...) + participant NS as NotificationService + participant DB as MongoDB + participant IO as Socket.IO + participant FE as Frontend + actor U as User + + Svc->>NS: createNotification({userId, title, message, ...}) + NS->>DB: Notification.create + NS->>IO: emit user-{userId} 'new-notification' + IO-->>FE: 'new-notification' payload + FE-->>U: badge++, toast, prepend to list + + U->>FE: open bell dropdown + FE->>NS: GET /api/notifications?page=1&limit=20 + NS->>DB: Notification.find({userId}).sort({createdAt:-1}) + NS-->>FE: { notifications, total, unreadCount } + + U->>FE: click notification + FE->>NS: PATCH /api/notifications/{id}/read + NS->>DB: Notification.findOneAndUpdate(isRead:true) + FE-->>U: badge--, mark item as read + FE-->>U: navigate to notification.actionUrl +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `GET` | `/api/notifications` | Paginated list with `unreadCount` | +| `GET` | `/api/notifications/unread-count` | Just the unread count for badge | +| `PATCH` | `/api/notifications/:id/read` | Mark single notification read | +| `POST` | `/api/notifications/read-all` | Mark all read | +| `DELETE` | `/api/notifications/:id` | Remove from list | + +## Database writes + +- **`notifications`** — insert on create, update on read, delete on remove. + +## Socket events emitted + +- **`new-notification`** → `user-{userId}`. Payload includes the full notification document (so the frontend doesn't need to re-fetch). +- **`level-up`** → `user-{userId}` from `PointsService.addPoints`. +- **`referral-signup`** → `user-{referrerId}` from auth verify. +- **`chat-notification`** → `user-{participantId}` from `ChatService.sendMessage` (these are not stored in the `notifications` collection — they live alongside but drive only the chat-list badge). + +## Side effects + +- Bell badge count is derived from `unreadCount` returned by the GET endpoint or computed client-side as items arrive. +- Notification actions deep-link via `actionUrl` (e.g. `/dashboard/buyer/requests/{id}`). +- Sentry breadcrumbs capture failed notification creations. + +## Error / edge cases + +- **User offline** → notification is persisted in MongoDB; the user sees it when they next sign in. The socket emit is lossy (no replay). +- **Multiple tabs / devices** → the same `user-{id}` room receives the event in each socket; all tabs update. +- **Disabled categories** (planned) → service should early-return without DB write if the user has opted out, otherwise persist but don't push (so it shows in history but not as a toast). +- **High volume** (e.g. fan-out to thousands of sellers) → today every notification is a separate Mongo insert + socket emit. For mass announcements, consider `insertMany` + per-room broadcast. The 50ms stagger in [[Purchase Request Flow]] mitigates the worst case. +- **Stale unread count** → if the frontend trusts a stale React Query cache, it can show wrong numbers; always reconcile against `unread-count` on bell-icon open. + +> [!tip] Always set `actionUrl` +> Every notification should have a deep-link target. Notifications without `actionUrl` lead to dead clicks. The factory methods in `NotificationService` (e.g. `notifyNewOfferReceived`) already enforce this — keep the pattern when adding new helpers. + +## Linked flows + +- Every other flow in this folder emits notifications via this service. +- [[Chat Flow]] — separate `chat-notification` socket channel for chat badges. + +## Source files + +- Backend: `backend/src/services/notification/NotificationService.ts` +- Backend: `backend/src/services/notification/notificationController.ts` +- Backend: `backend/src/services/notification/notificationControllerRoutes.ts` +- Backend: `backend/src/services/notification/routes.ts` +- Backend: `backend/src/models/Notification.ts` +- Frontend: `frontend/src/layouts/components/notifications-drawer/` +- Frontend: socket provider (joins `user-{id}` and listens for `new-notification`) diff --git a/04 - Flows/Passkey (WebAuthn) Flow.md b/04 - Flows/Passkey (WebAuthn) Flow.md new file mode 100644 index 0000000..648bddb --- /dev/null +++ b/04 - Flows/Passkey (WebAuthn) Flow.md @@ -0,0 +1,162 @@ +--- +title: Passkey (WebAuthn) Flow +tags: [flow, auth, passkey, webauthn, passwordless] +related_models: ["[[User]]"] +related_apis: ["POST /api/auth/passkey/register/challenge", "POST /api/auth/passkey/register", "POST /api/auth/passkey/authenticate/challenge", "POST /api/auth/passkey/authenticate", "GET /api/auth/passkey/list", "DELETE /api/auth/passkey/:passkeyId"] +--- + +# Passkey (WebAuthn) Flow + +Passwordless sign-in using **WebAuthn / Passkeys**. The backend issues challenges, validates signed assertions, stores credential metadata under `User.passkeys[]`, and finally issues the same JWT pair as the password flow. + +## Actors + +- **User** with a WebAuthn-capable authenticator (Touch ID, Face ID, Windows Hello, Android biometric, YubiKey). +- **Browser WebAuthn API** — `navigator.credentials.create()` / `.get()`. +- **Frontend** — `frontend/src/auth/components/PasskeyManagement.tsx` (registration UI on the account settings page) and `frontend/src/auth/components/PasskeySignIn.tsx` (sign-in entry). +- **Backend** — `backend/src/services/auth/passkeyService.ts` and the routes in `backend/src/services/auth/passkeyRoutes.ts`. +- **MongoDB** — `User.passkeys[]` subdocument array (id, publicKey, counter, deviceType, deviceName, createdAt). + +## Preconditions + +- The browser supports WebAuthn (`window.PublicKeyCredential`). The frontend checks this and throws `"WebAuthn در این مرورگر پشتیبانی نمی‌شود"` otherwise. +- The relying party ID derives from `config.frontendUrl` — `backend/src/services/auth/passkeyService.ts:36` strips scheme/port to produce the WebAuthn `rpId`. +- For **registration**, the user is already authenticated via password or Google (the `/passkey/register/*` routes are behind `authService.authenticateToken`). +- For **sign-in**, no auth is required — the authenticator's credential ID identifies the user. +- Env vars consumed by the frontend (commonly `NEXT_PUBLIC_PASSKEY_RP_ID`, `NEXT_PUBLIC_PASSKEY_RP_NAME`, `NEXT_PUBLIC_PASSKEY_ORIGIN` per the codebase conventions) drive WebAuthn options on the client. + +## Registration flow + +1. From `/dashboard/account/security`, the user opens the Passkey management card and clicks **"Add new passkey"**. +2. Frontend `PasskeyManagement.tsx` calls `POST /api/auth/passkey/register/challenge` (with the bearer access token). +3. Backend `passkeyService.generateRegistrationChallenge(userId)` (`passkeyService.ts:58-70`): + - `crypto.randomBytes(32).toString('base64url')` — a 256-bit challenge. + - Stored in an in-memory `Map` (5-min TTL via interval cleanup). + - Returns `{ challenge, rpId, userVerification: 'preferred', timeout: 60000 }`. +4. Frontend calls `navigator.credentials.create({ publicKey: { challenge, rp, user, pubKeyCredParams, ... } })`. The browser prompts the authenticator (Touch ID, etc.) and returns a `PublicKeyCredential` containing `id`, `rawId`, `response.clientDataJSON`, `response.attestationObject`. +5. Frontend POSTs `POST /api/auth/passkey/register` with `{ challenge, credential }`. +6. Backend `passkeyService.verifyRegistration(challenge, credential)` (`:108-146`): + - Looks up the stored challenge → `{ userId }`. Deletes it (single-use). + - Loads `User.findById(userId)`. + - Appends to `user.passkeys[]`: `{ id: credential.id, publicKey: 'simulated-public-key', counter: 0, deviceType: 'platform', deviceName: 'Default Device', createdAt: now }`. + - Saves. +7. Frontend re-fetches `GET /api/auth/passkey/list` and renders the new entry. + +> [!warning] Attestation validation is stubbed +> `passkeyService.verifyRegistration` currently **does not** parse the attestation object or extract the real COSE public key — see the comment block at `passkeyService.ts:122-128` ("In a real implementation, you would..."). The `publicKey` field is the literal string `'simulated-public-key'`. This means a malicious client could register an attacker-controlled credential ID under any user; harden this before production. Use `@simplewebauthn/server` to parse attestation and store the verified public key. + +## Authentication flow + +1. From `/auth/jwt/sign-in`, the user clicks **"Sign in with passkey"**. +2. Frontend `PasskeySignIn.tsx` calls `POST /api/auth/passkey/authenticate/challenge` — note this is a **public** route (no bearer token). +3. Backend `passkeyService.generateAuthenticationChallengeForSignIn()` (`:88-105`) generates a 32-byte challenge and stores it with `userId: 'pending'`. +4. Frontend calls `navigator.credentials.get({ publicKey: { challenge, rpId, userVerification: 'preferred' } })`. The browser surfaces all matching passkeys for the rpId; the user picks one and approves biometrically. +5. The authenticator returns a `PublicKeyCredential` whose `response` includes `clientDataJSON`, `authenticatorData`, `signature`, `userHandle`. +6. Frontend POSTs `POST /api/auth/passkey/authenticate` with `{ challenge, assertion }`. +7. Backend `passkeyService.verifyAuthentication(challenge, assertion)` (`:149-272`): + - Confirms the challenge exists (and deletes it). + - `User.findOne({ 'passkeys.id': assertion.id })` — finds the user whose passkey matches the credential ID supplied by the authenticator. + - `passkey.counter += 1` (the schema stores a counter; a real implementation must reject replays where the new counter is not strictly greater than the stored one). + - Issues JWT access + refresh tokens directly via `jwt.sign(...)` (`:230-248`). Note: these are signed by the same `config.jwtSecret` as in `authService`, so they are interchangeable with password-issued tokens. +8. Response: `{ success: true, userId, user: {...}, tokens: { accessToken, refreshToken } }`. +9. Frontend stores tokens in `localStorage` and redirects to the dashboard. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor U as User + participant FE as Frontend + participant W as WebAuthn (Browser) + participant BE as Backend + participant DB as MongoDB + + rect rgb(245,247,250) + Note over U,DB: Registration (user already authenticated) + U->>FE: Click "Add passkey" + FE->>BE: POST /api/auth/passkey/register/challenge (Bearer) + BE->>BE: generateRegistrationChallenge(userId)\nstore in Map + BE-->>FE: { challenge, rpId, ... } + FE->>W: navigator.credentials.create({ publicKey }) + W-->>FE: PublicKeyCredential + FE->>BE: POST /api/auth/passkey/register { challenge, credential } + BE->>BE: verifyRegistration → consume challenge + BE->>DB: user.passkeys.push({ id, counter, deviceType }) + BE-->>FE: { success: true } + end + + rect rgb(245,247,250) + Note over U,DB: Authentication (no prior session) + U->>FE: Click "Sign in with passkey" + FE->>BE: POST /api/auth/passkey/authenticate/challenge (public) + BE->>BE: generateAuthenticationChallengeForSignIn → store + BE-->>FE: { challenge, rpId, ... } + FE->>W: navigator.credentials.get({ publicKey }) + W-->>FE: PublicKeyCredential (assertion) + FE->>BE: POST /api/auth/passkey/authenticate { challenge, assertion } + BE->>BE: consume challenge + BE->>DB: User.findOne({ 'passkeys.id': assertion.id }) + DB-->>BE: user with matching passkey + BE->>DB: passkey.counter += 1 + BE->>BE: jwt.sign(access) / jwt.sign(refresh) + BE-->>FE: { success, user, tokens } + FE->>FE: localStorage.setItem(tokens) + FE-->>U: Redirect /dashboard + end +``` + +## API calls + +| Method | Endpoint | Auth | Source | +|---|---|---|---| +| `POST` | `/api/auth/passkey/register/challenge` | Bearer | `passkeyRoutes.ts:50` | +| `POST` | `/api/auth/passkey/register` | Bearer | `passkeyRoutes.ts:66` | +| `POST` | `/api/auth/passkey/authenticate/challenge` | Public | `passkeyRoutes.ts:10` | +| `POST` | `/api/auth/passkey/authenticate` | Public | `passkeyRoutes.ts:23` | +| `GET` | `/api/auth/passkey/list` | Bearer | `passkeyRoutes.ts:87` | +| `DELETE` | `/api/auth/passkey/:passkeyId` | Bearer | `passkeyRoutes.ts:103` | + +## Database writes + +- **`users.passkeys`** — append on register, increment `counter` on each successful auth, splice on delete. +- A new refresh token is **not** appended to `user.refreshTokens` in the current passkey path (the JWT is signed directly without round-tripping through `authService.generateRefreshToken`). This means the password-flow refresh-token allow-list does not apply to passkey logins. See edge cases. + +## Socket events emitted + +- None directly. The frontend joins the same Socket.IO rooms after login as in [[Authentication Flow]]. + +## Side effects + +- **In-memory `storedChallenges` map**: per-instance, not Redis. On a horizontally scaled deployment, the challenge created on instance A can only be verified on instance A. Either pin to a single instance, use sticky sessions, or move to Redis (`paymentRedisService`-style). +- **Cleanup interval**: every 5 minutes, expired challenges (>5 min old) are removed (`passkeyService.ts:42-55`). + +## Error / edge cases + +- **Browser without WebAuthn** → frontend throws localized error before issuing the challenge request. +- **User cancels biometric prompt** → `NotAllowedError` from the browser; frontend shows "Cancelled" toast. +- **Challenge expired or unknown** → backend `Invalid or expired challenge` (`:117`). Frontend asks user to retry. +- **Credential ID does not match any user** → `404 Passkey not found` (`passkeyRoutes.ts:40-44`). UX should suggest signing in by email instead. +- **Multi-instance backend** (challenge stored on instance A, verified on instance B) → verification fails. Fix by moving `storedChallenges` to Redis. +- **Replay** — current implementation does not strictly enforce monotonic counter; revisit before production. +- **Refresh-token rotation gap** — passkey-issued refresh tokens are not added to `user.refreshTokens[]`. The standard `/api/auth/refresh-token` will reject them on the next refresh. Until fixed, treat passkey access tokens as short-lived (the user must passkey-sign-in again after expiry) or unify token issuance through `authService.generateRefreshToken` and persist them. + +> [!warning] Production hardening checklist +> 1. Replace stub attestation parsing with `@simplewebauthn/server`. +> 2. Persist the COSE public key, not a stub string. +> 3. Enforce strictly increasing counter (signal of cloned authenticator if not). +> 4. Move challenge storage to Redis to support multi-instance deploys. +> 5. Add `excludeCredentials` during registration to prevent re-registering the same passkey. +> 6. Push the passkey-issued refresh token into `user.refreshTokens[]`. + +## Linked flows + +- [[Authentication Flow]] — token semantics are identical post-issuance. +- [[Registration Flow]] — passkey is an additional credential, not a replacement for initial account creation. + +## Source files + +- Backend: `backend/src/services/auth/passkeyService.ts` +- Backend: `backend/src/services/auth/passkeyRoutes.ts` +- Frontend: `frontend/src/auth/components/PasskeyManagement.tsx` +- Frontend: `frontend/src/auth/components/PasskeySignIn.tsx` diff --git a/04 - Flows/Password Reset Flow.md b/04 - Flows/Password Reset Flow.md new file mode 100644 index 0000000..b93a619 --- /dev/null +++ b/04 - Flows/Password Reset Flow.md @@ -0,0 +1,124 @@ +--- +title: Password Reset Flow +tags: [flow, auth, password-reset, email] +related_models: ["[[User]]"] +related_apis: ["POST /api/auth/request-password-reset", "POST /api/auth/reset-password-with-code"] +--- + +# Password Reset Flow + +Self-service password recovery: request a 6-digit code by email, submit it with the new password. + +## Actors + +- **User** who has forgotten their password. +- **Frontend** — `frontend/src/auth/view/jwt/jwt-reset-password-view.tsx` (request) and `jwt-update-password-view.tsx` (submit new password). +- **Backend** — `AuthController.requestPasswordReset` and `AuthController.resetPasswordWithCode` in `backend/src/services/auth/authController.ts`. +- **MongoDB** — `User` collection (`passwordResetCode`, `passwordResetCodeExpires`, `refreshTokens`). +- **Email service** — `emailService.sendPasswordResetCodeEmail`. + +## Preconditions + +- The account exists and `status === "active"` (deleted accounts are silently treated as non-existent). +- The user has access to the email inbox associated with the account. +- A 6-digit code is valid for **1 hour** (`authController.ts:556`). + +## Step-by-step narrative + +1. User clicks "Forgot password?" on the sign-in page and lands at `/auth/jwt/reset-password`. +2. User enters their email and submits. +3. Frontend POSTs `POST /api/auth/request-password-reset { email }`. +4. Backend `authController.requestPasswordReset` (`:542-574`): + - `User.findOne({ email, status: "active" })`. If absent, returns `200` with the same generic message — **no enumeration**. + - Generates a 6-digit code via `authService.generateVerificationCode()`. + - Saves `passwordResetCode` and `passwordResetCodeExpires = now + 3_600_000 ms` on the user. + - Calls `emailService.sendPasswordResetCodeEmail(email, firstName, code)`. +5. Response: `200 "If an account with this email exists, a password reset code has been sent"` regardless of outcome. +6. User receives the email and enters the code + new password on `/auth/jwt/update-password`. +7. Frontend POSTs `POST /api/auth/reset-password-with-code { email, code, password }`. +8. Backend `authController.resetPasswordWithCode` (`:611-657`): + - Validates code format `/^\d{6}$/`. + - `User.findOne({ email, passwordResetCode: code, passwordResetCodeExpires: { $gt: now }, status: "active" })`. Mismatch → `400 Invalid or expired reset code`. + - Hashes the new password with bcrypt cost 12. + - Sets `user.password = hashed`, clears `passwordResetCode` and `passwordResetCodeExpires`, **wipes `user.refreshTokens = []`** to invalidate all existing sessions. + - Saves. +9. Response: `200 "Password reset successfully"`. Frontend redirects to `/auth/jwt/sign-in` for a fresh login. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor U as User + participant FE as Frontend + participant BE as Backend + participant DB as MongoDB + participant MAIL as Email Service + + U->>FE: Click "Forgot password", enter email + FE->>BE: POST /api/auth/request-password-reset { email } + BE->>DB: User.findOne({ email, status: "active" }) + alt user found + BE->>BE: code = generateVerificationCode() + BE->>DB: user.passwordResetCode = code\nexpires = +1h + BE->>MAIL: sendPasswordResetCodeEmail(email, firstName, code) + MAIL-->>U: Email with 6-digit code + end + BE-->>FE: 200 "if account exists, code sent" + + U->>FE: Enter code + new password + FE->>BE: POST /api/auth/reset-password-with-code { email, code, password } + BE->>DB: User.findOne({ email, code, expires>now }) + BE->>BE: bcrypt.hash(password, 12) + BE->>DB: user.password = hash\nuser.refreshTokens = []\nclear reset fields + BE-->>FE: 200 "Password reset successfully" + FE-->>U: Redirect /auth/jwt/sign-in +``` + +## API calls + +| Method | Endpoint | Source | +|---|---|---| +| `POST` | `/api/auth/request-password-reset` | `authRoutes.ts:44-47` | +| `POST` | `/api/auth/reset-password-with-code` | `authRoutes.ts:54-56` | +| `POST` | `/api/auth/reset-password` | `authRoutes.ts:49-52` (legacy token-based variant) | + +## Database writes + +- **`users` collection**: on request, sets `passwordResetCode` + `passwordResetCodeExpires`. On submit, replaces `password`, clears reset fields, and empties `refreshTokens`. + +## Socket events emitted + +- None. + +## Side effects + +- **Email**: one transactional message containing the 6-digit code. +- **Server-side log**: `authController.ts:559` `console.log` includes the generated code in plain text — same hardening note as [[Registration Flow]]. +- **Session invalidation**: All refresh tokens cleared → all devices forced to re-login after password change. Access tokens still valid until expiry (typically minutes). + +## Error / edge cases + +- **Unknown email** → always `200`, generic message. No enumeration. +- **Invalid code format** → `400` from `isValidVerificationCode` guard before DB lookup. +- **Expired code** (>1h) → `400 Invalid or expired reset code`. +- **Multiple parallel requests** → each overwrites the previous `passwordResetCode`; the latest email wins, prior codes silently invalidated. +- **User attempts reset on deleted account** → treated as unknown (no email sent, `200` returned). +- **Email delivery failure** → response still `200`; user can request again. +- **Access tokens still valid post-reset** → unavoidable with stateless JWT; mitigated by short TTL. Critical operations should re-verify password. + +> [!warning] Plaintext code in logs +> Same as [[Registration Flow]]: the reset code is `console.log`-ed by the controller in all environments. Restrict log access in production or gate the log behind `NODE_ENV !== 'production'`. + +## Linked flows + +- [[Authentication Flow]] — user re-signs-in after reset. +- [[Registration Flow]] — same code-generation utility. + +## Source files + +- Backend: `backend/src/services/auth/authController.ts:542-657` +- Backend: `backend/src/services/email/emailService.ts` (`sendPasswordResetCodeEmail`) +- Frontend: `frontend/src/auth/view/jwt/jwt-reset-password-view.tsx` +- Frontend: `frontend/src/auth/view/jwt/jwt-update-password-view.tsx` +- Frontend: `frontend/src/auth/context/jwt/action.ts:181-200`, `:261-276` diff --git a/04 - Flows/Payment Flow - DePay & Web3.md b/04 - Flows/Payment Flow - DePay & Web3.md new file mode 100644 index 0000000..be209f7 --- /dev/null +++ b/04 - Flows/Payment Flow - DePay & Web3.md @@ -0,0 +1,172 @@ +--- +title: Payment Flow - DePay & Web3 +tags: [flow, payment, web3, wagmi, walletconnect, bsc] +related_models: ["[[Payment]]", "[[PurchaseRequest]]"] +related_apis: ["POST /api/payment/decentralized/create", "POST /api/payment/decentralized/verify"] +--- + +# Payment Flow — DePay & Web3 (Wallet-Direct) + +Alternative pay-in path: instead of routing through [[Payment Flow - SHKeeper]], the buyer connects their own wallet (MetaMask / WalletConnect / Coinbase Wallet) and signs an **on-chain transfer to the escrow wallet** directly. The backend then verifies the transaction against the BSC RPC. + +## Actors + +- **Buyer** — owner of the wallet doing the on-chain transfer. +- **Frontend** — `frontend/src/web3/` (wagmi provider, web3-provider context); wallet-connect UI in `frontend/src/sections/account/account-wallet-connection.tsx`; the in-checkout integration is in `frontend/src/sections/request/components/buyer-steps/payment-card.tsx` and `frontend/src/sections/request/components/buyer-steps/step-3-components/step-3-payment.tsx`. +- **Wagmi / WalletConnect / MetaMask** — wallet stack. +- **Backend** — `decentralizedPaymentService.ts` (intent), `BSCTransactionVerifier` (on-chain verification), `decentralizedPaymentRoutes.ts`. +- **Blockchain (BSC)** — verified via `https://bsc-dataseed.binance.org/` JSON-RPC. +- **MongoDB** — `payments` collection (same model as SHKeeper, different `provider` value). +- **Socket.IO** — `payment-created`, plus the cascade events from [[Payment Flow - SHKeeper]] when verification succeeds. + +## Preconditions + +- `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS` is set on the frontend (see `frontend/src/global-config.ts`). This is the destination address — typically a custodial BSC wallet operated by the platform admin. +- Wagmi is configured with WalletConnect projectID and supported chains (BSC mainnet `56`, optionally BSC testnet `97`). +- Buyer has a wallet with USDT/USDC balance and a small amount of BNB for gas. + +## Step-by-step narrative + +### Phase 1 — Connect wallet + +1. Buyer hits the payment step and sees both pay options. Clicking **"Pay with wallet"** opens the WalletConnect modal via wagmi's `useConnect()`. +2. The connection emits an `accountsChanged` event; the web3 context (`frontend/src/web3/context/web3-provider.tsx`) stores `wallet.address` and `wallet.chainId`. +3. If `chainId !== 56` (BSC), the UI prompts a `wallet_switchEthereumChain` request. + +### Phase 2 — Create intent on backend + +4. Frontend POSTs `POST /api/payment/decentralized/create` with `{ purchaseRequestId, sellerOfferId, amount, fromAddress: wallet.address, token: 'USDT', network: 'bsc' }`. The backend records a `Payment` with `provider: 'other'` (or `'decentralized'` depending on enum extension), `direction: 'in'`, `status: 'pending'`, `blockchain.{network, token, sender, receiver: ESCROW_WALLET_ADDRESS}`. +5. Response includes the **escrow wallet address** and the exact token amount (in decimals — for USDT-BEP20 that's 18 decimals; the helper `convertPaymentAmountForShkeeper` is shared from `currencyUtils.ts`). + +### Phase 3 — Token approval (ERC-20 / BEP-20) + +6. The frontend checks the user's current allowance via `useReadContract` / `allowance(owner, spender)` on the USDT contract. +7. If allowance < amount, the frontend prompts an `approve(escrow, amount)` transaction. After confirmation, it proceeds to step 8. + - Some flows skip `approve` and use **direct transfer** instead (`transfer(to, amount)` — no approval needed because the buyer is the holder, not a contract pulling from them). The codebase favours direct transfer; see usages of `web3Service.transferToken(...)` in `frontend/src/web3/web3Service.ts`. + +### Phase 4 — On-chain transfer + +8. Frontend calls `transfer(escrowAddress, amount)` on the USDT contract via wagmi's `useWriteContract`. The wallet popup asks the user to confirm gas. +9. The buyer signs; the transaction is broadcast. +10. Frontend listens via wagmi's `useWaitForTransactionReceipt({ hash })`. On `success` (1 confirmation), it captures the `transactionHash`. + +### Phase 5 — Backend verification + +11. Frontend POSTs `POST /api/payment/decentralized/verify` with `{ paymentId, transactionHash }`. +12. Backend `BSCTransactionVerifier.verifyTransaction(txHash)` (`decentralizedPaymentService.ts`): + - JSON-RPC `eth_getTransactionReceipt` against `bsc-dataseed.binance.org`. + - Confirms `receipt.status === '0x1'` (success). + - Computes confirmations = `current eth_blockNumber - receipt.blockNumber`. + - Optionally decodes the `Transfer` event log to confirm `from`, `to`, and `value` match the expected payment. +13. On success the backend: + - Updates the `Payment`: `status = 'completed'`, `escrowState = 'funded'`, `blockchain.transactionHash`, `blockchain.confirmations`, `blockchain.confirmedAt = now`. + - Triggers the **same cascade** as the SHKeeper webhook: mark winning offer accepted, reject others, transition request to `payment`, create chat, send notifications, emit socket events. +14. Returns `{ status: 'confirmed', confirmations, blockNumber }`. + +### Phase 6 — Frontend reaction + +15. The checkout UI shows "Payment verified" with the block-explorer link (`https://bscscan.com/tx/{hash}`) and transitions to the awaiting-delivery state. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor B as Buyer + participant W as Wallet (MetaMask/WC) + participant FE as Frontend (wagmi) + participant BE as Backend + participant BC as BSC RPC + participant DB as MongoDB + participant IO as Socket.IO + + B->>FE: Click "Pay with wallet" + FE->>W: connect() + W-->>FE: { address, chainId } + opt chainId != 56 + FE->>W: wallet_switchEthereumChain(0x38) + end + FE->>BE: POST /api/payment/decentralized/create + BE->>DB: Payment.create({provider:"other", direction:"in", receiver:ESCROW}) + BE-->>FE: { paymentId, escrowAddress, amount } + opt allowance < amount + FE->>W: approve(escrow, amount) + W-->>FE: tx confirmed + end + FE->>W: transfer(escrow, amount) + W-->>FE: tx broadcast + W-->>BC: signed tx + BC-->>W: tx confirmed + FE->>BE: POST /api/payment/decentralized/verify { paymentId, txHash } + BE->>BC: eth_getTransactionReceipt(txHash) + BC-->>BE: { status:0x1, blockNumber, logs } + BE->>BC: eth_blockNumber + BC-->>BE: currentBlock + BE->>BE: confirmations = currentBlock - txBlock + BE->>DB: Payment.status="completed"\nescrowState="funded"\ntx hash + confirmations + BE->>BE: cascade (mark offer accepted, others rejected,\nrequest→payment, chat, notifications) + BE->>IO: emit payment-completed events + BE-->>FE: { status:"confirmed", confirmations } + FE-->>B: "Payment verified ✓" + BscScan link +``` + +## API calls + +| Method | Endpoint | Source | +|---|---|---| +| `POST` | `/api/payment/decentralized/create` | `decentralizedPaymentRoutes.ts` | +| `POST` | `/api/payment/decentralized/verify` | `decentralizedPaymentRoutes.ts` | +| `GET` | `/api/payment/fetch-tx/:paymentId` | `paymentRoutes.ts` (manual rechecker) | + +## Database writes + +- **`payments`** — same model as the SHKeeper flow. `provider` distinguishes the source. +- **`selleroffers`**, **`purchaserequests`**, **`chats`**, **`notifications`** — identical cascade to [[Payment Flow - SHKeeper]] (offer accepted, others rejected, request → `payment`, chat created, notifications fanned out). + +## Socket events emitted + +- **`payment-created`** (admin dashboard) on intent creation. +- **`seller-offer-update`** `'payment-completed'` and `'offer-rejected'` post-verification. +- **`purchase-request-update`** `'status-changed'`. +- **`new-notification`** to both parties. + +## Side effects + +- **No SHKeeper involvement** — the escrow wallet is custodial; the platform admin holds the keys. Payouts from this wallet to sellers happen via [[Payout Flow]] (SHKeeper payouts API) or manual admin signing using `admin-wallet-payout.tsx` UI. +- **Verified-wallet field** — the buyer's connected wallet is also saved against `User.profile.walletAddress` (in `WalletConnectionCard`), which is later used to refund this same wallet if the order is disputed. + +## Error / edge cases + +- **Wrong network** → frontend forces a chain switch; if user refuses, transaction will fail or hit the wrong chain (escrow is BSC-only today). +- **Insufficient gas (BNB)** → wallet rejects the tx; nothing to verify. +- **Transaction reverted** (`receipt.status === '0x0'`) → verifier returns `failed`; backend marks `Payment.status = 'failed'`. Buyer can retry. +- **Transaction not yet mined** at verification time → verifier returns `pending` with `error: 'Transaction not found or still pending'`. Frontend retries verification on a backoff schedule. +- **Tx hash already used by another payment** — `blockchain.transactionHash` has a sparse index (`Payment.ts:178`); a duplicate verification attempt finds the existing payment and returns its status. +- **Wrong amount or wrong recipient** — must be enforced by log decoding (the verifier should reject if the `Transfer` event's `value` is less than expected or `to` ≠ escrow). The current verifier only checks the receipt's `status`; tightening this is recommended. +- **RPC throttling** — public BSC dataseed is generous but rate-limits exist; consider a dedicated RPC (Ankr, QuickNode) for production. +- **User closes the browser before verification** — the on-chain transfer still happened. A periodic reconciliation job (`/api/payment/fetch-tx/:paymentId`) or admin tool can replay verification from the txHash. +- **Confirmation depth** — currently 1 confirmation triggers `completed`. For larger amounts consider gating release until ≥ 12 confirmations on BSC. + +> [!warning] Verify the event log, not just the receipt +> A receipt status of `0x1` means the transaction did not revert — it does **not** confirm the right amount went to the right address. Decode the ERC-20 `Transfer(address,address,uint256)` event and assert `to == ESCROW_WALLET_ADDRESS` and `value >= expectedAmount`. The current implementation in `decentralizedPaymentService.ts` checks only the receipt status; harden this before accepting large payments. + +## Linked flows + +- [[Payment Flow - SHKeeper]] — sibling pay-in path; same downstream cascade. +- [[Escrow Flow]] — funded state semantics. +- [[Payout Flow]] — releasing the funded escrow to the seller. +- [[Dispute Flow]] — refunds back to the buyer's verified wallet. + +## Source files + +- Backend: `backend/src/services/payment/decentralizedPaymentService.ts` +- Backend: `backend/src/services/payment/decentralizedPaymentRoutes.ts` +- Backend: `backend/src/services/payment/paymentCoordinator.ts` +- Backend: `backend/src/models/Payment.ts` +- Frontend: `frontend/src/web3/web3Service.ts` +- Frontend: `frontend/src/web3/context/wagmi-provider.tsx` +- Frontend: `frontend/src/web3/context/web3-provider.tsx` +- Frontend: `frontend/src/sections/account/account-wallet-connection.tsx` +- Frontend: `frontend/src/sections/request/components/buyer-steps/payment-card.tsx` +- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-components/step-3-payment.tsx` +- Frontend: `frontend/src/global-config.ts` (`ESCROW_WALLET_ADDRESS`) diff --git a/04 - Flows/Payment Flow - SHKeeper.md b/04 - Flows/Payment Flow - SHKeeper.md new file mode 100644 index 0000000..fbcf7be --- /dev/null +++ b/04 - Flows/Payment Flow - SHKeeper.md @@ -0,0 +1,252 @@ +--- +title: Payment Flow - SHKeeper +tags: [flow, payment, shkeeper, crypto, escrow, webhook] +related_models: ["[[Payment]]", "[[PurchaseRequest]]", "[[SellerOffer]]"] +related_apis: ["POST /api/payment/shkeeper/create", "POST /api/payment/shkeeper/webhook", "GET /api/payment/shkeeper/status/:id"] +--- + +# Payment Flow — SHKeeper (Crypto Pay-In) + +End-to-end **crypto pay-in** via the self-hosted [SHKeeper](https://github.com/vsys-host/shkeeper.io) gateway at `pay.amn.gg`. The buyer pays in stablecoins/crypto; SHKeeper monitors the blockchain, sends webhooks back, and the backend marks the escrow funded. + +## Supported assets + +Pulled from env: `SHKEEPER_NETWORKS` and `SHKEEPER_ALLOWED_TOKENS` (`shkeeperService.ts:97-98`). +- **Networks**: `bsc`, `ethereum` (default in code). +- **Tokens**: `USDT`, `USDC` (default). The endpoint URL is built as `https://pay.amn.gg/api/v1/{NETWORK_PREFIX}-{TOKEN}/payment_request` (`shkeeperService.ts:413-417`). + - BSC → `BNB-USDT`, `BNB-USDC` (i.e. BEP-20). + - Ethereum → `ETH-USDT`, `ETH-USDC` (ERC-20). +- TRC-20 (`USDT-TRC20`) and native `BTC` are mentioned in the task brief but **not currently wired** in `shkeeperService.ts` — only BSC/ETH variants are produced from the code path. Verify SHKeeper-side configuration if those are required. + +## Actors + +- **Buyer** — pays. +- **Seller** — passive in this flow; gets notified on success. +- **Frontend** — checkout components under `frontend/src/sections/request/components/buyer-steps/step-3-components/` and `frontend/src/sections/payment/`. +- **Backend** — `shkeeperService.createPayInIntent` (`backend/src/services/payment/shkeeper/shkeeperService.ts:48-533`) and `shkeeperWebhook.handleShkeeperWebhook` (`backend/src/services/payment/shkeeper/shkeeperWebhook.ts`). +- **SHKeeper gateway** (`https://pay.amn.gg`) — issues per-payment deposit addresses, watches the chain, sends webhooks. +- **Blockchain** — BSC / Ethereum. +- **PaymentCoordinator** (`backend/src/services/payment/paymentCoordinator.ts`) — serialises concurrent payment-status updates from multiple sources (webhook, wallet monitor, manual confirm). +- **MongoDB** — `payments`, `purchaserequests`, `selleroffers`, `chats`, `notifications`. +- **Redis** — `paymentRedisService` (wallet-address cache, 2 h TTL). +- **Socket.IO** — `payment-created`, `seller-offer-update`, `purchase-request-update`. + +## Preconditions + +- Buyer has selected an offer (or is using the template-checkout shortcut). +- Backend env: `SHKEEPER_API_URL`, `SHKEEPER_API_KEY`, `SHKEEPER_WEBHOOK_SECRET`, `API_URL` (for the callback URL). +- Redis is reachable (graceful degradation if not — see `app.ts:361-367`). +- SHKeeper has wallets provisioned for each `crypto_name`. + +## Payment state machine + +```mermaid +stateDiagram-v2 + [*] --> pending: createPayInIntent\n(Payment.status="pending") + pending --> pending_partial: webhook PARTIAL\nescrowState="partial" + pending --> completed: webhook PAID/OVERPAID\nescrowState="funded" + pending --> failed: webhook EXPIRED/CANCELLED\nescrowState="cancelled" + pending_partial --> completed: top-up arrives, total ≥ amount + pending_partial --> failed: expires + completed --> released: admin release → seller payout\n[[Payout Flow]] + completed --> refunded: dispute resolution → buyer refund + refunded --> [*] + released --> [*] + failed --> [*] +``` + +## Step-by-step narrative + +### Phase 1 — Create intent + +1. Buyer clicks "Pay" on the chosen offer (`/dashboard/buyer/requests/{id}` → step-3-select-and-pay). +2. Frontend POSTs `POST /api/payment/shkeeper/create` with `{ purchaseRequestId, sellerOfferId, amount, token?, network? }`. +3. Backend `createPayInIntent`: + - Validates ObjectIds (`shkeeperService.ts:55-71`). Special path for **template checkout** (string IDs starting with `template-checkout-`). + - **Duplicate-guard #1** (`:75-116`): if there is already an active `Payment` (`status ∈ {pending, processing}`) for the same `{purchaseRequestId, sellerOfferId, buyerId}`, the existing record is reused — same `paymentUrl`, no new wallet allocation. + - **Duplicate-guard #2 (template checkout)** (`:131-198`): if a recent `completed`/`confirmed` payment exists for the same buyer + template session, reuse it; otherwise dedupe pending records within the last 5 minutes. + - **Upsert** (`:218-249`): atomic `Payment.findOneAndUpdate(filter, {$setOnInsert: {...}}, {upsert: true, new: true})` keyed by `{buyerId, purchaseRequestId, provider, direction:'in', status:'pending'}` — prevents race-condition duplicates. + - Sets `Payment.providerPaymentId = externalId`. For template checkouts, `externalId = template-{ts}-{rand}`; otherwise it's the `Payment._id`. + - **Wallet cache lookup** (`:421-450`): `paymentRedisService.getCachedWallet(cacheKey)` — if a wallet was allocated for the same `(amount, token, network, requestId)` within the last 2 h, reuse it. Avoids hammering SHKeeper for the same checkout. + - **Call SHKeeper API** (`:453-475`): `POST https://pay.amn.gg/api/v1/{cryptoName}/payment_request` with header `X-Shkeeper-Api-Key`. Body: `{ external_id, fiat: 'USD', amount: '12.34', callback_url: ${API_URL}/api/payment/shkeeper/webhook }`. The HTTP call is wrapped by `shkeeperFetch` (`shkeeperHealthCheck.ts`) which trips the breaker on repeated failures. + - **Persist response** (`:484-503`): updates `Payment.metadata.{shkeeperInvoiceId, shkeeperData, cryptoName, walletAddress}`; caches the wallet for 2 h; calls `walletMonitor.addWallet(...)` so the on-chain watcher can confirm independently (belt-and-braces against missed webhooks). +4. Returns `{ paymentId, paymentUrl, shkeeperInvoiceId, walletAddress, amount, exchangeRate, displayName, cryptoName }`. +5. Emits **`payment-created`** globally via `emitGlobalEvent` (`shkeeperService.ts:277-287`) so the admin dashboard sees the new pending payment in real time. + +### Phase 2 — Buyer pays + +6. Frontend renders a **QR code** for `${walletAddress}?amount=${amount}&token=...` and shows the exchange-rate-locked USDT amount, recalculate-after timer (`recalculate_after` from SHKeeper, typically 15 min), and a copy-to-clipboard button. +7. Buyer scans with MetaMask / Trust Wallet / Binance App and sends the on-chain transfer. +8. SHKeeper polls the chain, detects the deposit. When confirmations reach the threshold it marks the invoice `PAID` (or `OVERPAID` if the buyer sent extra). + +### Phase 3 — Webhook + +9. SHKeeper POSTs to `${API_URL}/api/payment/shkeeper/webhook` with the `ShkeeperWebhookPayload` shape (`shkeeperWebhook.ts:14-37`): + ``` + { + external_id, crypto: "BNB-USDT", addr, fiat: "USD", + balance_fiat, balance_crypto, paid: true, + status: "PAID" | "PENDING" | "EXPIRED" | "CANCELLED" | "PARTIAL" | "OVERPAID", + transactions: [{ txid, date, amount_crypto, amount_fiat, trigger: true, ... }], + fee_percent, fee_fixed, fee_policy, overpaid_fiat + } + ``` +10. **Signature verification** (`shkeeperWebhook.ts:84-120`): HMAC-SHA256 of the raw body with `SHKEEPER_WEBHOOK_SECRET`, header `x-shkeeper-signature` (also accepts `x-signature`, `signature`, `x-hub-signature`, `x-hub-signature-256`). Mismatch → `401` in production, allowed in dev. Length-mismatched signature → `401` (avoids `timingSafeEqual` crash). +11. **Fallback auth** (`:122-141`): if no signature header but env requires it, the route accepts `X-Shkeeper-Api-Key` matching `SHKEEPER_API_KEY`. Otherwise returns `202` to **stop SHKeeper retries** even if rejected (idempotency principle: always 2xx unless the request itself is mangled). +12. **DB reconnect** (`:143-155`): if Mongoose is disconnected, attempt reconnection. On failure → `202 OK` to avoid retry loop, log for investigation. +13. **Payment lookup**: `Payment.findOne({ providerPaymentId: payload.external_id })`. If not found and the external_id looks like a template checkout, hand off to `handleTemplateCheckoutWebhook` (`templateCheckoutWebhook.ts`). Otherwise → `202 OK` with a rate-limited log. +14. **Duplicate-webhook detection** (`:249-296`): if `metadata.shkeeperStatus`, `balance_fiat`, `paid` are identical to the previous webhook **and** less than 10 seconds have passed → return `202` (idempotent). Logged once per minute per payment. +15. **Map SHKeeper status → internal status** (`:387-411`): + | SHKeeper | Internal `status` | `escrowState` | + |---|---|---| + | `PAID` | `completed` | `funded` | + | `OVERPAID` | `completed` | `funded` | + | `EXPIRED`, `CANCELLED` | `failed` | `cancelled` | + | `PARTIAL` | `pending` | `partial` | + | `PENDING` | `pending` | — | +16. **Extract `transactionHash`** (`:311-385`) — prefers the transaction with `trigger === true`, then falls back to the latest, then to fetching from SHKeeper's invoice endpoint if the webhook somehow arrived without transactions. +17. **PaymentCoordinator** (`:482-507`) — `coordinatePaymentUpdate` returns false if another worker already started processing this state change, otherwise `executePaymentUpdate` writes the new status/escrowState/txHash atomically with metadata. +18. **Cascade on PAID/OVERPAID** (`:543-714`): + - Load `PurchaseRequest` and `SellerOffer` for this payment. + - **Mark winning offer accepted**: `selectedOffer.status = 'accepted'; save()`. + - **Reject all other offers**: `SellerOffer.updateMany({ purchaseRequestId, _id: { $ne: sellerOfferId } }, { status: 'rejected' })`. + - **Promote request**: `status = 'payment'; selectedOfferId = sellerOfferId`. + - **Create direct chat** (`chatService.createChat`, see [[Chat Flow]]). + - **Notifications**: `notifyPaymentConfirmed` (to both parties), `notifyOfferAccepted` (winner), `notifyRequestStatusChanged` (`received_offers → payment`). + - **Socket fan-out**: `seller-offer-update` `'payment-completed'` to winner, `'offer-rejected'` to losers (each carries `offerId`, `reason`). +19. **Cleanup**: `simpleAutoWebhook.removePayment(external_id)` stops the simple polling fallback. +20. Always respond `202 Accepted` (SHKeeper retries on non-2xx). `200` would cause infinite retries because SHKeeper expects `202` per its convention. + +### Phase 4 — Frontend reaction + +21. The buyer's checkout page subscribes to socket events and polls `GET /api/payment/shkeeper/status/{paymentId}`. When status flips to `completed`, the UI transitions to "Payment received" and unlocks the next buyer step (await delivery). +22. The seller's dashboard receives `seller-offer-update` `payment-completed` and surfaces the green "Order paid — start preparing" banner. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor B as Buyer + participant FE as Frontend + participant BE as Backend + participant R as Redis + participant SK as SHKeeper (pay.amn.gg) + participant BC as Blockchain + participant DB as MongoDB + participant IO as Socket.IO + actor S as Seller + + B->>FE: Choose offer, click "Pay" + FE->>BE: POST /api/payment/shkeeper/create + BE->>DB: dedupe / upsert Payment(status:"pending") + BE->>R: getCachedWallet(amount, token, network, requestId) + alt cache hit + R-->>BE: cached wallet + else cache miss + BE->>SK: POST /api/v1/{cryptoName}/payment_request\nX-Shkeeper-Api-Key + SK-->>BE: { id, wallet, amount, exchange_rate, ... } + BE->>R: setCachedWallet (TTL 2h) + BE->>BE: walletMonitor.addWallet (chain watcher) + end + BE->>IO: emit 'payment-created' (admin) + BE-->>FE: { paymentId, walletAddress, amount, QR-ready data } + FE-->>B: Render QR + countdown + copy address + B->>BC: Send USDT/USDC to walletAddress + BC-->>SK: deposit confirmed + SK->>BE: POST /api/payment/shkeeper/webhook\nx-shkeeper-signature + BE->>BE: HMAC verify + BE->>DB: Payment.findOne({providerPaymentId}) + BE->>BE: duplicate-webhook check + BE->>BE: PaymentCoordinator.coordinate + execute + BE->>DB: Payment.status="completed"\nescrowState="funded"\nblockchain.transactionHash=... + BE->>DB: SellerOffer.status="accepted" (others "rejected") + BE->>DB: PurchaseRequest.status="payment", selectedOfferId + BE->>DB: Chat.create (buyer + winning seller) + BE->>IO: emit seller-{winner} 'payment-completed' + BE->>IO: emit seller-{loser_i} 'offer-rejected' + BE-->>SK: 202 OK + IO-->>FE: status updated + IO-->>S: dashboard updates + FE-->>B: "Payment received ✓" +``` + +## API calls + +| Method | Endpoint | Purpose | Source | +|---|---|---|---| +| `POST` | `/api/payment/shkeeper/create` | Create pay-in intent | `shkeeperRoutes.ts` → `shkeeperService.createPayInIntent` | +| `POST` | `/api/payment/shkeeper/webhook` | Receive SHKeeper webhook | `shkeeperWebhook.handleShkeeperWebhook` | +| `GET` | `/api/payment/shkeeper/status/:paymentId` | Frontend polling | `shkeeperRoutes.ts` | +| `GET` | `/api/payment/fetch-tx/:paymentId` | Manual transaction lookup | `paymentRoutes.ts` | + +## Database writes + +- **`payments`**: insert on intent creation (`status: 'pending'`); update on each webhook (status, escrowState, blockchain.transactionHash, metadata). +- **`payments`** (unique index `uniq_pending_shkeeper_by_buyer_session`, see `Payment.ts:181-188`): partial unique on `{buyerId, purchaseRequestId, provider:'shkeeper', direction:'in', status:'pending'}` prevents duplicate pending pay-ins. +- **`selleroffers`**: `status` flipped (`accepted` / `rejected`) by the webhook cascade. +- **`purchaserequests`**: `status` → `payment`, `selectedOfferId` set. +- **`chats`**: a new `direct` chat (or reuse) — find-or-create via `ChatService.createChat`. +- **`notifications`**: 2–N entries depending on parties. + +## Socket events emitted + +- **`payment-created`** (global) — broadcast on intent creation. +- **`seller-offer-update`** with `eventType: 'payment-completed'` → winning seller. +- **`seller-offer-update`** with `eventType: 'offer-rejected'` → each losing seller. +- **`purchase-request-update`** with `eventType: 'status-changed'` (via `PurchaseRequestService`) → `request-{id}`. +- **`new-notification`** → both buyer and seller. + +## Side effects + +- **Redis**: `walletCache:{amount}_{token}_{network}_{requestId}` set for 2 hours. +- **Wallet monitor**: `walletMonitor.addWallet(addr, amount, paymentId, token, network)` — the on-chain watcher polls BSC/ETH RPCs directly; if SHKeeper's webhook is lost, the monitor still flips the payment to `completed`. This is the redundancy mechanism noted in the comments. +- **simpleAutoWebhook** (`shkeeperSimpleAuto.ts`): a poll-based fallback that asks SHKeeper for invoice status; cleaned up once a real webhook arrives. +- **Webhook stats**: `webhookStats.recordWebhook(...)` updates an in-memory ring buffer surfaced via `webhookStats.ts` admin endpoint. + +## Error / edge cases + +- **Duplicate intent submission** → reuse the existing pending payment (no new wallet). UX-safe. +- **SHKeeper API unreachable** → `shkeeperFetch` (with circuit breaker) throws; controller returns a **demo fallback** URL (`shkeeperService.ts:520-532`). In production this is observed as a Sentry error. +- **Webhook signature mismatch in prod** → `401`, SHKeeper retries — usually the secret has rotated; fix env and they catch up. +- **Webhook missing both signature and API key in prod** → `202 OK` (no-op) to prevent retry storm. +- **DB disconnected during webhook** → reconnect; on failure `202 OK` + log (consider DLQ). +- **`PARTIAL` payment** → state held as `pending/partial`; further deposits to the same address are aggregated by SHKeeper and a new webhook arrives. +- **`OVERPAID`** → treated as `completed/funded`; the overage stays with the platform unless an admin manually refunds (no automatic refund of overpayment today). +- **`EXPIRED`** → `failed/cancelled`. Buyer can re-initiate; the duplicate-guard will create a fresh intent because the old one is no longer `pending`. +- **External_id not found** → `202` with rate-limited log; common for orphaned webhooks from old tests. +- **Webhook arrives twice within 10 s with same data** → idempotency skip → `202`. +- **`PaymentCoordinator` deferral** → `202` with a "coordinator skipped update" log; the in-flight worker will finish the state change. +- **Wallet address reuse** — cached for 2 h means two parallel checkouts for the same `(amount, token, network, requestId)` share one address; whichever pays first wins (acceptable since the duplicate-guard reuses the same `Payment` doc anyway). +- **`crypto_name` mismatch** — only `BNB-*` and `ETH-*` are produced; for TRC-20, additional logic is needed in `shkeeperService.ts:415`. + +> [!warning] Webhook returns 202 even on errors +> The handler always responds 2xx to avoid SHKeeper's retry storm — even for unknown payments, signature failures (in non-production paths), DB errors, and unexpected exceptions. Operationally this means failed-to-process webhooks are silently swallowed unless someone tails the logs. Hook the catch-all into Sentry severity = `error` and alert on `webhookStats.errorCount`. + +> [!tip] Manual reconciliation +> Use `fix-transaction-hashes.js` at repo root to backfill `blockchain.transactionHash` for payments where the webhook arrived without transactions. See [[Payout Flow]] for the parallel payout-side script usage. + +## Linked flows + +- [[Purchase Request Flow]] — supplies the request being paid for. +- [[Seller Offer Flow]] — supplies the offer being accepted. +- [[Payment Flow - DePay & Web3]] — alternative direct-wallet route. +- [[Escrow Flow]] — what `escrowState=funded` means downstream. +- [[Chat Flow]] — auto-created on success. +- [[Notification Flow]] — both parties pinged. +- [[Payout Flow]] — pays the seller from the funded escrow. +- [[Dispute Flow]] — escape if the order goes wrong. + +## Source files + +- Backend: `backend/src/services/payment/shkeeper/shkeeperService.ts` (intent creation, ~650 lines) +- Backend: `backend/src/services/payment/shkeeper/shkeeperWebhook.ts` (webhook handler, ~750 lines) +- Backend: `backend/src/services/payment/shkeeper/shkeeperHealthCheck.ts` (circuit breaker) +- Backend: `backend/src/services/payment/shkeeper/shkeeperRoutes.ts` +- Backend: `backend/src/services/payment/paymentCoordinator.ts` +- Backend: `backend/src/services/payment/cleanupPendingPayments.ts` (periodic GC) +- Backend: `backend/src/services/blockchain/walletMonitor.ts` (chain watcher) +- Backend: `backend/src/services/redis/paymentRedisService.ts` (wallet cache) +- Backend: `backend/src/models/Payment.ts` +- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-components/step-3-payment.tsx` +- Frontend: `frontend/src/sections/payment/` diff --git a/04 - Flows/Payout Flow.md b/04 - Flows/Payout Flow.md new file mode 100644 index 0000000..11d8a22 --- /dev/null +++ b/04 - Flows/Payout Flow.md @@ -0,0 +1,133 @@ +--- +title: Payout Flow +tags: [flow, payment, payout, shkeeper, seller] +related_models: ["[[Payment]]"] +related_apis: ["POST /api/payment/shkeeper/payout", "GET /api/payment/shkeeper/payout/:taskId"] +--- + +# Payout Flow + +How the **seller receives the escrowed crypto** once the order is complete. Two variants are implemented: + +1. **SHKeeper Payouts API** (`shkeeperPayoutService.ts`) — the gateway signs and broadcasts on behalf of the platform. +2. **Manual admin wallet payout** (`admin-wallet-payout.tsx`) — an admin connects their own wallet and signs the transfer; the tx hash is reported back to the backend. + +Both result in `Payment.escrowState = 'released'` and an outgoing `Payment` record with `direction: 'out'`. + +## Actors + +- **Admin** (or scheduled system trigger) — initiates the payout. +- **Seller** — recipient, has saved their wallet address under `User.profile.walletAddress`. +- **Backend** — `shkeeperPayoutService.createPayoutTask` and the manual confirmation routes. +- **SHKeeper Payouts API** — `POST https://pay.amn.gg/api/v1/payout` (per SHKeeper docs). +- **Blockchain (BSC)** — final on-chain settlement. +- **MongoDB** — separate `Payment` document with `direction: 'out'`. + +## Preconditions + +- The original pay-in `Payment` has `escrowState = 'funded'` (or `releasable`). +- The seller has set `profile.walletAddress` (validated `^0x...` format). +- The corresponding `PurchaseRequest` is in a status that allows payout (`delivered`, `confirming`, `seller_paid`, or `completed`). + +## Step-by-step narrative + +### SHKeeper-mediated payout + +1. Admin (or the auto-release scheduler — not yet implemented) hits `POST /api/payment/shkeeper/payout` with `{ purchaseRequestId, sellerOfferId, buyerId, sellerId, amount, recipientAddress, token?, network? }`. +2. Backend `shkeeperPayoutService.createPayoutTask` (`shkeeperPayoutService.ts:40-150`): + - Validates ObjectIds and the `recipientAddress` (`startsWith('0x')`). + - **Idempotency**: `Payment.findOne({ purchaseRequestId, sellerOfferId, sellerId, provider:'shkeeper', direction:'out', status: { $in:['pending','processing','completed'] } })` — if found, reuses it. + - Creates a new `Payment` document with `direction: 'out'`, `escrowState: 'releasing'`, `blockchain.receiver = recipientAddress`. + - Calls SHKeeper Payouts API (`POST /api/v1/payout`) with the body documented at . SHKeeper returns a `task_id`. + - Stores `Payment.providerPaymentId = task_id`, `metadata.shkeeperTaskId = task_id`, `metadata.payoutType = 'seller-payment'`. +3. Polling or webhook: when SHKeeper completes the payout, it pushes a webhook (or the backend polls `GET /api/v1/payout/{task_id}`) and the system flips `Payment.status = 'completed'`, `escrowState = 'released'`, populates `blockchain.transactionHash`. +4. The original pay-in `Payment` is updated in tandem: `escrowState = 'released'`, `PurchaseRequest.status = 'seller_paid'` → `completed`. +5. Notifications: `notifyPayoutSent` to the seller, internal admin log. + +### Manual admin payout + +1. Admin opens the request detail in the admin view; the admin-step component `admin-wallet-payout.tsx` shows the recipient and amount. +2. Admin connects their wallet (`useWeb3` / `web3Service.connect()`). +3. Admin clicks "Send payout"; wagmi triggers `transfer(recipient, amount)` on the USDT contract. +4. After confirmation, the admin clicks "Confirm in system", which POSTs `POST /api/payment/admin/confirm-tx/:paymentId` with `{ txHash, kind: 'release' }`. +5. Backend `confirmAdminTx(paymentId, txHash, 'release')` (`shkeeperService.ts:628-647`) sets `status: 'completed'`, `escrowState: 'released'`, `blockchain.transactionHash = txHash`. + +### Sequence diagram (SHKeeper payout) + +```mermaid +sequenceDiagram + autonumber + actor A as Admin/System + participant BE as Backend + participant DB as MongoDB + participant SK as SHKeeper Payout API + participant BC as BSC + actor S as Seller + + A->>BE: POST /api/payment/shkeeper/payout + BE->>DB: Payment.create({direction:"out", escrowState:"releasing"}) + BE->>SK: POST /api/v1/payout {to, amount, crypto} + SK-->>BE: { task_id, status:"pending" } + BE->>DB: Payment.providerPaymentId=task_id + SK->>BC: signed payout tx (managed wallet) + BC-->>SK: confirmed + SK->>BE: webhook payout-completed (or BE polls) + BE->>DB: Payment.status="completed"\nescrowState="released"\ntxHash + BE->>DB: pay-in Payment.escrowState="released"\nPurchaseRequest.status="seller_paid" + BE->>S: notifyPayoutSent +``` + +## API calls + +| Method | Endpoint | Source | +|---|---|---| +| `POST` | `/api/payment/shkeeper/payout` | `shkeeperPayoutRoutes.ts` → `createPayoutTask` | +| `GET` | `/api/payment/shkeeper/payout/:taskId` | Polls SHKeeper task status | +| `POST` | `/api/payment/admin/confirm-tx/:paymentId` | Manual admin confirmation | +| `GET` | `/api/payment/admin/payouts` | List payouts (admin dashboard) | + +## Database writes + +- **`payments`** — new outgoing document; updates to `status`, `escrowState`, `blockchain.transactionHash` as the task progresses. +- **`payments`** (pay-in counterpart) — `escrowState = 'released'`. +- **`purchaserequests`** — `status` advances to `seller_paid` → `completed`. +- **`notifications`** — seller payout receipt. + +## Socket events emitted + +- **`payment-status`** (admin) on each transition. +- **`purchase-request-update`** `status-changed`. + +## Side effects + +- **`fix-transaction-hashes.js`** at repo root (`backend/fix-transaction-hashes.js`) — script used to backfill missing `blockchain.transactionHash` on payouts where the SHKeeper webhook arrived without the txid (e.g. signature length mismatch in dev). Run locally with the same Mongo URI to repair stale documents. Use it as the reference for the data-fix pattern — pull recent payouts, query SHKeeper for invoice/task details, write back the hash. +- **Hash repair** — periodic reconciliation against SHKeeper invoice GET endpoints ensures bookkeeping accuracy. + +## Error / edge cases + +- **Invalid recipient address** → throws synchronously, no DB record created. +- **SHKeeper insufficient hot-wallet balance** → SHKeeper returns an error; payout task stays `pending`, backend logs. +- **Duplicate payout request** → idempotency: existing payment returned with no extra SHKeeper call. +- **Payout reverted on chain** → SHKeeper marks the task `failed`; backend sets `Payment.status = 'failed'`, `escrowState = 'failed'`. Admin retries. +- **Missing `transactionHash` after success** → use `fix-transaction-hashes.js` to backfill. +- **Manual payout signed but never confirmed in system** → on-chain transfer happened, but `Payment.escrowState` stays `releasing`. Admin can run a reconciliation script that scans the escrow wallet's outgoing txs and matches by amount/timestamp. +- **Seller changes wallet address mid-flight** → the saved `recipientAddress` is the snapshot taken at payout creation; subsequent profile changes do not affect in-flight payouts. + +> [!warning] Auto-release is not yet implemented +> Today, payouts are admin-initiated. The flow is ready for an automatic trigger when [[Delivery Confirmation Flow]] completes — implement a cron job or queue worker that scans for `PurchaseRequest.status='delivered'` and auto-creates payouts after a configurable grace period. + +## Linked flows + +- [[Escrow Flow]] — sets up the conditions under which payout is allowed. +- [[Delivery Confirmation Flow]] — green-lights the payout. +- [[Dispute Flow]] — can divert funds to a refund instead. +- [[Notification Flow]] — payout receipt to seller. + +## Source files + +- Backend: `backend/src/services/payment/shkeeper/shkeeperPayoutService.ts` +- Backend: `backend/src/services/payment/shkeeper/shkeeperPayoutRoutes.ts` +- Backend: `backend/src/services/payment/shkeeper/shkeeperService.ts:614-647` (build & confirm admin tx payload) +- Backend: `backend/fix-transaction-hashes.js` (reconciliation script) +- Frontend: `frontend/src/sections/request/components/admin-steps/admin-wallet-payout.tsx` +- Frontend: `frontend/src/web3/web3Service.ts` diff --git a/04 - Flows/Purchase Request Flow.md b/04 - Flows/Purchase Request Flow.md new file mode 100644 index 0000000..6b22c7a --- /dev/null +++ b/04 - Flows/Purchase Request Flow.md @@ -0,0 +1,202 @@ +--- +title: Purchase Request Flow +tags: [flow, marketplace, buyer, purchase-request] +related_models: ["[[PurchaseRequest]]", "[[Category]]", "[[Address]]", "[[SellerOffer]]"] +related_apis: ["POST /api/marketplace/purchase-requests", "GET /api/marketplace/purchase-requests", "PATCH /api/marketplace/purchase-requests/:id"] +--- + +# Purchase Request Flow + +A **buyer** drafts and publishes a purchase request describing what they want to source. Once published, the request becomes visible to sellers (either all sellers or a curated subset), kicking off [[Seller Offer Flow]]. + +## Actors + +- **Buyer** — owner of the request. +- **Frontend** — multi-step wizard at `/dashboard/request/new` (`frontend/src/app/dashboard/request/new/page.tsx`). The wizard component lives in `frontend/src/sections/request/components/request-form-wizard.tsx` and uses the step files under `frontend/src/sections/request/components/steps/` (basic info, details, budget, review) plus `buyer-steps/` for the post-publish lifecycle. +- **Backend** — `PurchaseRequestService.createPurchaseRequest` and the marketplace controller (`backend/src/services/marketplace/marketplaceController.ts`). +- **MongoDB** — `purchaserequests`, with population from `users` and `categories`. +- **Socket.IO** — emits `purchase-request-update` to the `request-{id}` room and `seller-offer-update` to seller rooms. +- **Notification service** — pushes in-app notifications to all targeted sellers. + +## Preconditions + +- User is authenticated and `req.user.role === 'buyer'`. +- At least one category exists (seeded via `seedCategories`). +- Optional: the buyer has saved a delivery address under `/dashboard/account/addresses`. + +## State machine + +Status progression is enforced by `STATUS_PROGRESSION_ORDER` in `PurchaseRequestService.ts:12-26`. Moving backward is disallowed except into a terminal status. + +```mermaid +stateDiagram-v2 + [*] --> pending: createPurchaseRequest() + pending --> received_offers: first SellerOffer saved\nSellerOfferService.createOffer + received_offers --> in_negotiation: buyer/seller chat\n(counter-offer, see [[Negotiation Flow]]) + in_negotiation --> received_offers: counter rejected + received_offers --> payment: SHKeeper webhook PAID\n(selected offer) + in_negotiation --> payment: same + payment --> processing: seller acknowledges + processing --> delivery: seller marks shipped + delivery --> delivered: buyer enters delivery code + delivered --> confirming: optional auto-release timer + confirming --> completed: escrow released to seller + completed --> finalized: ratings exchanged + finalized --> archived: 30 days idle + pending --> cancelled: buyer cancels (any pre-payment status) + received_offers --> cancelled + in_negotiation --> cancelled + cancelled --> [*] + archived --> [*] +``` + +Terminal statuses: `completed`, `finalized`, `archived`, `cancelled` (`PurchaseRequestService.ts:28`). + +## Step-by-step narrative + +### Multi-step wizard + +1. Buyer clicks "New request" in the dashboard sidebar and lands at `/dashboard/request/new`. +2. **Step 1 — Basic info** (`steps/request-basic-info-step.tsx`): title (5–200 chars), description (20–2000 chars), category selection (dropdown populated from `GET /api/marketplace/categories`). +3. **Step 2 — Details** (`steps/request-details-step.tsx`): optional product link, size, color, quantity, free-form specifications (key/value pairs), AI-assisted description generation (calls `POST /api/ai/generate-description` if the user clicks the magic-wand button — see `backend/src/services/ai/`). +4. **Step 3 — Budget** (`steps/request-budget-step.tsx`): min/max in chosen currency (default USDT), urgency (low/medium/high), preferred sellers (typeahead bound to `GET /api/users/sellers`; `"all"` means public). +5. **Step 4 — Review** (`steps/request-review-step.tsx`): summary; user can attach a saved address (`GET /api/addresses`) or enter a one-off `deliveryInfo.address`. Upload optional attachments via `POST /api/files/upload` — returns URLs persisted into `attachments[]`. +6. **Draft vs. publish** — The wizard always POSTs on submit; drafts are not first-class today (the local wizard state is the "draft"). The persisted record is immediately `status: "pending"` and visible to sellers. + +### Submission + +7. Frontend POSTs `POST /api/marketplace/purchase-requests` with the full payload (`PurchaseRequestCreateData` in `PurchaseRequestService.ts:73-106`). +8. Backend `PurchaseRequestService.createPurchaseRequest` (`:123-188`): + - **Duplicate-guard** (`:128-143`): rejects a request with identical `buyerId`, `title`, `description` within the last 5 minutes. Returns `Error("درخواست مشابه در ۵ دقیقه گذشته ایجاد شده است")`. + - **Sanitise `preferredSellerIds`** (`:146-150`): drops literal `"all"` and any invalid ObjectIds. + - **`isPublic`** is `true` when the cleaned array is empty OR the original payload included `"all"`. Public requests are visible to every active seller; private requests only to listed sellers. + - Builds and saves the `PurchaseRequest` document with `status: "pending"`. +9. **Notify the buyer**: `notificationService.notifyPurchaseRequestCreated()` fires asynchronously (no `await`) — an info notification appears in the buyer's bell-icon dropdown. +10. **Fan-out to sellers** (`notifyAllSellersAboutNewRequest`, `:190-249`): + - If `isPublic`: `User.find({ role: "seller", status: "active" })`. + - Otherwise: only the curated `preferredSellerIds`. + - Iterates with **50 ms stagger** between notifications to avoid overwhelming Mongo/Socket.IO. + - For each seller: `notificationService.createNotification(...)` writes a `Notification` doc AND emits via Socket.IO (`actionUrl: /dashboard/seller/marketplace/request/{id}`). +11. **Real-time fan-out** is also performed when sellers eventually act on the request (offers, payments) via `emitPurchaseRequestUpdate(requestId, eventType, data)` (`PurchaseRequestService.ts:53-71`) and `emitOfferUpdate` in [[Seller Offer Flow]]. + +### Visibility filter + +When a seller hits `GET /api/marketplace/purchase-requests?sellerId=...`, `PurchaseRequestService.getPurchaseRequests` (`:251-364`) applies a per-status visibility filter: + +| Request status | Visible to seller if | +|---|---| +| `pending` | `isPublic` OR seller ∈ `preferredSellerIds` | +| `received_offers`, `in_negotiation` | seller has an offer for the request OR no offer is selected yet AND (public/preferred) | +| `payment`, `processing`, `delivery`, `delivered`, `confirming`, `seller_paid`, `completed` | seller is the **selected** seller (their offer is `selectedOfferId`) | + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor B as Buyer + participant FE as Frontend Wizard + participant BE as Backend + participant DB as MongoDB + participant N as NotificationService + participant IO as Socket.IO + actor S1 as Seller (n sellers) + + B->>FE: Open /dashboard/request/new + loop Steps 1-4 + B->>FE: Fill basic / details / budget / review + FE-->>FE: Local validation + end + opt AI assist + FE->>BE: POST /api/ai/generate-description + BE-->>FE: { description } + end + opt attachments + FE->>BE: POST /api/files/upload + BE-->>FE: { url } + end + B->>FE: Click "Publish" + FE->>BE: POST /api/marketplace/purchase-requests + BE->>DB: Duplicate check (same title+desc in 5m?) + BE->>BE: clean preferredSellerIds; compute isPublic + BE->>DB: PurchaseRequest.create({status: "pending"}) + DB-->>BE: savedRequest + BE->>N: notifyPurchaseRequestCreated(buyer, requestId) + par fan-out to sellers (staggered 50ms) + BE->>DB: User.find({role:"seller", status:"active"}) (or preferred) + BE->>N: createNotification(seller_i, ...) + N->>IO: emit user-{seller_i} 'new-notification' + N->>DB: Notification.create + end + BE-->>FE: 201 { request } + FE-->>B: Redirect /dashboard/buyer/requests/{id} + IO-->>S1: 'new-notification' (sellers receive in real time) +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `POST` | `/api/marketplace/purchase-requests` | Create the request | +| `GET` | `/api/marketplace/categories` | Step 1 dropdown | +| `GET` | `/api/users/sellers` | Step 3 preferred-sellers typeahead | +| `GET` | `/api/addresses` | Step 4 saved addresses | +| `POST` | `/api/files/upload` | Attachments | +| `POST` | `/api/ai/generate-description` | Optional AI-assisted description | +| `GET` | `/api/marketplace/purchase-requests` | Listing (buyer's own and seller's filtered view) | +| `GET` | `/api/marketplace/purchase-requests/:id` | Detail page (joins payment data) | +| `PATCH` | `/api/marketplace/purchase-requests/:id` | Generic update (status, attachments, etc.) | +| `DELETE` | `/api/marketplace/purchase-requests/:id` | Cancel (only before payment) | + +## Database writes + +- **`purchaserequests` collection**: full insert. Subsequent status transitions and `selectedOfferId` updates happen in [[Seller Offer Flow]], [[Payment Flow - SHKeeper]], and [[Delivery Confirmation Flow]]. +- **`notifications` collection**: one per notified seller plus one for the buyer. +- **`users.referralStats`** is not touched at request creation. + +## Socket events emitted + +- **`new-notification`** → `user-{sellerId}` for each notified seller (via `NotificationService.emitRealTimeNotification`). +- **`purchase-request-update`** → `request-{id}` on status changes (`emitPurchaseRequestUpdate`, `PurchaseRequestService.ts:53-71`). +- **`seller-offer-update`** → `seller-{id}` when an offer is created against this request (see [[Seller Offer Flow]]). +- **`request-cancelled`** → `user-{buyerId}` and `user-{sellerId}` when the buyer cancels (`PurchaseRequestService.ts:671-693`). + +## Side effects + +- One Mongo write per notification (potentially N+1 for a large seller base — mitigated by the 50 ms stagger). For mass markets this should be batched. +- The buyer is auto-subscribed to the `request-{id}` Socket.IO room on the detail page mount (frontend emits `join-request-room`). +- If `urgency === "high"`, the notification message uses the high-priority template — visible in [[Notification Flow]]. + +## Error / edge cases + +- **Duplicate within 5 minutes** → `400` with Persian message. Prevents double-submit on flaky networks. +- **Invalid category ObjectId** → `400` from Mongoose validation. +- **`preferredSellerIds` with invalid ObjectIds** → silently dropped, not an error. +- **Empty cleaned `preferredSellerIds` + no `"all"` in original** → `isPublic` is `true` (open marketplace). This is the intended fallback. +- **Buyer cancels after payment** → blocked by `STATUS_PROGRESSION_ORDER` (cannot move to `cancelled` from `processing`+ without admin intervention; in practice cancellations after payment must go through [[Dispute Flow]]). +- **Notification fan-out failure for one seller** → logged and resolved as `false`; the overall request creation still succeeds. +- **Invalid status progression on PATCH** → `400 Invalid status progression` (`PurchaseRequestService.ts:418-424`). +- **Seller calls GET listing without `sellerId` query** → logs `"No sellerId provided - returning ALL requests!"` (`:348`) and returns the full set. Frontend always supplies `sellerId` for seller dashboards; missing it is a bug worth catching. + +> [!tip] Status progression is forward-only +> Once `status` reaches `payment`, you cannot put it back to `received_offers` even via PATCH. The only escape hatches are the terminal statuses (`cancelled`, `archived`, etc.) and admin tools. + +## Linked flows + +- [[Seller Offer Flow]] — sellers respond to the published request. +- [[Negotiation Flow]] — counter-offer mechanics in `in_negotiation`. +- [[Payment Flow - SHKeeper]] — buyer pays for the accepted offer. +- [[Delivery Confirmation Flow]] — seller ships, buyer confirms. +- [[Dispute Flow]] — escape hatch for failed deliveries. +- [[Notification Flow]] — backbone of the seller fan-out. + +## Source files + +- Backend: `backend/src/services/marketplace/PurchaseRequestService.ts` +- Backend: `backend/src/services/marketplace/marketplaceController.ts` +- Backend: `backend/src/services/marketplace/controllerRoutes.ts` +- Backend: `backend/src/models/PurchaseRequest.ts` +- Frontend: `frontend/src/app/dashboard/request/new/page.tsx` +- Frontend: `frontend/src/sections/request/components/request-form-wizard.tsx` +- Frontend: `frontend/src/sections/request/components/steps/` (4 step files) +- Frontend: `frontend/src/sections/request/view/buyer-request-view.tsx` diff --git a/04 - Flows/Rating Flow.md b/04 - Flows/Rating Flow.md new file mode 100644 index 0000000..8d50c15 --- /dev/null +++ b/04 - Flows/Rating Flow.md @@ -0,0 +1,120 @@ +--- +title: Rating Flow +tags: [flow, rating, review, moderation] +related_models: ["[[Review]]", "[[ShopSettings]]", "[[PurchaseRequest]]"] +related_apis: ["POST /api/marketplace/reviews", "GET /api/marketplace/reviews/:subjectType/:subjectId"] +--- + +# Rating Flow + +After an order is `completed`, the buyer rates the seller and (optionally) leaves a review; the seller can rate the buyer in the reciprocal flow. Reviews are scoped by `subjectType` (`seller` | `template`) and constrained by the seller's `ShopSettings`. + +## Actors + +- **Buyer** (typical reviewer). +- **Seller** (subject; can also be a reviewer in the reciprocal direction). +- **System** — enforces uniqueness and moderation rules. +- **Backend** — `backend/src/services/marketplace/reviewRoutes.ts`. +- **MongoDB** — `reviews` collection (`backend/src/models/Review.ts`). + +## Preconditions + +- The associated `PurchaseRequest` is `completed` (or `finalized`). +- The reviewer is the buyer of that request (for `isVerifiedBuyer` to be `true`). +- The subject's `ShopSettings.allowSellerReviews` / `allowTemplateReviews` is not `false` (`reviewRoutes.ts:15-31`). + +## Step-by-step narrative + +1. From the request detail or seller profile, the buyer clicks "Leave review". The form captures `rating` (1–5) and `comment` (≤ 1000 chars). +2. Frontend POSTs `POST /api/marketplace/reviews` with `{ subjectType: 'seller' | 'template', subjectId, rating, comment, purchaseRequestId? }`. +3. Backend route handler: + - Validates payload. + - Calls `isReviewsAllowed(subjectType, subjectId)` — checks the seller's `ShopSettings` (for sellers, look up the seller directly; for templates, look up the template's owning seller). + - Sets `isVerifiedBuyer = true` if the user owns a `completed` purchase request from that seller. + - Defaults `status: 'published'` (no moderation queue today). +4. Inserts a `Review` document. Unique index `{ subjectType, subjectId, reviewerId }` prevents a user from reviewing the same subject twice (`Review.ts:34`). +5. Aggregated stats are recomputed on read via `computeStats` (`reviewRoutes.ts:33-62`) — count, average, per-star histogram. No denormalised counter on `User` today; everything is computed at read-time. + +## Visibility + +- Public — anyone hitting `GET /api/marketplace/reviews/seller/:sellerId` sees `status: 'published'` reviews paginated by 10. +- If `ShopSettings.allowSellerReviews === false` (or `allowTemplateReviews === false`), reads return `403 Reviews are disabled by seller`. +- The seller can flag a review for moderation (planned — current statuses include `pending` and `rejected`, but no UI to flip them today; admin can update via direct DB). + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor B as Buyer + participant FE as Frontend + participant BE as Backend + participant DB as MongoDB + + B->>FE: Open seller profile / request detail + B->>FE: Click "Leave review", choose stars + comment + FE->>BE: POST /api/marketplace/reviews + BE->>DB: ShopSettings.findOne({sellerId}) → allowSellerReviews? + alt allowed + BE->>DB: PurchaseRequest.exists({buyer, seller, status:"completed"})? + BE->>DB: Review.create({status:"published", isVerifiedBuyer}) + BE-->>FE: 201 { review } + else disabled + BE-->>FE: 403 Reviews disabled + end + + Note over FE: Public visitor: + FE->>BE: GET /api/marketplace/reviews/seller/{id} + BE->>DB: Review.find / computeStats aggregate + BE-->>FE: { items, pagination, stats:{count, avg, histogram} } +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `POST` | `/api/marketplace/reviews` | Submit review | +| `GET` | `/api/marketplace/reviews/:subjectType/:subjectId` | List reviews + stats | +| `PATCH` | `/api/marketplace/reviews/:id` | Edit own review (within edit window) | +| `DELETE` | `/api/marketplace/reviews/:id` | Delete own review | + +## Database writes + +- **`reviews`** — insert on submission; one document per `(subjectType, subjectId, reviewerId)`. +- **`shopsettings`** — read-only here; the seller controls `allowSellerReviews` / `allowTemplateReviews` in their shop settings. + +## Socket events emitted + +- None today. A `new-review` event broadcast to `user-{sellerId}` would be a useful enhancement so sellers see reviews appear live. + +## Side effects + +- Recompute aggregate on every list call — fine for small volumes; consider caching `stats` per subject when the review count grows. +- Order rating field also stamps `metadata.rating` on the purchase request when the marketplace endpoint accepts ratings inline (see `routes.ts` references in `backend/src/services/marketplace/routes.ts`). + +## Error / edge cases + +- **Duplicate review** → MongoDB `E11000` from the unique index; surface as `409 Already reviewed`. +- **Subject disabled reviews** → `403`. +- **Reviewer not a verified buyer** → review is still allowed but `isVerifiedBuyer = false`. Display this in the UI. +- **Rating out of 1–5** → Mongoose schema validator rejects. +- **Comment > 1000 chars** → schema-level rejection. +- **Seller toggles `allowSellerReviews=false` after reviews exist** → existing reviews remain stored but become unreadable via the public GET (`reviewRoutes.ts:81-83`). +- **Spam / abuse** → no automatic moderation; admin can flip `status` to `rejected` to hide. + +> [!tip] Verified-buyer badge +> The `isVerifiedBuyer` flag is the most credible signal for prospective buyers. Always render a "Verified buyer" pill next to reviews where this is `true`. + +## Linked flows + +- [[Purchase Request Flow]] — precursor that makes the buyer "verified". +- [[Seller Offer Flow]] — display average rating on offer cards. +- [[Dispute Flow]] — a resolved dispute could trigger a review prompt; today they are independent. + +## Source files + +- Backend: `backend/src/services/marketplace/reviewRoutes.ts` +- Backend: `backend/src/models/Review.ts` +- Backend: `backend/src/services/marketplace/shopSettingsController.ts` (allow flags) +- Backend: `backend/src/services/marketplace/routes.ts` (inline rating on order completion) +- Frontend: review components under `frontend/src/sections/account/` and seller profile views diff --git a/04 - Flows/Referral Flow.md b/04 - Flows/Referral Flow.md new file mode 100644 index 0000000..f2a2b1f --- /dev/null +++ b/04 - Flows/Referral Flow.md @@ -0,0 +1,163 @@ +--- +title: Referral Flow +tags: [flow, referral, points, growth] +related_models: ["[[User]]", "[[PointTransaction]]", "[[LevelConfig]]"] +related_apis: ["POST /api/points/generate-referral-code", "GET /api/points/referrals", "GET /api/points/leaderboard"] +--- + +# Referral Flow + +Each user can generate a personal referral code, share a short URL, and earn points/commission when their referees sign up and complete purchases. Codes can also be entered at sign-up time (Phase 1 attribution) — see [[Registration Flow]]. + +## Actors + +- **Referrer** — the user with the code. +- **Referred user** — the new sign-up. +- **Backend** — `PointsService` (`backend/src/services/points/PointsService.ts`), points routes at `backend/src/routes/pointsRoutes.ts`. +- **MongoDB** — `users` (`referralCode`, `referredBy`, `referralStats`, `points`), `pointtransactions`, `levelconfigs`. +- **Socket.IO** — `referral-signup` and `level-up` events. + +## Preconditions + +- Authenticated user (for referral-code generation and points endpoints). +- The 8-character code is unique (alphabet excludes I/O/0/1 to avoid confusion — `PointsService.ts:13`). + +## Step-by-step narrative + +### 1. Code generation + +1. User opens `/dashboard/account/referrals`. If they don't have a code yet, they click "Generate code". +2. Frontend POSTs `POST /api/points/generate-referral-code`. +3. `PointsService.generateReferralCode(userId)` (`:12-31`): + - Loops generating an 8-character code from `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` until uniqueness is confirmed by `User.findOne({ referralCode })`. + - Saves the code to the user. + - Returns it. +4. Frontend renders the share URL `https://amn.gg/r/{code}` and a copy button. + +### 2. Short-URL redirect + +5. When a friend clicks the short URL, `GET /r/:code` (`backend/src/app.ts:274-278`) redirects to `${FRONTEND_URL}/auth/jwt/sign-up?ref={code}`. +6. The sign-up form reads `?ref=` and pre-fills the referral field (hidden or visible). + +### 3. Attribution at sign-up + +7. During [[Registration Flow]] verification (or [[Google OAuth Flow]] sign-up), the controller looks up `User.findOne({ referralCode })`: + - Sets `user.referredBy = referrer._id` on the new user. + - Increments `referrer.referralStats.totalReferrals`. + - Emits **`referral-signup`** to `user-{referrerId}` with the referee's name, email, and updated total. +8. At this point the referee is **counted** but no points have changed hands yet. Points award on subsequent business events. + +### 4. Points awarding + +9. `PointsService.addPoints(userId, amount, source, metadata)` (`:36-100`) is called by other services on triggering events: + - **Purchase completion** (intended): when a referred user finishes an order, the referrer should get a commission. The hook point is `PurchaseRequestService` `notifyTransactionCompleted` — the exact wiring is implementation-specific; the service exposes `source: 'purchase' | 'referral' | 'bonus' | 'admin'`. + - **Bonus**: ad-hoc admin grants. +10. Inside `addPoints`: + - Transaction-scoped Mongo session. + - `user.points.total += amount; user.points.available += amount`. + - `PointTransaction.create({ type:'earn', source, amount, balance, metadata })`. + - `updateUserLevel(userId, session)` recomputes the user's tier from `LevelConfig`. + - Emits **`level-up`** on `user-{userId}` if the level changed (`:91-99`). +11. Both the referrer and the referee may earn points (e.g. "give 100, get 100" growth model). The current code awards per `addPoints` call — design decision lives in the caller, not in PointsService. + +### 5. Redemption / payout + +12. Users see their balance under `/dashboard/account/points` and can spend via `POST /api/points/redeem` (e.g. for service-credit or discount codes). +13. `PointTransaction` records `type: 'spend'` with negative `amount`, keeping `balance` running. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor R as Referrer + actor N as New User + participant FE as Frontend + participant BE as Backend + participant DB as MongoDB + participant IO as Socket.IO + + R->>FE: Generate referral code + FE->>BE: POST /api/points/generate-referral-code + BE->>DB: User.findByIdAndUpdate(referralCode=...) + BE-->>FE: { code } + R->>R: share https://amn.gg/r/{code} + + N->>BE: GET /r/{code} + BE-->>N: 302 → /auth/jwt/sign-up?ref={code} + N->>FE: Fills sign-up, completes email verification + FE->>BE: POST /api/auth/verify-email-code (with referralCode in TempVerification) + BE->>DB: User.create + BE->>DB: referrer.referralStats.totalReferrals += 1 + BE->>IO: emit user-{R} 'referral-signup' + + Note over BE,DB: Later, when N completes a purchase + BE->>BE: PointsService.addPoints(R, +X, 'referral', {referredUserId:N}) + BE->>DB: user.points += X; PointTransaction.create + BE->>BE: updateUserLevel → maybe 'level-up' + BE->>IO: emit user-{R} 'level-up' +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `POST` | `/api/points/generate-referral-code` | Generate or rotate referral code | +| `GET` | `/api/points/my-points` | Balance + level | +| `GET` | `/api/points/transactions` | History | +| `GET` | `/api/points/referrals` | Referred users list | +| `GET` | `/api/points/leaderboard` | Global top referrers | +| `GET` | `/api/points/levels` | Level config (public) | +| `POST` | `/api/points/redeem` | Spend points | +| `POST` | `/api/points/admin/add` | Admin-only manual grant | +| `GET` | `/r/:code` | Short-URL redirect to sign-up | + +## Database writes + +- **`users`**: `referralCode` on generation, `referredBy` on referee creation, `referralStats.{totalReferrals, activeReferrals, totalEarned}` and `points.{total, available, level}` on point events. +- **`pointtransactions`**: one document per earn/spend/refund. +- **`levelconfigs`**: read-only at runtime (seeded at deploy). + +## Socket events emitted + +- **`referral-signup`** → `user-{referrerId}` on referee creation. +- **`level-up`** → `user-{userId}` when crossing a tier. +- **`new-notification`** → standard notification channel for points-related milestones. + +## Side effects + +- The referee never sees the referrer's identity unless surfaced in UI. +- `points.available` is the spendable balance; `points.total` is the lifetime accumulation (used for level tiers). +- Transactions are wrapped in a Mongo session for atomicity (`addPoints:47-88`). + +## Error / edge cases + +- **Code collision** — extremely unlikely with 32^8 ≈ 1.1 × 10¹² combinations; the while-loop in `generateReferralCode` is a hard guarantee. +- **Self-referral** — not blocked at controller level. Add a check `if (referrer._id.equals(user._id)) return` in `verifyEmailWithCode` and `googleSignUp` to prevent gaming. +- **Referral code entered with leading/trailing spaces** — `.trim()` is applied (`authController.ts:74`, `:127`). +- **Referrer deleted** — `referredBy` still points to the deleted user; the new user is effectively un-attributed. Soft-delete preservation is acceptable. +- **Points overflow** — `Number` is sufficient up to 2⁵³; no overflow risk in practice. +- **Race on level-up** — the Mongo session ensures `user.points` and `PointTransaction` are atomically updated, but two parallel `addPoints` calls might both trigger level-up emit. Idempotent in practice (frontend shows toast once). +- **`activeReferrals`** — defined in `referralStats` but no code path increments it currently. Define "active" (e.g. referee has at least one completed purchase) and update accordingly. + +> [!tip] Track conversion, not just sign-ups +> `totalReferrals` is incremented on sign-up; consider also tracking `convertedReferrals` (completed purchases) to measure real growth value. + +## Linked flows + +- [[Registration Flow]] — attribution point. +- [[Google OAuth Flow]] — also supports `referralCode`. +- [[Notification Flow]] — `referral-signup`, `level-up`, and points events surface here. +- [[Payment Flow - SHKeeper]] — completion of a purchase is the canonical trigger for awarding referral commission. + +## Source files + +- Backend: `backend/src/services/points/PointsService.ts` +- Backend: `backend/src/controllers/pointsController.ts` +- Backend: `backend/src/routes/pointsRoutes.ts` +- Backend: `backend/src/models/PointTransaction.ts` +- Backend: `backend/src/models/LevelConfig.ts` +- Backend: `backend/src/services/auth/authController.ts:411-433` (referral attribution on email signup) +- Backend: `backend/src/services/auth/authController.ts:817-838` (referral on Google signup) +- Backend: `backend/src/app.ts:274-278` (short-URL redirect) +- Frontend: `/dashboard/account/referrals` view (see `frontend/src/sections/account/`) diff --git a/04 - Flows/Registration Flow.md b/04 - Flows/Registration Flow.md new file mode 100644 index 0000000..2715739 --- /dev/null +++ b/04 - Flows/Registration Flow.md @@ -0,0 +1,195 @@ +--- +title: Registration Flow +tags: [flow, auth, signup, email-verification, referral] +related_models: ["[[User]]", "[[TempVerification]]"] +related_apis: ["POST /api/auth/register", "POST /api/auth/verify-email-code", "POST /api/auth/resend-verification"] +--- + +# Registration Flow + +End-to-end specification for **email + password** registration with role selection (buyer/seller), six-digit email verification, optional referral code attribution, and terms acceptance. + +## Actors + +- **Prospective User** – submits the sign-up form. +- **Frontend** – `frontend/src/auth/view/jwt/jwt-sign-up-view.tsx`, calling `signUp()` and later `verifyEmailWithCode()` from `frontend/src/auth/context/jwt/action.ts`. +- **Backend** – `AuthController.register` and `AuthController.verifyEmailWithCode` in `backend/src/services/auth/authController.ts`. +- **MongoDB** – `TempVerification` collection (temporary), then `User` collection (final). +- **Email service** – `backend/src/services/email/emailService.ts` (SMTP/transactional provider) — `sendVerificationCodeEmail()`. +- **Socket.IO** – emits `referral-signup` to the referrer if a referral code is supplied. + +## Preconditions + +- The email is not already a verified `User`. If a `TempVerification` already exists, its code and metadata are **regenerated and resent** rather than throwing a conflict. +- Outbound SMTP credentials are configured (`EMAIL_*` env vars consumed by `emailService.ts`). +- If a `referralCode` is supplied, it does **not** need to exist for sign-up to succeed — invalid codes are silently ignored at verification time. + +## State machine: `TempVerification → User` + +```mermaid +stateDiagram-v2 + [*] --> NotStarted + NotStarted --> TempCreated: POST /api/auth/register\nemail + role [+ ref] + TempCreated --> TempCreated: POST /api/auth/resend-verification\n(new code, 15-min TTL) + TempCreated --> TempExpired: 15 minutes elapse\nor verification fails + TempExpired --> TempCreated: User clicks "Resend" + TempCreated --> UserActive: POST /api/auth/verify-email-code\n(code + password) + UserActive --> [*] + note right of TempCreated + TempVerification document holds: + email, firstName, lastName, role, + referralCode, code, codeExpires + end note + note right of UserActive + User created with isEmailVerified=true, + status="active"; tokens issued immediately. + end note +``` + +## Step-by-step narrative + +### Phase 1 — Submit registration + +1. **User visits `/auth/jwt/sign-up`** (optionally with `?ref=ABCD1234` from the short-URL referral redirect implemented at `backend/src/app.ts:274-278`). +2. **User selects role** (buyer or seller), enters email, password (held in client state only), accepts the Terms checkbox, and clicks "Create account". + +> [!tip] Password is **not** sent to `/register` +> The password is only included in the second step (`/verify-email-code`). The intent: never hash and store a password for an unverified account. The TempVerification document carries `password: ''` until verification. + +3. **HTTP request**: `POST /api/auth/register` with `{ email, password?, firstName?, lastName?, role, referralCode? }`. (The frontend currently passes the password through, but the controller stores `''` regardless — see `authController.ts:123`.) +4. **Validation middleware** `registerValidation` (`authValidation.ts`) checks email format, password complexity, and role enum. +5. **Duplicate check** (`authController.ts:55-64`): `User.findOne({ email })` — if found, returns `409 USER_EXISTS`. +6. **Idempotent temp record**: `TempVerification.findOne({ email })` — if present, the existing temp is **updated in place** (new name, role, referralCode, fresh 6-digit code, expiry pushed to now + 15 min). +7. **Verification code**: `authService.generateVerificationCode()` (`authService.ts:226-228`) returns a uniformly random 6-digit string. +8. **Persistence**: A new `TempVerification` is saved with `{ email, password: '', firstName: defaults to "کاربر", lastName: defaults to "جدید", role, referralCode, emailVerificationCode, emailVerificationCodeExpires }`. +9. **Email dispatch**: `emailService.sendVerificationCodeEmail(email, firstName, code)` is called. The email contains the 6-digit code, branding, and a 15-minute expiry notice. Failure to send is logged but the response still succeeds with `201` (the user can resend). +10. **Response**: `{ email, message: "Verification code sent to email" }` with HTTP `201` for first-time, `200` for resend. +11. **Frontend** transitions to the OTP screen `/auth/jwt/verify?email=...` (`frontend/src/auth/view/jwt/jwt-verify-view.tsx`). + +### Phase 2 — Verify code and finalise + +12. **User enters the 6-digit code** and confirms the password. The password may be re-entered here for safety. +13. **HTTP request**: `POST /api/auth/verify-email-code` with `{ email, code, password }`. +14. **Format guard**: `authService.isValidVerificationCode(code)` enforces `/^\d{6}$/` (`authService.ts:236-238`). +15. **Lookup**: `TempVerification.findOne({ email, emailVerificationCode: code, emailVerificationCodeExpires: { $gt: now } })` — if any field mismatches or the code is older than 15 minutes, returns `400`. +16. **Hash password**: `bcrypt.hash(password, 12)` via `authService.hashPassword()`. +17. **Create `User`** (`authController.ts:400-435`): `email`, `password: hashedPassword`, `firstName`, `lastName`, `role`, `isEmailVerified: true`, `status: "active"`. +18. **Apply referral** (`authController.ts:411-433`): if `tempVerification.referralCode` exists, find the referrer by `User.findOne({ referralCode })`. If found: + - `user.referredBy = referrer._id` + - `referrer.referralStats.totalReferrals += 1` + - Emit `referral-signup` on `user-${referrer._id}` Socket.IO room — see [[Referral Flow]] for the points-awarding side effect that happens later on the first purchase. +19. **Persist user**, then **delete** the TempVerification document (`findByIdAndDelete`). +20. **Token issuance**: identical to [[Authentication Flow]] — generate access + refresh, push the refresh into `user.refreshTokens[]`. +21. **Response**: `{ user, tokens: { accessToken, refreshToken } }`. Frontend writes both into `localStorage` (`action.ts:228-235`) and routes the user into the appropriate dashboard (`/dashboard/buyer` or `/dashboard/seller`). + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor U as User + participant FE as Frontend + participant BE as Backend + participant DB as MongoDB + participant MAIL as Email Service + participant IO as Socket.IO + + U->>FE: Fill sign-up form (email, role, ref?, password) + FE->>BE: POST /api/auth/register + BE->>DB: User.findOne({ email }) + DB-->>BE: null + BE->>DB: TempVerification.findOne({ email }) + DB-->>BE: null + BE->>BE: code = generateVerificationCode() + BE->>DB: TempVerification.create({...code, expires=+15m}) + BE->>MAIL: sendVerificationCodeEmail(email, firstName, code) + MAIL-->>U: Email with 6-digit code + BE-->>FE: 201 { email, message } + FE-->>U: Redirect /auth/jwt/verify + + U->>FE: Enter code + (re)password + FE->>BE: POST /api/auth/verify-email-code { email, code, password } + BE->>DB: TempVerification.findOne({ email, code, expires>now }) + DB-->>BE: tempVerification doc + BE->>BE: hashPassword(password) + BE->>DB: User.create({...isEmailVerified:true, status:active}) + opt referral present + BE->>DB: User.findOne({ referralCode }) + DB-->>BE: referrer + BE->>DB: referrer.referralStats.totalReferrals += 1 + BE->>IO: emit user-{refId} 'referral-signup' + end + BE->>DB: TempVerification.findByIdAndDelete(...) + BE->>BE: generate tokens; push refresh + BE-->>FE: 200 { user, tokens } + FE->>FE: localStorage.setItem(accessToken, refreshToken) + FE-->>U: Redirect /dashboard/{role} +``` + +## API calls + +| Method | Endpoint | Source | +|---|---|---| +| `POST` | `/api/auth/register` | `authRoutes.ts:21` → `authController.register` | +| `POST` | `/api/auth/verify-email-code` | `authRoutes.ts:34` → `authController.verifyEmailWithCode` | +| `POST` | `/api/auth/resend-verification` | `authRoutes.ts:36-40` → `authController.resendVerificationEmail` | +| `GET` | `/r/:code` | `app.ts:274-278` — short-URL redirect that injects `?ref=` into the sign-up page | +| `POST` | `/api/auth/force-verify-user` | Dev-only — `authController.forceVerifyUser` (rejects outside `NODE_ENV=development`) | + +## Database writes + +- **`tempverifications` collection**: insert on first POST, in-place update on duplicate POST (`authController.ts:66-108`), delete on successful verification. +- **`users` collection**: full insert on successful verification (`authController.ts:400-435`). The first refresh token is appended in the same save. +- **`users` collection (referrer)**: `referralStats.totalReferrals` incremented (`authController.ts:419`). + +## Socket events emitted + +- **`referral-signup`** → `user-${referrerId}` room when a referred user verifies. Payload: + ``` + { userId, userName, userEmail, timestamp, totalReferrals } + ``` + Source: `authController.ts:423-431`. + +## Side effects + +- **Email**: one transactional message per `/register` and per `/resend-verification`. Content is generated by `emailService.sendVerificationCodeEmail`. Plain-text fallback included. +- **Sentry**: errors during `User.create` or email dispatch are captured server-side. +- **Logs**: the controller `console.log`s the generated code in **all environments** (`authController.ts:88`, `:117`, `:518`). Useful in dev; in prod the same log line ends up in CloudWatch/Sentry breadcrumbs. (Tracked as a hardening item.) + +> [!warning] Verification code is logged server-side +> The generated 6-digit code is `console.log`-ed by the controller even in production. Anyone with log access can take over an unverified account. Move behind `if (NODE_ENV !== 'production')`. + +## Error / edge cases + +- **Email already registered (verified)** → `409 USER_EXISTS`. +- **Email already in temp (unverified)** → `200`, code regenerated, email re-sent. User-friendly; no error. +- **Code mismatch / expired (>15 min)** → `400 Invalid or expired verification code`. The TempVerification is **not** deleted, so the user can request a new code via "Resend". +- **Code format wrong (non-digits or wrong length)** → `400` from `isValidVerificationCode` guard before DB lookup. +- **Email delivery failure** → response still `201`/`200`; the user can hit "Resend" or check spam. +- **Referral code that does not match any user** → silently ignored; the user is still created with `referredBy: undefined`. +- **Race condition: two parallel registrations for the same email** → MongoDB unique index on `User.email` ensures only one user document; the loser of the race sees `E11000` and returns `409 USER_EXISTS`. +- **Race condition: verify request arrives twice with the same code** → second request finds no TempVerification and returns `400`. The created `User` is the canonical record. +- **Role tampering** → role is validated by `registerValidation` enum (`buyer | seller`). Admin role is created only via the bootstrap seed (`initializeAdminUser` in `app.ts:377`), never via this flow. + +## Defaults & quirks + +- `firstName` / `lastName` are not required by the frontend in many sign-up variants; the controller defaults them to Persian placeholders `"کاربر"` / `"جدید"` (`authController.ts:52-53`). They can be edited later under `/dashboard/account/profile`. +- The TempVerification TTL is enforced by the `emailVerificationCodeExpires` check, not by a Mongo TTL index — expired docs remain in the collection until overwritten or manually purged. + +## Linked flows + +- [[Authentication Flow]] — the next time the user signs in. +- [[Referral Flow]] — full points-awarding mechanics triggered here. +- [[Google OAuth Flow]] — alternative path that bypasses `TempVerification` (Google identities are pre-verified). +- [[Password Reset Flow]] — if the user forgets the password they set during verification. + +## Source files + +- Backend: `backend/src/services/auth/authController.ts:33-158` (register), `:364-469` (verify), `:498-539` (resend) +- Backend: `backend/src/services/auth/authValidation.ts` (validation rules) +- Backend: `backend/src/models/TempVerification.ts` (temp schema) +- Backend: `backend/src/services/email/emailService.ts` (`sendVerificationCodeEmail`) +- Backend: `backend/src/app.ts:274-278` (short referral redirect) +- Frontend: `frontend/src/auth/view/jwt/jwt-sign-up-view.tsx` +- Frontend: `frontend/src/auth/view/jwt/jwt-verify-view.tsx` +- Frontend: `frontend/src/auth/context/jwt/action.ts:121-256` diff --git a/04 - Flows/Seller Offer Flow.md b/04 - Flows/Seller Offer Flow.md new file mode 100644 index 0000000..cf62e51 --- /dev/null +++ b/04 - Flows/Seller Offer Flow.md @@ -0,0 +1,197 @@ +--- +title: Seller Offer Flow +tags: [flow, marketplace, seller, offer] +related_models: ["[[SellerOffer]]", "[[PurchaseRequest]]", "[[Notification]]"] +related_apis: ["POST /api/marketplace/offers", "GET /api/marketplace/offers/request/:requestId", "PATCH /api/marketplace/offers/:id"] +--- + +# Seller Offer Flow + +A **seller** browses open purchase requests and submits an offer with a price, delivery time, and notes. The buyer is notified in real time and can accept (which moves the request to [[Payment Flow - SHKeeper]]) or reject. + +## Actors + +- **Seller** — proposes an offer. +- **Buyer** — receives the offer in their request detail view. +- **Frontend (seller)** — `frontend/src/sections/request/components/seller-steps/step-1-send-proposal.tsx` and the seller marketplace listing under `frontend/src/app/dashboard/seller/marketplace/`. +- **Frontend (buyer)** — `frontend/src/sections/request/components/buyer-steps/step-3-select-and-pay.tsx` for the offer chooser. +- **Backend** — `SellerOfferService` (`backend/src/services/marketplace/SellerOfferService.ts`) and controller routes. +- **MongoDB** — `selleroffers` collection. +- **Socket.IO** — `seller-offer-update` and `purchase-request-update` events. +- **Notification service** — buyer notifications. + +## Preconditions + +- Seller is authenticated, `role === "seller"`, `status === "active"`. +- Target purchase request exists and `status` is `pending` or `received_offers` (`SellerOfferService.ts:83-85`). +- Seller does **not** already have an offer on this request (uniqueness enforced by `SellerOfferService.createOffer`). + +## Offer state machine + +```mermaid +stateDiagram-v2 + [*] --> pending: createOffer() + pending --> active: (optional — manual seller activation) + pending --> withdrawn: seller withdraws (only while pending) + pending --> rejected: another offer accepted\nor buyer rejects this one + pending --> accepted: acceptOffer()\nor SHKeeper PAID webhook + accepted --> [*] + rejected --> [*] + withdrawn --> [*] + pending --> expired_via_withdrawn: validUntil < now\n→ markExpiredOffersAsWithdrawn (cron) +``` + +The active enum values are `pending | accepted | rejected | withdrawn` (`SellerOfferService.ts:308`). `validUntil` expirations are converted to `withdrawn`. + +## Step-by-step narrative + +### Discovery + +1. Seller opens `/dashboard/seller/marketplace`. The page hits `GET /api/marketplace/purchase-requests?sellerId={me}` and renders cards. +2. Filtering rules: only `pending` or `received_offers` requests where the seller is public-eligible or in `preferredSellerIds` (see visibility table in [[Purchase Request Flow]]). +3. Clicking a card navigates to `/dashboard/seller/marketplace/request/{id}`; the seller sees the buyer's description, attachments, address city/region (full address withheld), and existing offers if visible. + +### Submission + +4. Seller clicks "Send proposal" — opens `step-1-send-proposal.tsx`. Fields: + - **Title** (defaults to a derivative of the request title) + - **Description / notes** + - **Price** (amount + currency, default USDT) + - **Delivery time** (amount + unit: hours / days / weeks) + - **Attachments** (optional, via `POST /api/files/upload`) + - **Valid until** (optional expiry) +5. Frontend POSTs `POST /api/marketplace/offers`. +6. Backend `SellerOfferService.createOffer` (`:51-140`): + - **Uniqueness**: `SellerOffer.findOne({ purchaseRequestId, sellerId })` — if present, throws `"شما قبلاً برای این درخواست پیشنهاد داده‌اید"` (`:74`). Use `updateOffer` to amend. + - **Status guard**: loads the `PurchaseRequest`; rejects if its status is anything other than `pending` or `received_offers`. + - Saves the offer (`status: "pending"` by default in the schema). + - Re-loads with `.populate('sellerId').populate('purchaseRequestId')` for the response. +7. **Real-time fan-out** (`emitOfferUpdate`, `:24-46`): emits `seller-offer-update` to `seller-{sellerId}` so the seller's other tabs reflect the new offer instantly. +8. **Status auto-progression**: if this is the **first** offer on a `pending` request, the request transitions to `received_offers` (`:107-122`). +9. **Buyer notification**: `notificationService.notifyNewOfferReceived(buyerId, requestId, requestTitle, sellerName)` writes a `Notification` and emits to `user-{buyerId}`. +10. Response: `200 { offer }` (populated). + +### Buyer review + +11. Buyer's request detail page (`/dashboard/buyer/requests/{id}`) joins `GET /api/marketplace/offers/request/{requestId}` — `SellerOfferService.getOffersByPurchaseRequest` returns all offers sorted by `createdAt: -1`. +12. Each offer card shows seller name, avatar, rating (from [[Rating Flow]]), price, ETA, notes. +13. Buyer either **negotiates** (opens chat → [[Negotiation Flow]]) or **accepts** the offer by triggering payment. + +### Accept → Payment + +14. The buyer's "Pay this offer" button kicks off [[Payment Flow - SHKeeper]] with `purchaseRequestId` and `sellerOfferId`. The offer is **not** immediately marked `accepted`; the SHKeeper webhook does that atomically when the on-chain payment is confirmed. +15. On `PAID`/`OVERPAID` webhook (see `backend/src/services/payment/shkeeper/shkeeperWebhook.ts:573-714`): + - The selected offer's `status` → `accepted`. + - All other offers on the same request → `rejected` via `SellerOffer.updateMany`. + - The purchase request: `status = "payment"`, `selectedOfferId = sellerOfferId`. + - A direct chat is created (see [[Chat Flow]]). + - Notifications: `notifyOfferAccepted` to the winning seller, generic rejection notifications to the others (`SellerOfferService.acceptOffer` does the same in the manual path). + - Socket events: `seller-offer-update` `payment-completed` to the winner, `seller-offer-update` `offer-rejected` to losers (`shkeeperWebhook.ts:679-705`). + +### Withdrawal + +16. Seller can withdraw their `pending` offer from `/dashboard/seller/marketplace/offers/{offerId}` → `withdrawOffer` (`SellerOfferService.ts:428-443`). The DB filter `{ status: 'pending' }` means withdrawal is impossible once `accepted` or `rejected`. + +## Sequence diagram + +```mermaid +sequenceDiagram + autonumber + actor S as Seller + actor B as Buyer + participant FE_S as Frontend (seller) + participant FE_B as Frontend (buyer) + participant BE as Backend + participant DB as MongoDB + participant N as NotificationService + participant IO as Socket.IO + + S->>FE_S: Browse /dashboard/seller/marketplace + FE_S->>BE: GET /api/marketplace/purchase-requests?sellerId + BE-->>FE_S: filtered list + S->>FE_S: Open request, click "Send proposal" + S->>FE_S: Fill price, ETA, notes; submit + FE_S->>BE: POST /api/marketplace/offers + BE->>DB: ensure no existing offer; check status + BE->>DB: SellerOffer.create({status:"pending"}) + opt first offer on the request + BE->>DB: PurchaseRequest.status = "received_offers" + end + BE->>N: notifyNewOfferReceived(buyer, requestId, sellerName) + N->>IO: emit user-{buyer} new-notification + BE->>IO: emit seller-{sellerId} 'new-offer' + BE-->>FE_S: 200 { offer } + IO-->>FE_B: new-notification (buyer's bell icon) + B->>FE_B: Open request detail + FE_B->>BE: GET /api/marketplace/offers/request/{id} + BE-->>FE_B: offers[] + alt Buyer accepts via payment + B->>FE_B: Click "Pay" → starts [[Payment Flow - SHKeeper]] + Note over BE,DB: SHKeeper webhook PAID arrives later;
winning offer → accepted, others → rejected + else Buyer negotiates + B->>FE_B: Open chat → [[Negotiation Flow]] + end +``` + +## API calls + +| Method | Endpoint | Purpose | +|---|---|---| +| `POST` | `/api/marketplace/offers` | Create offer | +| `GET` | `/api/marketplace/offers/request/:requestId` | Buyer view of offers on a request | +| `GET` | `/api/marketplace/offers/seller/:sellerId` | Seller's own offer history | +| `GET` | `/api/marketplace/offers/:id` | Single offer details | +| `PATCH` | `/api/marketplace/offers/:id` | Update price / ETA / notes (seller, while pending) | +| `POST` | `/api/marketplace/offers/:id/accept` | Manual acceptance (when not via webhook) | +| `POST` | `/api/marketplace/offers/:id/withdraw` | Seller withdraws | + +## Database writes + +- **`selleroffers`**: insert on create; update on accept/reject/withdraw; `updateMany` to bulk-reject other offers when one is accepted (`SellerOfferService.acceptOffer:376-388`). +- **`purchaserequests`**: status moves to `received_offers` on first offer, then `payment` on successful payment, and `selectedOfferId` is set. +- **`notifications`**: at least one per state change. + +## Socket events emitted + +- **`seller-offer-update`** with `eventType: 'new-offer'` → `seller-{sellerId}` (creator's other tabs). +- **`purchase-request-update`** with `eventType: 'offer-updated'` → `request-{requestId}` on edits (`SellerOfferService.ts:284-288`). +- **`seller-offer-update`** with `eventType: 'payment-completed'` to winning seller, `'offer-rejected'` to losers (emitted by the webhook handler). +- **`new-notification`** → `user-{buyerId}` for each new offer. + +## Side effects + +- Triggers chat creation **only after payment** (not on offer creation) — minimises noise from speculative offers. +- The `rejectionReason` field is set to `"Another offer was accepted by buyer"` for losers (`SellerOfferService.ts:387`). +- Seller statistics aggregate (`getOfferStatistics`, `:446-475`) is computed on demand for dashboards; no denormalised counter on the user document. + +## Error / edge cases + +- **Duplicate offer by same seller** → `400` with localized error. Use `updateOffer` instead. +- **Request status not open** → `400 "این درخواست دیگر برای پیشنهاد باز نیست"` (`:84`). +- **Price = 0 or negative** → Mongoose validator on `SellerOffer.price.amount` rejects (`SellerOfferService.ts:55-60` logs the validation state). +- **Seller withdraws an `accepted` offer** → blocked by the `{ status: 'pending' }` filter; returns `null`. +- **`validUntil` in the past at creation** → schema-level validator should reject; otherwise the next `markExpiredOffersAsWithdrawn` cron run flips it to `withdrawn`. +- **Race condition: two payments to two different offers** → unlikely (frontend disables payment buttons once one is chosen); even if both arrive, the SHKeeper webhook coordinator (`PaymentCoordinator`) is idempotent and the first PAID wins. +- **Offer for a deleted request** → orphan; the webhook handler logs `"Purchase request not found"` and continues. Periodic cleanup should remove orphans. + +> [!tip] Real-time UX +> Sellers should `socket.emit('join-seller-room', myUserId)` on dashboard mount so they see `seller-offer-update` events instantly when the buyer accepts/rejects. + +## Linked flows + +- [[Purchase Request Flow]] — produces the requests sellers offer on. +- [[Negotiation Flow]] — counter-offer in `in_negotiation`. +- [[Payment Flow - SHKeeper]] — locks in the accepted offer. +- [[Chat Flow]] — direct chat opened after payment. +- [[Notification Flow]] — channels for offer events. +- [[Rating Flow]] — seller's average rating displayed in the offer card. + +## Source files + +- Backend: `backend/src/services/marketplace/SellerOfferService.ts` +- Backend: `backend/src/services/marketplace/marketplaceController.ts` +- Backend: `backend/src/models/SellerOffer.ts` +- Backend: `backend/src/services/payment/shkeeper/shkeeperWebhook.ts:573-714` (acceptance via webhook) +- Frontend: `frontend/src/sections/request/components/seller-steps/step-1-send-proposal.tsx` +- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-select-and-pay.tsx` +- Frontend: `frontend/src/app/dashboard/seller/marketplace/` diff --git a/05 - Design System/Colors.md b/05 - Design System/Colors.md new file mode 100644 index 0000000..74b8a76 --- /dev/null +++ b/05 - Design System/Colors.md @@ -0,0 +1,207 @@ +--- +title: Colors +tags: [design-system, colors, palette] +created: 2026-05-23 +--- + +# Colors + +The palette is built from semantic groups (`primary`, `secondary`, `info`, `success`, `warning`, `error`, plus a 9-step `grey` scale) and exposed via the MUI theme. **Never hard-code hex values in components.** + +> [!warning] +> Hardcoded colors break dark mode and any future preset switch. Use `sx={{ color: 'primary.main' }}` or `theme.palette.primary.main`. + +--- + +## 1. Semantic groups + +Every color group has the shape: + +```ts +{ + lighter: '#…', // tint background, alerts + light: '#…', // hover surfaces, badges + main: '#…', // primary use (buttons, links) + dark: '#…', // hover state of `main` + darker: '#…', // pressed state + contrastText: '#…', // WCAG AA on `main` +} +``` + +### 1.1 Primary + +| Token | Light hex (default preset) | Dark hex | +|---|---|---| +| `primary.lighter` | `#D0ECFE` | `#CCF4FE` | +| `primary.light` | `#73BAFB` | `#68CDF9` | +| `primary.main` | `#1877F2` | `#078DEE` | +| `primary.dark` | `#0C44AE` | `#0351AB` | +| `primary.darker` | `#042174` | `#012972` | +| `primary.contrastText` | `#FFFFFF` | `#FFFFFF` | + +Used for primary actions, links, focused inputs. + +### 1.2 Secondary + +Typical: a complementary purple/teal. Confirm by reading `theme/options/palette.ts`. + +### 1.3 Info / Success / Warning / Error + +Standard semantic colors: + +| Group | Hue | Used for | +|---|---|---| +| `info` | cyan/blue | Neutral informative banners | +| `success` | green | Confirmed payments, accepted offers | +| `warning` | amber | Pending state, mild alerts | +| `error` | red | Disputes, validation errors, destructive | + +Each follows the same `lighter/light/main/dark/darker/contrastText` shape. + +### 1.4 Grey + +Nine-step scale (100 → 900). Use for: + +- `grey.100`–`grey.300` → backgrounds, dividers, disabled +- `grey.500` → secondary text, muted icons +- `grey.700`–`grey.900` → primary text, dark headings + +### 1.5 Text & Background + +```ts +text: { + primary: grey[800], // body + secondary: grey[600], // muted + disabled: grey[500], +}, +background: { + default: '#FFFFFF', // light mode page + paper: '#FFFFFF', // cards, dialogs + neutral: grey[200], // page wash +}, +``` + +In dark mode these invert (`default` becomes near-black, `paper` slightly lighter, `text.primary` near-white). + +--- + +## 2. Action states + +```ts +action: { + active: alpha(grey[600], 1), + hover: alpha(grey[500], 0.08), + selected: alpha(grey[500], 0.16), + disabled: alpha(grey[500], 0.8), + disabledBackground: alpha(grey[500], 0.24), + focus: alpha(grey[500], 0.24), + hoverOpacity: 0.08, + disabledOpacity: 0.48, +} +``` + +Use via `sx={{ bgcolor: 'action.hover' }}` etc. + +--- + +## 3. Color presets + +The settings drawer ships several **color presets** that swap only `primary` (and optionally `secondary`) while keeping every other group stable. Typical presets (confirm in `theme/options/presets/`): + +| Preset | Primary hue | Vibe | +|---|---|---| +| `default` | Blue | Trust, finance | +| `purple` | Indigo/Purple | Premium | +| `cyan` | Cyan | Tech, fresh | +| `blue` | Vibrant blue | Strong CTA | +| `orange` | Orange | Energetic | +| `red` | Brand red | Bold | + +To add a preset: + +1. Add a `.ts` file in `theme/options/presets/` exporting `{ lighter, light, main, dark, darker, contrastText }`. +2. Register in the presets index map. +3. Surface in the settings drawer color picker (`src/settings/drawer/`). + +--- + +## 4. Contrast verification + +WCAG AA targets: + +| Element | Min contrast | +|---|---| +| Body text (≥18 px regular or ≥14 px bold) | **4.5 : 1** | +| Large text (≥24 px regular or ≥18 px bold) | **3 : 1** | +| Icon-only buttons | **3 : 1** for their boundary | +| Form input borders | **3 : 1** | + +> [!tip] +> When picking a new `main` color, run it through [Stark](https://stark.co/) or the Chrome DevTools "Inspect color" checker against `background.paper` AND `background.default` for both light and dark modes. + +--- + +## 5. Using colors in `sx` + +```tsx +// Token-based — preferred + + +// Function form when you need to derive + ({ bgcolor: alpha(theme.palette.primary.main, 0.08) })} /> + +// In styled component +const Item = styled('div')(({ theme }) => ({ + background: theme.palette.background.paper, + color: theme.palette.text.primary, +})); +``` + +--- + +## 6. Charts & data viz + +Use the helper `theme.palette..main` array for series colors. The `chart/` component family (ApexCharts wrapper) reads these tokens so the chart palette stays in sync with the theme. + +For categorical data with >7 series, layer in `secondary.main`, `info.main`, `success.main`, `warning.main`, `error.main`, then darker variants — never invent ad-hoc hex. + +--- + +## 7. Diff visualisation (chat, evidence) + +| Concept | Color token | +|---|---| +| Added / accepted | `success.main` with `success.lighter` background | +| Removed / rejected | `error.main` with `error.lighter` background | +| Pending review | `warning.main` | +| Informational | `info.main` | + +--- + +## 8. Avatar backgrounds + +`MuiAvatar` override hashes the user's name to one of the semantic `main` colors so the same person always gets the same background. Implementation in `components/user-avatar/`. + +--- + +## 9. Brand vs semantic + +| Use case | Token | +|---|---| +| Primary CTA button | `primary.main` | +| Success snackbar | `success.main` | +| Error border on form field | `error.main` | +| Page background | `background.default` | +| Card background | `background.paper` | +| Sidebar background | `background.neutral` | +| Divider line | `divider` | +| Primary text | `text.primary` | +| Muted helper text | `text.secondary` | + +--- + +## 10. Related + +- [[Theme Configuration]] · [[Design System Overview]] · [[Typography]] +- [[Components]] — components consuming these tokens +- [[Settings & Theming]] — preset switcher diff --git a/05 - Design System/Components.md b/05 - Design System/Components.md new file mode 100644 index 0000000..b9527ed --- /dev/null +++ b/05 - Design System/Components.md @@ -0,0 +1,222 @@ +--- +title: Components +tags: [design-system, components, ui] +created: 2026-05-23 +--- + +# Components + +Inventory of reusable components in `frontend/src/components/`. Each component lives in its own folder (`index.ts` + `.tsx` + `classes.ts` + `types.ts`). + +> [!info] +> Pattern: components are reusable **and free of business logic**. Page-specific composition lives in `frontend/src/sections/`. Hooks that fetch data live in `frontend/src/hooks/`. + +--- + +## 1. Form components (`components/hook-form/`) + +RHF + MUI wrappers. Used inside a `` context. All accept `name`, plus the MUI props of the underlying component. + +| Component | Wraps | Notes | +|---|---|---| +| `RHFTextField` | `TextField` | The workhorse text input | +| `RHFSelect` | `Select` + `MenuItem` | Pass `children={...}` | +| `RHFAutocomplete` | `Autocomplete` | Async-loader friendly; pass `options` | +| `RHFCheckbox` | `Checkbox` + `FormControlLabel` | Single boolean | +| `RHFMultiCheckbox` | grid of checkboxes | Returns `string[]` | +| `RHFRadioGroup` | `RadioGroup` | Returns selected value | +| `RHFSwitch` | `Switch` + label | Boolean toggle | +| `RHFEditor` | TipTap wrapper | HTML output | +| `RHFUpload` | `components/upload` Dropzone | Single or multiple | +| `RHFDatePicker` | `@mui/x-date-pickers DatePicker` | Locale-aware | +| `RHFDateTimePicker` | `DateTimePicker` | — | +| `RHFTimePicker` | `TimePicker` | — | +| `RHFPhoneInput` | `react-phone-number-input` | E.164 output | +| `RHFCountrySelect` | `components/country-select` | ISO-2 output | +| `RHFNumberInput` | `components/number-input` | Locale-formatted, accepts min/max/step | +| `RHFRating` | `Rating` | 0–5 | +| `RHFSlider` | `Slider` | Single or range | + +> [!tip] +> Form-level errors render automatically via `formState.errors[name]` — pass `helperText={errors.name?.message}` is **not needed**; the wrapper handles it. + +--- + +## 2. Data display + +| Component | Purpose | +|---|---| +| `custom-data-grid/` | MUI DataGrid Pro wrapper — sets density, locale, custom toolbar, RTL support | +| `table/` | Lower-level table utilities (head, pagination, no-data row, skeleton) | +| `chart/` | ApexCharts wrapper with theme-synced palette | +| `markdown/` | `react-markdown` + `remark-gfm` for chat/evidence rendering | +| `lightbox/` | Image gallery (gallery / dispute evidence) | +| `image/` | Next `Image` wrapper with fallback skeleton + error state | +| `file-thumbnail/` | Renders icon by MIME type for non-image files | +| `flag-icon/` | Country flag SVG (used in phone input, locale switcher) | +| `iconify/` | Iconify SVG wrapper — `` | +| `svg-color/` | Renders SVG with `color` prop (mask-image trick) | + +--- + +## 3. Navigation + +| Component | Purpose | +|---|---| +| `nav-section/` | Sidebar nav with role-filtering, group headers, mini variant | +| `nav-basic/` | Simple horizontal nav for marketing pages | +| `custom-breadcrumbs/` | Page header breadcrumbs + action slot | +| `progress-bar/` | NProgress integration for route transitions | + +The sidebar reads from a configuration array — see `src/layouts/dashboard/config-nav-dashboard.ts` (or similar) — where each entry can specify `roles: ['admin']` to hide from other users. + +--- + +## 4. Feedback + +| Component | Purpose | +|---|---| +| `snackbar/` | Notistack wrapper — `enqueueSnackbar('Done', { variant: 'success' })` | +| `loading-screen/` | Full-screen splash for boot or guarded routes | +| `empty-content/` | "No results" placeholder with illustration | +| `search-not-found/` | Variant of empty-content for failed searches | +| `email-verification-banner/` | Persistent top banner when email unverified | + +--- + +## 5. Overlays & dialogs + +| Component | Purpose | +|---|---| +| `custom-dialog/` | Modal dialog with title/content/actions slots | +| `custom-popover/` | Click-or-hover popover (used for menus, tooltips, info) | + +--- + +## 6. Inputs (raw, non-RHF) + +| Component | Purpose | +|---|---| +| `number-input/` | Numeric with +/- buttons + locale format | +| `phone-input/` | Phone with country selector | +| `country-select/` | Country dropdown | +| `incrementer-button/` | Tiny `−` / `+` quantity stepper | +| `upload/` | Dropzone with progress, preview, removal | + +--- + +## 7. User & profile + +| Component | Purpose | +|---|---| +| `user-avatar/` | Hashes name → color; respects `src` if avatar uploaded | +| `user-profile-card/` | Profile summary card (name, role, level, points) | +| `notifications-drawer/` | Side drawer listing notifications, mark-as-read | + +--- + +## 8. Media + +| Component | Purpose | +|---|---| +| `video-player/` | Plyr / video.js wrapper for blog & template videos | +| `map/` | Mapbox + react-map-gl wrapper (delivery addresses) | +| `carousel/` | Embla carousel — multiple variants in `hooks/use-carousel-*` | + +--- + +## 9. Animation + +| Component | Purpose | +|---|---| +| `animate/` | Framer-motion presets (fade, slide, scale) for page transitions and lists | + +--- + +## 10. Utility + +| Component | Purpose | +|---|---| +| `scrollbar/` | SimpleBar wrapper — themed scrollbar with shadow on overflow | +| `label/` | Tag/badge — variants: filled, outlined, soft | +| `filters-result/` | Active filter chips with × to remove | +| `logo/` | App logo SVG (responsive: full mark + icon-only at small width) | +| `color-utils/` | Color manipulation helpers (alpha, darken, contrast) | + +--- + +## 11. Settings + +| Component | Purpose | +|---|---| +| `settings/` | The mounted Settings drawer + provider hooks | + +--- + +## 12. Debug + +| Component | Purpose | +|---|---| +| `debug/` | Dev-only diagnostic overlays (only rendered when `NEXT_PUBLIC_ENABLE_DEBUG=true`) | + +--- + +## 13. Editor (`components/editor/`) + +TipTap rich-text editor with this extension set: + +- `StarterKit` (paragraph, headings, bold/italic, lists, blockquote, history) +- `Link` (URL parsing, target=_blank by default for external) +- `Image` (drag-drop, uploads via `components/upload`) +- `Underline`, `TextAlign` +- `CodeBlock` + `CodeBlockLowlight` (syntax highlighting via `lowlight`) +- `Placeholder` + +Output: HTML string. Round-trip safe with `react-markdown` + `turndown` when Markdown storage is preferred. + +Used in: +- Blog post editor (`dashboard/post/new`) +- Long-form description fields (request, template) +- Dispute evidence narrative + +--- + +## 14. Payment-specific (`components/payment/`) + +| Component | Purpose | +|---|---| +| `` | USDT / USDC / BTC / ETH chip selector | +| `` | BSC / TRC20 / Ethereum / Polygon chip selector | +| `` | Renders the SHKeeper invoice QR + copy-address button | +| `` | Live status pill that updates from socket events | + +(Confirm names against actual exports — these are typical for this stack.) + +--- + +## 15. Adding a new component + +1. Create `src/components//` with `index.ts`, `.tsx`, `classes.ts`, `types.ts`. +2. Re-export from `src/components//index.ts`: `export * from './'`. +3. Place ONLY rendering + styling; do NOT call `useQuery` / `useMutation` — pass data via props. +4. Use the `sx` prop, not styled-components, unless the styling is non-trivial. +5. Cover with at least one Jest snapshot test in `__tests__/`. + +--- + +## 16. Component vs Section vs Hook decision + +| Question | Where it goes | +|---|---| +| "I want to render a chip with a delete icon." | `components/` | +| "I want a chat sidebar with conversation list + active chat + composer." | `sections/chat/` | +| "I want to subscribe to chat updates and keep the cache in sync." | `hooks/use-chat-socket.ts` | + +--- + +## 17. Related + +- [[Design System Overview]] · [[Theme Configuration]] · [[Colors]] · [[Typography]] +- [[Layouts]] · [[Internationalization & RTL]] +- [[Frontend Architecture]] — where these components live in the bigger picture +- [[Coding Standards]] — file layout & import order rules diff --git a/05 - Design System/Design System Overview.md b/05 - Design System/Design System Overview.md new file mode 100644 index 0000000..98e3c1c --- /dev/null +++ b/05 - Design System/Design System Overview.md @@ -0,0 +1,179 @@ +--- +title: Design System Overview +tags: [design-system, ui, mui] +created: 2026-05-23 +--- + +# Design System Overview + +The frontend design system is built on **Material-UI v7** with project-specific tokens, an LTR + RTL-aware emotion cache, and a user-controllable settings drawer (mode, layout, color preset, font, direction). + +> [!info] +> All design decisions documented here are enforced by the cursor rules at `backend/.cursor/rules/ui-development-standards.mdc`. When in doubt, that file is the authoritative source. + +--- + +## 1. Philosophy + +- **Material first, brand second.** Use MUI components and the `sx` prop with array syntax for responsive overrides. Do NOT subclass primitives unless absolutely necessary. +- **Tokens over magic numbers.** All colors, spacings, radii, and shadows come from the theme — never hard-code hex values or pixel offsets. +- **RTL-first.** Persian and Arabic are first-class. Layouts must mirror correctly without manual `direction` overrides. +- **Accessibility-by-default.** WCAG AA contrast (4.5:1), semantic HTML, keyboard navigation, ARIA labels on icon-only buttons. +- **One component, one folder.** Each component lives in `components//` with `index.ts` + `component.tsx` + `classes.ts` + `types.ts` so it can be refactored without grep-and-replace pain. +- **Compose, don't inherit.** Sections compose components, components compose primitives, primitives wrap MUI. + +--- + +## 2. Component layering + +```mermaid +flowchart TB + P[Pages — src/app/] + S[Sections — src/sections/] + C[Components — src/components/] + L[Layouts — src/layouts/] + H[Hooks — src/hooks/] + T[Theme — src/theme/] + M[MUI v7] + + P --> S + P --> L + S --> C + C --> M + L --> C + L --> M + C --> H + S --> H + M --> T +``` + +- **Pages** are thin — they mount a section and pass URL params. +- **Sections** are page-specific compositions of components + hooks + queries. +- **Components** are reusable, free of business logic. +- **Layouts** wrap pages with sidebar / topbar / auth scaffolding. + +--- + +## 3. Token system + +The theme exposes these token groups (see [[Theme Configuration]] for exact values): + +| Group | Examples | Used as | +|---|---|---| +| **Palette** | `primary.main`, `error.dark`, `grey.500`, `background.paper` | `sx={{ color: 'primary.main' }}` | +| **Typography** | `h1`-`h6`, `subtitle1/2`, `body1/2`, `caption`, `overline`, `button` | `` | +| **Spacing** | unit = 8 px; use multiples: `spacing(1)` = 8, `spacing(2)` = 16, … | `sx={{ p: 2, mt: 3 }}` | +| **Shape** | `borderRadius` = 8 (default), 12 for cards | `sx={{ borderRadius: 1 }}` | +| **Shadows** | `shadows[1]`–`shadows[24]` + custom `customShadows` | `sx={{ boxShadow: 1 }}` | +| **Breakpoints** | `xs`/`sm`/`md`/`lg`/`xl` = 0/600/900/1200/1536 | `sx={{ flexDirection: { xs: 'column', md: 'row' } }}` | +| **Z-index** | named layers: `appBar`, `drawer`, `modal`, `snackbar`, `tooltip` | `sx={{ zIndex: 'modal' }}` | + +The unit of spacing is **8 px**. Never use `padding: '14px'` — use `sx={{ p: 1.75 }}` if you really need it (or, better, fix the value to a multiple). + +--- + +## 4. Modes + +The app supports three independent user-controllable axes: + +| Axis | Values | Persistence | +|---|---|---| +| Mode | `light` · `dark` · `system` | `localStorage.settings.mode` | +| Contrast | `default` · `bold` | `localStorage.settings.contrast` | +| Layout | `vertical` · `mini` · `horizontal` | `localStorage.settings.layout` | +| Direction | `ltr` · `rtl` | `localStorage.settings.direction` (auto-set from locale) | +| Color preset | one of N curated palettes | `localStorage.settings.colorPresets` | +| Font family | per theme defaults or overridden | `localStorage.settings.fontFamily` | + +Toggled from the **Settings Drawer** (`src/settings/drawer/`). See [[Settings & Theming]]. + +--- + +## 5. Folder structure + +``` +frontend/src/ +├── theme/ +│ ├── index.ts # createTheme + provider wiring +│ ├── options/ +│ │ ├── palette.ts # Palette tokens (light + dark) +│ │ ├── typography.ts # Type scale & families +│ │ ├── shadows.ts # Elevation shadows +│ │ ├── custom-shadows.ts # Brand-specific glow/elevation +│ │ ├── overrides/ # MUI component overrides +│ │ └── … +│ └── styles/ # Global CSS resets, scrollbar +├── settings/ +│ ├── context/ # SettingsContext provider +│ └── drawer/ # The visible drawer UI +├── components/ # ~50 reusable components (see below) +└── layouts/ + ├── auth-centered/ + ├── auth-split/ + ├── dashboard/ # sidebar + topbar + └── main/ # public marketing-style +``` + +--- + +## 6. Naming & code conventions (recap from cursor rules) + +- File names: kebab-case (`custom-data-grid.tsx`). +- Component names: PascalCase (`CustomDataGrid`). +- Hook names: `use-*.ts` exporting `useXxx`. +- Folder per component with `index.ts` barrel. +- Import order: **Styles → Side-effects → Types → External libs → MUI → Internal**. +- Forbidden: inline hex colors, `!important`, mixing `style={{}}` with `sx`. +- Icons: **Iconify only** (`eva:*`, `solar:*` prefixes preferred). + +--- + +## 7. Accessibility checklist + +- All interactive elements reachable with `Tab`. +- Focus visible (don't override default outline without replacement). +- Icon-only buttons have `aria-label`. +- Form fields linked to labels via `htmlFor` or wrapping `