feat(web): add order tracking timeline#322
Conversation
Implements W-10. Shoppers can now view all their orders at /buyer/orders
and drill into a single order at /buyer/orders/[id] with a vertical
timeline stepper and status-driven actions.
Routes
- /buyer/orders → list with filter tabs (All / Processing /
Dispatched / Delivered / Returns)
- /buyer/orders/[id] → detail view with escrow banner, product
summary, timeline, delivery + payment
cards, and per-status action area
- /buyer/orders/confirmed/[id] → existing W-09b route, untouched (its
Track Order CTA already points here)
Action surface (verified against current backend contracts)
- PENDING_PAYMENT → [Cancel order] → POST /orders/:id/cancel (buyer
cancel is rejected after PAID; CTA is hidden for
every other status)
- DISPATCHED/IN_TRANSIT → 6-digit OTP input (the backend stores only the
bcrypt hash and never returns the plaintext; the
buyer enters the code from their notification) +
[Confirm I Received It] → POST /orders/:id/confirm-delivery
- DELIVERED → [Raise a dispute] text link → placeholder dialog
(W-10 does not call POST /orders/:id/disputes;
full filing UI is out of scope) + dispute-window
countdown only when backend disputeWindowEndsAt is
in the future
- COMPLETED → [Buy again] → /p/{productCode} when product code is
available, otherwise hidden; same dispute placeholder
- CANCELLED / DISPUTE / REFUND_PENDING → read-only notices
New components (apps/web/src/components/order/)
- OrderTimeline.tsx — 5-step vertical stepper. Timestamps read
from order.events; only two narrow fallbacks
(createdAt for "Order Placed", dispatchedAt
for "Dispatched"); missing times render as "—"
rather than being fabricated.
- DeliveryCodeInput.tsx — 6 individual digit inputs with auto-advance,
backspace step-back, arrow navigation, and
paste support (≥44px touch targets).
- OrderCard.tsx — list-row card with order code, time-ago,
product thumbnail/name, store name, status
badge, and escrow status line.
- EscrowBanner.tsx — active / completed / muted tones, driven by
status + payoutStatus.
- RaiseDisputeDialog.tsx — placeholder dialog only; copy: "Dispute
filing is coming soon. You'll be able to
raise a dispute from this order page during
the dispute window."
Library
- apps/web/src/lib/orders.ts expanded:
* ShopperOrder shape v2: adds product, store, dispatchedAt,
disputeWindowEndsAt, payoutStatus, deliveryMethod, events.
* Status enum reconciled with backend (PREPARING, IN_TRANSIT, DISPUTE,
REFUND_PENDING).
* fetchMyOrders, cancelOrder, confirmDelivery, fetchOrderTracking
added. All requests use the existing api helper.
* Defensive guard kept and extended: refuses to surface any row that
leaks dropshipperCostKobo / digitalMarginKobo / linkedOrderId /
sourcedProductId, or that has orderType === DROPSHIP_FULFILLMENT.
Backend (single read-only Prisma include extension — no schema, DTO, or
lifecycle changes)
- apps/backend/src/modules/order/order.service.ts:
* getById include now also selects product { name, imageUrl, productCode }
and storeProfile { storeName, storeHandle, businessName, verificationTier }.
* listByBuyer include extended the same way (replaces the previous
storeProfile.businessName-only select).
- stripDropshipPrivateFields still runs unchanged.
- Only public-safe fields are joined: physical store identity, bank
details, Paystack recipient fields, and dropship internals remain
invisible to shoppers.
Dropship safety
- Frontend never reads or computes orderType, storeType, physicalStoreId,
linkedOrderId, sourcedProductId, dropshipperCostKobo, digitalMarginKobo,
buyerId, raw storeId (except for internal routing), or fulfillment
order data.
- For DROPSHIP_CUSTOMER orders the joined product is the physical
product the buyer purchased (safe to show); the joined store is the
digital store (the public seller — safe to show). Physical store stays
invisible.
W-09b confirmation screen unchanged.
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
|
Warning Review limit reached
Your plan currently allows 1 review/hour. Refill in 56 minutes and 49 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR adds a complete buyer orders experience: backend service queries are narrowed to required fields, order types and client API are defined with new status states and payout/delivery tracking, reusable display components render order cards/timelines/escrow/OTP input, and server pages deliver order listing with status filtering and detailed order view with tracking events and delivery confirmation. ChangesBuyer Orders Experience
Sequence Diagram(s)sequenceDiagram
participant Backend
participant fetchMyOrders
participant normalizeOrder
participant ShopperUI
Backend->>fetchMyOrders: raw order array or paginated response
fetchMyOrders->>normalizeOrder: raw RawShopperOrder
normalizeOrder->>normalizeOrder: validate status, coerce fields, filter dropship
normalizeOrder->>normalizeOrder: normalize joined product/store/events
normalizeOrder->>ShopperUI: typed ShopperOrder
ShopperUI->>Backend: confirmDelivery/cancelOrder/fetchOrderTracking
sequenceDiagram
participant Shopper
participant OrderDetailClient
participant API
participant UI
Shopper->>OrderDetailClient: load /buyer/orders/{id}
OrderDetailClient->>API: fetchOrderById(orderId)
OrderDetailClient->>API: fetchOrderTracking(orderId)
API->>OrderDetailClient: ShopperOrder + tracking events
OrderDetailClient->>UI: render header, timeline, escrow
Shopper->>UI: enter OTP and click Confirm
UI->>OrderDetailClient: handleConfirm(otp)
OrderDetailClient->>API: confirmDelivery(orderId, otp)
API->>OrderDetailClient: success/error
OrderDetailClient->>UI: toast, reload
Shopper->>UI: click Raise Dispute
UI->>OrderDetailClient: open RaiseDisputeDialog
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/app/`(shopper)/buyer/orders/[id]/OrderDetailClient.tsx:
- Around line 173-176: The code assigns a user-facing string directly from
err.message (in OrderDetailClient where the variable message is built around
lines referenced and similarly at the other occurrence), which can leak internal
details; change this to always set a generic, user-friendly message (e.g., "We
couldn't verify that code. Double-check and try again.") for display and move
the raw error details into a separate log or telemetry call (console.error or
your monitoring function) so internals are recorded but not shown to shoppers;
update both places that build message (the block using typeof (err as {
message?: string })?.message and the similar block at the other location) to
follow this pattern.
- Around line 84-87: The loadTracking function can throw from fetchOrderTracking
and currently has no error handling; wrap the await fetchOrderTracking(orderId)
call in a try/catch, log or surface the error appropriately (e.g., console.error
or a UI error state) and only call setTracking(events) when not cancelled and
events were retrieved; keep the existing cancelled check and ensure the catch
block avoids unhandled promise rejections (and optionally set an error state or
no-op when cancelled).
- Around line 158-162: Replace the ad-hoc otp length checks with a Zod-driven
validation: define a Zod schema (e.g., otpSchema = z.string().length(6)) and
validate otp inside handleConfirm using otpSchema.safeParse(otp) instead of
otp.length !== 6; if validation fails, call setConfirmError with a suitable
message from the schema result, otherwise proceed with confirmation logic. Also
derive the confirm button disabled prop from the same schema result (e.g.,
!otpSchema.safeParse(otp).success) so both the runtime check in handleConfirm
and the UI disabled state use the identical Zod rule; update references to otp,
handleConfirm, and setConfirmError accordingly.
- Around line 45-79: Replace the ad-hoc useEffect/fetches in
OrderDetailClient.tsx with TanStack Query: create useQuery calls for fetching
the order and tracking using clear queryKeys (e.g., ['order', orderId] and
['order', orderId, 'tracking']) and remove the local fetch logic in
reload/useEffect; wrap fetchOrderTracking in a try/catch inside its query or in
an async function used by useQuery to avoid unhandled rejections; validate OTP
using a Zod schema (e.g., otpSchema.parse/ safeParse) instead of inline
otp.length !== 6; and update handleConfirm/handleCancel to map backend errors to
user-safe messages (don’t forward err.message) or log the raw error internally
while returning a generic error string to the UI.
In `@apps/web/src/app/`(shopper)/buyer/orders/[id]/page.tsx:
- Around line 9-17: The page types params incorrectly as a Promise and awaits
it; update OrderDetailPageProps to use params: { id: string } (synchronous) and
remove the await in OrderDetailPage so it directly reads const { id } = params
and returns <OrderDetailClient orderId={id} />; adjust the interface and the
OrderDetailPage function signature accordingly.
In `@apps/web/src/app/`(shopper)/buyer/orders/OrdersListClient.tsx:
- Around line 4-13: The component is using manual fetch state via useEffect
instead of TanStack Query; replace the bespoke loading/error logic (the
useEffect and local state around fetchMyOrders in OrdersListClient.tsx) with a
useQuery call that references the centralized query key queryKeys.orders.mine,
returning data typed as ShopperOrder[] and status as needed (useQuery for
fetchMyOrders). Remove local loading/error state, wire the query's
isLoading/isError/data into the existing UI that renders Skeleton,
AlertCircle/Package placeholders and OrderCard, and ensure any cache
invalidation or refetch elsewhere uses queryKeys.orders.mine for consistency.
In `@apps/web/src/components/order/DeliveryCodeInput.tsx`:
- Around line 40-45: The code currently uses arr.join("").trim() which collapses
empty slots and shifts digits when editing mid-code; replace any use of .trim()
(e.g., in writeDigit and the other callbacks that build the code string) with
arr.join("") so the resulting value preserves empty placeholders and positions,
and ensure onChange receives the fixed-length positional string (or empty-string
placeholders) rather than a trimmed, collapsed string.
In `@apps/web/src/components/order/OrderTimeline.tsx`:
- Around line 67-70: The switch hardcodes CANCELLED/DISPUTE/REFUND_PENDING to
return step index 1 which misrepresents different terminal paths; instead
compute the freeze step by inspecting the order's events or lifecycle metadata
(e.g., use the latest event type/timestamp or an existing mapping helper) and
return the corresponding step index. Locate the switch handling those statuses
in OrderTimeline (the case "CANCELLED"/"DISPUTE"/"REFUND_PENDING" block) and
replace the fixed return 1 with a call to a function that derives the correct
step (e.g., getStepIndexFromEvents(events) or mapLifecycleToStep(lifecycle)) so
the frozen timeline reflects where the terminal action actually occurred. Ensure
the logic falls back to a sensible default if no event evidence exists.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ee2e3d6a-85c5-46cc-b243-52da14e0611d
📒 Files selected for processing (11)
apps/backend/src/modules/order/order.service.tsapps/web/src/app/(shopper)/buyer/orders/OrdersListClient.tsxapps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsxapps/web/src/app/(shopper)/buyer/orders/[id]/page.tsxapps/web/src/app/(shopper)/buyer/orders/page.tsxapps/web/src/components/order/DeliveryCodeInput.tsxapps/web/src/components/order/EscrowBanner.tsxapps/web/src/components/order/OrderCard.tsxapps/web/src/components/order/OrderTimeline.tsxapps/web/src/components/order/RaiseDisputeDialog.tsxapps/web/src/lib/orders.ts
| const reload = useCallback(async () => { | ||
| try { | ||
| const next = await fetchOrderById(orderId); | ||
| if (!next) { | ||
| setState("error"); | ||
| return; | ||
| } | ||
| setOrder(next); | ||
| setState("ready"); | ||
| } catch { | ||
| setState("error"); | ||
| } | ||
| }, [orderId]); | ||
|
|
||
| useEffect(() => { | ||
| let cancelled = false; | ||
| async function load() { | ||
| try { | ||
| const fetched = await fetchOrderById(orderId); | ||
| if (cancelled) return; | ||
| if (!fetched) { | ||
| setState("error"); | ||
| return; | ||
| } | ||
| setOrder(fetched); | ||
| setState("ready"); | ||
| } catch { | ||
| if (!cancelled) setState("error"); | ||
| } | ||
| } | ||
| load(); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [orderId]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify this component is not using TanStack Query hooks and inspect existing order query key patterns.
rg -n --type=tsx 'useQuery|useMutation|useSuspenseQuery|queryClient|invalidateQueries' -- "apps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsx"
rg -n --type=ts 'queryKeys|orders|fetchOrderById|fetchOrderTracking' -- "apps/web/src/lib/orders.ts"Repository: coded-devs/twizrr
Length of output: 548
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsx"
echo "== Search for TanStack Query hooks in target component =="
rg -n "useQuery|useMutation|useSuspenseQuery|useQueryClient|invalidateQueries|queryClient|startTransition\(" "$FILE" || true
echo "== Show first 220 lines with numbers (for context/OTP validation) =="
wc -l "$FILE"
sed -n '1,220p' "$FILE" | cat -n
echo "== Inspect apps/web/src/lib/orders.ts for query key patterns =="
LIB="apps/web/src/lib/orders.ts"
wc -l "$LIB" || true
sed -n '1,260p' "$LIB" | cat -n
echo "== Search for centralized order queryKeys usage across apps/web/src =="
rg -n "queryKeys|ordersQuery|orderQuery|getOrders|orderDetails" apps/web/src --glob='*.ts' --glob='*.tsx' || trueRepository: coded-devs/twizrr
Length of output: 17561
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsx"
echo "== Search for TanStack Query hooks in target component =="
rg -n "useQuery|useMutation|useSuspenseQuery|useQueryClient|invalidateQueries|queryClient|startTransition\(" "$FILE" || true
echo "== Show first 220 lines with numbers (for context/OTP validation) =="
wc -l "$FILE"
sed -n '1,220p' "$FILE" | cat -n
echo "== Inspect apps/web/src/lib/orders.ts for query key patterns =="
LIB="apps/web/src/lib/orders.ts"
wc -l "$LIB" || true
sed -n '1,260p' "$LIB" | cat -n
echo "== Search for centralized order queryKeys usage across apps/web/src =="
rg -n "queryKeys|ordersQuery|orderQuery|getOrders|orderDetails" apps/web/src --glob='*.ts' --glob='*.tsx' || trueRepository: coded-devs/twizrr
Length of output: 17561
Ensure order/tracking data uses TanStack Query (queryKeys) instead of useEffect data fetching
OrderDetailClient.tsx loads the order and tracking via useEffect calling fetchOrderById / fetchOrderTracking and local component state (state, order, tracking) rather than using TanStack Query hooks with centralized queryKeys.
Additional gaps in the same component:
fetchOrderTracking(orderId)in the trackinguseEffecthas no error guard (try/catch), so failures can cause an unhandled rejection.- OTP validation is done inline via
otp.length !== 6(no Zod schema). - Error handling in
handleConfirm/handleCancelforwards backenderr.messagewhen present, which may leak backend details instead of using a safer, user-generic message.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/app/`(shopper)/buyer/orders/[id]/OrderDetailClient.tsx around
lines 45 - 79, Replace the ad-hoc useEffect/fetches in OrderDetailClient.tsx
with TanStack Query: create useQuery calls for fetching the order and tracking
using clear queryKeys (e.g., ['order', orderId] and ['order', orderId,
'tracking']) and remove the local fetch logic in reload/useEffect; wrap
fetchOrderTracking in a try/catch inside its query or in an async function used
by useQuery to avoid unhandled rejections; validate OTP using a Zod schema
(e.g., otpSchema.parse/ safeParse) instead of inline otp.length !== 6; and
update handleConfirm/handleCancel to map backend errors to user-safe messages
(don’t forward err.message) or log the raw error internally while returning a
generic error string to the UI.
| async function handleConfirm() { | ||
| if (otp.length !== 6) { | ||
| setConfirmError("Enter all 6 digits of your delivery code."); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether this file already uses Zod/RHF validation primitives.
rg -n --type=tsx 'zod|z\.object|safeParse|react-hook-form|useForm|resolver' -- "apps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsx"Repository: coded-devs/twizrr
Length of output: 89
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsx"
echo "== Lines around handleConfirm =="
nl -ba "$FILE" | sed -n '130,220p'
echo
echo "== Zod / RHF usage in this file =="
rg -n --hidden --no-ignore -S \
'zod|z\.object|safeParse|react-hook-form|useForm|resolver' \
"$FILE" || trueRepository: coded-devs/twizrr
Length of output: 155
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsx"
echo "== Lines 130-220 =="
awk 'NR>=130 && NR<=220 {printf "%d\t%s\n", NR, $0}' "$FILE"
echo
echo "== Zod / RHF usage in this file =="
rg -n --hidden --no-ignore -S \
'zod|z\.object|safeParse|react-hook-form|useForm|resolver' \
"$FILE" || trueRepository: coded-devs/twizrr
Length of output: 3602
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsx"
echo "== Lines 380-450 =="
awk 'NR>=380 && NR<=450 {printf "%d\t%s\n", NR, $0}' "$FILE"Repository: coded-devs/twizrr
Length of output: 3110
Replace inline OTP length checks with Zod schema validation
OrderDetailClient.tsx validates the 6-digit delivery code via otp.length !== 6 inside handleConfirm (158-162) and separately disables the confirm button via disabled={otp.length !== 6} (428-434), with no Zod/RHF validation primitives used in this component. Use a single Zod schema for the OTP (e.g., z.string().length(6)) and drive both the error message and the disabled state from the schema result.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/app/`(shopper)/buyer/orders/[id]/OrderDetailClient.tsx around
lines 158 - 162, Replace the ad-hoc otp length checks with a Zod-driven
validation: define a Zod schema (e.g., otpSchema = z.string().length(6)) and
validate otp inside handleConfirm using otpSchema.safeParse(otp) instead of
otp.length !== 6; if validation fails, call setConfirmError with a suitable
message from the schema result, otherwise proceed with confirmation logic. Also
derive the confirm button disabled prop from the same schema result (e.g.,
!otpSchema.safeParse(otp).success) so both the runtime check in handleConfirm
and the UI disabled state use the identical Zod rule; update references to otp,
handleConfirm, and setConfirmError accordingly.
| interface OrderDetailPageProps { | ||
| params: Promise<{ id: string }>; | ||
| } | ||
|
|
||
| export default async function OrderDetailPage({ | ||
| params, | ||
| }: OrderDetailPageProps) { | ||
| const { id } = await params; | ||
| return <OrderDetailClient orderId={id} />; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1) Confirm installed Next.js version in this repo.
fd -a package.json | xargs -I{} sh -c 'echo "--- {} ---"; jq -r ".dependencies.next // .devDependencies.next // empty" "{}"'
# 2) Find async Promise-typed route params usage in App Router pages.
rg -n --type=ts --type=tsx 'params\s*:\s*Promise<\{[^}]*\}>' -- "apps/web/src/app"Repository: coded-devs/twizrr
Length of output: 314
Align params typing with Next.js 14 App Router expectations.
This repo is on Next.js ^14.2.35 (apps/web/package.json), but the page types params as Promise<{ id: string }> and awaits it. Use synchronous params: { id: string } and remove the await.
Proposed fix
interface OrderDetailPageProps {
- params: Promise<{ id: string }>;
+ params: { id: string };
}
export default async function OrderDetailPage({
params,
}: OrderDetailPageProps) {
- const { id } = await params;
+ const { id } = params;
return <OrderDetailClient orderId={id} />;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/app/`(shopper)/buyer/orders/[id]/page.tsx around lines 9 - 17,
The page types params incorrectly as a Promise and awaits it; update
OrderDetailPageProps to use params: { id: string } (synchronous) and remove the
await in OrderDetailPage so it directly reads const { id } = params and returns
<OrderDetailClient orderId={id} />; adjust the interface and the OrderDetailPage
function signature accordingly.
| import { useEffect, useMemo, useState } from "react"; | ||
| import { AlertCircle, Package } from "lucide-react"; | ||
| import { Skeleton } from "@/components/ui/Skeleton"; | ||
| import { OrderCard } from "@/components/order/OrderCard"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { | ||
| fetchMyOrders, | ||
| type ShopperOrder, | ||
| type ShopperOrderStatus, | ||
| } from "@/lib/orders"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Replace manual fetch state with TanStack Query + centralized query key.
Line 66 implements bespoke loading/error lifecycle with useEffect, which diverges from the project’s required server-state pattern. Please move this to React Query (useQuery) with a shared queryKeys.orders.mine key for consistent caching, retries, and invalidation across buyer-order flows.
As per coding guidelines, "Use React Query (TanStack Query v5) for server state management with centralized queryKeys".
Also applies to: 62-82
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/app/`(shopper)/buyer/orders/OrdersListClient.tsx around lines 4
- 13, The component is using manual fetch state via useEffect instead of
TanStack Query; replace the bespoke loading/error logic (the useEffect and local
state around fetchMyOrders in OrdersListClient.tsx) with a useQuery call that
references the centralized query key queryKeys.orders.mine, returning data typed
as ShopperOrder[] and status as needed (useQuery for fetchMyOrders). Remove
local loading/error state, wire the query's isLoading/isError/data into the
existing UI that renders Skeleton, AlertCircle/Package placeholders and
OrderCard, and ensure any cache invalidation or refetch elsewhere uses
queryKeys.orders.mine for consistency.
- DeliveryCodeInput: stop collapsing positional slots when the user fills a non-final box first. The wire-format string is now always 6 chars with " " for empty slots; positions are preserved. OrderDetailClient extracts the digit-only payload (cleanOtp via /\D/g) for validation and submission, so a 6-char value with placeholder spaces no longer passes the length check. - OrderTimeline: terminal-state (CANCELLED / DISPUTE / REFUND_PENDING) freeze step is now derived from the order's event history rather than hardcoded to step 1. A cancel-at-PENDING_PAYMENT freezes at "Order Placed"; a dispute raised after dispatch freezes at "Dispatched"; etc. - OrderDetailClient: confirm-delivery and cancel error paths no longer forward backend err.message to the UI. Both surface generic copy and the OTP helper text above the input already gives the buyer context for failed attempts. - OrderDetailClient: tracking effect now has an explicit try/catch as defense in depth. fetchOrderTracking already swallows errors and returns [], but this guards against future regressions. Intentionally NOT actioned: - Migrating order/tracking data loading to TanStack Query (PR #322 comments 1, 6) and migrating OTP validation to Zod + RHF (comment 3): the apps/web codebase has no centralized queryKeys module and no @hookform/resolvers dependency. Same posture as PR #317 — these are cross-cutting refactors that need their own task to establish the patterns. - Switching `params: Promise<{ id: string }>` to a synchronous shape (comment 5): W-09a and W-09b already use the async-params pattern successfully on Next 14.2.x; reverting would diverge from established routes. The Promise form is also forward-compatible with Next 15 where async params become required.
What does this PR do?
Implements W-10 — Order Tracking + Vertical Timeline. This adds the shopper order list and order detail pages, including filter tabs, safe order cards, escrow banner, vertical order timeline, OTP confirmation input, and status-specific actions.
Task
W-10 — Order Tracking + Vertical Timeline
What changed
/buyer/orders/buyer/orders/[id]apps/web/src/lib/orders.tswith shopper-safe order helpersFiles changed
apps/web/src/app/(shopper)/buyer/orders/page.tsxapps/web/src/app/(shopper)/buyer/orders/OrdersListClient.tsxapps/web/src/app/(shopper)/buyer/orders/[id]/page.tsxapps/web/src/app/(shopper)/buyer/orders/[id]/OrderDetailClient.tsxapps/web/src/components/order/OrderTimeline.tsxapps/web/src/components/order/DeliveryCodeInput.tsxapps/web/src/components/order/OrderCard.tsxapps/web/src/components/order/EscrowBanner.tsxapps/web/src/components/order/RaiseDisputeDialog.tsxapps/web/src/lib/orders.tsapps/backend/src/modules/order/order.service.tsBackend endpoints verified
GET /orders/meGET /orders/:idPOST /orders/:id/cancelPOST /orders/:id/confirm-deliveryGET /orders/:id/trackingPOST /orders/:id/disputesexists but is not called in this PRBackend change note
This PR includes one read-only backend change in:
apps/backend/src/modules/order/order.service.tsThe order detail/list Prisma includes were extended with safe shopper display fields only:
No schema changes were added.
No DTO changes were added.
No order lifecycle, status transition, payment, payout, ledger, refund, stock, or dropship logic was changed.
No unsafe fields were added to the select.
Behavior implemented
Order list
/buyer/ordersrenders shopper order cards/buyer/orders/[id]Order detail
/buyer/orders/[id]renders:Timeline
Timeline steps:
Timestamps are derived from backend order events where available.
Fallbacks are limited to:
createdAtfor Order PlaceddispatchedAtfor DispatchedMissing times render as unavailable instead of being fabricated.
Status actions
PENDING_PAYMENT: cancel order actionDISPATCHED/IN_TRANSIT: OTP input and Confirm I Received It actionDELIVERED: dispute placeholder and review coming soonCOMPLETED: buy again link when product code is available, review coming soonCANCELLED,DISPUTE,REFUND_PENDING: safe read-only noticesDispute CTA
This PR does not implement full dispute filing UI.
The Raise Dispute CTA opens a placeholder dialog with approved copy:
“Dispute filing is coming soon. You'll be able to raise a dispute from this order page during the dispute window.”
No
POST /orders/:id/disputescall is made.Dropship safety
The frontend uses a strict shopper order whitelist and never spreads raw order responses.
The UI never renders:
orderTypestoreTypephysicalStoreIdlinkedOrderIdsourcedProductIddropshipperCostKobodigitalMarginKobobuyerIdstoreIdThe normalizer refuses rows with non-null dropship internals or
orderType === "DROPSHIP_FULFILLMENT"as defense in depth.No frontend payout-split computation was added.
Verification
Web:
pnpm run lint— PASSnpx tsc --noEmit— PASSpnpm run build— PASSBackend:
npx prisma generate— PASSnpx tsc --noEmit— PASSpnpm run lint— PASSBuild confirms these routes are registered:
/buyer/orders/buyer/orders/[id]Type of change
Area affected
How to test this
/buyer/orders/[id]loads./buyer/orders.Expected result:
Pre-commit checklist
console.logleft in production code.envfiles committedanytypes addedScreenshots
Required for UI changes. Add mobile and desktop screenshots of:
/buyer/orders/buyer/orders/[id]Notes for reviewer
This PR intentionally does not implement W-11 Cart, W-16 Store Orders, full dispute filing UI, review flow, logistics booking UI, payment changes, payout changes, or backend order lifecycle changes.
Summary by CodeRabbit
Release Notes