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.
This commit is contained in:
barsa 2025-10-22 11:55:47 +09:00
parent fcd324df09
commit e56d6f5e20
9 changed files with 53 additions and 60 deletions

View File

@ -141,14 +141,6 @@ export class WhmcsLinkWorkflowService {
const createdUser = await this.usersService.create({ const createdUser = await this.usersService.create({
email, email,
passwordHash: null, passwordHash: null,
firstName: clientDetails.firstname ?? "",
lastName: clientDetails.lastname ?? "",
company: clientDetails.companyname ?? "", // Raw WHMCS field name
phone:
clientDetails.phonenumberformatted ??
clientDetails.phonenumber ??
clientDetails.telephoneNumber ??
"",
emailVerified: true, emailVerified: true,
}); });

View File

@ -185,7 +185,7 @@ export class AuthController {
@Res({ passthrough: true }) res: Response @Res({ passthrough: true }) res: Response
) { ) {
const token = extractTokenFromRequest(req); 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); this.clearAuthCookies(res);
return { message: "Logout successful" }; return { message: "Logout successful" };
} }

View File

@ -26,7 +26,7 @@ import {
sanitizeCreateRequest, sanitizeCreateRequest,
sanitizeUpdateRequest, sanitizeUpdateRequest,
} from "@customer-portal/domain/mappings"; } 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"; import { mapPrismaMappingToDomain } from "@bff/infra/mappers";
@Injectable() @Injectable()
@ -68,7 +68,7 @@ export class MappingsService {
]); ]);
const existingMappings = [byUser, byWhmcs, bySf] const existingMappings = [byUser, byWhmcs, bySf]
.filter((mapping): mapping is Prisma.IdMapping => mapping !== null) .filter((mapping): mapping is PrismaIdMapping => mapping !== null)
.map(mapPrismaMappingToDomain); .map(mapPrismaMappingToDomain);
const conflictCheck = validateNoConflicts(sanitizedRequest, existingMappings); const conflictCheck = validateNoConflicts(sanitizedRequest, existingMappings);

View File

@ -530,9 +530,17 @@ export class UsersService {
let currency = "JPY"; // Default let currency = "JPY"; // Default
try { try {
const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId); const client = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
const currencyCode = client.currency_code ?? client.raw.currency_code ?? null; const currencyCodeFromClient =
if (currencyCode) { typeof client.currency_code === "string" && client.currency_code.trim().length > 0
currency = currencyCode; ? 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) { } catch (error) {
this.logger.warn("Could not fetch currency from WHMCS client", { this.logger.warn("Could not fetch currency from WHMCS client", {

View File

@ -32,35 +32,21 @@ export function filterProducts(
} }
): CatalogProduct[] { ): CatalogProduct[] {
return products.filter(product => { 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") { if (typeof filters.priceMin === "number") {
const price = (product as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? const price =
(product as { oneTimePrice?: number }).oneTimePrice ?? 0; (product as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ??
(product as { oneTimePrice?: number }).oneTimePrice ??
0;
if (price < filters.priceMin) { if (price < filters.priceMin) {
return false; return false;
} }
} }
if (typeof filters.priceMax === "number") { if (typeof filters.priceMax === "number") {
const price = (product as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? const price =
(product as { oneTimePrice?: number }).oneTimePrice ?? 0; (product as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ??
(product as { oneTimePrice?: number }).oneTimePrice ??
0;
if (price > filters.priceMax) { if (price > filters.priceMax) {
return false; return false;
} }
@ -68,8 +54,11 @@ export function filterProducts(
const search = filters.search?.toLowerCase(); const search = filters.search?.toLowerCase();
if (search) { if (search) {
const nameMatch = product.name.toLowerCase().includes(search); const nameMatch =
const descriptionMatch = product.description?.toLowerCase().includes(search) ?? false; 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) { if (!nameMatch && !descriptionMatch) {
return false; return false;
} }
@ -89,10 +78,14 @@ export function sortProducts(
const sorted = [...products]; const sorted = [...products];
if (sortBy === "price") { if (sortBy === "price") {
return sorted.sort((a, b) => { return sorted.sort((a, b) => {
const aPrice = (a as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? const aPrice =
(a as { oneTimePrice?: number }).oneTimePrice ?? 0; (a as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ??
const bPrice = (b as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ?? (a as { oneTimePrice?: number }).oneTimePrice ??
(b as { oneTimePrice?: number }).oneTimePrice ?? 0; 0;
const bPrice =
(b as { monthlyPrice?: number; oneTimePrice?: number }).monthlyPrice ??
(b as { oneTimePrice?: number }).oneTimePrice ??
0;
return aPrice - bPrice; return aPrice - bPrice;
}); });
} }

View File

@ -23,10 +23,7 @@ import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
export function InternetPlansContainer() { export function InternetPlansContainer() {
const { data, isLoading, error } = useInternetCatalog(); const { data, isLoading, error } = useInternetCatalog();
const plans: InternetPlanCatalogItem[] = useMemo( const plans: InternetPlanCatalogItem[] = useMemo(() => data?.plans ?? [], [data?.plans]);
() => data?.plans ?? [],
[data?.plans]
);
const installations: InternetInstallationCatalogItem[] = useMemo( const installations: InternetInstallationCatalogItem[] = useMemo(
() => data?.installations ?? [], () => data?.installations ?? [],
[data?.installations] [data?.installations]

View File

@ -29,7 +29,6 @@ export function useCheckout() {
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 [checkoutState, setCheckoutState] = useState<AsyncState<CheckoutCart>>({ const [checkoutState, setCheckoutState] = useState<AsyncState<CheckoutCart>>({
status: "loading", status: "loading",
@ -155,7 +154,9 @@ export function useCheckout() {
const orderData = { const orderData = {
orderType, orderType,
skus: uniqueSkus, 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 // Client-side guard: prevent Internet orders if an Internet subscription already exists
@ -186,12 +187,11 @@ export function useCheckout() {
const confirmAddress = useCallback((address?: Address) => { const confirmAddress = useCallback((address?: Address) => {
setAddressConfirmed(true); setAddressConfirmed(true);
setConfirmedAddress(address || null); void address;
}, []); }, []);
const markAddressIncomplete = useCallback(() => { const markAddressIncomplete = useCallback(() => {
setAddressConfirmed(false); setAddressConfirmed(false);
setConfirmedAddress(null);
}, []); }, []);
const navigateBackToConfigure = useCallback(() => { const navigateBackToConfigure = useCallback(() => {

View File

@ -1,4 +1,4 @@
import { apiClient } from "@/lib/api"; import { apiClient, getDataOrThrow } from "@/lib/api";
import type { CheckoutCart, OrderConfigurations } from "@customer-portal/domain/orders"; import type { CheckoutCart, OrderConfigurations } from "@customer-portal/domain/orders";
export const checkoutService = { export const checkoutService = {
@ -10,17 +10,21 @@ export const checkoutService = {
selections: Record<string, string>, selections: Record<string, string>,
configuration?: OrderConfigurations configuration?: OrderConfigurations
): Promise<CheckoutCart> { ): Promise<CheckoutCart> {
return apiClient.POST("/checkout/cart", { const response = await apiClient.POST<CheckoutCart>("/checkout/cart", {
orderType, body: {
selections, orderType,
configuration, selections,
configuration,
},
}); });
return getDataOrThrow(response, "Failed to build checkout cart");
}, },
/** /**
* Validate checkout cart * Validate checkout cart
*/ */
async validateCart(cart: CheckoutCart): Promise<void> { async validateCart(cart: CheckoutCart): Promise<void> {
await apiClient.POST("/checkout/validate", cart); await apiClient.POST("/checkout/validate", { body: cart });
}, },
}; };

View File

@ -1,6 +1,5 @@
import { import {
apiErrorResponseSchema, apiErrorResponseSchema,
apiSuccessResponseSchema,
type ApiErrorResponse, type ApiErrorResponse,
type ApiSuccessResponse, type ApiSuccessResponse,
} from "@customer-portal/domain/common"; } from "@customer-portal/domain/common";
@ -88,8 +87,8 @@ export function buildSuccessResponse<T extends ZodTypeAny>(
data: ZodInfer<T>, data: ZodInfer<T>,
schema: T schema: T
): ApiSuccessResponse<ZodInfer<T>> { ): ApiSuccessResponse<ZodInfer<T>> {
return apiSuccessResponseSchema(schema).parse({ return {
success: true, success: true,
data, data: schema.parse(data),
}); };
} }