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.
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 (20–2000 chars), category selection (dropdown populated from GET /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 (calls POST /api/ai/generate-description if the user clicks the magic-wand button — see backend/src/services/ai/).
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).
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[].
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-requests with the full payload (PurchaseRequestCreateData in PurchaseRequestService.ts:73-106).
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".
Notify the buyer: notificationService.notifyPurchaseRequestCreated() fires asynchronously (no await) — an info notification appears in the buyer's bell-icon dropdown.
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}).
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)
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/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)
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.