--- 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"] --- > **Last updated:** 2026-05-31 — template checkout delivery/payment rail behavior added. > [!warning] Audit — 2026-05-29 > This document was corrected against the live codebase. Key changes: status enum updated (added `pending_payment`, `active`; removed undocumented `finalized`/`archived`); urgency values expanded to include `urgent`; sellers endpoint corrected; attachment upload endpoint corrected; `request-cancelled` socket event removed (non-existent); `new-purchase-request` fan-out target corrected to shared `sellers` room; socket room join/leave events documented; description minimum corrected to 5 chars; PUT vs PATCH mismatch flagged as known bug; two frontend actions hitting non-existent backend endpoints flagged as not implemented. # 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 --> active: request activated pending --> pending_payment: payment initiated pending_payment --> active: payment confirmed active --> 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: Request Network payment confirmed\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 pending --> cancelled: buyer cancels (any pre-payment status) active --> cancelled received_offers --> cancelled in_negotiation --> cancelled cancelled --> [*] completed --> [*] ``` Terminal statuses: `completed`, `cancelled` (`PurchaseRequestService.ts:28`). > [!note] Statuses `finalized` and `archived` do NOT exist in the frontend `IPurchaseRequest` type and are not live statuses. They are not part of the active state machine. ## 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 (5–2000 chars, **minimum is 5 characters** per frontend Zod schema — not 20), 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 | urgent`), preferred sellers (typeahead bound to `GET /api/marketplace/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/marketplace/purchase-requests/:id/attachments` — 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`: emits `new-purchase-request` to the shared **`sellers` room** (all connected sellers receive it in a single emit — no per-seller iteration for the socket event itself). - For per-seller in-app notifications (bell icon): `User.find({ role: "seller", status: "active" })` OR only the curated `preferredSellerIds`. - Iterates with **50 ms stagger** between notification writes to avoid overwhelming Mongo. - 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/marketplace/purchase-requests/:id/attachments 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 BE->>BE: compute isPublic BE->>DB: PurchaseRequest.create({status: "pending"}) DB-->>BE: savedRequest BE->>N: notifyPurchaseRequestCreated(buyer, requestId) par fan-out to sellers (staggered 50ms for DB writes) BE->>IO: emit 'new-purchase-request' to 'sellers' room (public requests) 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-purchase-request' (sellers room) + 'new-notification' (per-user) ``` ## API calls | Method | Endpoint | Purpose | |---|---|---| | `POST` | `/api/marketplace/purchase-requests` | Create the request | | `GET` | `/api/marketplace/categories` | Step 1 dropdown | | `GET` | `/api/marketplace/sellers` | Step 3 preferred-sellers typeahead | | `GET` | `/api/addresses` | Step 4 saved addresses | | `POST` | `/api/marketplace/purchase-requests/:id/attachments` | Attachments upload | | `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) | > [!bug] ⚠️ KNOWN BUG — PUT vs PATCH mismatch > The frontend `updatePurchaseRequest` action sends `PUT /api/marketplace/purchase-requests/:id`, but the backend only registers a `PATCH` handler for that route. The `PUT` call will receive a `404` or `405` response. The backend handler must be updated to also accept `PUT`, or the frontend action must be changed to use `PATCH`. > [!warning] ⚠️ NOT IMPLEMENTED — Frontend actions with no backend endpoints > The following frontend actions target backend routes that do not exist: > - `searchPurchaseRequests` → `GET /marketplace/purchase-requests/search` — this endpoint does not exist. Use query parameters on the standard list endpoint (`GET /api/marketplace/purchase-requests?q=...`) instead. > - `getMarketplaceStats` → `GET /marketplace/purchase-requests/stats` — this endpoint does not exist. No stats aggregation route is registered. ## Database writes - **`purchaserequests` collection**: full insert. Subsequent status transitions and `selectedOfferId` updates happen in [[Seller Offer Flow]], [[PRD - Request Network In-House Checkout]], 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-purchase-request`** → `sellers` room for public purchase requests (shared room, single broadcast; emitted by `notifyAllSellersAboutNewRequest`). - **`new-notification`** → `user-{sellerId}` for each notified seller (via `NotificationService.emitRealTimeNotification`). - **`purchase-request-update`** → `request-{id}` on status changes (`emitPurchaseRequestUpdate`, `PurchaseRequestService.ts:53-71`). Cancellation emits this event with `eventType: 'status-changed'` — there is **no** separate `request-cancelled` event. - **`seller-offer-update`** → `seller-{id}` when an offer is created against this request (see [[Seller Offer Flow]]). ### Socket room join/leave events | Event | Direction | Emitted by | |---|---|---| | `join-request-room` | client → server | Buyer detail page on mount (subscribes to `request-{id}`) | | `join-seller-room` | client → server | `useSellerMarketplaceSocket` on mount | | `leave-seller-room` | client → server | `useSellerMarketplaceSocket` on unmount | | `join-buyer-room` | client → server | Buyer socket hook on mount | | `leave-buyer-room` | client → server | Buyer socket hook on unmount | ## 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"` or `urgency === "urgent"`, 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`) and admin tools. ## Linked flows - [[RequestTemplate]] checkout — seller chooses physical vs online delivery on the template; buyer checkout collects only the required address/email details and `batch-convert` creates one [[PurchaseRequest]] per seller/template group. - [[Seller Offer Flow]] — sellers respond to the published request. - [[Negotiation Flow]] — counter-offer mechanics in `in_negotiation`. - [[PRD - Request Network In-House Checkout]] — 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`