From e56d6f5e2077e5a7eb09f208cd2579801a96031f Mon Sep 17 00:00:00 2001 From: barsa Date: Wed, 22 Oct 2025 11:55:47 +0900 Subject: [PATCH] Refactor user creation and improve type safety in services - Removed unnecessary fields from user creation in WhmcsLinkWorkflowService for cleaner data handling. - Updated logout method in AuthController to ensure proper request type casting. - Enhanced type safety in MappingsService by refining type filters for existing mappings. - Improved currency resolution logic in UsersService for better handling of client currency codes. - Streamlined product filtering and sorting logic in catalog utilities for improved readability and performance. - Refactored InternetPlans component to simplify memoization of plans. - Cleaned up state management in useCheckout hook by removing unused state variables. - Enhanced checkout service to improve API request structure and error handling. - Updated response helpers to streamline success response construction. --- .../workflows/whmcs-link-workflow.service.ts | 8 --- .../auth/presentation/http/auth.controller.ts | 2 +- .../modules/id-mappings/mappings.service.ts | 4 +- apps/bff/src/modules/users/users.service.ts | 14 ++++-- .../features/catalog/utils/catalog.utils.ts | 49 ++++++++----------- .../features/catalog/views/InternetPlans.tsx | 5 +- .../features/checkout/hooks/useCheckout.ts | 8 +-- .../checkout/services/checkout.service.ts | 16 +++--- apps/portal/src/lib/api/response-helpers.ts | 7 ++- 9 files changed, 53 insertions(+), 60 deletions(-) diff --git a/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts index 380ed587..55d7dc6f 100644 --- a/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/workflows/whmcs-link-workflow.service.ts @@ -141,14 +141,6 @@ export class WhmcsLinkWorkflowService { const createdUser = await this.usersService.create({ email, passwordHash: null, - firstName: clientDetails.firstname ?? "", - lastName: clientDetails.lastname ?? "", - company: clientDetails.companyname ?? "", // Raw WHMCS field name - phone: - clientDetails.phonenumberformatted ?? - clientDetails.phonenumber ?? - clientDetails.telephoneNumber ?? - "", emailVerified: true, }); diff --git a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts index 9597b04a..8c3a4222 100644 --- a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts +++ b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts @@ -185,7 +185,7 @@ export class AuthController { @Res({ passthrough: true }) res: Response ) { const token = extractTokenFromRequest(req); - await this.authFacade.logout(req.user.id, token, req); + await this.authFacade.logout(req.user.id, token, req as Request); this.clearAuthCookies(res); return { message: "Logout successful" }; } diff --git a/apps/bff/src/modules/id-mappings/mappings.service.ts b/apps/bff/src/modules/id-mappings/mappings.service.ts index 80450c0e..936d2973 100644 --- a/apps/bff/src/modules/id-mappings/mappings.service.ts +++ b/apps/bff/src/modules/id-mappings/mappings.service.ts @@ -26,7 +26,7 @@ import { sanitizeCreateRequest, sanitizeUpdateRequest, } from "@customer-portal/domain/mappings"; -import type { Prisma } from "@prisma/client"; +import type { Prisma, IdMapping as PrismaIdMapping } from "@prisma/client"; import { mapPrismaMappingToDomain } from "@bff/infra/mappers"; @Injectable() @@ -68,7 +68,7 @@ export class MappingsService { ]); const existingMappings = [byUser, byWhmcs, bySf] - .filter((mapping): mapping is Prisma.IdMapping => mapping !== null) + .filter((mapping): mapping is PrismaIdMapping => mapping !== null) .map(mapPrismaMappingToDomain); const conflictCheck = validateNoConflicts(sanitizedRequest, existingMappings); diff --git a/apps/bff/src/modules/users/users.service.ts b/apps/bff/src/modules/users/users.service.ts index e1594a59..3fdb51d2 100644 --- a/apps/bff/src/modules/users/users.service.ts +++ b/apps/bff/src/modules/users/users.service.ts @@ -530,9 +530,17 @@ export class UsersService { let currency = "JPY"; // Default try { const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId); - const currencyCode = client.currency_code ?? client.raw.currency_code ?? null; - if (currencyCode) { - currency = currencyCode; + const currencyCodeFromClient = + typeof client.currency_code === "string" && client.currency_code.trim().length > 0 + ? client.currency_code + : undefined; + const currencyCodeFromRaw = + typeof client.raw.currency_code === "string" && client.raw.currency_code.trim().length > 0 + ? client.raw.currency_code + : undefined; + const resolvedCurrency = currencyCodeFromClient ?? currencyCodeFromRaw ?? null; + if (resolvedCurrency) { + currency = resolvedCurrency; } } catch (error) { this.logger.warn("Could not fetch currency from WHMCS client", { diff --git a/apps/portal/src/features/catalog/utils/catalog.utils.ts b/apps/portal/src/features/catalog/utils/catalog.utils.ts index 8ea3d7c0..6cfc394c 100644 --- a/apps/portal/src/features/catalog/utils/catalog.utils.ts +++ b/apps/portal/src/features/catalog/utils/catalog.utils.ts @@ -32,35 +32,21 @@ export function filterProducts( } ): CatalogProduct[] { return products.filter(product => { - if (filters.category) { - const normalizedCategory = filters.category.toLowerCase(); - const hasItemClass = - "itemClass" in product && typeof product.itemClass === "string"; - const hasInternetTier = "internetPlanTier" in product; - const hasSimType = "simPlanType" in product; - const hasVpnRegion = "vpnRegion" in product; - - const categoryMatches = - (hasItemClass && product.itemClass.toLowerCase() === normalizedCategory) || - (hasInternetTier && normalizedCategory === "internet") || - (hasSimType && normalizedCategory === "sim") || - (hasVpnRegion && normalizedCategory === "vpn"); - if (!categoryMatches) { - return false; - } - } - if (typeof filters.priceMin === "number") { - const price = (product as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? - (product as { oneTimePrice?: number }).oneTimePrice ?? 0; + const price = + (product as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? + (product as { oneTimePrice?: number }).oneTimePrice ?? + 0; if (price < filters.priceMin) { return false; } } if (typeof filters.priceMax === "number") { - const price = (product as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? - (product as { oneTimePrice?: number }).oneTimePrice ?? 0; + const price = + (product as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? + (product as { oneTimePrice?: number }).oneTimePrice ?? + 0; if (price > filters.priceMax) { return false; } @@ -68,8 +54,11 @@ export function filterProducts( const search = filters.search?.toLowerCase(); if (search) { - const nameMatch = product.name.toLowerCase().includes(search); - const descriptionMatch = product.description?.toLowerCase().includes(search) ?? false; + const nameMatch = + typeof product.name === "string" && product.name.toLowerCase().includes(search); + const descriptionText = + typeof product.description === "string" ? product.description.toLowerCase() : ""; + const descriptionMatch = descriptionText.includes(search); if (!nameMatch && !descriptionMatch) { return false; } @@ -89,10 +78,14 @@ export function sortProducts( const sorted = [...products]; if (sortBy === "price") { return sorted.sort((a, b) => { - const aPrice = (a as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? - (a as { oneTimePrice?: number }).oneTimePrice ?? 0; - const bPrice = (b as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? - (b as { oneTimePrice?: number }).oneTimePrice ?? 0; + const aPrice = + (a as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? + (a as { oneTimePrice?: number }).oneTimePrice ?? + 0; + const bPrice = + (b as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? + (b as { oneTimePrice?: number }).oneTimePrice ?? + 0; return aPrice - bPrice; }); } diff --git a/apps/portal/src/features/catalog/views/InternetPlans.tsx b/apps/portal/src/features/catalog/views/InternetPlans.tsx index 7d0d1c47..eecf93d3 100644 --- a/apps/portal/src/features/catalog/views/InternetPlans.tsx +++ b/apps/portal/src/features/catalog/views/InternetPlans.tsx @@ -23,10 +23,7 @@ import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; export function InternetPlansContainer() { const { data, isLoading, error } = useInternetCatalog(); - const plans: InternetPlanCatalogItem[] = useMemo( - () => data?.plans ?? [], - [data?.plans] - ); + const plans: InternetPlanCatalogItem[] = useMemo(() => data?.plans ?? [], [data?.plans]); const installations: InternetInstallationCatalogItem[] = useMemo( () => data?.installations ?? [], [data?.installations] diff --git a/apps/portal/src/features/checkout/hooks/useCheckout.ts b/apps/portal/src/features/checkout/hooks/useCheckout.ts index 60bd969a..79e01eb0 100644 --- a/apps/portal/src/features/checkout/hooks/useCheckout.ts +++ b/apps/portal/src/features/checkout/hooks/useCheckout.ts @@ -29,7 +29,6 @@ export function useCheckout() { const [submitting, setSubmitting] = useState(false); const [addressConfirmed, setAddressConfirmed] = useState(false); - const [confirmedAddress, setConfirmedAddress] = useState
(null); const [checkoutState, setCheckoutState] = useState>({ status: "loading", @@ -155,7 +154,9 @@ export function useCheckout() { const orderData = { orderType, skus: uniqueSkus, - ...(Object.keys(cart.configuration).length > 0 ? { configurations: cart.configuration } : {}), + ...(Object.keys(cart.configuration).length > 0 + ? { configurations: cart.configuration } + : {}), }; // Client-side guard: prevent Internet orders if an Internet subscription already exists @@ -186,12 +187,11 @@ export function useCheckout() { const confirmAddress = useCallback((address?: Address) => { setAddressConfirmed(true); - setConfirmedAddress(address || null); + void address; }, []); const markAddressIncomplete = useCallback(() => { setAddressConfirmed(false); - setConfirmedAddress(null); }, []); const navigateBackToConfigure = useCallback(() => { diff --git a/apps/portal/src/features/checkout/services/checkout.service.ts b/apps/portal/src/features/checkout/services/checkout.service.ts index c43ca574..f2829ec2 100644 --- a/apps/portal/src/features/checkout/services/checkout.service.ts +++ b/apps/portal/src/features/checkout/services/checkout.service.ts @@ -1,4 +1,4 @@ -import { apiClient } from "@/lib/api"; +import { apiClient, getDataOrThrow } from "@/lib/api"; import type { CheckoutCart, OrderConfigurations } from "@customer-portal/domain/orders"; export const checkoutService = { @@ -10,17 +10,21 @@ export const checkoutService = { selections: Record, configuration?: OrderConfigurations ): Promise { - return apiClient.POST("/checkout/cart", { - orderType, - selections, - configuration, + const response = await apiClient.POST("/checkout/cart", { + body: { + orderType, + selections, + configuration, + }, }); + + return getDataOrThrow(response, "Failed to build checkout cart"); }, /** * Validate checkout cart */ async validateCart(cart: CheckoutCart): Promise { - await apiClient.POST("/checkout/validate", cart); + await apiClient.POST("/checkout/validate", { body: cart }); }, }; diff --git a/apps/portal/src/lib/api/response-helpers.ts b/apps/portal/src/lib/api/response-helpers.ts index cd453585..ff33092e 100644 --- a/apps/portal/src/lib/api/response-helpers.ts +++ b/apps/portal/src/lib/api/response-helpers.ts @@ -1,6 +1,5 @@ import { apiErrorResponseSchema, - apiSuccessResponseSchema, type ApiErrorResponse, type ApiSuccessResponse, } from "@customer-portal/domain/common"; @@ -88,8 +87,8 @@ export function buildSuccessResponse( data: ZodInfer, schema: T ): ApiSuccessResponse> { - return apiSuccessResponseSchema(schema).parse({ + return { success: true, - data, - }); + data: schema.parse(data), + }; }