16 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Purchase Request Flow |
|
|
|
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 undocumentedfinalized/archived); urgency values expanded to includeurgent; sellers endpoint corrected; attachment upload endpoint corrected;request-cancelledsocket event removed (non-existent);new-purchase-requestfan-out target corrected to sharedsellersroom; 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 infrontend/src/sections/request/components/request-form-wizard.tsxand uses the step files underfrontend/src/sections/request/components/steps/(basic info, details, budget, review) plusbuyer-steps/for the post-publish lifecycle. - Backend —
PurchaseRequestService.createPurchaseRequestand the marketplace controller (backend/src/services/marketplace/marketplaceController.ts). - MongoDB —
purchaserequests, with population fromusersandcategories. - Socket.IO — emits
purchase-request-updateto therequest-{id}room andseller-offer-updateto 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.
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
finalizedandarchiveddo NOT exist in the frontendIPurchaseRequesttype and are not live statuses. They are not part of the active state machine.
Step-by-step narrative
Multi-step wizard
- Buyer clicks "New request" in the dashboard sidebar and lands at
/dashboard/request/new. - 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 fromGET /api/marketplace/categories). - Step 2 — Details (
steps/request-details-step.tsx): optional product link, size, color, quantity, free-form specifications (key/value pairs), AI-assisted description generation (callsPOST /api/ai/generate-descriptionif the user clicks the magic-wand button — seebackend/src/services/ai/). - Step 3 — Budget (
steps/request-budget-step.tsx): min/max in chosen currency (default USDT), urgency (low | medium | high | urgent), preferred sellers (typeahead bound toGET /api/marketplace/sellers;"all"means public). - Step 4 — Review (
steps/request-review-step.tsx): summary; user can attach a saved address (GET /api/addresses) or enter a one-offdeliveryInfo.address. Upload optional attachments viaPOST /api/marketplace/purchase-requests/:id/attachments— returns URLs persisted intoattachments[]. - 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
- Frontend POSTs
POST /api/marketplace/purchase-requestswith the full payload (PurchaseRequestCreateDatainPurchaseRequestService.ts:73-106). - Backend
PurchaseRequestService.createPurchaseRequest(:123-188):- Duplicate-guard (
:128-143): rejects a request with identicalbuyerId,title,descriptionwithin the last 5 minutes. ReturnsError("درخواست مشابه در ۵ دقیقه گذشته ایجاد شده است"). - Sanitise
preferredSellerIds(:146-150): drops literal"all"and any invalid ObjectIds. isPublicistruewhen 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
PurchaseRequestdocument withstatus: "pending".
- Duplicate-guard (
- Notify the buyer:
notificationService.notifyPurchaseRequestCreated()fires asynchronously (noawait) — an info notification appears in the buyer's bell-icon dropdown. - Fan-out to sellers (
notifyAllSellersAboutNewRequest,:190-249):- If
isPublic: emitsnew-purchase-requestto the sharedsellersroom (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 curatedpreferredSellerIds. - Iterates with 50 ms stagger between notification writes to avoid overwhelming Mongo.
- For each seller:
notificationService.createNotification(...)writes aNotificationdoc AND emits via Socket.IO (actionUrl: /dashboard/seller/marketplace/request/{id}).
- If
- 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) andemitOfferUpdatein 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
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
updatePurchaseRequestaction sendsPUT /api/marketplace/purchase-requests/:id, but the backend only registers aPATCHhandler for that route. ThePUTcall will receive a404or405response. The backend handler must be updated to also acceptPUT, or the frontend action must be changed to usePATCH.
[!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
purchaserequestscollection: full insert. Subsequent status transitions andselectedOfferIdupdates happen in Seller Offer Flow, PRD - Request Network In-House Checkout, and Delivery Confirmation Flow.notificationscollection: one per notified seller plus one for the buyer.users.referralStatsis not touched at request creation.
Socket events emitted
new-purchase-request→sellersroom for public purchase requests (shared room, single broadcast; emitted bynotifyAllSellersAboutNewRequest).new-notification→user-{sellerId}for each notified seller (viaNotificationService.emitRealTimeNotification).purchase-request-update→request-{id}on status changes (emitPurchaseRequestUpdate,PurchaseRequestService.ts:53-71). Cancellation emits this event witheventType: 'status-changed'— there is no separaterequest-cancelledevent.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 emitsjoin-request-room). - If
urgency === "high"orurgency === "urgent", the notification message uses the high-priority template — visible in Notification Flow.
Error / edge cases
- Duplicate within 5 minutes →
400with Persian message. Prevents double-submit on flaky networks. - Invalid category ObjectId →
400from Mongoose validation. preferredSellerIdswith invalid ObjectIds → silently dropped, not an error.- Empty cleaned
preferredSellerIds+ no"all"in original →isPublicistrue(open marketplace). This is the intended fallback. - Buyer cancels after payment → blocked by
STATUS_PROGRESSION_ORDER(cannot move tocancelledfromprocessing+ 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
sellerIdquery → logs"No sellerId provided - returning ALL requests!"(:348) and returns the full set. Frontend always suppliessellerIdfor seller dashboards; missing it is a bug worth catching.
[!tip] Status progression is forward-only Once
statusreachespayment, you cannot put it back toreceived_offerseven 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-convertcreates 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