Add CurrencyModule to router configuration and enhance WHMCS subscription service response handling

- Integrated CurrencyModule into the API routes for improved currency management.
- Updated WHMCS subscription service to utilize a new response schema for better error handling and data normalization.
- Refactored address and profile API endpoints in account service to use consistent API paths.
- Introduced normalization functions for WHMCS product response types to enhance data integrity.
This commit is contained in:
barsa 2025-10-21 14:41:22 +09:00
parent afa0c5306b
commit cff6c21bae
5 changed files with 83 additions and 34 deletions

View File

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

View File

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

View File

@ -11,22 +11,22 @@ type ProfileUpdateInput = {
export const accountService = {
async getProfile() {
const response = await apiClient.GET<UserProfile>("/me");
const response = await apiClient.GET<UserProfile>("/api/me");
return getNullableData<UserProfile>(response);
},
async updateProfile(update: ProfileUpdateInput) {
const response = await apiClient.PATCH<UserProfile>("/me", { body: update });
const response = await apiClient.PATCH<UserProfile>("/api/me", { body: update });
return getDataOrThrow<UserProfile>(response, "Failed to update profile");
},
async getAddress() {
const response = await apiClient.GET<Address>("/me/address");
const response = await apiClient.GET<Address>("/api/me/address");
return getNullableData<Address>(response);
},
async updateAddress(address: Address) {
const response = await apiClient.PATCH<Address>("/me/address", { body: address });
const response = await apiClient.PATCH<Address>("/api/me/address", { body: address });
return getDataOrThrow<Address>(response, "Failed to update address");
},
};

View File

@ -35,3 +35,7 @@ export type {
// Response types
WhmcsProductListResponse,
} from "./providers/whmcs/raw.types";
export {
whmcsProductListResponseSchema,
} from "./providers/whmcs/raw.types";

View File

@ -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<typeof whmcsCustomFieldSchema>;
* 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<typeof whmcsProductListResponseSchema>;