import { useState, useCallback, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { CheckoutOrderResult, CheckoutValidateRequest, GenericPaymentResponse, Order, PluginSettings, } from '../../../types'; import { useCheckoutValidation } from '../../../shared/hooks/useCheckoutValidation'; import { useCheckoutOrder } from '../../../shared/hooks'; import { useOrderStatusPoller } from '../../../shared/hooks/useOrderStatusPoller'; import { markBulkOrdersAsPaid, markOrderAsBooked, markOrderAsComplete, } from '../../order-ship/utils/shippingStatus'; import ordersToCheckoutValidatePayload from '../utils/ordersToCheckoutValidatePayload'; import { getSiteUrl } from '../../../shared/utils/urls'; import { captureException } from '../../../shared/sentry'; type BulkStep = 'idle' | 'confirming' | 'payment' | 'success' | 'prepay-topup' | 'prepay-processing'; type BulkMetaValue = string | string[] | boolean | Record; export function useBulkShipFlow( selectedOrders: Order[], settings: PluginSettings, onComplete: () => void ) { const [step, setStep] = useState('idle'); const [error, setError] = useState(null); const [ordering, setOrdering] = useState(false); const [meta, setMeta] = useState>({}); const [orderReference, setOrderReference] = useState<{ reference: number; orderLineCount: number; } | null>(null); const [pendingPayment, setPendingPayment] = useState<{ order: CheckoutValidateRequest; orderResult: CheckoutOrderResult; } | null>(null); const [hasSuccessfullyPaid, setHasSuccessfullyPaid] = useState(false); const { validate, isValidating, validationResult, resetValidation } = useCheckoutValidation(); const { createOrderFromPayload, isCreatingOrder } = useCheckoutOrder(); const isPaid = meta.order_status === 'Paid'; const isBooked = meta.order_status === 'Booked'; const isFulfilled = meta.order_status === 'Fulfilled'; const p2gOrderId = typeof meta.p2gOrderId === 'string' ? meta.p2gOrderId : ''; const p2gOrderLineIds = Array.isArray(meta.p2gOrderLineIds) ? meta.p2gOrderLineIds .map((orderLineId) => String(orderLineId ?? '').trim()) .filter(Boolean) : []; const labelHashSavedThisSession = meta.labelHashSavedThisSession === true; const inProgress = isPaid || isBooked || ordering; const working = isValidating || isCreatingOrder || ordering; const poller = useOrderStatusPoller({ onOrderBooked: async (orderId, orderLineIds, labelHash) => { await markOrderAsBooked( [orderId], labelHash || undefined, orderLineIds ); setMeta((m) => ({ ...m, order_status: 'Booked', ...(orderLineIds.length > 0 ? { p2gOrderLineIds: orderLineIds } : {}), ...(labelHash ? { labelHashSavedThisSession: true } : {}), })); }, onOrderComplete: async (orderId, parcelNumbers, orderLineIds) => { const result = await markOrderAsComplete( [orderId], parcelNumbers, orderLineIds ); if (result.success) { setMeta((m) => ({ ...m, order_status: 'Fulfilled', ...(orderLineIds.length > 0 ? { p2gOrderLineIds: orderLineIds } : {}), })); } }, onAllComplete: () => { setStep('success'); }, onTimeout: () => setError( __( 'Order status check timed out. Please refresh to see the latest status.', 'parcel2go-shipping' ) ), onError: (err) => setError(err.message), }); const buildPayload = useCallback(() => { const payload = ordersToCheckoutValidatePayload({ orders: selectedOrders, settings, }); if (!payload) { setError(__('Failed to prepare order data.', 'parcel2go-shipping')); return null; } const wooShop = getSiteUrl(); for (const item of payload.items) { item.metadata = { ...(item.metadata as Record | undefined), wooShop, wooOrderId: payload.ref, }; } return payload; }, [selectedOrders, settings]); const validateOrder = useCallback(async () => { setStep('confirming'); setError(null); try { const payload = buildPayload(); if (payload) await validate(payload); } catch (err) { setError( __( 'An error occurred while validating the order.', 'parcel2go-shipping' ) ); captureException(err, { tags: { feature: 'bulk-ship', action: 'validateOrder' }, }); } }, [buildPayload, validate]); const createOrder = useCallback(async (): Promise<{ order: CheckoutValidateRequest; orderResult: CheckoutOrderResult; } | null> => { setError(null); try { const payload = buildPayload(); if (!payload) return null; const result = await createOrderFromPayload(payload); if (!result) return null; setOrderReference({ reference: parseInt(result.orderId ?? '0'), orderLineCount: payload.items.length, }); return { order: payload, orderResult: result }; } catch (err) { setError( __( 'An error occurred while creating the order.', 'parcel2go-shipping' ) ); captureException(err, { tags: { feature: 'bulk-ship', action: 'createOrder' }, }); return null; } }, [buildPayload, createOrderFromPayload]); const onPostPayment = useCallback( async (p2gOrderId: string, completeHash: string) => { const orderIds = selectedOrders.map((o) => String(o.id)); setStep('confirming'); poller.start(orderIds, p2gOrderId, completeHash); }, [selectedOrders, poller, setStep] ); const onPaymentSuccess = useCallback( async ( paymentResponse: GenericPaymentResponse, payment: { order: CheckoutValidateRequest; orderResult: CheckoutOrderResult; } ) => { const p2gOrderId = paymentResponse.orderId; const completeHash = paymentResponse.completeHash; if (!p2gOrderId || !completeHash) { setError( __( 'An error occurred while processing the payment.', 'parcel2go-shipping' ) ); return; } try { setHasSuccessfullyPaid(true); await markBulkOrdersAsPaid(selectedOrders, paymentResponse); setMeta((m) => ({ ...m, order_status: 'Paid', p2gOrderId, })); await onPostPayment(p2gOrderId, completeHash); } catch (err) { const message = err instanceof Error ? err.message : __( 'Failed to mark orders as paid. Please try again.', 'parcel2go-shipping' ); setError(message); captureException( err instanceof Error ? err : new Error(String(err)) ); } }, [selectedOrders, onPostPayment] ); const handlePayment = useCallback( async ( method: 'card' | 'prepay', makePrepayPayment: ( orderId: string, hash: string ) => Promise ) => { const response = await createOrder(); if (!response) return; if (method === 'prepay') { setStep('prepay-processing'); // ← show loading immediately const { orderId, hash } = response.orderResult; const paymentResponse = await makePrepayPayment(orderId, hash); if (paymentResponse) { await onPaymentSuccess(paymentResponse, response); } return; } setPendingPayment(response); setStep('payment'); }, [createOrder, onPaymentSuccess] ); const reset = useCallback(() => { setStep('idle'); setError(null); setMeta({}); setHasSuccessfullyPaid(false); setOrdering(false); setOrderReference(null); setPendingPayment(null); // ← add this resetValidation(); poller.stop(); }, [resetValidation, poller]); const handleClose = useCallback(() => { const shouldRefresh = hasSuccessfullyPaid || isPaid || isBooked || isFulfilled; reset(); if (shouldRefresh) onComplete(); }, [reset, hasSuccessfullyPaid, isPaid, isBooked, isFulfilled, onComplete]); const progress = useMemo( () => ({ paid: isPaid, booked: poller.bookedOrders.size > 0, fulfilled: poller.completedOrders.size === poller.totalOrders && poller.totalOrders > 0, bookedOrderLineCount: poller.bookedOrders.size, orderLineCount: poller.totalOrders, attempts: poller.attempts, }), [ isPaid, poller.bookedOrders.size, poller.completedOrders.size, poller.totalOrders, poller.attempts, ] ); return { step, setStep, error, setError, pendingPayment, working, inProgress, isFulfilled, p2gOrderId, p2gOrderLineIds, labelHashSavedThisSession, isValidating, isCreatingOrder, validationResult, orderReference, progress, poller, validateOrder, handlePayment, onPaymentSuccess, handleClose, reset, }; }