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({
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,
});

View File

@ -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" };
}

View File

@ -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);

View File

@ -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", {

View File

@ -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;
});
}

View File

@ -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]

View File

@ -29,7 +29,6 @@ export function useCheckout() {
const [submitting, setSubmitting] = useState(false);
const [addressConfirmed, setAddressConfirmed] = useState(false);
const [confirmedAddress, setConfirmedAddress] = useState<Address | null>(null);
const [checkoutState, setCheckoutState] = useState<AsyncState<CheckoutCart>>({
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(() => {

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";
export const checkoutService = {
@ -10,17 +10,21 @@ export const checkoutService = {
selections: Record<string, string>,
configuration?: OrderConfigurations
): Promise<CheckoutCart> {
return apiClient.POST("/checkout/cart", {
orderType,
selections,
configuration,
const response = await apiClient.POST<CheckoutCart>("/checkout/cart", {
body: {
orderType,
selections,
configuration,
},
});
return getDataOrThrow(response, "Failed to build checkout cart");
},
/**
* Validate checkout cart
*/
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 {
apiErrorResponseSchema,
apiSuccessResponseSchema,
type ApiErrorResponse,
type ApiSuccessResponse,
} from "@customer-portal/domain/common";
@ -88,8 +87,8 @@ export function buildSuccessResponse<T extends ZodTypeAny>(
data: ZodInfer<T>,
schema: T
): ApiSuccessResponse<ZodInfer<T>> {
return apiSuccessResponseSchema(schema).parse({
return {
success: true,
data,
});
data: schema.parse(data),
};
}