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

232 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-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.
- **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 (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
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
- [[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`