/** * Reusable hook: fetch and cache quotes for a given origin + parcels. * Does one thing: fetch quotes from the API and return quotes + error; handles caching. * Caller is responsible for ensuring payload has valid weight before calling fetchQuotes. */ import { useState, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import type { Quote } from '../../types/quote'; import type { QuotePayload } from '../../types/quote'; import { getApiErrorMessage } from '../utils'; const DEFAULT_CACHE_TTL_MS = 3 * 60 * 1000; // 3 minutes export interface UseQuotesFetcherOptions { /** Origin for the quote (country, postcode). */ origin: QuotePayload['origin']; destination?: QuotePayload['destination']; /** Parcels (weight, dimensions). Caller must ensure weight is set before fetching. */ parcels: QuotePayload['parcels']; cacheTtlMs?: number; } export interface UseQuotesFetcherReturn { quotesList: Quote[]; quotesLoading: boolean; quotesError: string | null; activeCacheKey: string | null; setActiveCacheKey: (key: string | null) => void; /** * Fetch quotes for the given destination. Uses cache if valid. * @param parcelsOverride - Optional one-off parcels for this request (e.g. when user confirms weight). */ fetchQuotes: ( cacheKey: string, destinationOverride?: { country: string; countryName?: string; postcode?: string }, parcelsOverride?: QuotePayload['parcels'] ) => void; } export function useQuotesFetcher( options: UseQuotesFetcherOptions ): UseQuotesFetcherReturn { const { origin, parcels, destination, cacheTtlMs = DEFAULT_CACHE_TTL_MS } = options; const [quotesList, setQuotesList] = useState([]); const [quotesLoading, setQuotesLoading] = useState(false); const [quotesError, setQuotesError] = useState(null); const [activeCacheKey, setActiveCacheKey] = useState(null); const [cache, setCache] = useState< Record >({}); const performFetch = useCallback( async ( destinationToUse: QuotePayload['destination'], cacheKey: string, parcelsToUse: QuotePayload['parcels'] ) => { const payload: QuotePayload = { origin, destination: destinationToUse, parcels: parcelsToUse, }; setQuotesError(null); setActiveCacheKey(cacheKey); setQuotesLoading(true); setQuotesList([]); try { const response = (await apiFetch({ path: '/parcel2go-shipping/v1/quotes', method: 'POST', data: payload, })) as { quotes?: Quote[] }; const list = Array.isArray(response?.quotes) ? response.quotes : []; setQuotesList(list); setCache((prev) => ({ ...prev, [cacheKey]: { quotes: list, fetchedAt: Date.now() }, })); } catch (err: unknown) { setQuotesError( getApiErrorMessage( err, __( 'Failed to fetch quotes.', 'parcel2go-shipping' ) ) ); } finally { setQuotesLoading(false); } }, [origin] ); const fetchQuotes = useCallback( ( cacheKey: string, destinationOverride?: QuotePayload['destination'], parcelsOverride?: QuotePayload['parcels'] ) => { const cached = cache[cacheKey]; const now = Date.now(); if (!parcelsOverride && cached && now - cached.fetchedAt < cacheTtlMs) { setQuotesError(null); setActiveCacheKey(cacheKey); setQuotesList(cached.quotes); return; } const destinationToUse = destinationOverride || destination; if(!destinationToUse){ setQuotesError( __( 'Missing destination data to fetch quotes.', 'parcel2go-shipping' ) ); return; } const parcelsToUse = parcelsOverride || parcels; if (!parcelsToUse?.length) { setQuotesError( __( 'Missing parcel data to fetch quotes.', 'parcel2go-shipping' ) ); return; } performFetch(destinationToUse, cacheKey, parcelsToUse); }, [cache, cacheTtlMs, parcels, performFetch, destination] ); return { quotesList, quotesLoading, quotesError, activeCacheKey, setActiveCacheKey, fetchQuotes, }; }