/** * Ship order page: single API call returns order, quotes, and dropshops. */ import { __, sprintf } from '@wordpress/i18n'; import { Section, Spinner } from '@woocommerce/components'; import { useMemo, useEffect, useRef, lazy, Suspense, useState, } from '@wordpress/element'; import { Button, Flex, Notice } from '@wordpress/components'; import { useWindowSize, useOrderShipData, useParcel2goAuth } from '../shared/hooks'; import { getEditOrderUrl, getSiteUrl, getSettingsPageUrl, isUK, validateAddresses, validateInternationalRequirements, getPluginSettingsPageUrl, } from '../shared/utils'; import { setTag, addBreadcrumb, withSpan, captureException, } from '../shared/sentry'; import { LazyBookingPrereqsNotice, OrderCompleteView, OrderProgress } from '../shared/components'; import { AddressBlock, OrderProductsCard, PackagingSection, OrderShipHeader, OrderStatusCard, } from '../features/order-ship'; import { OrderCompleteResponse } from '../types/completeOrder'; import apiFetch from '@wordpress/api-fetch'; import { markOrderAsBooked, markOrderAsComplete, } from '../features/order-ship/utils/shippingStatus'; const LazyServiceQuotes = lazy( () => import('../features/order-ship/service-quotes') ); interface OrderShipPageProps { orderId: number; onBack: () => void; } const MAX_P2G_STATUS_POLL_TRIES = 30; const P2G_STATUS_POLL_INTERVAL_MS = 2000; export default function OrderShipPage({ orderId, onBack }: OrderShipPageProps) { const { isLinked } = useParcel2goAuth(); const [orderReference, setOrderReference] = useState(null); const [p2gOrderLineIds, setP2gOrderLineIds] = useState([]); const [attempts, setAttempts] = useState(0); const [meta, setMeta] = useState>( {} ); const hasMarkedBookedRef = useRef(false); const { shippingOrder, settings, quotes, dropshops, isLoading, orderError, quotesError, dropshopsError, fetchShipData, fetchQuotesWithPayload, } = useOrderShipData(orderId); const order = shippingOrder != null ? shippingOrder : null; const orderLabel = order?.number ?? String(order?.id ?? orderId); const isProcessing = order?.rawStatus === 'processing' || order?.status === 'processing'; const isCompleted = order?.rawStatus === 'completed'; const isPaid = order?.p2g?.shipStatus === 'Paid' || meta.order_status === 'Paid'; const isBooked = order?.p2g?.shipStatus === 'Booked' || meta.order_status === 'Booked'; const inProgress = isPaid || isBooked; const isFulfilled = order?.p2g?.shipStatus === 'Fulfilled' || meta.order_status === 'Fulfilled'; useEffect(() => { setTag('page', 'shipping_quotes_page'); setTag('order_id', String(orderId)); addBreadcrumb( 'shipping_quotes_page_loaded', { orderId: String(orderId), shop: getSiteUrl(), origin: settings?.shipping, destination: shippingOrder?.shipping, products: shippingOrder?.items, }, 'shipping_quotes_page' ); }, [orderId, settings, shippingOrder]); useEffect(() => { if ( !shippingOrder?.p2g?.completeHash || !orderId || !inProgress || isFulfilled ) { return; } const orderIdParam = shippingOrder?.p2g?.orderId; const hashParam = shippingOrder?.p2g?.completeHash ?? ''; const query = new URLSearchParams({ orderId: String(orderIdParam), hash: hashParam, }).toString(); hasMarkedBookedRef.current = isBooked; let tries = 0; const interval = setInterval(async () => { try { const response = (await withSpan( 'Checking P2G order status', 'http.client', async () => apiFetch({ path: `/parcel2go-shipping/v1/order/p2g-status?${query}`, method: 'GET', }) as Promise, { orderId: orderIdParam, hash: hashParam } )) as OrderCompleteResponse; const items = response.result.order?.items ?? []; const orderLineIds = items .filter((item) => item.reference === String(orderId)) .map((item) => String(item.id ?? '')) .filter(Boolean); if (orderLineIds.length > 0) { setP2gOrderLineIds(orderLineIds); } if (!hasMarkedBookedRef.current) { const orderItems = items.filter( (item) => item.reference === String(orderId) ); const isOrderBooked = orderItems.length > 0 && orderItems.every((item) => item.booked); if (isOrderBooked) { hasMarkedBookedRef.current = true; const labelHashFromP2g = response.result.order?.labelHash ?? ''; await markOrderAsBooked( [String(orderId)], labelHashFromP2g || undefined, orderLineIds ); setMeta((m) => ({ ...m, order_status: 'Booked', ...(orderLineIds.length > 0 ? { p2gOrderLineIds: orderLineIds } : {}), ...(labelHashFromP2g ? { labelHashSavedThisSession: true } : {}), })); } } const parcelNumbers = items .flatMap((item) => (item.parcels ?? []).flatMap((parcel) => (parcel.references ?? []) .filter( (reference) => reference.type === 'ParcelNumber' ) .map((reference) => reference.reference) ) ) .filter(Boolean); if (parcelNumbers.length > 0) { const result = await markOrderAsComplete( [String(orderId)], parcelNumbers, orderLineIds ); if (result.success) { setMeta((m) => ({ ...m, order_status: 'Fulfilled', ...(orderLineIds.length > 0 ? { p2gOrderLineIds: orderLineIds } : {}), })); clearInterval(interval); } } const firstItem = items[0]; if (firstItem) setOrderReference(firstItem.id ?? null); } catch (error) { captureException( error instanceof Error ? error : new Error(String(error)) ); } finally { tries++; setAttempts((a) => a + 1); if (tries >= MAX_P2G_STATUS_POLL_TRIES) { clearInterval(interval); captureException(new Error('Order status check timed out')); } } }, P2G_STATUS_POLL_INTERVAL_MS); return () => { clearInterval(interval); setAttempts(0); }; }, [ orderId, shippingOrder?.p2g?.completeHash, inProgress, isBooked, isFulfilled, ]); const { width } = useWindowSize(); const isWide = width >= 783; const store = settings?.shipping ?? null; const isStoreUK = useMemo( () => isUK(store?.country ?? ''), [store?.country] ); const totalValue = useMemo( () => shippingOrder?.items?.reduce( (acc, item) => acc + Number(item.total ?? 0), 0 ), [shippingOrder?.items] ); const customerSelectionText = useMemo(() => { const title = shippingOrder?.shipping_method?.title; return title ? sprintf( __( 'Your customer selected %s shipping at checkout. Choose a service below to fulfil this order.', 'parcel2go-shipping' ), title ) : undefined; }, [shippingOrder?.shipping_method?.title]); const protectionLabel = useMemo(() => { return totalValue ? sprintf( __('Fully protect this £%s item', 'parcel2go-shipping'), totalValue.toFixed(2) ) : undefined; }, [totalValue]); const addressValidation = useMemo( () => validateAddresses(store, shippingOrder), [store, shippingOrder] ); const internationalMissing = useMemo( () => validateInternationalRequirements( shippingOrder ?? null, shippingOrder?.shipping?.country ?? '', isUK, { defaultTariffCode: settings?.overrides?.defaultTariffCode, defaultCountryOfManufacture: settings?.overrides?.defaultCountryOfManufacture, } ), [settings?.overrides, shippingOrder] ); const hasInternationalIssues = internationalMissing.settings.length > 0 || internationalMissing.products.length > 0; const showPrereqsNotice = !addressValidation.valid || hasInternationalIssues; /* Loading state */ if (isLoading) { return (
); } /* Error / missing data */ if (orderError || !shippingOrder || !settings) { return (
{orderError ?? __('Order not found.', 'parcel2go-shipping')}
); } /* Completed order */ if (isCompleted || isFulfilled) { return (
); } /* Non-processing order */ if (!isProcessing && !inProgress) { return (
); } return (
{showPrereqsNotice && (
item.dimensions?.weight && Number(item.dimensions.weight) > 0 ) ?? false } orderId={order.id} settingsUrl={isLinked ? getPluginSettingsPageUrl() : getSettingsPageUrl()} getEditOrderUrl={getEditOrderUrl} />
)} {store && ( )} {inProgress ? ( ) : isStoreUK ? ( } > ) : ( {__( 'Parcel2Go Shipping only supports shops and orders in the United Kingdom.', 'parcel2go-shipping' )} )}
); }