diff --git a/apps/bff/src/core/config/router.config.ts b/apps/bff/src/core/config/router.config.ts index e97edd50..71837cca 100644 --- a/apps/bff/src/core/config/router.config.ts +++ b/apps/bff/src/core/config/router.config.ts @@ -6,6 +6,7 @@ import { CatalogModule } from "@bff/modules/catalog/catalog.module"; import { OrdersModule } from "@bff/modules/orders/orders.module"; import { InvoicesModule } from "@bff/modules/invoices/invoices.module"; import { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.module"; +import { CurrencyModule } from "@bff/modules/currency/currency.module"; import { SecurityModule } from "@bff/core/security/security.module"; export const apiRoutes: Routes = [ @@ -19,6 +20,7 @@ export const apiRoutes: Routes = [ { path: "", module: OrdersModule }, { path: "", module: InvoicesModule }, { path: "", module: SubscriptionsModule }, + { path: "", module: CurrencyModule }, { path: "", module: SecurityModule }, ], }, diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts index d0e1dc84..615deff8 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts @@ -5,8 +5,10 @@ import { Subscription, SubscriptionList, Providers } from "@customer-portal/doma import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service"; import { WhmcsCurrencyService } from "./whmcs-currency.service"; import { WhmcsCacheService } from "../cache/whmcs-cache.service"; -import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions"; -import type { WhmcsProductListResponse } from "@customer-portal/domain/subscriptions"; +import { + type WhmcsGetClientsProductsParams, + whmcsProductListResponseSchema, +} from "@customer-portal/domain/subscriptions"; export interface SubscriptionFilters { status?: string; @@ -57,11 +59,20 @@ export class WhmcsSubscriptionService { order: "DESC", }; - const response = await this.connectionService.getClientsProducts(params); + const rawResponse = await this.connectionService.getClientsProducts(params); - if (!response || response.result !== "success") { - const message = response?.message || "GetClientsProducts call failed"; - this.logger.error("WHMCS GetClientsProducts returned error", { + if (!rawResponse) { + this.logger.error("WHMCS GetClientsProducts returned empty response", { + clientId, + }); + throw new Error("GetClientsProducts call failed"); + } + + const response = whmcsProductListResponseSchema.parse(rawResponse); + + if (response.result === "error") { + const message = response.message || "GetClientsProducts call failed"; + this.logger.error("WHMCS GetClientsProducts returned error result", { clientId, response, }); @@ -75,20 +86,20 @@ export class WhmcsSubscriptionService { ? [productContainer] : []; + const totalResults = + response.totalresults !== undefined ? Number(response.totalresults) : products.length; + this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, { - totalresults: response.totalresults, - startnumber: response.startnumber, - numreturned: response.numreturned, + totalresults: totalResults, + startnumber: response.startnumber ?? 0, + numreturned: response.numreturned ?? products.length, productCount: products.length, }); if (products.length === 0) { - this.logger.warn(`No products found for client ${clientId}`, { - responseStructure: response ? Object.keys(response) : "null response", - }); return { subscriptions: [], - totalCount: 0, + totalCount: totalResults, }; } @@ -111,7 +122,7 @@ export class WhmcsSubscriptionService { const result: SubscriptionList = { subscriptions, - totalCount: subscriptions.length, + totalCount: totalResults, }; // Cache the result diff --git a/apps/portal/src/features/account/services/account.service.ts b/apps/portal/src/features/account/services/account.service.ts index cd58df16..295e3cf7 100644 --- a/apps/portal/src/features/account/services/account.service.ts +++ b/apps/portal/src/features/account/services/account.service.ts @@ -11,22 +11,22 @@ type ProfileUpdateInput = { export const accountService = { async getProfile() { - const response = await apiClient.GET("/me"); + const response = await apiClient.GET("/api/me"); return getNullableData(response); }, async updateProfile(update: ProfileUpdateInput) { - const response = await apiClient.PATCH("/me", { body: update }); + const response = await apiClient.PATCH("/api/me", { body: update }); return getDataOrThrow(response, "Failed to update profile"); }, async getAddress() { - const response = await apiClient.GET
("/me/address"); + const response = await apiClient.GET
("/api/me/address"); return getNullableData
(response); }, async updateAddress(address: Address) { - const response = await apiClient.PATCH
("/me/address", { body: address }); + const response = await apiClient.PATCH
("/api/me/address", { body: address }); return getDataOrThrow
(response, "Failed to update address"); }, }; diff --git a/packages/domain/subscriptions/index.ts b/packages/domain/subscriptions/index.ts index 31975b6e..86810593 100644 --- a/packages/domain/subscriptions/index.ts +++ b/packages/domain/subscriptions/index.ts @@ -35,3 +35,7 @@ export type { // Response types WhmcsProductListResponse, } from "./providers/whmcs/raw.types"; + +export { + whmcsProductListResponseSchema, +} from "./providers/whmcs/raw.types"; diff --git a/packages/domain/subscriptions/providers/whmcs/raw.types.ts b/packages/domain/subscriptions/providers/whmcs/raw.types.ts index eca03150..d5d0cc9b 100644 --- a/packages/domain/subscriptions/providers/whmcs/raw.types.ts +++ b/packages/domain/subscriptions/providers/whmcs/raw.types.ts @@ -8,6 +8,39 @@ import { z } from "zod"; +const normalizeRequiredNumber = z.preprocess( + value => { + if (typeof value === "number") return value; + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : value; + } + return value; + }, + z.number() +); + +const normalizeOptionalNumber = z.preprocess( + value => { + if (value === undefined || value === null || value === "") return undefined; + if (typeof value === "number") return value; + if (typeof value === "string") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; + } + return undefined; + }, + z.number().optional() +); + +const normalizeOptionalString = z.preprocess( + value => { + if (value === undefined || value === null || value === "") return undefined; + return String(value); + }, + z.string().optional() +); + // ============================================================================ // Request Parameter Types // ============================================================================ @@ -44,12 +77,12 @@ export const whmcsCustomFieldsContainerSchema = z.object({ // Raw WHMCS Product/Service (Subscription) export const whmcsProductRawSchema = z.object({ - id: z.number(), - clientid: z.number(), - serviceid: z.number().optional(), - pid: z.number().optional(), - orderid: z.number().optional(), - ordernumber: z.string().optional(), + id: normalizeRequiredNumber, + clientid: normalizeRequiredNumber, + serviceid: normalizeOptionalNumber, + pid: normalizeOptionalNumber, + orderid: normalizeOptionalNumber, + ordernumber: normalizeOptionalString, regdate: z.string(), name: z.string(), translated_name: z.string().optional(), @@ -57,12 +90,12 @@ export const whmcsProductRawSchema = z.object({ translated_groupname: z.string().optional(), domain: z.string().optional(), dedicatedip: z.string().optional(), - serverid: z.number().optional(), + serverid: normalizeOptionalNumber, servername: z.string().optional(), serverip: z.string().optional(), serverhostname: z.string().optional(), suspensionreason: z.string().optional(), - promoid: z.number().optional(), + promoid: normalizeOptionalNumber, subscriptionid: z.string().optional(), // Pricing @@ -84,10 +117,10 @@ export const whmcsProductRawSchema = z.object({ // Notes notes: z.string().optional(), - diskusage: z.number().optional(), - disklimit: z.number().optional(), - bwusage: z.number().optional(), - bwlimit: z.number().optional(), + diskusage: normalizeOptionalNumber, + disklimit: normalizeOptionalNumber, + bwusage: normalizeOptionalNumber, + bwlimit: normalizeOptionalNumber, lastupdate: z.string().optional(), // Custom fields @@ -113,19 +146,18 @@ export type WhmcsCustomField = z.infer; * WHMCS GetClientsProducts API response schema */ export const whmcsProductListResponseSchema = z.object({ - result: z.enum(["success", "error"]), + result: z.enum(["success", "error"]).optional(), message: z.string().optional(), clientid: z.union([z.number(), z.string()]).optional(), serviceid: z.union([z.number(), z.string(), z.null()]).optional(), pid: z.union([z.number(), z.string(), z.null()]).optional(), domain: z.string().nullable().optional(), totalresults: z.union([z.number(), z.string()]).optional(), - startnumber: z.number().optional(), - numreturned: z.number().optional(), + startnumber: normalizeOptionalNumber, + numreturned: normalizeOptionalNumber, products: z.object({ product: z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional(), }).optional(), }); export type WhmcsProductListResponse = z.infer; -