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:
parent
fcd324df09
commit
e56d6f5e20
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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" };
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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 });
|
||||
},
|
||||
};
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user