/** * Hook: fetch order + quotes + dropshops for the ship page. * Store (origin) and overrides come from settings APIs. */ import { useEffect, useState, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import type { Quote, Order, Dropshop, PluginSettings } from '../../../types'; import type { QuotePayload } from '../../../types/quote'; import { getApiErrorMessage } from '../../../shared/utils'; import { captureException, addBreadcrumb, withSpan, captureMessage, } from '../../../shared/sentry'; import type { StoreSettingsResponse, DefaultSettingsOverrides } from '../../../types/settings'; export interface ShipDataOverrides extends DefaultSettingsOverrides {} interface ShipDataResponse { order: Order; quotes: Quote[]; dropshops: Record; quotesError?: string | null; dropshopsError?: string | null; } export interface UseOrderShipDataReturn { shippingOrder: Order | null; settings: PluginSettings | null; quotes: Quote[]; dropshops: Record; isLoading: boolean; orderError: string | null; quotesError: string | null; dropshopsError: string | null; fetchShipData: () => Promise; /** Fetch quotes with a custom payload (e.g. after user edits package dimensions). */ fetchQuotesWithPayload: (payload: QuotePayload) => Promise; } export function useOrderShipData(orderId: number): UseOrderShipDataReturn { const [shippingOrder, setShippingOrder] = useState(null); const [settings, setSettings] = useState(null); const [quotes, setQuotes] = useState([]); const [dropshops, setDropshops] = useState>({}); const [isLoading, setIsLoading] = useState(true); const [orderError, setOrderError] = useState(null); const [quotesError, setQuotesError] = useState(null); const [dropshopsError, setDropshopsError] = useState(null); const fetchShipData = useCallback(async () => { setIsLoading(true); setOrderError(null); setQuotesError(null); setDropshopsError(null); try { // Fetch ship data and settings in parallel const [shipData, settings] = await Promise.all([ withSpan( 'Get shipping quotes data', 'http.client', async () => apiFetch({ path: `/parcel2go-shipping/v1/orders/${orderId}/ship`, }) as Promise, { orderId: String(orderId) } ), apiFetch({ path: '/parcel2go-shipping/v1/settings', }) as Promise, ]); setShippingOrder(shipData.order); setSettings(settings); setQuotes(shipData.quotes ?? []); setDropshops(shipData.dropshops ?? {}); if (shipData.quotesError) { setQuotesError( getApiErrorMessage( shipData.quotesError, __('Failed to load quotes.', 'parcel2go-shipping') ) ); captureMessage('quotes_error_returned', 'warning', { orderId: String(orderId), quotesError: shipData.quotesError, }); } if (shipData.dropshopsError) { setDropshopsError( getApiErrorMessage( shipData.dropshopsError, __('Failed to load dropshops.', 'parcel2go-shipping') ) ); captureMessage('dropshops_error_returned', 'warning', { orderId: String(orderId), dropshopsError: shipData.dropshopsError, }); } } catch (err: unknown) { captureException( err instanceof Error ? err : new Error(String(err)) ); addBreadcrumb( 'shipping_quotes_data_fetch_failed', { orderId: String(orderId) }, 'api' ); setOrderError( getApiErrorMessage( err, __('Failed to load order.', 'parcel2go-shipping') ) ); } finally { setIsLoading(false); } }, [orderId]); const fetchQuotesWithPayload = useCallback(async (payload: QuotePayload) => { setQuotesError(null); try { const response = (await apiFetch({ path: '/parcel2go-shipping/v1/quotes', method: 'POST', data: payload, })) as { quotes?: Quote[] }; setQuotes(Array.isArray(response?.quotes) ? response.quotes : []); } catch (err: unknown) { const message = getApiErrorMessage( err, __('Failed to fetch quotes.', 'parcel2go-shipping') ); setQuotesError(message); throw err; } }, []); useEffect(() => { fetchShipData(); }, [fetchShipData]); return { shippingOrder, settings, quotes, dropshops, isLoading, orderError, quotesError, dropshopsError, fetchShipData, fetchQuotesWithPayload, }; }