"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { catalogService } from "@/features/catalog/services/catalog.service"; import { ordersService } from "@/features/orders/services/orders.service"; import { usePaymentMethods } from "@/features/billing/hooks/useBilling"; import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh"; import type { CatalogProductBase } from "@customer-portal/domain/catalog"; import { createLoadingState, createSuccessState, createErrorState } from "@customer-portal/domain/toolkit"; import type { AsyncState } from "@customer-portal/domain/toolkit"; import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions"; import { ORDER_TYPE, orderConfigurationsSchema, type OrderConfigurations, type OrderTypeValue, } from "@customer-portal/domain/orders"; // Use domain Address type import type { Address } from "@customer-portal/domain/customer"; type CheckoutItemType = "plan" | "installation" | "addon" | "activation" | "vpn"; interface CheckoutItem extends CatalogProductBase { quantity: number; itemType: CheckoutItemType; autoAdded?: boolean; } interface CheckoutTotals { monthlyTotal: number; oneTimeTotal: number; } interface CheckoutCart { items: CheckoutItem[]; totals: CheckoutTotals; configuration: OrderConfigurations; } export function useCheckout() { const params = useSearchParams(); const router = useRouter(); const [submitting, setSubmitting] = useState(false); const [addressConfirmed, setAddressConfirmed] = useState(false); const [confirmedAddress, setConfirmedAddress] = useState
(null); const [checkoutState, setCheckoutState] = useState>({ status: "loading", }); // Load active subscriptions to enforce business rules client-side before submission const { data: activeSubs } = useActiveSubscriptions(); const { data: paymentMethods, isLoading: paymentMethodsLoading, error: paymentMethodsError, refetch: refetchPaymentMethods, } = usePaymentMethods(); const paymentRefresh = usePaymentRefresh({ refetch: refetchPaymentMethods, attachFocusListeners: true, }); const orderType: OrderTypeValue = useMemo(() => { const type = params.get("type")?.toLowerCase() ?? "internet"; switch (type) { case "sim": return ORDER_TYPE.SIM; case "vpn": return ORDER_TYPE.VPN; case "other": return ORDER_TYPE.OTHER; case "internet": default: return ORDER_TYPE.INTERNET; } }, [params]); const selections = useMemo(() => { const obj: Record = {}; params.forEach((v, k) => { if (k !== "type") obj[k] = v; }); return obj; }, [params]); const simConfig = useMemo(() => { if (orderType !== ORDER_TYPE.SIM) { return null; } const rawConfig = params.get("simConfig"); if (!rawConfig) { return null; } try { const parsed = JSON.parse(rawConfig) as unknown; return orderConfigurationsSchema.parse(parsed); } catch (error) { console.warn("Failed to parse SIM order configuration from query params", error); return null; } }, [orderType, params]); useEffect(() => { let mounted = true; const collectAddonRefs = () => { const refs = new Set(); params.getAll("addonSku").forEach(sku => { if (sku) refs.add(sku); }); if (selections.addonSku) refs.add(selections.addonSku); if (selections.addons) { selections.addons .split(",") .map(value => value.trim()) .filter(Boolean) .forEach(value => refs.add(value)); } return Array.from(refs); }; const calculateTotals = (items: CheckoutItem[]): CheckoutTotals => items.reduce( (acc, item) => { if (typeof item.monthlyPrice === "number") { acc.monthlyTotal += item.monthlyPrice * item.quantity; } if (typeof item.oneTimePrice === "number") { acc.oneTimeTotal += item.oneTimePrice * item.quantity; } return acc; }, { monthlyTotal: 0, oneTimeTotal: 0 } ); void (async () => { try { setCheckoutState(createLoadingState()); const planRef = selections.plan ?? selections.planId ?? selections.planSku ?? selections.planIdSku ?? null; if (!planRef) { throw new Error("No plan selected. Please go back and select a plan."); } const addonRefs = collectAddonRefs(); const items: CheckoutItem[] = []; if (orderType === "Internet") { const { plans, addons, installations } = await catalogService.getInternetCatalog(); const plan = plans.find(p => p.sku === planRef || p.id === planRef) ?? null; if (!plan) { throw new Error( `Internet plan not found for reference: ${planRef}. Please go back and select a valid plan.` ); } items.push({ ...plan, quantity: 1, itemType: "plan" }); if (selections.installationSku) { const installation = installations.find( inst => inst.sku === selections.installationSku || inst.id === selections.installationSku ) ?? null; if (!installation) { throw new Error( `Installation option not found for reference: ${selections.installationSku}. Please reselect your installation method.` ); } items.push({ ...installation, quantity: 1, itemType: "installation" }); } addonRefs.forEach(ref => { const addon = addons.find(a => a.sku === ref || a.id === ref) ?? null; if (addon) { items.push({ ...addon, quantity: 1, itemType: "addon" }); } }); } else if (orderType === "SIM") { const { plans, activationFees, addons } = await catalogService.getSimCatalog(); const plan = plans.find(p => p.sku === planRef || p.id === planRef) ?? null; if (!plan) { throw new Error( `SIM plan not found for reference: ${planRef}. Please go back and select a valid plan.` ); } items.push({ ...plan, quantity: 1, itemType: "plan" }); addonRefs.forEach(ref => { const addon = addons.find(a => a.sku === ref || a.id === ref) ?? null; if (addon) { items.push({ ...addon, quantity: 1, itemType: "addon" }); } }); const simType = selections.simType ?? "eSIM"; const activation = activationFees.find(fee => { const feeSimType = (fee as unknown as { simType?: string }).simType || ((fee.catalogMetadata as { simType?: string } | undefined)?.simType ?? undefined); return feeSimType ? feeSimType === simType : fee.sku === selections.activationFeeSku || fee.id === selections.activationFeeSku; }) ?? null; if (activation) { items.push({ ...activation, quantity: 1, itemType: "activation" }); } } else if (orderType === "VPN") { const { plans, activationFees } = await catalogService.getVpnCatalog(); const plan = plans.find(p => p.sku === planRef || p.id === planRef) ?? null; if (!plan) { throw new Error( `VPN plan not found for reference: ${planRef}. Please go back and select a valid plan.` ); } items.push({ ...plan, quantity: 1, itemType: "vpn" }); const activation = activationFees.find( fee => fee.sku === selections.activationSku || fee.id === selections.activationSku ) ?? null; if (activation) { items.push({ ...activation, quantity: 1, itemType: "activation" }); } } else { throw new Error("Unsupported order type. Please begin checkout from the catalog."); } if (!mounted) return; const totals = calculateTotals(items); const configuration = orderType === ORDER_TYPE.SIM && simConfig ? simConfig : ({} as OrderConfigurations); setCheckoutState( createSuccessState({ items, totals, configuration, }) ); } catch (error) { if (mounted) { const reason = error instanceof Error ? error.message : "Failed to load checkout data"; setCheckoutState(createErrorState(new Error(reason))); } } })(); return () => { mounted = false; }; }, [orderType, params, selections, simConfig]); const handleSubmitOrder = useCallback(async () => { try { setSubmitting(true); if (checkoutState.status !== "success") { throw new Error("Checkout data not loaded"); } const uniqueSkus = Array.from( new Set( checkoutState.data.items .map(item => item.sku) .filter((sku): sku is string => typeof sku === "string" && sku.trim().length > 0) ) ); if (uniqueSkus.length === 0) { throw new Error("No products selected for order. Please go back and select products."); } let configurationAccumulator: Partial = {}; if (orderType === ORDER_TYPE.SIM) { if (simConfig) { configurationAccumulator = { ...simConfig }; } else { configurationAccumulator = { ...(selections.simType ? { simType: selections.simType as OrderConfigurations["simType"] } : {}), ...(selections.activationType ? { activationType: selections.activationType as OrderConfigurations["activationType"] } : {}), ...(selections.scheduledAt ? { scheduledAt: selections.scheduledAt } : {}), ...(selections.eid ? { eid: selections.eid } : {}), ...(selections.isMnp ? { isMnp: selections.isMnp } : {}), ...(selections.reservationNumber ? { mnpNumber: selections.reservationNumber } : {}), ...(selections.expiryDate ? { mnpExpiry: selections.expiryDate } : {}), ...(selections.phoneNumber ? { mnpPhone: selections.phoneNumber } : {}), ...(selections.mvnoAccountNumber ? { mvnoAccountNumber: selections.mvnoAccountNumber } : {}), ...(selections.portingLastName ? { portingLastName: selections.portingLastName } : {}), ...(selections.portingFirstName ? { portingFirstName: selections.portingFirstName } : {}), ...(selections.portingLastNameKatakana ? { portingLastNameKatakana: selections.portingLastNameKatakana } : {}), ...(selections.portingFirstNameKatakana ? { portingFirstNameKatakana: selections.portingFirstNameKatakana } : {}), ...(selections.portingGender ? { portingGender: selections.portingGender as OrderConfigurations["portingGender"], } : {}), ...(selections.portingDateOfBirth ? { portingDateOfBirth: selections.portingDateOfBirth } : {}), }; } } else { configurationAccumulator = { ...(selections.accessMode ? { accessMode: selections.accessMode as OrderConfigurations["accessMode"] } : {}), ...(selections.activationType ? { activationType: selections.activationType as OrderConfigurations["activationType"] } : {}), ...(selections.scheduledAt ? { scheduledAt: selections.scheduledAt } : {}), }; } if (confirmedAddress) { configurationAccumulator.address = { street: confirmedAddress.address1 ?? undefined, streetLine2: confirmedAddress.address2 ?? undefined, city: confirmedAddress.city ?? undefined, state: confirmedAddress.state ?? undefined, postalCode: confirmedAddress.postcode ?? undefined, country: confirmedAddress.country ?? undefined, }; } const hasConfiguration = Object.keys(configurationAccumulator).length > 0; const configurations = hasConfiguration ? orderConfigurationsSchema.parse(configurationAccumulator) : undefined; const orderData = { orderType, skus: uniqueSkus, ...(configurations ? { configurations } : {}), }; if (orderType === ORDER_TYPE.SIM) { if (!configurations) { throw new Error("SIM configuration is incomplete. Please restart the SIM configuration flow."); } if (configurations?.simType === "eSIM" && !configurations.eid) { throw new Error( "EID is required for eSIM activation. Please go back and provide your EID." ); } if (!configurations?.mnpPhone) { throw new Error( "Phone number is required for SIM activation. Please go back and provide a phone number." ); } } // Client-side guard: prevent Internet orders if an Internet subscription already exists if (orderType === "Internet" && Array.isArray(activeSubs)) { const hasActiveInternet = activeSubs.some( s => String(s.groupName || s.productName || "") .toLowerCase() .includes("internet") && String(s.status || "").toLowerCase() === "active" ); if (hasActiveInternet) { throw new Error( "You already have an active Internet subscription. Please contact support to modify your service." ); } } const response = await ordersService.createOrder(orderData); router.push(`/orders/${response.sfOrderId}?status=success`); } catch (error) { let errorMessage = "Order submission failed"; if (error instanceof Error) errorMessage = error.message; setCheckoutState(createErrorState(new Error(errorMessage))); } finally { setSubmitting(false); } }, [checkoutState, confirmedAddress, orderType, selections, router, simConfig, activeSubs]); const confirmAddress = useCallback((address?: Address) => { setAddressConfirmed(true); setConfirmedAddress(address || null); }, []); const markAddressIncomplete = useCallback(() => { setAddressConfirmed(false); setConfirmedAddress(null); }, []); const navigateBackToConfigure = useCallback(() => { const urlParams = new URLSearchParams(params.toString()); const reviewStep = orderType === "Internet" ? "4" : "5"; urlParams.set("step", reviewStep); const configureUrl = orderType === "Internet" ? `/catalog/internet/configure?${urlParams.toString()}` : `/catalog/sim/configure?${urlParams.toString()}`; router.push(configureUrl); }, [orderType, params, router]); return { checkoutState, submitting, orderType, addressConfirmed, paymentMethods, paymentMethodsLoading, paymentMethodsError, paymentRefresh, confirmAddress, markAddressIncomplete, handleSubmitOrder, navigateBackToConfigure, } as const; }