barsa ce42664965 Add Checkout Registration Module and Enhance Public Contact Features
- Integrated CheckoutRegistrationModule into the application for handling checkout-related functionalities.
- Updated router configuration to include the new CheckoutRegistrationModule for API routing.
- Enhanced SalesforceAccountService with methods for account creation and email lookup to support checkout registration.
- Implemented public contact form functionality in SupportController, allowing unauthenticated users to submit inquiries.
- Added rate limiting to the public contact form to prevent spam submissions.
- Updated CatalogController and CheckoutController to allow public access for browsing and cart validation without authentication.
2025-12-17 14:07:22 +09:00

230 lines
7.2 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { logger } from "@/lib/logger";
import { ordersService } from "@/features/orders/services/orders.service";
import { checkoutService } from "@/features/checkout/services/checkout.service";
import { ACTIVE_INTERNET_SUBSCRIPTION_WARNING } from "@/features/checkout/constants";
import { usePaymentMethods } from "@/features/billing/hooks/useBilling";
import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh";
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,
orderWithSkuValidationSchema,
prepareOrderFromCart,
type CheckoutCart,
} from "@customer-portal/domain/orders";
import { CheckoutParamsService } from "@/features/checkout/services/checkout-params.service";
import { useAuthSession, useAuthStore } from "@/features/auth/services/auth.store";
import { ZodError } from "zod";
// Use domain Address type
import type { Address } from "@customer-portal/domain/customer";
export function useCheckout() {
const params = useSearchParams();
const router = useRouter();
const { isAuthenticated } = useAuthSession();
const [submitting, setSubmitting] = useState(false);
const [addressConfirmed, setAddressConfirmed] = useState(false);
const [checkoutState, setCheckoutState] = useState<AsyncState<CheckoutCart>>({
status: "loading",
});
// Load active subscriptions to enforce business rules client-side before submission
const { data: activeSubs } = useActiveSubscriptions();
const hasActiveInternetSubscription = useMemo(() => {
if (!Array.isArray(activeSubs)) return false;
return activeSubs.some(
subscription =>
String(subscription.groupName || subscription.productName || "")
.toLowerCase()
.includes("internet") && String(subscription.status || "").toLowerCase() === "active"
);
}, [activeSubs]);
const [activeInternetWarning, setActiveInternetWarning] = useState<string | null>(null);
const {
data: paymentMethods,
isLoading: paymentMethodsLoading,
error: paymentMethodsError,
refetch: refetchPaymentMethods,
} = usePaymentMethods();
const paymentRefresh = usePaymentRefresh({
refetch: refetchPaymentMethods,
attachFocusListeners: true,
});
const paramsKey = params.toString();
const checkoutSnapshot = CheckoutParamsService.buildSnapshot(new URLSearchParams(paramsKey));
const { orderType, warnings } = checkoutSnapshot;
const lastWarningSignature = useRef<string | null>(null);
useEffect(() => {
if (warnings.length === 0) {
return;
}
const signature = warnings.join("|");
if (signature === lastWarningSignature.current) {
return;
}
lastWarningSignature.current = signature;
warnings.forEach(message => {
logger.warn("Checkout parameter warning", { message });
});
}, [warnings]);
useEffect(() => {
if (orderType !== ORDER_TYPE.INTERNET || !hasActiveInternetSubscription) {
setActiveInternetWarning(null);
return;
}
setActiveInternetWarning(ACTIVE_INTERNET_SUBSCRIPTION_WARNING);
}, [orderType, hasActiveInternetSubscription]);
useEffect(() => {
// Wait for authentication before building cart
if (!isAuthenticated) {
return;
}
let mounted = true;
void (async () => {
const snapshot = CheckoutParamsService.buildSnapshot(new URLSearchParams(paramsKey));
const {
orderType: snapshotOrderType,
selections,
configuration,
planReference: snapshotPlan,
} = snapshot;
try {
setCheckoutState(createLoadingState());
if (!snapshotPlan) {
throw new Error("No plan selected. Please go back and select a plan.");
}
// Build cart using BFF service
const cart = await checkoutService.buildCart(snapshotOrderType, selections, configuration);
if (!mounted) return;
setCheckoutState(createSuccessState(cart));
} catch (error) {
if (mounted) {
const reason = error instanceof Error ? error.message : "Failed to load checkout data";
setCheckoutState(createErrorState(new Error(reason)));
}
}
})();
return () => {
mounted = false;
};
}, [isAuthenticated, paramsKey]);
const handleSubmitOrder = useCallback(async () => {
try {
setSubmitting(true);
if (checkoutState.status !== "success") {
throw new Error("Checkout data not loaded");
}
const cart = checkoutState.data;
// Debug logging to check cart contents
console.log("[DEBUG] Cart data:", cart);
console.log("[DEBUG] Cart items:", cart.items);
// Validate cart before submission
await checkoutService.validateCart(cart);
// Use domain helper to prepare order data
// This encapsulates SKU extraction and payload formatting
const orderData = prepareOrderFromCart(cart, orderType);
console.log("[DEBUG] Extracted SKUs from cart:", orderData.skus);
const currentUserId = useAuthStore.getState().user?.id;
if (currentUserId) {
try {
orderWithSkuValidationSchema.parse({
...orderData,
userId: currentUserId,
});
} catch (validationError) {
if (validationError instanceof ZodError) {
const firstIssue = validationError.issues.at(0);
throw new Error(firstIssue?.message || "Order contains invalid data");
}
throw validationError;
}
}
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, orderType, router]);
const confirmAddress = useCallback((address?: Address) => {
setAddressConfirmed(true);
void address;
}, []);
const markAddressIncomplete = useCallback(() => {
setAddressConfirmed(false);
}, []);
const navigateBackToConfigure = useCallback(() => {
// State is already persisted in Zustand store
// Just need to restore params and navigate
const urlParams = new URLSearchParams(paramsKey);
urlParams.delete("type"); // Remove type param as it's not needed
const configureUrl =
orderType === ORDER_TYPE.INTERNET
? `/shop/internet/configure?${urlParams.toString()}`
: `/shop/sim/configure?${urlParams.toString()}`;
router.push(configureUrl);
}, [orderType, paramsKey, router]);
return {
checkoutState,
submitting,
orderType,
addressConfirmed,
paymentMethods,
paymentMethodsLoading,
paymentMethodsError,
paymentRefresh,
confirmAddress,
markAddressIncomplete,
handleSubmitOrder,
navigateBackToConfigure,
activeInternetWarning,
} as const;
}