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>
12 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Negotiation Flow |
|
|
|
Negotiation Flow
Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
After an offer is submitted (Seller Offer Flow), the buyer and seller can negotiate the price/ETA via counter-offers exchanged through the chat. The request status moves to in_negotiation, and either party can finalise with accept/reject.
Actors
- Buyer — initiates negotiation by replying to an offer or opening chat from the request detail.
- Seller — receives counter, can accept or counter back.
- Frontend — chat component (
frontend/src/sections/chat/) overlaid on the request view; offer-edit modal underfrontend/src/sections/request/components/buyer-steps/step-3-components/. - Backend —
ChatService.sendMessagefor chat lines,SellerOfferService.updateOfferfor price/ETA edits,PurchaseRequestService.updatePurchaseRequestfor the status flip. - MongoDB —
chats,selleroffers,purchaserequests. - Socket.IO —
new-message,purchase-request-update.
Preconditions
- A
SellerOfferexists on the purchase request (statuspending). - The purchase request is
received_offersorin_negotiation. - Both parties are still active users.
[!info] Status vocabulary The negotiation drives the PurchaseRequest into the
in_negotiationstatus. The SellerOffer moves only betweenpending,accepted,rejected, andwithdrawn(backend/src/models/SellerOffer.ts:80). There is no'active'SellerOffer status — any documentation or UI that references an "active" offer is incorrect.
Step-by-step narrative
- Open negotiation chat — when a buyer first clicks "Chat with seller" on an offer card, the frontend calls
POST /api/chatto find-or-create adirectchat tied to the purchase request (ChatService.createChat,chat.ts:90-192). The chat'srelatedTo = { type: 'PurchaseRequest', id }makes it discoverable from the request view.
[!tip] Pre-payment chats vs. post-payment chats A negotiation chat may exist before payment confirmation creates the post-payment chat. The
ChatService.createChatdirectfind-or-create logic (ChatService.ts:95-108) prevents duplicates -- the same chat object is reused.
-
Status flip to
in_negotiation— the first message in the negotiation chat triggers a backend hook (or a manual frontend PATCH) that callsPurchaseRequestService.updatePurchaseRequestwith{ status: 'in_negotiation' }. The status-progression guard allows this (received_offers → in_negotiation). -
Buyer proposes a counter — the buyer types a message like "Can you do $80 instead of $100?". Two patterns are used:
- Free-form — just a chat message; the seller eyeballs the offer-edit screen and updates the price.
- Structured counter — the buyer opens an "edit offer" modal that (via the frontend
updateOfferaction) sendsPUT /api/marketplace/offers/{id}with the new desired terms. This is a seller-side edit endpoint; structured buyer counters typically come as a system message in the chat (messageType: 'system') referencing the new price.
-
Seller updates the offer —
SellerOfferService.updateOffer(:271-295):SellerOffer.findByIdAndUpdate(id, { ...updateData, updatedAt: now }, { new: true }).- Emits
purchase-request-updatewitheventType: 'offer-updated'torequest-{requestId}(SellerOfferService.ts:284-288) — both parties' open tabs refresh.
[!bug] ⚠️ KNOWN BUG — PUT/PATCH method mismatch on offer edit The frontend
updateOfferaction (frontend/src/actions/marketplace.ts:286-297) sendsPUT /marketplace/offers/:id, but the legacy backend router registers onlyPATCH /offers/:id(backend/src/services/marketplace/routes.ts:1260). NoPUT /offers/:idhandler is registered, so structured offer edits from the UI may 404. Fix by aligning on a single method (registerPUTon the backend, or switch the frontend toPATCH).
-
Buyer accepts -- clicks "Accept this offer", which kicks off PRD - Request Network In-House Checkout with the selected
sellerOfferId. Payment confirmation flips offer ->acceptedand request ->payment. -
Buyer rejects — the frontend
rejectOfferaction callsPUT /api/marketplace/offers/{id}/statuswith{ status: 'rejected' }.SellerOfferService.updateOfferStatus(:306-353) sendsnotifyOfferRejectedto the seller and stampsrejectedAt+rejectionReason. -
Seller withdraws — there is no dedicated
/withdrawendpoint (see warning below). The only way to withdraw isPUT /api/marketplace/offers/{id}/statuswith{ status: 'withdrawn' }(routes.ts:1914).withdrawOffer(:428-443) only works whilestatus === 'pending'. After rejection/acceptance, withdrawal is impossible. -
Chat continues — even after status flips, the chat remains open for clarifications (delivery details, dispute prep). See Chat Flow for message-level semantics.
[!warning] ⚠️ NOT IMPLEMENTED —
POST /api/marketplace/offers/:id/withdrawNoPOST .../offers/:id/withdrawroute is registered anywhere in the backend; calling it returns 404. Withdrawal is performed exclusively through the status endpoint:PUT /api/marketplace/offers/:id/statuswith body{ status: 'withdrawn' }.
Sequence diagram
sequenceDiagram
autonumber
actor B as Buyer
actor S as Seller
participant FE_B as Frontend (buyer)
participant FE_S as Frontend (seller)
participant BE as Backend
participant DB as MongoDB
participant IO as Socket.IO
B->>FE_B: Click "Chat with seller" on offer
FE_B->>BE: POST /api/chat (type:direct, relatedTo:PR)
BE->>DB: find-or-create Chat
BE-->>FE_B: { chat }
B->>FE_B: Send "Can you do $80?"
FE_B->>BE: POST /api/chat/{id}/messages
BE->>DB: Chat.addMessage(...)
BE->>IO: emit chat-{id} 'new-message'
IO-->>FE_S: 'new-message' (seller sees in real time)
BE->>BE: optionally trigger request → in_negotiation
BE->>DB: PurchaseRequest.status = "in_negotiation"
BE->>IO: emit request-{id} 'purchase-request-update' (status-changed)
S->>FE_S: Open edit-offer modal, set new price
FE_S->>BE: PUT /api/marketplace/offers/{id} {price:{amount:80}} ⚠️ backend only registers PATCH
BE->>DB: SellerOffer update (if PUT handled; else 404)
BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated)
IO-->>FE_B: refresh offer card
alt Buyer accepts
B->>FE_B: Click "Pay" -> [[PRD - Request Network In-House Checkout]]
Note over BE: Webhook PAID flips offer→accepted, request→payment
else Buyer rejects
B->>FE_B: Click "Reject"
FE_B->>BE: PUT /api/marketplace/offers/{id}/status {status:"rejected"}
BE->>DB: offer.status = "rejected"
BE->>BE: notifyOfferRejected(seller)
IO-->>FE_S: 'new-notification'
else Seller withdraws
S->>FE_S: Click "Withdraw offer"
FE_S->>BE: PUT /api/marketplace/offers/{id}/status {status:"withdrawn"}
BE->>DB: offer.status = "withdrawn"
BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated)
end
API calls
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/chat |
Find-or-create negotiation chat |
POST |
/api/chat/:chatId/messages |
Send chat message |
POST |
/api/marketplace/purchase-requests/:id/offers |
Create offer (scoped) — routes.ts:1163 |
GET |
/api/marketplace/purchase-requests/:id/offers |
List offers for a request (scoped) — routes.ts:1223 |
PUT |
/api/marketplace/offers/:id |
Seller updates price / ETA / notes (counter). ⚠️ KNOWN BUG: frontend sends PUT, backend registers only PATCH /offers/:id (routes.ts:1260) → may 404. |
PUT |
/api/marketplace/offers/:id/status |
Reject ({ status: 'rejected' }) and withdraw ({ status: 'withdrawn' }) — routes.ts:1914. There is no separate /withdraw endpoint. |
PATCH |
/api/marketplace/purchase-requests/:id |
Status transition to in_negotiation |
Database writes
chats: messages appended viachat.addMessage;metadata.lastActivitybumped;unreadCountsincremented for non-sender participants.selleroffers: counter changes updateprice,deliveryTime,notes,updatedAt; status moves betweenpending/accepted/rejected/withdrawn.purchaserequests: status flips when first counter arrives.notifications: created per status change (accept/reject), not per chat message (chat has its own real-time channel).
Socket events emitted
new-message→chat-{chatId}(the canonical chat event).chat-notification→user-{participantId}for non-senders (badge increment).purchase-request-updatewitheventType: 'offer-updated'→request-{id}whenever the offer is edited.purchase-request-updatewitheventType: 'status-changed'→request-{id}on thereceived_offers → in_negotiationflip.
Side effects
- The chat's unread badge grows in the recipient's chat list (
Chat.unreadCountsarray). Resets when they open the chat andPOST /api/chat/{id}/read. - Typing indicators are emitted via
typing-start/typing-stopsocket events (seebackend/src/app.ts:142-158) — purely client-driven.
Error / edge cases
- Offer edit returns 404 — see the KNOWN BUG above (PUT vs PATCH method mismatch).
- Sender not a chat participant →
403 "User is not a participant in this chat"(ChatService.sendMessage:209-211). - Counter on an offer the buyer doesn't own a request for → blocked by the controller (the buyer must be the request's owner).
- Counter on an
acceptedoffer →updateOfferdoes not enforce status; the schema/controller should reject. Current code allows the price change, which is dangerous post-payment. Recommended hardening: guard withif (offer.status !== 'pending') throw. - Withdraw after accept/reject →
withdrawOfferonly acts whilestatus === 'pending', so withdrawal is rejected once the offer leaves that state. - Status regression attempt (
in_negotiation → received_offers) → blocked byisValidStatusProgression(PurchaseRequestService.ts:31-50). - Two simultaneous edits — last-write-wins on
findByIdAndUpdate; consider optimistic concurrency via__vif conflicts become an issue. - Chat created in negotiation but buyer never pays → orphan chat remains; the post-payment chat (in Chat Flow) reuses it because the find-or-create logic matches by participants + relatedTo.
[!warning] No structured "counter-offer object" Today, counter-offer negotiations are mostly free-form chat plus an
updateOfferedit. There is noCounterOffercollection with provenance. If audit/regulatory needs emerge, capture each counter as a snapshot ({ oldPrice, newPrice, byUserId, atTime }) on the offer.
Linked flows
- Seller Offer Flow — the prior step.
- PRD - Request Network In-House Checkout — closes the negotiation with an on-chain payment.
- Chat Flow — message-level mechanics, attachments, read receipts.
- Notification Flow — accept/reject notifications.
Source files
- Backend:
backend/src/services/marketplace/SellerOfferService.ts:271-443 - Backend:
backend/src/services/marketplace/routes.ts:1163-1278,1914(offer routes) - Backend:
backend/src/services/marketplace/PurchaseRequestService.ts:408-495 - Backend:
backend/src/services/chat/ChatService.ts:90-260 - Backend:
backend/src/models/SellerOffer.ts:17,80(status enum) - Frontend:
frontend/src/actions/marketplace.ts:286-308(updateOffer,rejectOffer) - Frontend:
frontend/src/sections/request/components/buyer-steps/step-3-components/ - Frontend:
frontend/src/sections/chat/(chat UI)