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