import { useCallback, useRef, useState } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { captureException, withSpan } from '../sentry'; import { OrderCompleteResponse } from '../../types'; const MAX_TRIES = 40; const POLL_INTERVAL_MS = 2000; interface OrderStatusPollerOptions { onOrderBooked?: (orderId: string, orderLineIds: string[], labelHash: string) => Promise; onOrderComplete?: (orderId: string, parcelNumbers: string[], orderLineIds: string[]) => Promise; onTimeout?: () => void; onError?: (error: Error) => void; onAllComplete?: () => void; } export function useOrderStatusPoller(options: OrderStatusPollerOptions) { const timeoutRef = useRef | null>(null); const runningRef = useRef(false); const triesRef = useRef(0); const bookedRefs = useRef>(new Set()); const totalOrders = useRef(0); const [attempts, setAttempts] = useState(0); const [completedOrders, setCompletedOrders] = useState>(new Set()); const [bookedOrders, setBookedOrders] = useState>(new Set()); const stop = useCallback(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } runningRef.current = false; triesRef.current = 0; bookedRefs.current.clear(); }, []); const reset = useCallback(() => { stop(); setAttempts(0); setCompletedOrders(new Set()); setBookedOrders(new Set()); totalOrders.current = 0; }, [stop]); const handleOrderBooked = useCallback(async ( orderId: string, orderLineIds: string[], labelHash: string, ) => { await options.onOrderBooked?.(orderId, orderLineIds, labelHash); setBookedOrders(prev => new Set([...prev, orderId])); }, [options]); const handleOrderComplete = useCallback(async ( orderId: string, parcelNumbers: string[], orderLineIds: string[], ) => { await options.onOrderComplete?.(orderId, parcelNumbers, orderLineIds); setCompletedOrders(prev => { const next = new Set([...prev, orderId]); if (next.size === totalOrders.current) { stop(); options.onAllComplete?.(); } return next; }); }, [options, stop]); const pollOnce = useCallback(async ( p2gOrderId: string, hash: string, orderIds: string[], ) => { const query = new URLSearchParams({ orderId: String(p2gOrderId), hash, }).toString(); const response = await withSpan( 'Check P2G order status', 'http.client', () => apiFetch({ path: `/parcel2go-shipping/v1/order/p2g-status?${query}`, method: 'GET', }) as Promise, { orderId: p2gOrderId, hash } ) as OrderCompleteResponse & { error?: { message?: string; code?: string }; }; if (!response?.success) { const message = response?.error?.message || 'Failed to check order status.'; throw new Error(message); } const items = response.result.order?.items ?? []; const labelHash = response.result.order?.labelHash ?? ''; for (const currentOrderId of orderIds) { const orderItems = items.filter(item => item.reference === String(currentOrderId)); const orderLineIds = orderItems .map(item => String(item.id ?? '')) .filter(Boolean); // Mark booked — once per order if (!bookedRefs.current.has(currentOrderId)) { const isBooked = orderItems.length > 0 && orderItems.every(item => item.booked); if (isBooked) { bookedRefs.current.add(currentOrderId); await handleOrderBooked(currentOrderId, orderLineIds, labelHash); } } // Mark complete when parcel numbers arrive const parcelNumbers = orderItems .flatMap(item => (item.parcels ?? []) .flatMap(parcel => (parcel.references ?? []) .filter(ref => ref.type === 'ParcelNumber') .map(ref => ref.reference) ) ) .filter(Boolean); if (parcelNumbers.length > 0) { await handleOrderComplete(currentOrderId, parcelNumbers, orderLineIds); } } return items; }, [handleOrderBooked, handleOrderComplete]); const start = useCallback(( orderIds: string[], p2gOrderId: string, hash: string, ) => { reset(); totalOrders.current = orderIds.length; runningRef.current = true; const tick = async () => { if (!runningRef.current) return; try { await pollOnce(p2gOrderId, hash, orderIds); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); captureException(err, { tags: { action: 'poll_order_status' }, extra: { orderIds, p2gOrderId }, }); options.onError?.(err); } finally { triesRef.current += 1; setAttempts(triesRef.current); if (triesRef.current >= MAX_TRIES) { stop(); captureException(new Error('Bulk order status polling timed out'), { tags: { action: 'poll_order_status_timeout' }, extra: { orderIds, p2gOrderId, tries: triesRef.current }, }); options.onTimeout?.(); return; } if (runningRef.current) { timeoutRef.current = setTimeout(tick, POLL_INTERVAL_MS); } } }; tick(); // fire immediately; schedule next tick only after this one settles }, [reset, pollOnce, stop, options]); return { start, stop, reset, attempts, maxAttempts: MAX_TRIES, totalOrders: totalOrders.current, bookedCount: bookedOrders.size, completedCount: completedOrders.size, completedOrders, bookedOrders, progressPct: totalOrders.current > 0 ? Math.round((completedOrders.size / totalOrders.current) * 100) : 0, isPolling: runningRef.current, }; }