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 { OrdersModule } from "@bff/modules/orders/orders.module";
import { InvoicesModule } from "@bff/modules/invoices/invoices.module"; import { InvoicesModule } from "@bff/modules/invoices/invoices.module";
import { SubscriptionsModule } from "@bff/modules/subscriptions/subscriptions.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"; import { SecurityModule } from "@bff/core/security/security.module";
export const apiRoutes: Routes = [ export const apiRoutes: Routes = [
@ -19,6 +20,7 @@ export const apiRoutes: Routes = [
{ path: "", module: OrdersModule }, { path: "", module: OrdersModule },
{ path: "", module: InvoicesModule }, { path: "", module: InvoicesModule },
{ path: "", module: SubscriptionsModule }, { path: "", module: SubscriptionsModule },
{ path: "", module: CurrencyModule },
{ path: "", module: SecurityModule }, { 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 { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service";
import { WhmcsCurrencyService } from "./whmcs-currency.service"; import { WhmcsCurrencyService } from "./whmcs-currency.service";
import { WhmcsCacheService } from "../cache/whmcs-cache.service"; import { WhmcsCacheService } from "../cache/whmcs-cache.service";
import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions"; import {
import type { WhmcsProductListResponse } from "@customer-portal/domain/subscriptions"; type WhmcsGetClientsProductsParams,
whmcsProductListResponseSchema,
} from "@customer-portal/domain/subscriptions";
export interface SubscriptionFilters { export interface SubscriptionFilters {
status?: string; status?: string;
@ -57,11 +59,20 @@ export class WhmcsSubscriptionService {
order: "DESC", order: "DESC",
}; };
const response = await this.connectionService.getClientsProducts(params); const rawResponse = await this.connectionService.getClientsProducts(params);
if (!response || response.result !== "success") { if (!rawResponse) {
const message = response?.message || "GetClientsProducts call failed"; this.logger.error("WHMCS GetClientsProducts returned empty response", {
this.logger.error("WHMCS GetClientsProducts returned error", { 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, clientId,
response, response,
}); });
@ -75,20 +86,20 @@ export class WhmcsSubscriptionService {
? [productContainer] ? [productContainer]
: []; : [];
const totalResults =
response.totalresults !== undefined ? Number(response.totalresults) : products.length;
this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, { this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, {
totalresults: response.totalresults, totalresults: totalResults,
startnumber: response.startnumber, startnumber: response.startnumber ?? 0,
numreturned: response.numreturned, numreturned: response.numreturned ?? products.length,
productCount: products.length, productCount: products.length,
}); });
if (products.length === 0) { if (products.length === 0) {
this.logger.warn(`No products found for client ${clientId}`, {
responseStructure: response ? Object.keys(response) : "null response",
});
return { return {
subscriptions: [], subscriptions: [],
totalCount: 0, totalCount: totalResults,
}; };
} }
@ -111,7 +122,7 @@ export class WhmcsSubscriptionService {
const result: SubscriptionList = { const result: SubscriptionList = {
subscriptions, subscriptions,
totalCount: subscriptions.length, totalCount: totalResults,
}; };
// Cache the result // Cache the result

View File

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

View File

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

View File

@ -8,6 +8,39 @@
import { z } from "zod"; 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 // Request Parameter Types
// ============================================================================ // ============================================================================
@ -44,12 +77,12 @@ export const whmcsCustomFieldsContainerSchema = z.object({
// Raw WHMCS Product/Service (Subscription) // Raw WHMCS Product/Service (Subscription)
export const whmcsProductRawSchema = z.object({ export const whmcsProductRawSchema = z.object({
id: z.number(), id: normalizeRequiredNumber,
clientid: z.number(), clientid: normalizeRequiredNumber,
serviceid: z.number().optional(), serviceid: normalizeOptionalNumber,
pid: z.number().optional(), pid: normalizeOptionalNumber,
orderid: z.number().optional(), orderid: normalizeOptionalNumber,
ordernumber: z.string().optional(), ordernumber: normalizeOptionalString,
regdate: z.string(), regdate: z.string(),
name: z.string(), name: z.string(),
translated_name: z.string().optional(), translated_name: z.string().optional(),
@ -57,12 +90,12 @@ export const whmcsProductRawSchema = z.object({
translated_groupname: z.string().optional(), translated_groupname: z.string().optional(),
domain: z.string().optional(), domain: z.string().optional(),
dedicatedip: z.string().optional(), dedicatedip: z.string().optional(),
serverid: z.number().optional(), serverid: normalizeOptionalNumber,
servername: z.string().optional(), servername: z.string().optional(),
serverip: z.string().optional(), serverip: z.string().optional(),
serverhostname: z.string().optional(), serverhostname: z.string().optional(),
suspensionreason: z.string().optional(), suspensionreason: z.string().optional(),
promoid: z.number().optional(), promoid: normalizeOptionalNumber,
subscriptionid: z.string().optional(), subscriptionid: z.string().optional(),
// Pricing // Pricing
@ -84,10 +117,10 @@ export const whmcsProductRawSchema = z.object({
// Notes // Notes
notes: z.string().optional(), notes: z.string().optional(),
diskusage: z.number().optional(), diskusage: normalizeOptionalNumber,
disklimit: z.number().optional(), disklimit: normalizeOptionalNumber,
bwusage: z.number().optional(), bwusage: normalizeOptionalNumber,
bwlimit: z.number().optional(), bwlimit: normalizeOptionalNumber,
lastupdate: z.string().optional(), lastupdate: z.string().optional(),
// Custom fields // Custom fields
@ -113,19 +146,18 @@ export type WhmcsCustomField = z.infer<typeof whmcsCustomFieldSchema>;
* WHMCS GetClientsProducts API response schema * WHMCS GetClientsProducts API response schema
*/ */
export const whmcsProductListResponseSchema = z.object({ export const whmcsProductListResponseSchema = z.object({
result: z.enum(["success", "error"]), result: z.enum(["success", "error"]).optional(),
message: z.string().optional(), message: z.string().optional(),
clientid: z.union([z.number(), z.string()]).optional(), clientid: z.union([z.number(), z.string()]).optional(),
serviceid: z.union([z.number(), z.string(), z.null()]).optional(), serviceid: z.union([z.number(), z.string(), z.null()]).optional(),
pid: z.union([z.number(), z.string(), z.null()]).optional(), pid: z.union([z.number(), z.string(), z.null()]).optional(),
domain: z.string().nullable().optional(), domain: z.string().nullable().optional(),
totalresults: z.union([z.number(), z.string()]).optional(), totalresults: z.union([z.number(), z.string()]).optional(),
startnumber: z.number().optional(), startnumber: normalizeOptionalNumber,
numreturned: z.number().optional(), numreturned: normalizeOptionalNumber,
products: z.object({ products: z.object({
product: z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional(), product: z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional(),
}).optional(), }).optional(),
}); });
export type WhmcsProductListResponse = z.infer<typeof whmcsProductListResponseSchema>; export type WhmcsProductListResponse = z.infer<typeof whmcsProductListResponseSchema>;