From 543afc8a10475757372e9e0b671bd4215547a88d Mon Sep 17 00:00:00 2001 From: tema Date: Sat, 30 Aug 2025 18:51:51 +0900 Subject: [PATCH] Enhance order processing and address confirmation features - Added detailed logging for order creation and error handling in OrdersController and OrderOrchestrator. - Introduced address management enhancements in Order DTO to support updated checkout processes. - Improved address confirmation component to handle controlled state and prevent unnecessary updates for Internet orders. - Added validation checks for Internet orders to ensure required service plans and installations are selected. - Enhanced debugging information in Checkout component for better tracking of order submission and address confirmation states. --- apps/bff/src/orders/dto/order.dto.ts | 12 +++ apps/bff/src/orders/orders.controller.ts | 3 + .../services/order-orchestrator.service.ts | 11 +++ apps/portal/src/app/checkout/page.tsx | 89 +++++++++++++---- .../checkout/address-confirmation.tsx | 96 +++++++++++++++---- 5 files changed, 177 insertions(+), 34 deletions(-) diff --git a/apps/bff/src/orders/dto/order.dto.ts b/apps/bff/src/orders/dto/order.dto.ts index cb48327b..6ff25775 100644 --- a/apps/bff/src/orders/dto/order.dto.ts +++ b/apps/bff/src/orders/dto/order.dto.ts @@ -78,6 +78,18 @@ export class OrderConfigurations { @IsString() portingDateOfBirth?: string; + // Address (when address is updated during checkout) + @IsOptional() + @IsObject() + address?: { + street?: string | null; + streetLine2?: string | null; + city?: string | null; + state?: string | null; + postalCode?: string | null; + country?: string | null; + }; + // VPN region is inferred from product VPN_Region__c field, no user input needed } diff --git a/apps/bff/src/orders/orders.controller.ts b/apps/bff/src/orders/orders.controller.ts index 648db600..cf4647d2 100644 --- a/apps/bff/src/orders/orders.controller.ts +++ b/apps/bff/src/orders/orders.controller.ts @@ -24,6 +24,7 @@ export class OrdersController { userId: req.user?.id, orderType: body.orderType, skuCount: body.skus?.length || 0, + requestBody: JSON.stringify(body, null, 2), }, "Order creation request received" ); @@ -34,8 +35,10 @@ export class OrdersController { this.logger.error( { error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, userId: req.user?.id, orderType: body.orderType, + fullRequestBody: JSON.stringify(body, null, 2), }, "Order creation failed" ); diff --git a/apps/bff/src/orders/services/order-orchestrator.service.ts b/apps/bff/src/orders/services/order-orchestrator.service.ts index 58a9b46c..3f6b8bfd 100644 --- a/apps/bff/src/orders/services/order-orchestrator.service.ts +++ b/apps/bff/src/orders/services/order-orchestrator.service.ts @@ -57,13 +57,24 @@ export class OrderOrchestrator { // 4) Create Order in Salesforce let created: { id: string }; try { + this.logger.log( + { + orderFields: JSON.stringify(orderFields, null, 2), + fieldsCount: Object.keys(orderFields).length + }, + "About to create Salesforce Order with fields" + ); + created = (await this.sf.sobject("Order").create(orderFields)) as { id: string }; this.logger.log({ orderId: created.id }, "Salesforce Order created successfully"); } catch (error) { this.logger.error( { error: error instanceof Error ? error.message : String(error), + errorDetails: error, + stack: error instanceof Error ? error.stack : undefined, orderType: orderFields.Type, + orderFields: JSON.stringify(orderFields, null, 2), }, "Failed to create Salesforce Order" ); diff --git a/apps/portal/src/app/checkout/page.tsx b/apps/portal/src/app/checkout/page.tsx index f19f8b42..745c5426 100644 --- a/apps/portal/src/app/checkout/page.tsx +++ b/apps/portal/src/app/checkout/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useMemo, Suspense } from "react"; +import { useState, useEffect, useMemo, useCallback, Suspense } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { PageLayout } from "@/components/layout/page-layout"; import { ShieldCheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline"; @@ -38,6 +38,7 @@ function CheckoutContent() { const [submitting, setSubmitting] = useState(false); const [addressConfirmed, setAddressConfirmed] = useState(false); const [confirmedAddress, setConfirmedAddress] = useState
(null); + const [forceUpdate, setForceUpdate] = useState(0); const [checkoutState, setCheckoutState] = useState({ loading: true, error: null, @@ -169,6 +170,11 @@ function CheckoutContent() { }; }, [orderType, selections]); + // Debug effect to track addressConfirmed changes + useEffect(() => { + console.log("🎯 PARENT: addressConfirmed state changed to:", addressConfirmed); + }, [addressConfirmed]); + const handleSubmitOrder = async () => { try { setSubmitting(true); @@ -180,6 +186,28 @@ function CheckoutContent() { throw new Error("No products selected for order. Please go back and select products."); } + // Additional validation for Internet orders + if (orderType === "Internet") { + const hasServicePlan = checkoutState.orderItems.some(item => item.type === "service"); + const hasInstallation = checkoutState.orderItems.some(item => item.type === "installation"); + + console.log("🔍 Internet order validation:", { + hasServicePlan, + hasInstallation, + orderItems: checkoutState.orderItems, + selections: selections + }); + + if (!hasServicePlan) { + throw new Error("Internet service plan is required. Please go back and select a plan."); + } + + // Installation is typically required for Internet orders + if (!hasInstallation) { + console.warn("⚠️ No installation selected for Internet order - this might cause issues"); + } + } + // Send SKUs + configurations - backend resolves product data from SKUs, // uses configurations for fields that cannot be inferred const configurations: Record = {}; @@ -221,10 +249,29 @@ function CheckoutContent() { ...(Object.keys(configurations).length > 0 && { configurations }), }; + console.log("🚀 Submitting order with data:", JSON.stringify(orderData, null, 2)); + console.log("🚀 Address confirmed state:", addressConfirmed); + console.log("🚀 Confirmed address:", confirmedAddress); + console.log("🚀 Order type:", orderType); + console.log("🚀 SKUs:", skus); + const response = await authenticatedApi.post<{ sfOrderId: string }>("/orders", orderData); router.push(`/orders/${response.sfOrderId}?status=success`); } catch (error) { - console.error("Order submission failed:", error); + console.error("🚨 Order submission failed:", error); + + // Enhanced error logging for debugging + if (error instanceof Error) { + console.error("🚨 Error name:", error.name); + console.error("🚨 Error message:", error.message); + console.error("🚨 Error stack:", error.stack); + } + + // If it's an API error, try to get more details + if (error && typeof error === 'object' && 'status' in error) { + console.error("🚨 HTTP Status:", error.status); + console.error("🚨 Error details:", error); + } let errorMessage = "Order submission failed"; if (error instanceof Error) { @@ -240,18 +287,25 @@ function CheckoutContent() { } }; - const handleAddressConfirmed = (address?: Address) => { + const handleAddressConfirmed = useCallback((address?: Address) => { console.log("🎯 PARENT: handleAddressConfirmed called with:", address); console.log("🎯 PARENT: Current addressConfirmed state before:", addressConfirmed); - setAddressConfirmed(true); - setConfirmedAddress(address || null); - console.log("🎯 PARENT: addressConfirmed state set to true"); - // Force a log after state update (in next tick) - setTimeout(() => { - console.log("🎯 PARENT: addressConfirmed state after update:", addressConfirmed); - }, 0); - }; + console.log("🎯 PARENT: About to call setAddressConfirmed(true)..."); + setAddressConfirmed(prev => { + console.log("🎯 PARENT: setAddressConfirmed functional update - prev:", prev, "-> true"); + return true; + }); + console.log("🎯 PARENT: setAddressConfirmed(true) called"); + + console.log("🎯 PARENT: About to call setConfirmedAddress..."); + setConfirmedAddress(address || null); + console.log("🎯 PARENT: setConfirmedAddress called"); + + // Force a re-render to ensure the UI updates + setForceUpdate(prev => prev + 1); + console.log("🎯 PARENT: Force update triggered"); + }, [addressConfirmed]); const handleAddressIncomplete = () => { setAddressConfirmed(false); @@ -432,11 +486,13 @@ function CheckoutContent() { )} - {/* Debug Info - Remove in production */} -
- Debug Info: Address Confirmed: {addressConfirmed ? '✅' : '❌'} ({String(addressConfirmed)}) | - Payment Methods: {paymentMethodsLoading ? '⏳ Loading...' : paymentMethodsError ? '❌ Error' : paymentMethods ? `✅ ${paymentMethods.paymentMethods.length} found` : '❌ None'} | + {/* Debug Info - Remove in production */} +
+ Debug Info: Address Confirmed: {addressConfirmed ? '✅ TRUE' : '❌ FALSE'} | + Order Type: {orderType} | Order Items: {checkoutState.orderItems.length} | + Payment Methods: {paymentMethodsLoading ? '⏳ Loading...' : paymentMethodsError ? '❌ Error' : paymentMethods ? `✅ ${paymentMethods.paymentMethods.length} found` : '❌ None'} | + Force Update: {forceUpdate} | Can Submit: {!( submitting || checkoutState.orderItems.length === 0 || @@ -444,8 +500,7 @@ function CheckoutContent() { paymentMethodsLoading || !paymentMethods || paymentMethods.paymentMethods.length === 0 - ) ? '✅' : '❌'} | - Render Time: {new Date().toLocaleTimeString()} + ) ? '✅ YES' : '❌ NO'}
diff --git a/apps/portal/src/components/checkout/address-confirmation.tsx b/apps/portal/src/components/checkout/address-confirmation.tsx index 278754cd..71f6bcbd 100644 --- a/apps/portal/src/components/checkout/address-confirmation.tsx +++ b/apps/portal/src/components/checkout/address-confirmation.tsx @@ -31,19 +31,38 @@ interface AddressConfirmationProps { onAddressConfirmed: (address?: Address) => void; onAddressIncomplete: () => void; orderType?: string; // Add order type to customize behavior + // Optional controlled props for parent state management + addressConfirmed?: boolean; // If provided, use this instead of internal state + onAddressConfirmationChange?: (confirmed: boolean) => void; // Callback for controlled mode } export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete, orderType, + addressConfirmed: controlledAddressConfirmed, + onAddressConfirmationChange, }: AddressConfirmationProps) { const [billingInfo, setBillingInfo] = useState(null); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); const [editedAddress, setEditedAddress] = useState
(null); const [error, setError] = useState(null); - const [addressConfirmed, setAddressConfirmed] = useState(false); + const [internalAddressConfirmed, setInternalAddressConfirmed] = useState(false); + + // Use controlled prop if provided, otherwise use internal state + const addressConfirmed = controlledAddressConfirmed ?? internalAddressConfirmed; + const setAddressConfirmed = (value: boolean | ((prev: boolean) => boolean)) => { + const newValue = typeof value === 'function' ? value(addressConfirmed) : value; + + if (controlledAddressConfirmed !== undefined && onAddressConfirmationChange) { + // Controlled mode: notify parent + onAddressConfirmationChange(newValue); + } else { + // Uncontrolled mode: update internal state + setInternalAddressConfirmed(newValue); + } + }; const isInternetOrder = orderType === "Internet"; const requiresAddressVerification = isInternetOrder; @@ -57,20 +76,33 @@ export function AddressConfirmation({ // Since address is required at signup, it should always be complete // But we still need verification for Internet orders if (requiresAddressVerification) { - // For Internet orders, don't auto-confirm - require explicit verification - setAddressConfirmed(false); - onAddressIncomplete(); // Keep disabled until explicitly confirmed + // For Internet orders, only reset confirmation state if not already confirmed + // This prevents clobbering existing confirmation on re-renders/re-fetches + if (!addressConfirmed) { + console.log("🏠 Internet order: Setting initial unconfirmed state"); + setAddressConfirmed(false); + onAddressIncomplete(); // Keep disabled until explicitly confirmed + } else { + console.log("🏠 Internet order: Preserving existing confirmation state"); + // Address is already confirmed, don't clobber the state + } } else { // For other order types, auto-confirm since address exists from signup - onAddressConfirmed(data.address); - setAddressConfirmed(true); + // Only call parent callback if we're not already confirmed to avoid spam + if (!addressConfirmed) { + console.log("🏠 Non-Internet order: Auto-confirming address"); + onAddressConfirmed(data.address); + setAddressConfirmed(true); + } else { + console.log("🏠 Non-Internet order: Already confirmed, skipping callback"); + } } } catch (err) { setError(err instanceof Error ? err.message : "Failed to load address"); } finally { setLoading(false); } - }, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed]); + }, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed, addressConfirmed]); useEffect(() => { void fetchBillingInfo(); @@ -109,11 +141,11 @@ export function AddressConfirmation({ try { setError(null); - // Use the edited address for the order (will be flagged as changed) - onAddressConfirmed(editedAddress); + + // UX-FIRST: Update UI immediately setEditing(false); setAddressConfirmed(true); - + // Update local state to show the new address if (billingInfo) { setBillingInfo({ @@ -122,25 +154,39 @@ export function AddressConfirmation({ isComplete: true, }); } + + // SIDE-EFFECT SECOND: Use the edited address for the order (will be flagged as changed) + onAddressConfirmed(editedAddress); } catch (err) { setError(err instanceof Error ? err.message : "Failed to update address"); } }; - const handleConfirmAddress = () => { + const handleConfirmAddress = (e: React.MouseEvent) => { + // Prevent any default behavior and event propagation + e.preventDefault(); + e.stopPropagation(); + console.log("🏠 CONFIRM ADDRESS CLICKED", { billingInfo, hasAddress: !!billingInfo?.address, - address: billingInfo?.address + address: billingInfo?.address, + currentAddressConfirmed: addressConfirmed }); if (billingInfo?.address) { - console.log("🏠 Calling onAddressConfirmed with:", billingInfo.address); - onAddressConfirmed(billingInfo.address); + console.log("🏠 UX-First approach: Updating local state immediately for instant UI feedback"); + + // UX-FIRST: Update local state immediately for instant UI response setAddressConfirmed(true); - console.log("🏠 Address confirmed state set to true"); + console.log("🏠 ✅ Local addressConfirmed set to true (UI will update immediately)"); + + // SIDE-EFFECT SECOND: Notify parent after local state update + console.log("🏠 Notifying parent component..."); + onAddressConfirmed(billingInfo.address); + console.log("🏠 ✅ Parent onAddressConfirmed() called with:", billingInfo.address); } else { - console.log("🏠 No billing info or address available"); + console.log("🏠 ❌ No billing info or address available"); } }; @@ -170,6 +216,7 @@ export function AddressConfirmation({

Address Error

{error}

@@ -392,6 +453,7 @@ export function AddressConfirmation({

No address on file