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.
This commit is contained in:
tema 2025-08-30 18:51:51 +09:00
parent 807d37a729
commit 543afc8a10
5 changed files with 177 additions and 34 deletions

View File

@ -78,6 +78,18 @@ export class OrderConfigurations {
@IsString() @IsString()
portingDateOfBirth?: string; 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 // VPN region is inferred from product VPN_Region__c field, no user input needed
} }

View File

@ -24,6 +24,7 @@ export class OrdersController {
userId: req.user?.id, userId: req.user?.id,
orderType: body.orderType, orderType: body.orderType,
skuCount: body.skus?.length || 0, skuCount: body.skus?.length || 0,
requestBody: JSON.stringify(body, null, 2),
}, },
"Order creation request received" "Order creation request received"
); );
@ -34,8 +35,10 @@ export class OrdersController {
this.logger.error( this.logger.error(
{ {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
userId: req.user?.id, userId: req.user?.id,
orderType: body.orderType, orderType: body.orderType,
fullRequestBody: JSON.stringify(body, null, 2),
}, },
"Order creation failed" "Order creation failed"
); );

View File

@ -57,13 +57,24 @@ export class OrderOrchestrator {
// 4) Create Order in Salesforce // 4) Create Order in Salesforce
let created: { id: string }; let created: { id: string };
try { 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 }; created = (await this.sf.sobject("Order").create(orderFields)) as { id: string };
this.logger.log({ orderId: created.id }, "Salesforce Order created successfully"); this.logger.log({ orderId: created.id }, "Salesforce Order created successfully");
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
{ {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
errorDetails: error,
stack: error instanceof Error ? error.stack : undefined,
orderType: orderFields.Type, orderType: orderFields.Type,
orderFields: JSON.stringify(orderFields, null, 2),
}, },
"Failed to create Salesforce Order" "Failed to create Salesforce Order"
); );

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useMemo, Suspense } from "react"; import { useState, useEffect, useMemo, useCallback, Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation"; import { useSearchParams, useRouter } from "next/navigation";
import { PageLayout } from "@/components/layout/page-layout"; import { PageLayout } from "@/components/layout/page-layout";
import { ShieldCheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ShieldCheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
@ -38,6 +38,7 @@ function CheckoutContent() {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [addressConfirmed, setAddressConfirmed] = useState(false); const [addressConfirmed, setAddressConfirmed] = useState(false);
const [confirmedAddress, setConfirmedAddress] = useState<Address | null>(null); const [confirmedAddress, setConfirmedAddress] = useState<Address | null>(null);
const [forceUpdate, setForceUpdate] = useState(0);
const [checkoutState, setCheckoutState] = useState<CheckoutState>({ const [checkoutState, setCheckoutState] = useState<CheckoutState>({
loading: true, loading: true,
error: null, error: null,
@ -169,6 +170,11 @@ function CheckoutContent() {
}; };
}, [orderType, selections]); }, [orderType, selections]);
// Debug effect to track addressConfirmed changes
useEffect(() => {
console.log("🎯 PARENT: addressConfirmed state changed to:", addressConfirmed);
}, [addressConfirmed]);
const handleSubmitOrder = async () => { const handleSubmitOrder = async () => {
try { try {
setSubmitting(true); setSubmitting(true);
@ -180,6 +186,28 @@ function CheckoutContent() {
throw new Error("No products selected for order. Please go back and select products."); 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, // Send SKUs + configurations - backend resolves product data from SKUs,
// uses configurations for fields that cannot be inferred // uses configurations for fields that cannot be inferred
const configurations: Record<string, unknown> = {}; const configurations: Record<string, unknown> = {};
@ -221,10 +249,29 @@ function CheckoutContent() {
...(Object.keys(configurations).length > 0 && { configurations }), ...(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); const response = await authenticatedApi.post<{ sfOrderId: string }>("/orders", orderData);
router.push(`/orders/${response.sfOrderId}?status=success`); router.push(`/orders/${response.sfOrderId}?status=success`);
} catch (error) { } 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"; let errorMessage = "Order submission failed";
if (error instanceof Error) { 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: handleAddressConfirmed called with:", address);
console.log("🎯 PARENT: Current addressConfirmed state before:", addressConfirmed); 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) console.log("🎯 PARENT: About to call setAddressConfirmed(true)...");
setTimeout(() => { setAddressConfirmed(prev => {
console.log("🎯 PARENT: addressConfirmed state after update:", addressConfirmed); console.log("🎯 PARENT: setAddressConfirmed functional update - prev:", prev, "-> true");
}, 0); 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 = () => { const handleAddressIncomplete = () => {
setAddressConfirmed(false); setAddressConfirmed(false);
@ -432,11 +486,13 @@ function CheckoutContent() {
)} )}
</div> </div>
{/* Debug Info - Remove in production */} {/* Debug Info - Remove in production */}
<div className="bg-gray-100 border rounded-lg p-3 mb-4 text-xs text-gray-600"> <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4 text-xs text-gray-700">
<strong>Debug Info:</strong> Address Confirmed: {addressConfirmed ? '✅' : '❌'} ({String(addressConfirmed)}) | <strong>Debug Info:</strong> Address Confirmed: {addressConfirmed ? '✅ TRUE' : '❌ FALSE'} |
Payment Methods: {paymentMethodsLoading ? '⏳ Loading...' : paymentMethodsError ? '❌ Error' : paymentMethods ? `${paymentMethods.paymentMethods.length} found` : '❌ None'} | Order Type: {orderType} |
Order Items: {checkoutState.orderItems.length} | Order Items: {checkoutState.orderItems.length} |
Payment Methods: {paymentMethodsLoading ? '⏳ Loading...' : paymentMethodsError ? '❌ Error' : paymentMethods ? `${paymentMethods.paymentMethods.length} found` : '❌ None'} |
Force Update: {forceUpdate} |
Can Submit: {!( Can Submit: {!(
submitting || submitting ||
checkoutState.orderItems.length === 0 || checkoutState.orderItems.length === 0 ||
@ -444,8 +500,7 @@ function CheckoutContent() {
paymentMethodsLoading || paymentMethodsLoading ||
!paymentMethods || !paymentMethods ||
paymentMethods.paymentMethods.length === 0 paymentMethods.paymentMethods.length === 0
) ? '✅' : '❌'} | ) ? '✅ YES' : '❌ NO'}
Render Time: {new Date().toLocaleTimeString()}
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">

View File

@ -31,19 +31,38 @@ interface AddressConfirmationProps {
onAddressConfirmed: (address?: Address) => void; onAddressConfirmed: (address?: Address) => void;
onAddressIncomplete: () => void; onAddressIncomplete: () => void;
orderType?: string; // Add order type to customize behavior 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({ export function AddressConfirmation({
onAddressConfirmed, onAddressConfirmed,
onAddressIncomplete, onAddressIncomplete,
orderType, orderType,
addressConfirmed: controlledAddressConfirmed,
onAddressConfirmationChange,
}: AddressConfirmationProps) { }: AddressConfirmationProps) {
const [billingInfo, setBillingInfo] = useState<BillingInfo | null>(null); const [billingInfo, setBillingInfo] = useState<BillingInfo | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [editedAddress, setEditedAddress] = useState<Address | null>(null); const [editedAddress, setEditedAddress] = useState<Address | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(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 isInternetOrder = orderType === "Internet";
const requiresAddressVerification = isInternetOrder; const requiresAddressVerification = isInternetOrder;
@ -57,20 +76,33 @@ export function AddressConfirmation({
// Since address is required at signup, it should always be complete // Since address is required at signup, it should always be complete
// But we still need verification for Internet orders // But we still need verification for Internet orders
if (requiresAddressVerification) { if (requiresAddressVerification) {
// For Internet orders, don't auto-confirm - require explicit verification // For Internet orders, only reset confirmation state if not already confirmed
setAddressConfirmed(false); // This prevents clobbering existing confirmation on re-renders/re-fetches
onAddressIncomplete(); // Keep disabled until explicitly confirmed 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 { } else {
// For other order types, auto-confirm since address exists from signup // For other order types, auto-confirm since address exists from signup
onAddressConfirmed(data.address); // Only call parent callback if we're not already confirmed to avoid spam
setAddressConfirmed(true); 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) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to load address"); setError(err instanceof Error ? err.message : "Failed to load address");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed]); }, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed, addressConfirmed]);
useEffect(() => { useEffect(() => {
void fetchBillingInfo(); void fetchBillingInfo();
@ -109,11 +141,11 @@ export function AddressConfirmation({
try { try {
setError(null); setError(null);
// Use the edited address for the order (will be flagged as changed)
onAddressConfirmed(editedAddress); // UX-FIRST: Update UI immediately
setEditing(false); setEditing(false);
setAddressConfirmed(true); setAddressConfirmed(true);
// Update local state to show the new address // Update local state to show the new address
if (billingInfo) { if (billingInfo) {
setBillingInfo({ setBillingInfo({
@ -122,25 +154,39 @@ export function AddressConfirmation({
isComplete: true, isComplete: true,
}); });
} }
// SIDE-EFFECT SECOND: Use the edited address for the order (will be flagged as changed)
onAddressConfirmed(editedAddress);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to update address"); setError(err instanceof Error ? err.message : "Failed to update address");
} }
}; };
const handleConfirmAddress = () => { const handleConfirmAddress = (e: React.MouseEvent<HTMLButtonElement>) => {
// Prevent any default behavior and event propagation
e.preventDefault();
e.stopPropagation();
console.log("🏠 CONFIRM ADDRESS CLICKED", { console.log("🏠 CONFIRM ADDRESS CLICKED", {
billingInfo, billingInfo,
hasAddress: !!billingInfo?.address, hasAddress: !!billingInfo?.address,
address: billingInfo?.address address: billingInfo?.address,
currentAddressConfirmed: addressConfirmed
}); });
if (billingInfo?.address) { if (billingInfo?.address) {
console.log("🏠 Calling onAddressConfirmed with:", billingInfo.address); console.log("🏠 UX-First approach: Updating local state immediately for instant UI feedback");
onAddressConfirmed(billingInfo.address);
// UX-FIRST: Update local state immediately for instant UI response
setAddressConfirmed(true); 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 { } else {
console.log("🏠 No billing info or address available"); console.log("🏠 No billing info or address available");
} }
}; };
@ -170,6 +216,7 @@ export function AddressConfirmation({
<h3 className="text-sm font-medium text-red-800">Address Error</h3> <h3 className="text-sm font-medium text-red-800">Address Error</h3>
<p className="text-sm text-red-700 mt-1">{error}</p> <p className="text-sm text-red-700 mt-1">{error}</p>
<button <button
type="button"
onClick={() => void fetchBillingInfo()} onClick={() => void fetchBillingInfo()}
className="text-sm text-red-600 hover:text-red-500 font-medium mt-2" className="text-sm text-red-600 hover:text-red-500 font-medium mt-2"
> >
@ -185,6 +232,16 @@ export function AddressConfirmation({
return ( return (
<div className="bg-white border rounded-xl p-6 mb-6"> <div className="bg-white border rounded-xl p-6 mb-6">
{/* Debug Info - Remove in production */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4 text-xs text-gray-700">
<strong>AddressConfirmation Debug:</strong> isInternetOrder: {isInternetOrder ? '✅' : '❌'} |
addressConfirmed: {addressConfirmed ? '✅' : '❌'} |
controlledMode: {controlledAddressConfirmed !== undefined ? '✅' : '❌'} |
billingInfo: {billingInfo ? '✅' : '❌'} |
hasAddress: {billingInfo?.address ? '✅' : '❌'} |
showConfirmButton: {(isInternetOrder && !addressConfirmed) ? '✅' : '❌'}
</div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<MapPinIcon className="h-5 w-5 text-blue-600" /> <MapPinIcon className="h-5 w-5 text-blue-600" />
@ -198,6 +255,7 @@ export function AddressConfirmation({
</div> </div>
{billingInfo.isComplete && !editing && ( {billingInfo.isComplete && !editing && (
<button <button
type="button"
onClick={handleEdit} onClick={handleEdit}
className="flex items-center space-x-2 text-blue-600 hover:text-blue-700 text-sm font-medium" className="flex items-center space-x-2 text-blue-600 hover:text-blue-700 text-sm font-medium"
> >
@ -326,6 +384,7 @@ export function AddressConfirmation({
<div className="flex items-center space-x-3 pt-4"> <div className="flex items-center space-x-3 pt-4">
<button <button
type="button"
onClick={handleSave} onClick={handleSave}
className="flex items-center space-x-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors" className="flex items-center space-x-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
> >
@ -333,6 +392,7 @@ export function AddressConfirmation({
<span>Save Address</span> <span>Save Address</span>
</button> </button>
<button <button
type="button"
onClick={handleCancel} onClick={handleCancel}
className="flex items-center space-x-2 bg-gray-100 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-200 transition-colors" className="flex items-center space-x-2 bg-gray-100 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-200 transition-colors"
> >
@ -366,8 +426,9 @@ export function AddressConfirmation({
</span> </span>
</div> </div>
<button <button
type="button"
onClick={handleConfirmAddress} onClick={handleConfirmAddress}
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors font-medium" className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors font-medium active:bg-green-800 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
> >
Confirm Installation Address Confirm Installation Address
</button> </button>
@ -392,6 +453,7 @@ export function AddressConfirmation({
<MapPinIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" /> <MapPinIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600 mb-4">No address on file</p> <p className="text-gray-600 mb-4">No address on file</p>
<button <button
type="button"
onClick={handleEdit} onClick={handleEdit}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors" className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
> >