Refactor audit and signup workflows to streamline user data handling
- Removed unnecessary fields (firstName, lastName, company, phone) from user creation in AuditService and SignupWorkflowService for cleaner data management. - Enhanced error logging in GlobalAuthGuard to differentiate between unauthorized access attempts and other authentication errors. - Updated CurrencyController to mark endpoints as public for improved access control. - Improved button components across various steps in the internet and SIM configuration processes for better user experience and consistency. - Added active internet subscription warning in checkout process to prevent duplicate subscriptions.
This commit is contained in:
parent
e56d6f5e20
commit
3d17f36c2f
@ -142,8 +142,6 @@ export class AuditService {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
firstName: true,
|
|
||||||
lastName: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -277,10 +277,6 @@ export class SignupWorkflowService {
|
|||||||
data: {
|
data: {
|
||||||
email,
|
email,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
company: company || null,
|
|
||||||
phone: phone || null,
|
|
||||||
emailVerified: false,
|
emailVerified: false,
|
||||||
failedLoginAttempts: 0,
|
failedLoginAttempts: 0,
|
||||||
lockedUntil: null,
|
lockedUntil: null,
|
||||||
|
|||||||
@ -83,7 +83,16 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate {
|
|||||||
this.logger.debug(`Authenticated access to: ${route}`);
|
this.logger.debug(`Authenticated access to: ${route}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Authentication error for route ${route}: ${getErrorMessage(error)}`);
|
if (error instanceof UnauthorizedException) {
|
||||||
|
const token = extractTokenFromRequest(request);
|
||||||
|
const log =
|
||||||
|
typeof token === "string"
|
||||||
|
? () => this.logger.warn(`Unauthorized access attempt to ${route}`)
|
||||||
|
: () => this.logger.debug(`Unauthenticated request blocked for ${route}`);
|
||||||
|
log();
|
||||||
|
} else {
|
||||||
|
this.logger.error(`Authentication error for route ${route}: ${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { Controller, Get } from "@nestjs/common";
|
import { Controller, Get } from "@nestjs/common";
|
||||||
|
import { Public } from "@bff/modules/auth/decorators/public.decorator";
|
||||||
import { WhmcsCurrencyService } from "../../integrations/whmcs/services/whmcs-currency.service";
|
import { WhmcsCurrencyService } from "../../integrations/whmcs/services/whmcs-currency.service";
|
||||||
|
|
||||||
@Controller("currency")
|
@Controller("currency")
|
||||||
export class CurrencyController {
|
export class CurrencyController {
|
||||||
constructor(private readonly currencyService: WhmcsCurrencyService) {}
|
constructor(private readonly currencyService: WhmcsCurrencyService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
@Get("default")
|
@Get("default")
|
||||||
getDefaultCurrency() {
|
getDefaultCurrency() {
|
||||||
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
const defaultCurrency = this.currencyService.getDefaultCurrency();
|
||||||
@ -17,6 +19,7 @@ export class CurrencyController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
@Get("all")
|
@Get("all")
|
||||||
getAllCurrencies() {
|
getAllCurrencies() {
|
||||||
return this.currencyService.getAllCurrencies();
|
return this.currencyService.getAllCurrencies();
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { PageLayout } from "@/components/templates/PageLayout";
|
import { PageLayout } from "@/components/templates/PageLayout";
|
||||||
import { ProgressSteps } from "@/components/molecules";
|
import { ProgressSteps } from "@/components/molecules";
|
||||||
import { ServerIcon } from "@heroicons/react/24/outline";
|
import { Button } from "@/components/atoms/button";
|
||||||
|
import { ServerIcon, ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||||
import type {
|
import type {
|
||||||
InternetPlanCatalogItem,
|
InternetPlanCatalogItem,
|
||||||
InternetInstallationCatalogItem,
|
InternetInstallationCatalogItem,
|
||||||
@ -84,6 +85,19 @@ export function InternetConfigureContainer({
|
|||||||
description="Set up your internet service options"
|
description="Set up your internet service options"
|
||||||
>
|
>
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="mb-6">
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
|
href="/catalog/internet"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
|
className="text-gray-600 hover:text-gray-900"
|
||||||
|
>
|
||||||
|
Back to Internet Plans
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Plan Header */}
|
{/* Plan Header */}
|
||||||
<PlanHeader plan={plan} monthlyTotal={monthlyTotal} oneTimeTotal={oneTimeTotal} />
|
<PlanHeader plan={plan} monthlyTotal={monthlyTotal} oneTimeTotal={oneTimeTotal} />
|
||||||
|
|
||||||
|
|||||||
@ -47,13 +47,15 @@ export function AddonsStep({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<Button onClick={onBack} variant="outline" className="flex items-center">
|
<Button
|
||||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
onClick={onBack}
|
||||||
|
variant="outline"
|
||||||
|
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
|
>
|
||||||
Back to Installation
|
Back to Installation
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onNext} className="flex items-center">
|
<Button onClick={onNext} rightIcon={<ArrowRightIcon className="w-4 h-4" />}>
|
||||||
Review Order
|
Review Order
|
||||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
|
|||||||
@ -48,13 +48,19 @@ export function InstallationStep({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<Button onClick={onBack} variant="outline" className="flex items-center">
|
<Button
|
||||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
onClick={onBack}
|
||||||
|
variant="outline"
|
||||||
|
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
|
>
|
||||||
Back to Configuration
|
Back to Configuration
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onNext} disabled={!selectedInstallation} className="flex items-center">
|
<Button
|
||||||
|
onClick={onNext}
|
||||||
|
disabled={!selectedInstallation}
|
||||||
|
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
|
>
|
||||||
Continue to Add-ons
|
Continue to Add-ons
|
||||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
|
|||||||
@ -65,13 +65,22 @@ export function ReviewOrderStep({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-6 border-t">
|
<div className="flex justify-between items-center pt-6 border-t">
|
||||||
<Button onClick={onBack} variant="outline" size="lg" className="px-8 py-4 text-lg">
|
<Button
|
||||||
<ArrowLeftIcon className="w-5 h-5 mr-2" />
|
onClick={onBack}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
className="px-8 py-4 text-lg"
|
||||||
|
leftIcon={<ArrowLeftIcon className="w-5 h-5" />}
|
||||||
|
>
|
||||||
Back to Add-ons
|
Back to Add-ons
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onConfirm} size="lg" className="px-12 py-4 text-lg font-semibold">
|
<Button
|
||||||
|
onClick={onConfirm}
|
||||||
|
size="lg"
|
||||||
|
className="px-12 py-4 text-lg font-semibold"
|
||||||
|
rightIcon={<ArrowRightIcon className="w-5 h-5" />}
|
||||||
|
>
|
||||||
Proceed to Checkout
|
Proceed to Checkout
|
||||||
<ArrowRightIcon className="w-5 h-5 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
|
|||||||
@ -61,10 +61,9 @@ export function ServiceConfigurationStep({ plan, mode, setMode, isTransitioning,
|
|||||||
<Button
|
<Button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={plan?.internetPlanTier === "Silver" && !mode}
|
disabled={plan?.internetPlanTier === "Silver" && !mode}
|
||||||
className="flex items-center"
|
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
Continue to Installation
|
Continue to Installation
|
||||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
@ -122,12 +121,14 @@ function ModeSelectionCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => onSelect(mode)}
|
onClick={() => onSelect(mode)}
|
||||||
className={`p-6 rounded-xl border-2 text-left transition-all duration-200 ${
|
className={`p-6 rounded-xl border-2 text-left transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||||
isSelected
|
isSelected
|
||||||
? "border-blue-500 bg-blue-50 shadow-lg transform scale-105"
|
? "border-blue-500 bg-blue-50 shadow-md"
|
||||||
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50 hover:shadow-md"
|
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
|
||||||
}`}
|
}`}
|
||||||
|
aria-pressed={isSelected}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h5 className="text-lg font-semibold text-gray-900">{title}</h5>
|
<h5 className="text-lg font-semibold text-gray-900">{title}</h5>
|
||||||
|
|||||||
@ -139,8 +139,14 @@ export function SimConfigureView({
|
|||||||
>
|
>
|
||||||
<div className="max-w-4xl mx-auto space-y-8">
|
<div className="max-w-4xl mx-auto space-y-8">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Button as="a" href="/catalog/sim" variant="outline" size="sm" className="group">
|
<Button
|
||||||
<ArrowLeftIcon className="w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform duration-300" />
|
as="a"
|
||||||
|
href="/catalog/sim"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
|
className="text-gray-600 hover:text-gray-900"
|
||||||
|
>
|
||||||
Back to SIM Plans
|
Back to SIM Plans
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -221,9 +227,11 @@ export function SimConfigureView({
|
|||||||
errors={errors}
|
errors={errors}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end mt-6">
|
<div className="flex justify-end mt-6">
|
||||||
<Button onClick={() => transitionToStep(2)} className="flex items-center">
|
<Button
|
||||||
|
onClick={() => transitionToStep(2)}
|
||||||
|
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
|
>
|
||||||
Continue to Activation
|
Continue to Activation
|
||||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
@ -252,14 +260,15 @@ export function SimConfigureView({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => transitionToStep(1)}
|
onClick={() => transitionToStep(1)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center"
|
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
|
||||||
Back to SIM Type
|
Back to SIM Type
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => transitionToStep(3)} className="flex items-center">
|
<Button
|
||||||
|
onClick={() => transitionToStep(3)}
|
||||||
|
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
|
>
|
||||||
Continue to Add-ons
|
Continue to Add-ons
|
||||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
@ -297,14 +306,15 @@ export function SimConfigureView({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => transitionToStep(2)}
|
onClick={() => transitionToStep(2)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center"
|
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
|
||||||
Back to Activation
|
Back to Activation
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => transitionToStep(4)} className="flex items-center">
|
<Button
|
||||||
|
onClick={() => transitionToStep(4)}
|
||||||
|
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
|
>
|
||||||
Continue to Number Porting
|
Continue to Number Porting
|
||||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
@ -333,9 +343,8 @@ export function SimConfigureView({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => transitionToStep(3)}
|
onClick={() => transitionToStep(3)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center"
|
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
|
||||||
Back to Add-ons
|
Back to Add-ons
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@ -343,10 +352,9 @@ export function SimConfigureView({
|
|||||||
if (wantsMnp && !validate()) return;
|
if (wantsMnp && !validate()) return;
|
||||||
transitionToStep(5);
|
transitionToStep(5);
|
||||||
}}
|
}}
|
||||||
className="flex items-center"
|
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
Review Order
|
Review Order
|
||||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
@ -490,13 +498,17 @@ export function SimConfigureView({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="px-8 py-4 text-lg"
|
className="px-8 py-4 text-lg"
|
||||||
|
leftIcon={<ArrowLeftIcon className="w-5 h-5" />}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="w-5 h-5 mr-2" />
|
|
||||||
Back to Number Porting
|
Back to Number Porting
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onConfirm} size="lg" className="px-12 py-4 text-lg font-semibold">
|
<Button
|
||||||
|
onClick={onConfirm}
|
||||||
|
size="lg"
|
||||||
|
className="px-12 py-4 text-lg font-semibold"
|
||||||
|
rightIcon={<ArrowRightIcon className="w-5 h-5" />}
|
||||||
|
>
|
||||||
Proceed to Checkout
|
Proceed to Checkout
|
||||||
<ArrowRightIcon className="w-5 h-5 ml-2" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
|
|||||||
@ -19,9 +19,9 @@ export function SimTypeSelector({
|
|||||||
<div>
|
<div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<label
|
<label
|
||||||
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all duration-300 ease-in-out hover:scale-[1.01] ${
|
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors duration-200 ease-in-out ${
|
||||||
simType === "Physical SIM"
|
simType === "Physical SIM"
|
||||||
? "border-blue-500 bg-blue-50 ring-2 ring-blue-100 shadow-sm"
|
? "border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-200"
|
||||||
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
|
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -43,9 +43,9 @@ export function SimTypeSelector({
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all duration-300 ease-in-out hover:scale-[1.01] ${
|
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors duration-200 ease-in-out ${
|
||||||
simType === "eSIM"
|
simType === "eSIM"
|
||||||
? "border-blue-500 bg-blue-50 ring-2 ring-blue-100 shadow-sm"
|
? "border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-200"
|
||||||
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
|
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -68,7 +68,12 @@ export function SimTypeSelector({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* EID Input for eSIM */}
|
{/* EID Input for eSIM */}
|
||||||
{simType === "eSIM" && (
|
<div
|
||||||
|
className={`overflow-hidden transition-[max-height,opacity] duration-300 ease-out ${
|
||||||
|
simType === "eSIM" ? "max-h-[360px] opacity-100" : "max-h-0 opacity-0"
|
||||||
|
}`}
|
||||||
|
aria-hidden={simType !== "eSIM"}
|
||||||
|
>
|
||||||
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
|
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
|
||||||
<h4 className="font-medium text-blue-900 mb-2">eSIM Device Information</h4>
|
<h4 className="font-medium text-blue-900 mb-2">eSIM Device Information</h4>
|
||||||
<div>
|
<div>
|
||||||
@ -91,7 +96,7 @@ export function SimTypeSelector({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,16 @@ const parsePortingGenderParam = (value: string | null): MnpData["portingGender"]
|
|||||||
return undefined;
|
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 {
|
export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { data: simData, isLoading: simLoading } = useSimCatalog();
|
const { data: simData, isLoading: simLoading } = useSimCatalog();
|
||||||
@ -90,7 +100,9 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
|||||||
const configureParams = useSimConfigureParams();
|
const configureParams = useSimConfigureParams();
|
||||||
|
|
||||||
// Step orchestration state
|
// Step orchestration state
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState<number>(() =>
|
||||||
|
parseStepParam(searchParams.get("step"))
|
||||||
|
);
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
|
||||||
// Initialize form with Zod
|
// Initialize form with Zod
|
||||||
@ -349,7 +361,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
|||||||
const transitionToStep = useCallback((nextStep: number) => {
|
const transitionToStep = useCallback((nextStep: number) => {
|
||||||
setIsTransitioning(true);
|
setIsTransitioning(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCurrentStep(nextStep);
|
setCurrentStep(Math.min(Math.max(nextStep, MIN_STEP), MAX_STEP));
|
||||||
setIsTransitioning(false);
|
setIsTransitioning(false);
|
||||||
}, 150);
|
}, 150);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@ -23,6 +23,12 @@ import {
|
|||||||
// Use domain Address type
|
// Use domain Address type
|
||||||
import type { Address } from "@customer-portal/domain/customer";
|
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() {
|
export function useCheckout() {
|
||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -36,6 +42,17 @@ export function useCheckout() {
|
|||||||
|
|
||||||
// Load active subscriptions to enforce business rules client-side before submission
|
// Load active subscriptions to enforce business rules client-side before submission
|
||||||
const { data: activeSubs } = useActiveSubscriptions();
|
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 {
|
const {
|
||||||
data: paymentMethods,
|
data: paymentMethods,
|
||||||
@ -91,6 +108,18 @@ export function useCheckout() {
|
|||||||
}
|
}
|
||||||
}, [orderType, params]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
||||||
@ -160,18 +189,8 @@ export function useCheckout() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Client-side guard: prevent Internet orders if an Internet subscription already exists
|
// Client-side guard: prevent Internet orders if an Internet subscription already exists
|
||||||
if (orderType === "Internet" && Array.isArray(activeSubs)) {
|
if (orderType === ORDER_TYPE.INTERNET && hasActiveInternetSubscription && !isDevEnvironment) {
|
||||||
const hasActiveInternet = activeSubs.some(
|
throw new Error(ACTIVE_INTERNET_WARNING_MESSAGE);
|
||||||
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);
|
const response = await ordersService.createOrder(orderData);
|
||||||
@ -183,7 +202,7 @@ export function useCheckout() {
|
|||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
}, [checkoutState, orderType, activeSubs, router]);
|
}, [checkoutState, orderType, hasActiveInternetSubscription, router]);
|
||||||
|
|
||||||
const confirmAddress = useCallback((address?: Address) => {
|
const confirmAddress = useCallback((address?: Address) => {
|
||||||
setAddressConfirmed(true);
|
setAddressConfirmed(true);
|
||||||
@ -218,5 +237,6 @@ export function useCheckout() {
|
|||||||
markAddressIncomplete,
|
markAddressIncomplete,
|
||||||
handleSubmitOrder,
|
handleSubmitOrder,
|
||||||
navigateBackToConfigure,
|
navigateBackToConfigure,
|
||||||
|
activeInternetWarning,
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export function CheckoutContainer() {
|
|||||||
markAddressIncomplete,
|
markAddressIncomplete,
|
||||||
handleSubmitOrder,
|
handleSubmitOrder,
|
||||||
navigateBackToConfigure,
|
navigateBackToConfigure,
|
||||||
|
activeInternetWarning,
|
||||||
} = useCheckout();
|
} = useCheckout();
|
||||||
|
|
||||||
if (isLoading(checkoutState)) {
|
if (isLoading(checkoutState)) {
|
||||||
@ -98,6 +99,16 @@ export function CheckoutContainer() {
|
|||||||
tone={paymentRefresh.toast.tone}
|
tone={paymentRefresh.toast.tone}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{activeInternetWarning && (
|
||||||
|
<AlertBanner
|
||||||
|
variant="warning"
|
||||||
|
title="Existing Internet Subscription"
|
||||||
|
elevated
|
||||||
|
>
|
||||||
|
<span className="text-sm text-gray-700">{activeInternetWarning}</span>
|
||||||
|
</AlertBanner>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="bg-gray-50 border border-gray-200 rounded-2xl p-6 md:p-7 shadow-sm">
|
<div className="bg-gray-50 border border-gray-200 rounded-2xl p-6 md:p-7 shadow-sm">
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<ShieldCheckIcon className="w-6 h-6 text-blue-600" />
|
<ShieldCheckIcon className="w-6 h-6 text-blue-600" />
|
||||||
|
|||||||
4
cookies.txt
Normal file
4
cookies.txt
Normal file
@ -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.
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user