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