152 lines
10 KiB
Markdown
152 lines
10 KiB
Markdown
---
|
|
title: Delivery Confirmation Flow
|
|
tags: [flow, delivery, escrow-release, code]
|
|
related_models: ["[[PurchaseRequest]]", "[[Payment]]"]
|
|
related_apis: ["POST /api/marketplace/purchase-requests/:id/delivery-code/generate", "POST /api/marketplace/purchase-requests/:id/delivery-code/verify"]
|
|
---
|
|
|
|
# Delivery Confirmation Flow
|
|
|
|
> **Last updated:** 2026-06-06 — buyer fast-track confirmation is buyer/admin-only and uses cross-store id matching.
|
|
|
|
After the escrow is funded ([[PRD - Request Network In-House Checkout]] / [[Escrow Flow]]) and the seller has prepared the item, the seller **marks shipped**, the buyer **generates and reads out the delivery code**, the seller **verifies the code** to confirm receipt, and the escrow becomes eligible for release ([[Payout Flow]]).
|
|
|
|
## Actors
|
|
|
|
- **Buyer** — after the order reaches `delivery` status, explicitly generates the delivery code and reads it out to the seller at hand-off.
|
|
- **Seller** — types the code into their dashboard to confirm delivery.
|
|
- **Backend** — `DeliveryService` (`backend/src/services/delivery/DeliveryService.ts`), exposed through the marketplace routes (`backend/src/services/marketplace/routes.ts`).
|
|
- **MongoDB** — `purchaserequests.deliveryInfo` subdocument fields.
|
|
- **Socket.IO** — `delivery-code-generated`, `delivery-update`.
|
|
|
|
## Preconditions
|
|
|
|
- `PurchaseRequest.status` is `payment`, `processing`, or `delivery`.
|
|
- `Payment.escrowState === 'funded'`.
|
|
|
|
## Step-by-step narrative
|
|
|
|
1. **Seller marks shipped** — from the seller step `frontend/src/sections/request/components/seller-steps/step-3-ship-goods.tsx`, clicks "Mark as shipped". The frontend action `updateDelivery` calls `PUT /api/marketplace/purchase-requests/:id/delivery`. The controller's `updateDeliveryInfo` sets `shippedAt` and advances status to `delivery`. No code is generated at this point.
|
|
2. **Buyer generates the delivery code** — once status is `delivery`, the buyer explicitly triggers `POST /api/marketplace/purchase-requests/:id/delivery-code/generate` (buyerId is enforced server-side). `DeliveryService.generateDeliveryCode(requestId)`:
|
|
- Generates a 6-digit code (`Math.floor(100000 + Math.random()*900000)`).
|
|
- Sets `deliveryInfo.deliveryCode`, `deliveryCodeGeneratedAt = now`, `deliveryCodeExpiresAt = now + 7d`, `deliveryCodeUsed = false`.
|
|
- Emits `delivery-code-generated` and `delivery-update` to `request-{requestId}`.
|
|
- The code is displayed to the buyer in `frontend/src/sections/request/components/buyer-steps/step-5-receive-goods.tsx`.
|
|
3. **Buyer reads code to seller** — at hand-off the buyer reads the 6-digit code out loud (or shows it) to the seller.
|
|
4. **Seller enters code** — seller types the code into `frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx`.
|
|
5. **Verification** — `POST /api/marketplace/purchase-requests/:id/delivery-code/verify` with `{ code }` (selectedOffer.sellerId is enforced server-side). Handled by `DeliveryService.verifyDeliveryCode` (lines 180-212):
|
|
- Matches `code` against `deliveryInfo.deliveryCode`.
|
|
- Checks `deliveryCodeExpiresAt > now` and `deliveryCodeUsed === false`.
|
|
- On success: `deliveryInfo.deliveryCodeUsed = true; deliveryCodeUsedAt = now`. Status flips `delivery → delivered`.
|
|
- Emits `purchase-request-update` `status-changed`.
|
|
- Sends delivery-confirmed notifications to both buyer and seller directly within `DeliveryService.verifyDeliveryCode`.
|
|
6. **Alternative path — buyer fast-track** — the buyer can also call `PATCH .../confirm-delivery` to set status to `delivered` without any code (used when the code path fails, e.g. code expired or lost). The endpoint is buyer/admin-only and uses the same cross-store `sameUser()` id comparison as the seller delivery gates, so legacy ObjectId sessions and Postgres UUID request rows compare correctly. It emits only `purchase-request-update` with `status-changed` — it does **not** send delivery-specific notifications to either party.
|
|
7. **Optional auto-release timer** — once `status === 'delivered'`, a scheduled job can flip the request to `confirming` and then to `seller_paid` after a grace period (e.g. 48h). The auto-release worker is not yet implemented; today an admin completes the chain via [[Payout Flow]].
|
|
|
|
## Sequence diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
autonumber
|
|
actor S as Seller
|
|
actor B as Buyer
|
|
participant FE as Frontend
|
|
participant BE as Backend
|
|
participant DB as MongoDB
|
|
participant IO as Socket.IO
|
|
|
|
S->>FE: Click "Mark as shipped"
|
|
FE->>BE: PUT /api/marketplace/purchase-requests/{id}/delivery
|
|
BE->>DB: PurchaseRequest.shippedAt=now, status="delivery"
|
|
Note over BE,DB: No code generated here
|
|
|
|
B->>FE: View delivery code in step-5-receive-goods
|
|
FE->>BE: POST /api/marketplace/purchase-requests/{id}/delivery-code/generate
|
|
BE->>DB: deliveryInfo.deliveryCode=XXXXXX\nexpires=+7d
|
|
BE->>IO: emit request-{id} 'delivery-code-generated' {code, expiresAt}
|
|
FE->>B: Display 6-digit code
|
|
|
|
B->>S: At hand-off, read the 6-digit code aloud
|
|
S->>FE: Enter code in delivery-code-verification
|
|
FE->>BE: POST /api/marketplace/purchase-requests/{id}/delivery-code/verify {code}
|
|
BE->>DB: match code, expires>now, !used
|
|
BE->>DB: set deliveryCodeUsed = true
|
|
BE->>DB: set status = "delivered"
|
|
BE->>IO: emit request-{id} 'purchase-request-update' status-changed
|
|
BE->>B: notifyDeliveryConfirmed (DeliveryService.verifyDeliveryCode)
|
|
BE->>S: notifyDeliveryConfirmed (DeliveryService.verifyDeliveryCode)
|
|
Note over BE: Auto-release timer (planned) → seller_paid → payout
|
|
```
|
|
|
|
## API calls
|
|
|
|
| Method | Endpoint | Purpose |
|
|
|---|---|---|
|
|
| `PUT` | `/api/marketplace/purchase-requests/:id/delivery` | Seller marks shipped (sets shippedAt, advances to `delivery`) |
|
|
| `GET` | `/api/marketplace/purchase-requests/:id/delivery-code` | Retrieve current code (buyer + seller) |
|
|
| `POST` | `/api/marketplace/purchase-requests/:id/delivery-code/generate` | Buyer generates delivery code (buyer only) |
|
|
| `POST` | `/api/marketplace/purchase-requests/:id/delivery-code/verify` | Seller verifies code (seller only) |
|
|
| `GET` | `/api/marketplace/purchase-requests/:id/delivery-code/status` | Check code status (buyer + seller) |
|
|
| `PATCH` | `/api/marketplace/purchase-requests/:id/confirm-delivery` | Buyer/admin fast-track confirm (no code); no delivery-specific notifications |
|
|
|
|
### Phantom frontend actions (routes do NOT exist on backend)
|
|
|
|
These Redux/API actions exist in the frontend but call endpoints that return 404:
|
|
|
|
| Frontend action | Called path | Behaviour |
|
|
|---|---|---|
|
|
| `regenerateDeliveryCode` | `/delivery-code/regenerate` | 404s; frontend falls back to `/delivery-code/generate` |
|
|
| `getDeliveryAttempts` | `/delivery-code/attempts` | 404s — feature not implemented |
|
|
| `getDeliveryStats` | `/delivery/stats` | 404s — feature not implemented |
|
|
|
|
## Two paths to `delivered` status
|
|
|
|
1. **Code path** — seller calls `POST .../delivery-code/verify` with the correct, unexpired code → status becomes `delivered`. Both buyer and seller receive delivery-confirmed notifications (sent by `DeliveryService.verifyDeliveryCode`).
|
|
2. **Fast-track path** — buyer calls `PATCH .../confirm-delivery` (no code required) → also becomes `delivered`. Authorization is buyer/admin-only, with cross-store id matching for legacy ObjectId/PG UUID seams. No delivery-specific notifications are sent to either party.
|
|
|
|
## Database writes
|
|
|
|
- **`purchaserequests.deliveryInfo`** — `deliveryCode`, `deliveryCodeGeneratedAt`, `deliveryCodeExpiresAt`, `deliveryCodeUsed`, `deliveryCodeUsedAt`.
|
|
- **`purchaserequests.shippedAt`** — set when seller calls `PUT .../delivery`.
|
|
- **`purchaserequests.status`** — `delivery` → `delivered` → (eventually `seller_paid` → `completed`).
|
|
- **`notifications`** — generated for both parties (code path only).
|
|
|
|
## Socket events emitted
|
|
|
|
- **`delivery-code-generated`** → `request-{id}` room (payload: `{ requestId, code, expiresAt, timestamp }`). **⚠️ Security note:** the full 6-digit code is included in the payload and broadcast to all subscribers in the room, including the seller. The buyer dashboard displays the code; the seller receives it via socket as well.
|
|
- **`delivery-update`** → `request-{id}` (`type: 'code-generated'`).
|
|
- **`purchase-request-update`** `status-changed` on `delivery → delivered`.
|
|
|
|
## Side effects
|
|
|
|
- The code is displayed to the **buyer** in their dashboard. The buyer verbally shares it with the seller at hand-off. Note that the `delivery-code-generated` socket event also broadcasts the raw code to the entire request room (including the seller — see socket events section above).
|
|
- Triggers the path that eventually frees up the escrow (manual today via [[Payout Flow]], auto in the future).
|
|
|
|
## Error / edge cases
|
|
|
|
- **Wrong code** → `400 Invalid delivery code`.
|
|
- **Expired code** (>7 days) → `400 Code expired`. Buyer can generate a new code via `POST .../delivery-code/generate` (the `regenerateDeliveryCode` frontend action also falls through to this endpoint).
|
|
- **Already used code** → `400 Code already used`.
|
|
- **Buyer never generates / confirms** → status remains `delivery`. Auto-release timer (not yet built) should trigger `delivered` after N days. Until then, admin intervention.
|
|
- **Seller delivers but never marks shipped** → buyer can dispute via [[Dispute Flow]]; the dispute resolution will release the escrow regardless.
|
|
- **Lost / expired code** → buyer re-triggers `POST .../delivery-code/generate` to get a fresh code, invalidating the old one.
|
|
|
|
> [!tip] The buyer holds the code, not the seller
|
|
> The seller should ask the buyer for the code at hand-off. If the buyer disputes "never received", an unused code is strong circumstantial evidence that delivery has not been confirmed; a used code = seller confirmed receipt.
|
|
|
|
## Linked flows
|
|
|
|
- [[PRD - Request Network In-House Checkout]] / [[Escrow Flow]] — funding precondition.
|
|
- [[Escrow Flow]] — state transitions triggered by confirmation.
|
|
- [[Payout Flow]] — fires after confirmation (manual today).
|
|
- [[Dispute Flow]] — escape hatch.
|
|
- [[Notification Flow]] — channel for the code.
|
|
|
|
## Source files
|
|
|
|
- Backend: `backend/src/services/delivery/DeliveryService.ts` (generateDeliveryCode, verifyDeliveryCode lines 180-212)
|
|
- Backend: `backend/src/services/marketplace/routes.ts` (delivery endpoints)
|
|
- Frontend: `frontend/src/sections/request/components/seller-steps/step-3-ship-goods.tsx`
|
|
- Frontend: `frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx`
|
|
- Frontend: `frontend/src/sections/request/components/buyer-steps/step-5-receive-goods.tsx`
|