Files
nick-doc/04 - Flows/Purchase Request Flow.md
Siavash Sameni 7a616744f4 docs: complete code-reality alignment for remaining docs + reconcile issue set
Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
  Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
  added undocumented endpoints (ton-proof challenge, profile email verify,
  GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
  enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
  90-day notification TTL, soft-delete semantics, wallet fields

Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
  page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
  TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation

Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
  ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:15:02 +04:00

15 KiB
Raw Blame History

title, tags, related_models, related_apis
title tags related_models related_apis
Purchase Request Flow
flow
marketplace
buyer
purchase-request
PurchaseRequest
Category
Address
SellerOffer
POST /api/marketplace/purchase-requests
GET /api/marketplace/purchase-requests
PATCH /api/marketplace/purchase-requests/:id

Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)

[!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.
  • BackendPurchaseRequestService.createPurchaseRequest and the marketplace controller (backend/src/services/marketplace/marketplaceController.ts).
  • MongoDBpurchaserequests, 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.

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 (5200 chars), description (52000 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

  1. Frontend POSTs POST /api/marketplace/purchase-requests with the full payload (PurchaseRequestCreateData in PurchaseRequestService.ts:73-106).
  2. 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".
  3. Notify the buyer: notificationService.notifyPurchaseRequestCreated() fires asynchronously (no await) — an info notification appears in the buyer's bell-icon dropdown.
  4. 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}).
  5. 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

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:

  • searchPurchaseRequestsGET /marketplace/purchase-requests/search — this endpoint does not exist. Use query parameters on the standard list endpoint (GET /api/marketplace/purchase-requests?q=...) instead.
  • getMarketplaceStatsGET /marketplace/purchase-requests/stats — this endpoint does not exist. No stats aggregation route is registered.

Database writes

Socket events emitted

  • new-purchase-requestsellers room for public purchase requests (shared room, single broadcast; emitted by notifyAllSellersAboutNewRequest).
  • new-notificationuser-{sellerId} for each notified seller (via NotificationService.emitRealTimeNotification).
  • purchase-request-updaterequest-{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-updateseller-{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 minutes400 with Persian message. Prevents double-submit on flaky networks.
  • Invalid category ObjectId400 from Mongoose validation.
  • preferredSellerIds with invalid ObjectIds → silently dropped, not an error.
  • Empty cleaned preferredSellerIds + no "all" in originalisPublic 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 PATCH400 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

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