Files
nick-doc/04 - Flows/Purchase Request Flow.md

204 lines
12 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"]
---
# 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 --> 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: SHKeeper webhook PAID\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
completed --> finalized: ratings exchanged
finalized --> archived: 30 days idle
pending --> cancelled: buyer cancels (any pre-payment status)
received_offers --> cancelled
in_negotiation --> cancelled
cancelled --> [*]
archived --> [*]
```
Terminal statuses: `completed`, `finalized`, `archived`, `cancelled` (`PurchaseRequestService.ts:28`).
## 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 (202000 chars), 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), preferred sellers (typeahead bound to `GET /api/users/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/files/upload` — 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`: `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}`).
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/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
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)
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)
```
## API calls
| Method | Endpoint | Purpose |
|---|---|---|
| `POST` | `/api/marketplace/purchase-requests` | Create the request |
| `GET` | `/api/marketplace/categories` | Step 1 dropdown |
| `GET` | `/api/users/sellers` | Step 3 preferred-sellers typeahead |
| `GET` | `/api/addresses` | Step 4 saved addresses |
| `POST` | `/api/files/upload` | Attachments |
| `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) |
## Database writes
- **`purchaserequests` collection**: full insert. Subsequent status transitions and `selectedOfferId` updates happen in [[Seller Offer Flow]], [[Payment Flow - SHKeeper]], 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-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.
## Linked flows
- [[Seller Offer Flow]] — sellers respond to the published request.
- [[Negotiation Flow]] — counter-offer mechanics in `in_negotiation`.
- [[Payment Flow - SHKeeper]] — 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`