@@ -91,7 +96,7 @@ export function SimTypeSelector({
- )}
+
);
}
diff --git a/apps/portal/src/features/catalog/hooks/useSimConfigure.ts b/apps/portal/src/features/catalog/hooks/useSimConfigure.ts
index 3e0c43e7..b270672f 100644
--- a/apps/portal/src/features/catalog/hooks/useSimConfigure.ts
+++ b/apps/portal/src/features/catalog/hooks/useSimConfigure.ts
@@ -83,6 +83,16 @@ const parsePortingGenderParam = (value: string | null): MnpData["portingGender"]
return undefined;
};
+const MIN_STEP = 1;
+const MAX_STEP = 5;
+
+const parseStepParam = (value: string | null): number => {
+ if (!value) return MIN_STEP;
+ const parsed = Number.parseInt(value, 10);
+ if (Number.isNaN(parsed)) return MIN_STEP;
+ return Math.min(Math.max(parsed, MIN_STEP), MAX_STEP);
+};
+
export function useSimConfigure(planId?: string): UseSimConfigureResult {
const searchParams = useSearchParams();
const { data: simData, isLoading: simLoading } = useSimCatalog();
@@ -90,7 +100,9 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
const configureParams = useSimConfigureParams();
// Step orchestration state
- const [currentStep, setCurrentStep] = useState(0);
+ const [currentStep, setCurrentStep] = useState(() =>
+ parseStepParam(searchParams.get("step"))
+ );
const [isTransitioning, setIsTransitioning] = useState(false);
// Initialize form with Zod
@@ -349,7 +361,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
const transitionToStep = useCallback((nextStep: number) => {
setIsTransitioning(true);
setTimeout(() => {
- setCurrentStep(nextStep);
+ setCurrentStep(Math.min(Math.max(nextStep, MIN_STEP), MAX_STEP));
setIsTransitioning(false);
}, 150);
}, []);
diff --git a/apps/portal/src/features/checkout/hooks/useCheckout.ts b/apps/portal/src/features/checkout/hooks/useCheckout.ts
index 79e01eb0..14adfc76 100644
--- a/apps/portal/src/features/checkout/hooks/useCheckout.ts
+++ b/apps/portal/src/features/checkout/hooks/useCheckout.ts
@@ -23,6 +23,12 @@ import {
// Use domain Address type
import type { Address } from "@customer-portal/domain/customer";
+const ACTIVE_INTERNET_WARNING_MESSAGE =
+ "You already have an active Internet subscription. Please contact support to modify your service.";
+const DEVELOPMENT_WARNING_SUFFIX =
+ "Development mode override allows checkout to continue for testing.";
+const isDevEnvironment = process.env.NODE_ENV === "development";
+
export function useCheckout() {
const params = useSearchParams();
const router = useRouter();
@@ -36,6 +42,17 @@ export function useCheckout() {
// 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(null);
const {
data: paymentMethods,
@@ -91,6 +108,18 @@ export function useCheckout() {
}
}, [orderType, params]);
+ useEffect(() => {
+ if (orderType !== ORDER_TYPE.INTERNET || !hasActiveInternetSubscription) {
+ setActiveInternetWarning(null);
+ return;
+ }
+
+ const warningMessage = isDevEnvironment
+ ? `${ACTIVE_INTERNET_WARNING_MESSAGE} ${DEVELOPMENT_WARNING_SUFFIX}`
+ : ACTIVE_INTERNET_WARNING_MESSAGE;
+ setActiveInternetWarning(warningMessage);
+ }, [orderType, hasActiveInternetSubscription]);
+
useEffect(() => {
let mounted = true;
@@ -160,18 +189,8 @@ export function useCheckout() {
};
// 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."
- );
- }
+ if (orderType === ORDER_TYPE.INTERNET && hasActiveInternetSubscription && !isDevEnvironment) {
+ throw new Error(ACTIVE_INTERNET_WARNING_MESSAGE);
}
const response = await ordersService.createOrder(orderData);
@@ -183,7 +202,7 @@ export function useCheckout() {
} finally {
setSubmitting(false);
}
- }, [checkoutState, orderType, activeSubs, router]);
+ }, [checkoutState, orderType, hasActiveInternetSubscription, router]);
const confirmAddress = useCallback((address?: Address) => {
setAddressConfirmed(true);
@@ -218,5 +237,6 @@ export function useCheckout() {
markAddressIncomplete,
handleSubmitOrder,
navigateBackToConfigure,
+ activeInternetWarning,
} as const;
}
diff --git a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx
index bb208ba8..0610ea6e 100644
--- a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx
+++ b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx
@@ -25,6 +25,7 @@ export function CheckoutContainer() {
markAddressIncomplete,
handleSubmitOrder,
navigateBackToConfigure,
+ activeInternetWarning,
} = useCheckout();
if (isLoading(checkoutState)) {
@@ -98,6 +99,16 @@ export function CheckoutContainer() {
tone={paymentRefresh.toast.tone}
/>
+ {activeInternetWarning && (
+
+ {activeInternetWarning}
+
+ )}
+
diff --git a/cookies.txt b/cookies.txt
new file mode 100644
index 00000000..c31d9899
--- /dev/null
+++ b/cookies.txt
@@ -0,0 +1,4 @@
+# Netscape HTTP Cookie File
+# https://curl.se/docs/http-cookies.html
+# This file was generated by libcurl! Edit at your own risk.
+