Refactor WHMCS client handling to improve type safety and address management. Updated methods in WhmcsService and WhmcsClientService to return NormalizedWhmcsClient type, enhancing consistency. Simplified address retrieval logic and improved caching mechanisms in WhmcsCacheService. Additionally, introduced utility functions for custom field handling in WHMCS integration workflows, ensuring better data management and clarity.
This commit is contained in:
parent
d041831434
commit
8a90003075
@ -5,7 +5,7 @@ import { CacheService } from "@bff/infra/cache/cache.service";
|
||||
import { Invoice, InvoiceList } from "@customer-portal/domain/billing";
|
||||
import { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
|
||||
import { PaymentMethodList, PaymentGatewayList } from "@customer-portal/domain/payments";
|
||||
import { Providers as CustomerProviders } from "@customer-portal/domain/customer";
|
||||
import type { NormalizedWhmcsClient } from "../utils/whmcs-client.utils";
|
||||
|
||||
export interface CacheOptions {
|
||||
ttl?: number;
|
||||
@ -148,15 +148,15 @@ export class WhmcsCacheService {
|
||||
* Get cached client data
|
||||
* Returns WhmcsClient (type inferred from domain)
|
||||
*/
|
||||
async getClientData(clientId: number) {
|
||||
async getClientData(clientId: number): Promise<NormalizedWhmcsClient | null> {
|
||||
const key = this.buildClientKey(clientId);
|
||||
return this.get(key, "client");
|
||||
return this.get<NormalizedWhmcsClient>(key, "client");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache client data
|
||||
*/
|
||||
async setClientData(clientId: number, data: ReturnType<typeof CustomerProviders.Whmcs.transformWhmcsClientResponse>) {
|
||||
async setClientData(clientId: number, data: NormalizedWhmcsClient) {
|
||||
const key = this.buildClientKey(clientId);
|
||||
await this.set(key, data, "client", [`client:${clientId}`]);
|
||||
}
|
||||
|
||||
@ -13,9 +13,9 @@ import type {
|
||||
WhmcsValidateLoginResponse,
|
||||
} from "@customer-portal/domain/customer";
|
||||
import {
|
||||
Providers as CustomerProviders,
|
||||
type Address,
|
||||
} from "@customer-portal/domain/customer";
|
||||
normalizeWhmcsClientResponse,
|
||||
type NormalizedWhmcsClient,
|
||||
} from "../utils/whmcs-client.utils";
|
||||
|
||||
@Injectable()
|
||||
export class WhmcsClientService {
|
||||
@ -57,7 +57,7 @@ export class WhmcsClientService {
|
||||
* Get client details by ID
|
||||
* Returns WhmcsClient (type inferred from domain mapper)
|
||||
*/
|
||||
async getClientDetails(clientId: number) {
|
||||
async getClientDetails(clientId: number): Promise<NormalizedWhmcsClient> {
|
||||
try {
|
||||
// Try cache first
|
||||
const cached = await this.cacheService.getClientData(clientId);
|
||||
@ -76,12 +76,11 @@ export class WhmcsClientService {
|
||||
throw new NotFoundException(`Client ${clientId} not found`);
|
||||
}
|
||||
|
||||
const customer = CustomerProviders.Whmcs.transformWhmcsClientResponse(response);
|
||||
|
||||
await this.cacheService.setClientData(clientId, customer);
|
||||
const normalized = normalizeWhmcsClientResponse(response);
|
||||
await this.cacheService.setClientData(normalized.id, normalized);
|
||||
|
||||
this.logger.log(`Fetched client details for client ${clientId}`);
|
||||
return customer;
|
||||
return normalized;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to fetch client details for client ${clientId}`, {
|
||||
error: getErrorMessage(error),
|
||||
@ -94,7 +93,7 @@ export class WhmcsClientService {
|
||||
* Get client details by email
|
||||
* Returns WhmcsClient (type inferred from domain mapper)
|
||||
*/
|
||||
async getClientDetailsByEmail(email: string) {
|
||||
async getClientDetailsByEmail(email: string): Promise<NormalizedWhmcsClient> {
|
||||
try {
|
||||
const response = await this.connectionService.getClientDetailsByEmail(email);
|
||||
|
||||
@ -106,12 +105,11 @@ export class WhmcsClientService {
|
||||
throw new NotFoundException(`Client with email ${email} not found`);
|
||||
}
|
||||
|
||||
const customer = CustomerProviders.Whmcs.transformWhmcsClientResponse(response);
|
||||
|
||||
await this.cacheService.setClientData(customer.id, customer);
|
||||
const normalized = normalizeWhmcsClientResponse(response);
|
||||
await this.cacheService.setClientData(normalized.id, normalized);
|
||||
|
||||
this.logger.log(`Fetched client details by email: ${email}`);
|
||||
return customer;
|
||||
return normalized;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to fetch client details by email: ${email}`, {
|
||||
error: getErrorMessage(error),
|
||||
|
||||
286
apps/bff/src/integrations/whmcs/utils/whmcs-client.utils.ts
Normal file
286
apps/bff/src/integrations/whmcs/utils/whmcs-client.utils.ts
Normal file
@ -0,0 +1,286 @@
|
||||
import {
|
||||
addressSchema,
|
||||
userSchema,
|
||||
type Address,
|
||||
type User,
|
||||
type UserAuth,
|
||||
} from "@customer-portal/domain/customer";
|
||||
import type {
|
||||
WhmcsClient,
|
||||
WhmcsClientResponse,
|
||||
WhmcsCustomField,
|
||||
WhmcsUser,
|
||||
} from "@customer-portal/domain/customer/providers/whmcs/raw.types";
|
||||
|
||||
type RawCustomFields = WhmcsClient["customfields"];
|
||||
type RawUsers = WhmcsClient["users"];
|
||||
|
||||
const truthyStrings = new Set(["1", "true", "yes", "y", "on"]);
|
||||
const falsyStrings = new Set(["0", "false", "no", "n", "off"]);
|
||||
|
||||
const toNumber = (value: unknown): number | null => {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const toOptionalString = (value: unknown): string | undefined => {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (typeof value === "string") return value;
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const toNullableBoolean = (value: unknown): boolean | null | undefined => {
|
||||
if (value === undefined) return undefined;
|
||||
if (value === null) return null;
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value === 0 ? false : true;
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (truthyStrings.has(normalized)) return true;
|
||||
if (falsyStrings.has(normalized)) return false;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isCustomFieldObject = (value: unknown): value is WhmcsCustomField =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const isRecordOfStrings = (value: unknown): value is Record<string, string> =>
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.values(value).every(v => typeof v === "string");
|
||||
|
||||
const normalizeCustomFields = (
|
||||
raw: RawCustomFields
|
||||
): Record<string, string> | undefined => {
|
||||
if (!raw) return undefined;
|
||||
|
||||
if (Array.isArray(raw)) {
|
||||
const map = raw.reduce<Record<string, string>>((acc, field) => {
|
||||
if (!field) return acc;
|
||||
const idKey = toOptionalString(field.id)?.trim();
|
||||
const nameKey = toOptionalString(field.name)?.trim();
|
||||
const value =
|
||||
field.value === undefined || field.value === null
|
||||
? ""
|
||||
: String(field.value);
|
||||
|
||||
if (idKey) acc[idKey] = value;
|
||||
if (nameKey) acc[nameKey] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.keys(map).length ? map : undefined;
|
||||
}
|
||||
|
||||
if (isRecordOfStrings(raw)) {
|
||||
const stringRecord = raw as Record<string, string>;
|
||||
const map = Object.entries(stringRecord).reduce<Record<string, string>>(
|
||||
(acc, [key, value]) => {
|
||||
const trimmedKey = key.trim();
|
||||
if (!trimmedKey) return acc;
|
||||
acc[trimmedKey] = value;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return Object.keys(map).length ? map : undefined;
|
||||
}
|
||||
|
||||
if (isCustomFieldObject(raw) && "customfield" in raw) {
|
||||
const nested = raw.customfield;
|
||||
if (!nested) return undefined;
|
||||
if (Array.isArray(nested)) return normalizeCustomFields(nested);
|
||||
if (isCustomFieldObject(nested)) return normalizeCustomFields([nested]);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const normalizeUsers = (raw: RawUsers): NormalizedWhmcsUser[] | undefined => {
|
||||
if (!raw) return undefined;
|
||||
|
||||
const rawUsers: WhmcsUser[] = Array.isArray(raw)
|
||||
? raw
|
||||
: Array.isArray(raw.user)
|
||||
? raw.user
|
||||
: raw.user
|
||||
? [raw.user]
|
||||
: [];
|
||||
|
||||
if (rawUsers.length === 0) return undefined;
|
||||
|
||||
const normalized = rawUsers
|
||||
.map<NormalizedWhmcsUser | null>(user => {
|
||||
if (!user) return null;
|
||||
const id = toNumber(user.id);
|
||||
const name = toOptionalString(user.name);
|
||||
const email = toOptionalString(user.email);
|
||||
if (id === null || !name || !email) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
is_owner: toNullableBoolean(user.is_owner) ?? undefined,
|
||||
};
|
||||
})
|
||||
.filter((user): user is NormalizedWhmcsUser => user !== null);
|
||||
|
||||
return normalized.length ? normalized : undefined;
|
||||
};
|
||||
|
||||
export interface NormalizedWhmcsUser {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
is_owner?: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedWhmcsClient
|
||||
extends Omit<
|
||||
WhmcsClient,
|
||||
| "id"
|
||||
| "client_id"
|
||||
| "owner_user_id"
|
||||
| "userid"
|
||||
| "customfields"
|
||||
| "users"
|
||||
| "allowSingleSignOn"
|
||||
| "email_verified"
|
||||
| "marketing_emails_opt_in"
|
||||
| "defaultpaymethodid"
|
||||
| "currency"
|
||||
> {
|
||||
id: number;
|
||||
client_id?: number | null;
|
||||
owner_user_id?: number | null;
|
||||
userid?: number | null;
|
||||
customfields?: Record<string, string>;
|
||||
users?: NormalizedWhmcsUser[];
|
||||
allowSingleSignOn?: boolean | null;
|
||||
email_verified?: boolean | null;
|
||||
marketing_emails_opt_in?: boolean | null;
|
||||
defaultpaymethodid: number | null;
|
||||
currency: number | null;
|
||||
address?: Address;
|
||||
raw: WhmcsClient;
|
||||
}
|
||||
|
||||
export const deriveAddressFromClient = (
|
||||
client: WhmcsClient
|
||||
): Address | undefined => {
|
||||
const address = addressSchema.parse({
|
||||
address1: client.address1 ?? null,
|
||||
address2: client.address2 ?? null,
|
||||
city: client.city ?? null,
|
||||
state: client.fullstate ?? client.state ?? null,
|
||||
postcode: client.postcode ?? null,
|
||||
country: client.country ?? null,
|
||||
countryCode: client.countrycode ?? null,
|
||||
phoneNumber:
|
||||
client.phonenumberformatted ?? client.phonenumber ?? client.telephoneNumber ?? null,
|
||||
phoneCountryCode: client.phonecc ?? null,
|
||||
});
|
||||
|
||||
const hasValues = Object.values(address).some(
|
||||
value => value !== undefined && value !== null && value !== ""
|
||||
);
|
||||
|
||||
return hasValues ? address : undefined;
|
||||
};
|
||||
|
||||
export const normalizeWhmcsClient = (
|
||||
rawClient: WhmcsClient
|
||||
): NormalizedWhmcsClient => {
|
||||
const id = toNumber(rawClient.id);
|
||||
if (id === null) {
|
||||
throw new Error("WHMCS client ID missing or invalid.");
|
||||
}
|
||||
|
||||
const clientId = toNumber(rawClient.client_id);
|
||||
const ownerUserId = toNumber(rawClient.owner_user_id);
|
||||
const userId = toNumber(rawClient.userid);
|
||||
|
||||
return {
|
||||
...rawClient,
|
||||
id,
|
||||
client_id: clientId,
|
||||
owner_user_id: ownerUserId,
|
||||
userid: userId,
|
||||
customfields: normalizeCustomFields(rawClient.customfields),
|
||||
users: normalizeUsers(rawClient.users),
|
||||
allowSingleSignOn: toNullableBoolean(rawClient.allowSingleSignOn) ?? null,
|
||||
email_verified: toNullableBoolean(rawClient.email_verified) ?? null,
|
||||
marketing_emails_opt_in:
|
||||
toNullableBoolean(rawClient.marketing_emails_opt_in) ?? null,
|
||||
defaultpaymethodid: toNumber(rawClient.defaultpaymethodid),
|
||||
currency: toNumber(rawClient.currency),
|
||||
address: deriveAddressFromClient(rawClient),
|
||||
raw: rawClient,
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeWhmcsClientResponse = (
|
||||
response: WhmcsClientResponse
|
||||
): NormalizedWhmcsClient => normalizeWhmcsClient(response.client);
|
||||
|
||||
export const getCustomFieldValue = (
|
||||
client: NormalizedWhmcsClient,
|
||||
key: string | number
|
||||
): string | undefined => {
|
||||
if (!client.customfields) return undefined;
|
||||
const lookupKey = typeof key === "number" ? String(key) : key;
|
||||
const value = client.customfields[lookupKey];
|
||||
if (value !== undefined) return value;
|
||||
|
||||
if (typeof key === "string") {
|
||||
const numeric = Number.parseInt(key, 10);
|
||||
if (!Number.isNaN(numeric)) {
|
||||
return client.customfields[String(numeric)];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const buildUserProfile = (
|
||||
userAuth: UserAuth,
|
||||
client: NormalizedWhmcsClient
|
||||
): User => {
|
||||
const payload = {
|
||||
id: userAuth.id,
|
||||
email: userAuth.email,
|
||||
role: userAuth.role,
|
||||
emailVerified: userAuth.emailVerified,
|
||||
mfaEnabled: userAuth.mfaEnabled,
|
||||
lastLoginAt: userAuth.lastLoginAt,
|
||||
createdAt: userAuth.createdAt,
|
||||
updatedAt: userAuth.updatedAt,
|
||||
firstname: client.firstname ?? null,
|
||||
lastname: client.lastname ?? null,
|
||||
fullname: client.fullname ?? null,
|
||||
companyname: client.companyname ?? null,
|
||||
phonenumber:
|
||||
client.phonenumberformatted ??
|
||||
client.phonenumber ??
|
||||
client.telephoneNumber ??
|
||||
null,
|
||||
language: client.language ?? null,
|
||||
currency_code: client.currency_code ?? null,
|
||||
address: client.address ?? undefined,
|
||||
};
|
||||
|
||||
return userSchema.parse(payload);
|
||||
};
|
||||
|
||||
export const getNumericClientId = (
|
||||
client: NormalizedWhmcsClient
|
||||
): number => client.id;
|
||||
@ -26,6 +26,7 @@ import type {
|
||||
WhmcsCatalogProductListResponse,
|
||||
} from "@customer-portal/domain/catalog";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { deriveAddressFromClient, type NormalizedWhmcsClient } from "./utils/whmcs-client.utils";
|
||||
|
||||
@Injectable()
|
||||
export class WhmcsService {
|
||||
@ -131,7 +132,7 @@ export class WhmcsService {
|
||||
* Get client details by ID
|
||||
* Returns internal WhmcsClient (type inferred)
|
||||
*/
|
||||
async getClientDetails(clientId: number) {
|
||||
async getClientDetails(clientId: number): Promise<NormalizedWhmcsClient> {
|
||||
return this.clientService.getClientDetails(clientId);
|
||||
}
|
||||
|
||||
@ -139,7 +140,7 @@ export class WhmcsService {
|
||||
* Get client details by email
|
||||
* Returns internal WhmcsClient (type inferred)
|
||||
*/
|
||||
async getClientDetailsByEmail(email: string) {
|
||||
async getClientDetailsByEmail(email: string): Promise<NormalizedWhmcsClient> {
|
||||
return this.clientService.getClientDetailsByEmail(email);
|
||||
}
|
||||
|
||||
@ -158,11 +159,7 @@ export class WhmcsService {
|
||||
*/
|
||||
async getClientAddress(clientId: number): Promise<Address> {
|
||||
const customer = await this.clientService.getClientDetails(clientId);
|
||||
if (!customer || typeof customer !== 'object') {
|
||||
return {} as Address;
|
||||
}
|
||||
const custWithAddress = customer as any;
|
||||
return (custWithAddress.address || {}) as Address;
|
||||
return customer.address ?? deriveAddressFromClient(customer.raw) ?? ({} as Address);
|
||||
}
|
||||
|
||||
async updateClientAddress(clientId: number, address: Partial<Address>): Promise<void> {
|
||||
|
||||
@ -13,6 +13,7 @@ import { SalesforceService } from "@bff/integrations/salesforce/salesforce.servi
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { mapPrismaUserToDomain } from "@bff/infra/mappers";
|
||||
import type { User } from "@customer-portal/domain/customer";
|
||||
import { getCustomFieldValue, getNumericClientId } from "@bff/integrations/whmcs/utils/whmcs-client.utils";
|
||||
// No direct Customer import - use inferred type from WHMCS service
|
||||
|
||||
@Injectable()
|
||||
@ -63,8 +64,10 @@ export class WhmcsLinkWorkflowService {
|
||||
throw new UnauthorizedException("Unable to verify account. Please try again later.");
|
||||
}
|
||||
|
||||
const clientNumericId = getNumericClientId(clientDetails);
|
||||
|
||||
try {
|
||||
const existingMapping = await this.mappingsService.findByWhmcsClientId(clientDetails.id);
|
||||
const existingMapping = await this.mappingsService.findByWhmcsClientId(clientNumericId);
|
||||
if (existingMapping) {
|
||||
throw new ConflictException("This billing account is already linked. Please sign in.");
|
||||
}
|
||||
@ -103,8 +106,9 @@ export class WhmcsLinkWorkflowService {
|
||||
throw new UnauthorizedException("Unable to verify credentials. Please try again later.");
|
||||
}
|
||||
|
||||
const customFields = clientDetails.customfields ?? {}; // Raw WHMCS field name
|
||||
const customerNumber = customFields["198"]?.trim() ?? customFields["Customer Number"]?.trim();
|
||||
const customerNumber =
|
||||
getCustomFieldValue(clientDetails, "198")?.trim() ??
|
||||
getCustomFieldValue(clientDetails, "Customer Number")?.trim();
|
||||
|
||||
if (!customerNumber) {
|
||||
throw new BadRequestException(
|
||||
@ -113,7 +117,7 @@ export class WhmcsLinkWorkflowService {
|
||||
}
|
||||
|
||||
this.logger.log("Found Customer Number for WHMCS client", {
|
||||
whmcsClientId: clientDetails.id,
|
||||
whmcsClientId: clientNumericId,
|
||||
hasCustomerNumber: !!customerNumber,
|
||||
});
|
||||
|
||||
@ -134,16 +138,22 @@ export class WhmcsLinkWorkflowService {
|
||||
const createdUser = await this.usersService.create({
|
||||
email,
|
||||
passwordHash: null,
|
||||
firstName: clientDetails.firstname || "",
|
||||
lastName: clientDetails.lastname || "",
|
||||
company: clientDetails.companyname || "", // Raw WHMCS field name
|
||||
phone: clientDetails.phonenumberformatted || clientDetails.phonenumber || clientDetails.telephoneNumber || "", // Raw WHMCS field names
|
||||
firstName: String(clientDetails.firstname ?? ""),
|
||||
lastName: String(clientDetails.lastname ?? ""),
|
||||
company: String(clientDetails.companyname ?? ""), // Raw WHMCS field name
|
||||
phone:
|
||||
String(
|
||||
clientDetails.phonenumberformatted ??
|
||||
clientDetails.phonenumber ??
|
||||
clientDetails.telephoneNumber ??
|
||||
""
|
||||
), // Raw WHMCS field names
|
||||
emailVerified: true,
|
||||
});
|
||||
|
||||
await this.mappingsService.createMapping({
|
||||
userId: createdUser.id,
|
||||
whmcsClientId: clientDetails.id,
|
||||
whmcsClientId: clientNumericId,
|
||||
sfAccountId: sfAccount.id,
|
||||
});
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
type UpdateCustomerProfileRequest,
|
||||
} from "@customer-portal/domain/auth";
|
||||
import {
|
||||
combineToUser,
|
||||
Providers as CustomerProviders,
|
||||
addressSchema,
|
||||
type Address,
|
||||
@ -21,6 +20,7 @@ import type { Activity, DashboardSummary, NextInvoice } from "@customer-portal/d
|
||||
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
|
||||
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||
import { buildUserProfile } from "@bff/integrations/whmcs/utils/whmcs-client.utils";
|
||||
|
||||
// Use a subset of PrismaUser for auth-related updates only
|
||||
type UserUpdateData = Partial<
|
||||
@ -137,8 +137,7 @@ export class UsersService {
|
||||
// Map Prisma user to UserAuth
|
||||
const userAuth = CustomerProviders.Portal.mapPrismaUserToUserAuth(user);
|
||||
|
||||
// Domain combines UserAuth + WhmcsClient → User
|
||||
return combineToUser(userAuth, whmcsClient as CustomerProviders.Whmcs.WhmcsClient);
|
||||
return buildUserProfile(userAuth, whmcsClient);
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to fetch client profile from WHMCS", {
|
||||
error: getErrorMessage(error),
|
||||
|
||||
@ -5,16 +5,22 @@ const withBundleAnalyzer = bundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Enable standalone output only for production deployment
|
||||
output: process.env.NODE_ENV === "production" ? "standalone" : undefined,
|
||||
|
||||
// Ensure workspace package resolves/transpiles correctly in monorepo
|
||||
transpilePackages: ["@customer-portal/domain"],
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
},
|
||||
// Ensure workspace packages are transpiled correctly
|
||||
transpilePackages: [
|
||||
"@customer-portal/domain",
|
||||
"@customer-portal/logging",
|
||||
"@customer-portal/validation",
|
||||
],
|
||||
|
||||
// Tell Next to NOT bundle these server-only libs
|
||||
serverExternalPackages: [
|
||||
@ -102,11 +108,37 @@ const nextConfig = {
|
||||
removeConsole: process.env.NODE_ENV === "production",
|
||||
},
|
||||
|
||||
// Simple bundle optimization
|
||||
// Experimental flags
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
optimizePackageImports: ["@heroicons/react", "lucide-react", "@tanstack/react-query"],
|
||||
},
|
||||
|
||||
webpack(config) {
|
||||
const workspaceRoot = path.resolve(__dirname, "..", "..");
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
"@customer-portal/domain": path.join(workspaceRoot, "packages/domain"),
|
||||
"@customer-portal/logging": path.join(workspaceRoot, "packages/logging/src"),
|
||||
"@customer-portal/validation": path.join(workspaceRoot, "packages/validation/src"),
|
||||
};
|
||||
const preferredExtensions = [".ts", ".tsx", ".mts", ".cts"];
|
||||
const existingExtensions = config.resolve.extensions || [];
|
||||
config.resolve.extensions = [
|
||||
...new Set([...preferredExtensions, ...existingExtensions]),
|
||||
];
|
||||
config.resolve.extensionAlias = {
|
||||
...(config.resolve.extensionAlias || {}),
|
||||
".js": [".ts", ".tsx", ".js"],
|
||||
".mjs": [".mts", ".ts", ".tsx", ".mjs"],
|
||||
};
|
||||
config.module.rules.push({
|
||||
test: /packages\/domain\/.*\.js$/,
|
||||
type: "javascript/esm",
|
||||
});
|
||||
return config;
|
||||
},
|
||||
|
||||
// Keep type checking enabled; monorepo paths provide types
|
||||
typescript: { ignoreBuildErrors: false },
|
||||
|
||||
|
||||
@ -166,6 +166,42 @@ const statsSchema = z.record(
|
||||
z.union([z.string(), z.number(), z.boolean()])
|
||||
).optional();
|
||||
|
||||
const whmcsRawCustomFieldSchema = z
|
||||
.object({
|
||||
id: numberLike.optional(),
|
||||
value: z.string().optional().nullable(),
|
||||
name: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const whmcsRawCustomFieldsArraySchema = z.array(whmcsRawCustomFieldSchema);
|
||||
|
||||
const whmcsCustomFieldsSchema = z
|
||||
.union([
|
||||
z.record(z.string(), z.string()),
|
||||
whmcsRawCustomFieldsArraySchema,
|
||||
z
|
||||
.object({
|
||||
customfield: z
|
||||
.union([whmcsRawCustomFieldSchema, whmcsRawCustomFieldsArraySchema])
|
||||
.optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
])
|
||||
.optional();
|
||||
|
||||
const whmcsUsersSchema = z
|
||||
.union([
|
||||
z.array(subUserSchema),
|
||||
z
|
||||
.object({
|
||||
user: z.union([subUserSchema, z.array(subUserSchema)]).optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
])
|
||||
.optional();
|
||||
|
||||
/**
|
||||
* WhmcsClient - Full WHMCS client data
|
||||
*
|
||||
@ -213,8 +249,8 @@ export const whmcsClientSchema = z.object({
|
||||
// Relations
|
||||
address: addressSchema.nullable().optional(),
|
||||
email_preferences: emailPreferencesSchema.nullable().optional(), // snake_case from WHMCS
|
||||
customfields: z.record(z.string(), z.string()).optional(),
|
||||
users: z.array(subUserSchema).optional(),
|
||||
customfields: whmcsCustomFieldsSchema,
|
||||
users: whmcsUsersSchema,
|
||||
stats: statsSchema.optional(),
|
||||
}).transform(data => ({
|
||||
...data,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user