diff --git a/frontend/src/features/orders/api/orders.ts b/frontend/src/features/orders/api/orders.ts index 653b51b9..7a08dab0 100644 --- a/frontend/src/features/orders/api/orders.ts +++ b/frontend/src/features/orders/api/orders.ts @@ -4,6 +4,8 @@ import { userManager } from '@/core/auth' import { env } from '@/core/env' import { getJsonAuthed } from '@/shared/api/http' +import { isSagaSettled, type OrderStatus } from '../saga' + export interface OrderLineSummary { productId: string productName: string @@ -14,13 +16,16 @@ export interface OrderLineSummary { export interface OrderSummary { orderId: string buyerId: string - status: string + status: OrderStatus totalAmount: number currency: string placedAt: string lines: OrderLineSummary[] } +/** Poll cadence while the saga is in flight — it completes in seconds on the live stack. */ +export const SAGA_POLL_INTERVAL_MS = 2000 + // Query-key factory per frontend/CLAUDE.md "Server state": [feature, entity, params]. export const orderKeys = { byBuyer: (buyerId: string) => ['orders', 'byBuyer', buyerId] as const, @@ -51,5 +56,14 @@ export function orderByIdQuery(orderId: string) { const { data } = await getJsonAuthed(env.orderApiUrl, `/api/v1/orders/${orderId}`, await token(), signal) return data }, + // The saga narrator: poll while the order is still moving through the saga + // (Placed/Paid), stop the moment it settles (Shipped/Delivered/Cancelled/ + // PaymentFailed). Polling lives HERE, in the query definition, so every consumer + // of this query gets the same lifecycle — no component-level timers. + refetchInterval: (query) => { + const status = query.state.data?.status + if (status == null || isSagaSettled(status)) return false + return SAGA_POLL_INTERVAL_MS + }, }) } diff --git a/frontend/src/features/orders/components/OrderDetail.tsx b/frontend/src/features/orders/components/OrderDetail.tsx index f43b0337..f4409330 100644 --- a/frontend/src/features/orders/components/OrderDetail.tsx +++ b/frontend/src/features/orders/components/OrderDetail.tsx @@ -4,10 +4,12 @@ import { formatPrice } from '@/shared/format' import { orderByIdQuery } from '../api/orders' +import { SagaTimeline } from './SagaTimeline' + /** - * Single order view. Phase 3 turns the status line into the saga timeline + narrator - * (Placed → Paid → Shipped with "what the backend just did" panels) and polls this query; - * for now it's a static read of current status. + * Single order view — the saga-narrator page (epic #130 Phase 3). orderByIdQuery polls + * while the saga is in flight, so the timeline below advances live as PaymentService and + * ShippingService consume and re-publish events; polling stops once the order settles. */ export function OrderDetail({ orderId }: Readonly<{ orderId: string }>) { const { data, isPending, isError, fetchStatus } = useQuery(orderByIdQuery(orderId)) @@ -33,6 +35,7 @@ export function OrderDetail({ orderId }: Readonly<{ orderId: string }>) {

Order {data.orderId.slice(0, 8)}

{data.status} +