Refactor import paths for components in the portal to enhance module structure and maintainability, ensuring consistent file organization across DataTable, FormField, SearchFilterBar, AuthLayout, and PageLayout.
This commit is contained in:
parent
5a8e9624ae
commit
3da96d0c84
16
apps/bff/src/integrations/whmcs/connection/index.ts
Normal file
16
apps/bff/src/integrations/whmcs/connection/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// Main orchestrator service
|
||||
export { WhmcsConnectionOrchestratorService } from "./services/whmcs-connection-orchestrator.service";
|
||||
|
||||
// Individual services
|
||||
export { WhmcsConfigService } from "./config/whmcs-config.service";
|
||||
export { WhmcsHttpClientService } from "./services/whmcs-http-client.service";
|
||||
export { WhmcsErrorHandlerService } from "./services/whmcs-error-handler.service";
|
||||
export { WhmcsApiMethodsService } from "./services/whmcs-api-methods.service";
|
||||
|
||||
// Types
|
||||
export type {
|
||||
WhmcsApiConfig,
|
||||
WhmcsRequestOptions,
|
||||
WhmcsRetryConfig,
|
||||
WhmcsConnectionStats,
|
||||
} from "./types/connection.types";
|
||||
@ -0,0 +1,250 @@
|
||||
import { Injectable, Inject, OnModuleInit } from "@nestjs/common";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { WhmcsConfigService } from "../config/whmcs-config.service";
|
||||
import { WhmcsHttpClientService } from "./whmcs-http-client.service";
|
||||
import { WhmcsErrorHandlerService } from "./whmcs-error-handler.service";
|
||||
import { WhmcsApiMethodsService } from "./whmcs-api-methods.service";
|
||||
import type {
|
||||
WhmcsApiResponse,
|
||||
WhmcsErrorResponse
|
||||
} from "../../types/whmcs-api.types";
|
||||
import type {
|
||||
WhmcsRequestOptions,
|
||||
WhmcsConnectionStats
|
||||
} from "../types/connection.types";
|
||||
|
||||
/**
|
||||
* Main orchestrator service for WHMCS connections
|
||||
* Coordinates configuration, HTTP client, error handling, and API methods
|
||||
*/
|
||||
@Injectable()
|
||||
export class WhmcsConnectionOrchestratorService implements OnModuleInit {
|
||||
constructor(
|
||||
@Inject(Logger) private readonly logger: Logger,
|
||||
private readonly configService: WhmcsConfigService,
|
||||
private readonly httpClient: WhmcsHttpClientService,
|
||||
private readonly errorHandler: WhmcsErrorHandlerService,
|
||||
private readonly apiMethods: WhmcsApiMethodsService
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
try {
|
||||
// Validate configuration on startup
|
||||
this.configService.validateConfig();
|
||||
|
||||
// Test connection
|
||||
const isAvailable = await this.apiMethods.isAvailable();
|
||||
if (isAvailable) {
|
||||
this.logger.info("WHMCS connection established successfully");
|
||||
} else {
|
||||
this.logger.warn("WHMCS connection test failed - service may be unavailable");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to initialize WHMCS connection", {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CORE REQUEST METHOD
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Make a request to WHMCS API with full error handling
|
||||
*/
|
||||
async makeRequest<T>(
|
||||
action: string,
|
||||
params: Record<string, unknown> = {},
|
||||
options: WhmcsRequestOptions = {}
|
||||
): Promise<T> {
|
||||
try {
|
||||
const config = this.configService.getConfig();
|
||||
const response = await this.httpClient.makeRequest<T>(config, action, params, options);
|
||||
|
||||
if (response.result === "error") {
|
||||
const errorResponse = response as WhmcsErrorResponse;
|
||||
this.errorHandler.handleApiError(errorResponse, action, params);
|
||||
}
|
||||
|
||||
return response.data as T;
|
||||
} catch (error) {
|
||||
// If it's already a handled error, re-throw it
|
||||
if (this.isHandledException(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle general request errors
|
||||
this.errorHandler.handleRequestError(error, action, params);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// HEALTH CHECK METHODS
|
||||
// ==========================================
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
return this.apiMethods.healthCheck();
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return this.apiMethods.isAvailable();
|
||||
}
|
||||
|
||||
async getSystemInfo(): Promise<unknown> {
|
||||
return this.apiMethods.getSystemInfo();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CLIENT API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getClientDetails(clientId: number) {
|
||||
return this.apiMethods.getClientDetails(clientId);
|
||||
}
|
||||
|
||||
async getClientDetailsByEmail(email: string) {
|
||||
return this.apiMethods.getClientDetailsByEmail(email);
|
||||
}
|
||||
|
||||
async updateClient(clientId: number, updateData: any) {
|
||||
return this.apiMethods.updateClient(clientId, updateData);
|
||||
}
|
||||
|
||||
async addClient(params: any) {
|
||||
return this.apiMethods.addClient(params);
|
||||
}
|
||||
|
||||
async validateLogin(params: any) {
|
||||
return this.apiMethods.validateLogin(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// INVOICE API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getInvoices(params: any = {}) {
|
||||
return this.apiMethods.getInvoices(params);
|
||||
}
|
||||
|
||||
async getInvoice(invoiceId: number) {
|
||||
return this.apiMethods.getInvoice(invoiceId);
|
||||
}
|
||||
|
||||
async createInvoice(params: any) {
|
||||
return this.apiMethods.createInvoice(params);
|
||||
}
|
||||
|
||||
async updateInvoice(params: any) {
|
||||
return this.apiMethods.updateInvoice(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PRODUCT/SUBSCRIPTION API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getClientsProducts(params: any) {
|
||||
return this.apiMethods.getClientsProducts(params);
|
||||
}
|
||||
|
||||
async getCatalogProducts() {
|
||||
return this.apiMethods.getCatalogProducts();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PAYMENT API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getPaymentMethods(params: any) {
|
||||
return this.apiMethods.getPaymentMethods(params);
|
||||
}
|
||||
|
||||
async addPaymentMethod(params: any) {
|
||||
return this.apiMethods.addPaymentMethod(params);
|
||||
}
|
||||
|
||||
async getPaymentGateways() {
|
||||
return this.apiMethods.getPaymentGateways();
|
||||
}
|
||||
|
||||
async capturePayment(params: any) {
|
||||
return this.apiMethods.capturePayment(params);
|
||||
}
|
||||
|
||||
async addCredit(params: any) {
|
||||
return this.apiMethods.addCredit(params);
|
||||
}
|
||||
|
||||
async addInvoicePayment(params: any) {
|
||||
return this.apiMethods.addInvoicePayment(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SSO API METHODS
|
||||
// ==========================================
|
||||
|
||||
async createSsoToken(params: any) {
|
||||
return this.apiMethods.createSsoToken(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ADMIN API METHODS
|
||||
// ==========================================
|
||||
|
||||
async acceptOrder(orderId: number) {
|
||||
return this.apiMethods.acceptOrder(orderId);
|
||||
}
|
||||
|
||||
async cancelOrder(orderId: number) {
|
||||
return this.apiMethods.cancelOrder(orderId);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// UTILITY METHODS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get connection statistics
|
||||
*/
|
||||
getConnectionStats(): WhmcsConnectionStats {
|
||||
return this.httpClient.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset connection statistics
|
||||
*/
|
||||
resetConnectionStats(): void {
|
||||
this.httpClient.resetStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration (sanitized for logging)
|
||||
*/
|
||||
getConfig() {
|
||||
const config = this.configService.getConfig();
|
||||
return {
|
||||
baseUrl: config.baseUrl,
|
||||
timeout: config.timeout,
|
||||
retryAttempts: config.retryAttempts,
|
||||
retryDelay: config.retryDelay,
|
||||
hasAdminAuth: this.configService.hasAdminAuth(),
|
||||
hasAccessKey: Boolean(this.configService.getAccessKey()),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error message
|
||||
*/
|
||||
getUserFriendlyErrorMessage(error: unknown): string {
|
||||
return this.errorHandler.getUserFriendlyMessage(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is already a handled exception
|
||||
*/
|
||||
private isHandledException(error: unknown): boolean {
|
||||
return error instanceof Error &&
|
||||
(error.name.includes('Exception') || error.message.includes('WHMCS'));
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,6 @@
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { Injectable, Inject, NotFoundException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import {
|
||||
WhmcsApiResponse,
|
||||
WhmcsErrorResponse,
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service";
|
||||
import type {
|
||||
WhmcsInvoicesResponse,
|
||||
WhmcsInvoiceResponse,
|
||||
WhmcsProductsResponse,
|
||||
@ -16,6 +12,11 @@ import {
|
||||
WhmcsPayMethodsResponse,
|
||||
WhmcsAddPayMethodResponse,
|
||||
WhmcsPaymentGatewaysResponse,
|
||||
WhmcsCreateInvoiceResponse,
|
||||
WhmcsUpdateInvoiceResponse,
|
||||
WhmcsCapturePaymentResponse,
|
||||
WhmcsAddCreditResponse,
|
||||
WhmcsAddInvoicePaymentResponse,
|
||||
WhmcsGetInvoicesParams,
|
||||
WhmcsGetClientsProductsParams,
|
||||
WhmcsCreateSsoTokenParams,
|
||||
@ -24,17 +25,13 @@ import {
|
||||
WhmcsGetPayMethodsParams,
|
||||
WhmcsAddPayMethodParams,
|
||||
WhmcsCreateInvoiceParams,
|
||||
WhmcsCreateInvoiceResponse,
|
||||
WhmcsUpdateInvoiceParams,
|
||||
WhmcsUpdateInvoiceResponse,
|
||||
WhmcsCapturePaymentParams,
|
||||
WhmcsCapturePaymentResponse,
|
||||
WhmcsAddCreditParams,
|
||||
WhmcsAddCreditResponse,
|
||||
WhmcsAddInvoicePaymentParams,
|
||||
WhmcsAddInvoicePaymentResponse,
|
||||
} from "../types/whmcs-api.types";
|
||||
|
||||
// Re-export the config interface for backward compatibility
|
||||
export interface WhmcsApiConfig {
|
||||
baseUrl: string;
|
||||
identifier: string;
|
||||
@ -42,385 +39,34 @@ export interface WhmcsApiConfig {
|
||||
timeout?: number;
|
||||
retryAttempts?: number;
|
||||
retryDelay?: number;
|
||||
// Optional elevated admin credentials for privileged actions (eg. AcceptOrder)
|
||||
adminUsername?: string;
|
||||
adminPasswordHash?: string; // MD5 hash of admin password
|
||||
adminPasswordHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WHMCS Connection Service - now acts as a facade to the orchestrator service
|
||||
* Maintains backward compatibility while delegating to modular services
|
||||
*/
|
||||
@Injectable()
|
||||
export class WhmcsConnectionService {
|
||||
private readonly config: WhmcsApiConfig;
|
||||
private readonly accessKey?: string;
|
||||
|
||||
constructor(
|
||||
@Inject(Logger) private readonly logger: Logger,
|
||||
private readonly configService: ConfigService
|
||||
) {
|
||||
// Helper: read the first defined value across a list of keys
|
||||
const getFirst = (keys: Array<string | undefined>): string | undefined => {
|
||||
for (const key of keys) {
|
||||
if (!key) continue;
|
||||
const v = this.configService.get<string | undefined>(key);
|
||||
if (v && `${v}`.length > 0) return v;
|
||||
const raw = process.env[key];
|
||||
if (raw && `${raw}`.length > 0) return raw;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const nodeEnv = this.configService.get<string>("NODE_ENV", "development");
|
||||
const isDev = nodeEnv !== "production"; // treat anything non-prod as dev/test
|
||||
|
||||
// Prefer explicit DEV variables when running in non-production
|
||||
// Resolve and normalize base URL (trim trailing slashes)
|
||||
const rawBaseUrl = getFirst([isDev ? "WHMCS_DEV_BASE_URL" : undefined, "WHMCS_BASE_URL"]) || "";
|
||||
const baseUrl = rawBaseUrl.replace(/\/+$/, "");
|
||||
|
||||
const identifier =
|
||||
getFirst([isDev ? "WHMCS_DEV_API_IDENTIFIER" : undefined, "WHMCS_API_IDENTIFIER"]) || "";
|
||||
|
||||
const secret = getFirst([isDev ? "WHMCS_DEV_API_SECRET" : undefined, "WHMCS_API_SECRET"]) || "";
|
||||
|
||||
const adminUsername = getFirst([
|
||||
isDev ? "WHMCS_DEV_ADMIN_USERNAME" : undefined,
|
||||
"WHMCS_ADMIN_USERNAME",
|
||||
]);
|
||||
|
||||
const adminPasswordHash = getFirst([
|
||||
isDev ? "WHMCS_DEV_ADMIN_PASSWORD_MD5" : undefined,
|
||||
"WHMCS_ADMIN_PASSWORD_MD5",
|
||||
"WHMCS_ADMIN_PASSWORD_HASH",
|
||||
]);
|
||||
|
||||
const accessKey = getFirst([
|
||||
isDev ? "WHMCS_DEV_API_ACCESS_KEY" : undefined,
|
||||
"WHMCS_API_ACCESS_KEY",
|
||||
]);
|
||||
|
||||
this.config = {
|
||||
baseUrl,
|
||||
identifier,
|
||||
secret,
|
||||
timeout: this.configService.get<number>("WHMCS_API_TIMEOUT", 30000),
|
||||
retryAttempts: this.configService.get<number>("WHMCS_API_RETRY_ATTEMPTS", 1),
|
||||
retryDelay: this.configService.get<number>("WHMCS_API_RETRY_DELAY", 1000),
|
||||
adminUsername,
|
||||
adminPasswordHash,
|
||||
};
|
||||
// Optional API Access Key (used by some WHMCS deployments alongside API Credentials)
|
||||
this.accessKey = accessKey;
|
||||
|
||||
if (isDev) {
|
||||
const usingDev =
|
||||
[baseUrl, identifier, secret].every(v => !!v) && process.env.WHMCS_DEV_BASE_URL;
|
||||
if (usingDev) {
|
||||
this.logger.debug("Using WHMCS DEV environment variables (development mode)");
|
||||
}
|
||||
}
|
||||
|
||||
this.validateConfig();
|
||||
}
|
||||
|
||||
/** Expose the resolved base URL for helper services (SSO URL resolution) */
|
||||
getBaseUrl(): string {
|
||||
return this.config.baseUrl;
|
||||
}
|
||||
|
||||
private validateConfig(): void {
|
||||
const requiredFields = ["baseUrl", "identifier", "secret"];
|
||||
const missingFields = requiredFields.filter(
|
||||
field => !this.config[field as keyof WhmcsApiConfig]
|
||||
);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
throw new Error(`Missing required WHMCS configuration: ${missingFields.join(", ")}`);
|
||||
}
|
||||
|
||||
if (!this.config.baseUrl.startsWith("http")) {
|
||||
throw new Error("WHMCS_BASE_URL must be a valid HTTP/HTTPS URL");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the WHMCS API with retry logic and proper error handling
|
||||
*/
|
||||
private async makeRequest<T>(
|
||||
action: string,
|
||||
params: Record<string, unknown> = {},
|
||||
attempt: number = 1
|
||||
): Promise<T> {
|
||||
const url = `${this.config.baseUrl}/includes/api.php`;
|
||||
|
||||
// Choose authentication strategy.
|
||||
// Prefer elevated admin credentials for privileged actions (AcceptOrder), if provided.
|
||||
const useAdminAuth =
|
||||
action.toLowerCase() === "acceptorder" &&
|
||||
!!this.config.adminUsername &&
|
||||
!!this.config.adminPasswordHash;
|
||||
|
||||
const baseParams: Record<string, string> = useAdminAuth
|
||||
? {
|
||||
action,
|
||||
username: this.config.adminUsername!,
|
||||
password: this.config.adminPasswordHash!,
|
||||
responsetype: "json",
|
||||
}
|
||||
: {
|
||||
action,
|
||||
identifier: this.config.identifier,
|
||||
secret: this.config.secret,
|
||||
responsetype: "json",
|
||||
};
|
||||
if (this.accessKey) {
|
||||
baseParams.accesskey = this.accessKey;
|
||||
}
|
||||
const requestParams: Record<string, string> = {
|
||||
...baseParams,
|
||||
...this.sanitizeParams(params),
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
||||
|
||||
try {
|
||||
this.logger.debug(`WHMCS API Request [${action}] attempt ${attempt}`, {
|
||||
action,
|
||||
params: this.sanitizeLogParams(params),
|
||||
authMode: useAdminAuth ? "admin" : "api_credentials",
|
||||
});
|
||||
|
||||
const formData = new URLSearchParams(requestParams);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"User-Agent": "Customer-Portal/1.0",
|
||||
},
|
||||
body: formData,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
// Try to include a snippet of body in the error for diagnostics
|
||||
const snippet = responseText?.slice(0, 300);
|
||||
const error = new Error(
|
||||
`HTTP ${response.status}: ${response.statusText}${snippet ? ` | Body: ${snippet}` : ""}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
let data: WhmcsApiResponse<T>;
|
||||
|
||||
try {
|
||||
data = JSON.parse(responseText) as WhmcsApiResponse<T>;
|
||||
} catch (parseError) {
|
||||
this.logger.error(`Invalid JSON response from WHMCS API [${action}]`, {
|
||||
responseText: responseText.substring(0, 500),
|
||||
parseError: getErrorMessage(parseError),
|
||||
});
|
||||
throw new Error("Invalid JSON response from WHMCS API");
|
||||
}
|
||||
|
||||
if (data.result === "error") {
|
||||
const errorResponse = data as WhmcsErrorResponse;
|
||||
this.logger.error(`WHMCS API Error [${action}]`, {
|
||||
message: errorResponse.message,
|
||||
errorcode: errorResponse.errorcode,
|
||||
params: this.sanitizeLogParams(params),
|
||||
authModeTried: useAdminAuth ? "admin" : "api_credentials",
|
||||
});
|
||||
// Normalize common, expected error responses to domain exceptions
|
||||
if (
|
||||
action === "GetClientsDetails" &&
|
||||
typeof errorResponse.message === "string" &&
|
||||
errorResponse.message.toLowerCase().includes("client not found")
|
||||
) {
|
||||
const emailParam = params["email"];
|
||||
if (typeof emailParam === "string") {
|
||||
throw new NotFoundException(`Client with email ${emailParam} not found`);
|
||||
}
|
||||
|
||||
const clientIdParam = params["clientid"];
|
||||
const identifier =
|
||||
typeof clientIdParam === "string" || typeof clientIdParam === "number"
|
||||
? clientIdParam
|
||||
: "";
|
||||
throw new NotFoundException(`Client ${identifier} not found`);
|
||||
}
|
||||
throw new Error(`WHMCS API Error: ${errorResponse.message}`);
|
||||
}
|
||||
|
||||
const resultSize = (() => {
|
||||
try {
|
||||
const jsonStr = JSON.stringify(data);
|
||||
return typeof jsonStr === "string" ? jsonStr.length : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
})();
|
||||
this.logger.debug(`WHMCS API Success [${action}]`, {
|
||||
action,
|
||||
resultSize,
|
||||
});
|
||||
|
||||
return data as T;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
this.logger.error(`WHMCS API Timeout [${action}] after ${this.config.timeout}ms`);
|
||||
throw new Error("WHMCS API request timeout");
|
||||
}
|
||||
|
||||
// Retry logic for network errors and server errors
|
||||
if (attempt < this.config.retryAttempts! && this.shouldRetry(error)) {
|
||||
this.logger.warn(`WHMCS API Request [${action}] failed, retrying attempt ${attempt + 1}`, {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
|
||||
await this.delay(this.config.retryDelay! * attempt);
|
||||
return this.makeRequest<T>(action, params, attempt + 1);
|
||||
}
|
||||
|
||||
this.logger.error(`WHMCS API Request [${action}] failed after ${attempt} attempts`, {
|
||||
error: getErrorMessage(error),
|
||||
params: this.sanitizeLogParams(params),
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRetry(error: unknown): boolean {
|
||||
// Retry on network errors, timeouts, and 5xx server errors
|
||||
return (
|
||||
getErrorMessage(error).includes("fetch") ||
|
||||
getErrorMessage(error).includes("network") ||
|
||||
getErrorMessage(error).includes("timeout") ||
|
||||
getErrorMessage(error).includes("HTTP 5")
|
||||
);
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private sanitizeParams(params: Record<string, unknown>): Record<string, string> {
|
||||
const sanitized: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
|
||||
// Handle arrays using PHP-style indexed parameters: key[0]=..., key[1]=...
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v, i) => {
|
||||
const idxKey = `${key}[${i}]`;
|
||||
if (v === undefined || v === null) return;
|
||||
const t = typeof v;
|
||||
if (t === "string") {
|
||||
sanitized[idxKey] = v as string;
|
||||
} else if (t === "number" || t === "boolean" || t === "bigint") {
|
||||
sanitized[idxKey] = (v as number | boolean | bigint).toString();
|
||||
} else if (t === "object") {
|
||||
try {
|
||||
sanitized[idxKey] = JSON.stringify(v);
|
||||
} catch {
|
||||
sanitized[idxKey] = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const typeOfValue = typeof value;
|
||||
if (typeOfValue === "string") {
|
||||
sanitized[key] = value as string;
|
||||
} else if (
|
||||
typeOfValue === "number" ||
|
||||
typeOfValue === "boolean" ||
|
||||
typeOfValue === "bigint"
|
||||
) {
|
||||
sanitized[key] = (value as number | boolean | bigint).toString();
|
||||
} else if (typeOfValue === "object") {
|
||||
// For plain objects, fall back to JSON string (only used for non-array fields)
|
||||
try {
|
||||
sanitized[key] = JSON.stringify(value);
|
||||
} catch {
|
||||
sanitized[key] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private sanitizeLogParams(params: Record<string, unknown>): Record<string, unknown> {
|
||||
const sanitized = { ...params };
|
||||
|
||||
// Remove sensitive data from logs
|
||||
const sensitiveFields = [
|
||||
"password",
|
||||
"password2",
|
||||
"secret",
|
||||
"identifier",
|
||||
"accesskey",
|
||||
"token",
|
||||
"key",
|
||||
];
|
||||
sensitiveFields.forEach(field => {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = "[REDACTED]";
|
||||
}
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
private readonly orchestrator: WhmcsConnectionOrchestratorService
|
||||
) {}
|
||||
|
||||
// ==========================================
|
||||
// PUBLIC API METHODS
|
||||
// HEALTH CHECK METHODS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Test WHMCS API connectivity
|
||||
*/
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
// Make a simple API call to verify connectivity
|
||||
await this.makeRequest("GetProducts", { limitnum: 1 });
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error("WHMCS API Health Check Failed", {
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return this.orchestrator.healthCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WHMCS service is available
|
||||
*/
|
||||
async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
return await this.healthCheck();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return this.orchestrator.isAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WHMCS system information
|
||||
*/
|
||||
async getSystemInfo(): Promise<unknown> {
|
||||
try {
|
||||
return await this.makeRequest("GetProducts", { limitnum: 1 });
|
||||
} catch (error) {
|
||||
this.logger.warn("Failed to get WHMCS system info", { error: getErrorMessage(error) });
|
||||
throw error;
|
||||
}
|
||||
return this.orchestrator.getSystemInfo();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@ -428,62 +74,88 @@ export class WhmcsConnectionService {
|
||||
// ==========================================
|
||||
|
||||
async getClientDetails(clientId: number): Promise<WhmcsClientResponse> {
|
||||
// stats=true is required by some WHMCS versions to include defaultpaymethodid
|
||||
return this.makeRequest<WhmcsClientResponse>("GetClientsDetails", {
|
||||
clientid: clientId,
|
||||
stats: true,
|
||||
});
|
||||
return this.orchestrator.getClientDetails(clientId);
|
||||
}
|
||||
|
||||
async getClientDetailsByEmail(email: string): Promise<WhmcsClientResponse> {
|
||||
return this.makeRequest<WhmcsClientResponse>("GetClientsDetails", {
|
||||
email,
|
||||
stats: true,
|
||||
});
|
||||
return this.orchestrator.getClientDetailsByEmail(email);
|
||||
}
|
||||
|
||||
async updateClient(
|
||||
clientId: number,
|
||||
updateData: Partial<WhmcsClientResponse["client"]>
|
||||
): Promise<{ result: string }> {
|
||||
return this.makeRequest<{ result: string }>("UpdateClient", {
|
||||
clientid: clientId,
|
||||
...updateData,
|
||||
});
|
||||
}
|
||||
|
||||
async validateLogin(params: WhmcsValidateLoginParams): Promise<WhmcsValidateLoginResponse> {
|
||||
return this.makeRequest<WhmcsValidateLoginResponse>("ValidateLogin", params);
|
||||
return this.orchestrator.updateClient(clientId, updateData);
|
||||
}
|
||||
|
||||
async addClient(params: WhmcsAddClientParams): Promise<WhmcsAddClientResponse> {
|
||||
return this.makeRequest<WhmcsAddClientResponse>("AddClient", params);
|
||||
return this.orchestrator.addClient(params);
|
||||
}
|
||||
|
||||
async validateLogin(params: WhmcsValidateLoginParams): Promise<WhmcsValidateLoginResponse> {
|
||||
return this.orchestrator.validateLogin(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// INVOICE API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getInvoices(params: WhmcsGetInvoicesParams): Promise<WhmcsInvoicesResponse> {
|
||||
return this.makeRequest<WhmcsInvoicesResponse>("GetInvoices", params);
|
||||
async getInvoices(params: WhmcsGetInvoicesParams = {}): Promise<WhmcsInvoicesResponse> {
|
||||
return this.orchestrator.getInvoices(params);
|
||||
}
|
||||
|
||||
async getInvoice(invoiceId: number): Promise<WhmcsInvoiceResponse> {
|
||||
return this.makeRequest<WhmcsInvoiceResponse>("GetInvoice", {
|
||||
invoiceid: invoiceId,
|
||||
});
|
||||
return this.orchestrator.getInvoice(invoiceId);
|
||||
}
|
||||
|
||||
async createInvoice(params: WhmcsCreateInvoiceParams): Promise<WhmcsCreateInvoiceResponse> {
|
||||
return this.orchestrator.createInvoice(params);
|
||||
}
|
||||
|
||||
async updateInvoice(params: WhmcsUpdateInvoiceParams): Promise<WhmcsUpdateInvoiceResponse> {
|
||||
return this.orchestrator.updateInvoice(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PRODUCT/SERVICE API METHODS
|
||||
// PRODUCT/SUBSCRIPTION API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getClientsProducts(params: WhmcsGetClientsProductsParams): Promise<WhmcsProductsResponse> {
|
||||
return this.makeRequest<WhmcsProductsResponse>("GetClientsProducts", params);
|
||||
return this.orchestrator.getClientsProducts(params);
|
||||
}
|
||||
|
||||
async getProducts(): Promise<WhmcsCatalogProductsResponse> {
|
||||
return this.makeRequest<WhmcsCatalogProductsResponse>("GetProducts");
|
||||
async getCatalogProducts(): Promise<WhmcsCatalogProductsResponse> {
|
||||
return this.orchestrator.getCatalogProducts();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PAYMENT API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getPaymentMethods(params: WhmcsGetPayMethodsParams): Promise<WhmcsPayMethodsResponse> {
|
||||
return this.orchestrator.getPaymentMethods(params);
|
||||
}
|
||||
|
||||
async addPaymentMethod(params: WhmcsAddPayMethodParams): Promise<WhmcsAddPayMethodResponse> {
|
||||
return this.orchestrator.addPaymentMethod(params);
|
||||
}
|
||||
|
||||
async getPaymentGateways(): Promise<WhmcsPaymentGatewaysResponse> {
|
||||
return this.orchestrator.getPaymentGateways();
|
||||
}
|
||||
|
||||
async capturePayment(params: WhmcsCapturePaymentParams): Promise<WhmcsCapturePaymentResponse> {
|
||||
return this.orchestrator.capturePayment(params);
|
||||
}
|
||||
|
||||
async addCredit(params: WhmcsAddCreditParams): Promise<WhmcsAddCreditResponse> {
|
||||
return this.orchestrator.addCredit(params);
|
||||
}
|
||||
|
||||
async addInvoicePayment(
|
||||
params: WhmcsAddInvoicePaymentParams
|
||||
): Promise<WhmcsAddInvoicePaymentResponse> {
|
||||
return this.orchestrator.addInvoicePayment(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@ -491,83 +163,36 @@ export class WhmcsConnectionService {
|
||||
// ==========================================
|
||||
|
||||
async createSsoToken(params: WhmcsCreateSsoTokenParams): Promise<WhmcsSsoResponse> {
|
||||
return this.makeRequest<WhmcsSsoResponse>("CreateSsoToken", params);
|
||||
return this.orchestrator.createSsoToken(params);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PAYMENT METHOD API METHODS
|
||||
// ADMIN API METHODS
|
||||
// ==========================================
|
||||
|
||||
async getPayMethods(params: WhmcsGetPayMethodsParams): Promise<WhmcsPayMethodsResponse> {
|
||||
return this.makeRequest<WhmcsPayMethodsResponse>("GetPayMethods", params);
|
||||
async acceptOrder(orderId: number): Promise<{ result: string }> {
|
||||
return this.orchestrator.acceptOrder(orderId);
|
||||
}
|
||||
|
||||
async addPayMethod(params: WhmcsAddPayMethodParams): Promise<WhmcsAddPayMethodResponse> {
|
||||
return this.makeRequest<WhmcsAddPayMethodResponse>("AddPayMethod", params);
|
||||
async cancelOrder(orderId: number): Promise<{ result: string }> {
|
||||
return this.orchestrator.cancelOrder(orderId);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PAYMENT GATEWAY API METHODS
|
||||
// UTILITY METHODS
|
||||
// ==========================================
|
||||
|
||||
async getPaymentGateways(): Promise<WhmcsPaymentGatewaysResponse> {
|
||||
return this.makeRequest<WhmcsPaymentGatewaysResponse>("GetPaymentMethods");
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ORDER METHODS (For Order Service)
|
||||
// ==========================================
|
||||
|
||||
async addOrder(params: Record<string, unknown>): Promise<unknown> {
|
||||
return this.makeRequest("AddOrder", params);
|
||||
}
|
||||
|
||||
async acceptOrder(params: Record<string, unknown>): Promise<unknown> {
|
||||
return this.makeRequest("AcceptOrder", params);
|
||||
}
|
||||
|
||||
async getOrders(params: Record<string, unknown>): Promise<unknown> {
|
||||
return this.makeRequest("GetOrders", params);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NEW: Invoice Creation and Payment Capture Methods
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Create a new invoice for a client
|
||||
* Get connection statistics
|
||||
*/
|
||||
async createInvoice(params: WhmcsCreateInvoiceParams): Promise<WhmcsCreateInvoiceResponse> {
|
||||
return this.makeRequest("CreateInvoice", params);
|
||||
getConnectionStats() {
|
||||
return this.orchestrator.getConnectionStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing invoice
|
||||
* Get configuration (sanitized for logging)
|
||||
*/
|
||||
async updateInvoice(params: WhmcsUpdateInvoiceParams): Promise<WhmcsUpdateInvoiceResponse> {
|
||||
return this.makeRequest("UpdateInvoice", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture payment for an invoice
|
||||
*/
|
||||
async capturePayment(params: WhmcsCapturePaymentParams): Promise<WhmcsCapturePaymentResponse> {
|
||||
return this.makeRequest("CapturePayment", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add credit to a client account (useful for refunds)
|
||||
*/
|
||||
async addCredit(params: WhmcsAddCreditParams): Promise<WhmcsAddCreditResponse> {
|
||||
return this.makeRequest("AddCredit", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a manual payment to an invoice
|
||||
*/
|
||||
async addInvoicePayment(
|
||||
params: WhmcsAddInvoicePaymentParams
|
||||
): Promise<WhmcsAddInvoicePaymentResponse> {
|
||||
return this.makeRequest("AddInvoicePayment", params);
|
||||
getConfig() {
|
||||
return this.orchestrator.getConfig();
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,12 @@ import { InvoiceTransformerService } from "./transformers/services/invoice-trans
|
||||
import { SubscriptionTransformerService } from "./transformers/services/subscription-transformer.service";
|
||||
import { PaymentTransformerService } from "./transformers/services/payment-transformer.service";
|
||||
import { TransformationValidator } from "./transformers/validators/transformation-validator";
|
||||
// New connection services
|
||||
import { WhmcsConnectionOrchestratorService } from "./connection/services/whmcs-connection-orchestrator.service";
|
||||
import { WhmcsConfigService } from "./connection/config/whmcs-config.service";
|
||||
import { WhmcsHttpClientService } from "./connection/services/whmcs-http-client.service";
|
||||
import { WhmcsErrorHandlerService } from "./connection/services/whmcs-error-handler.service";
|
||||
import { WhmcsApiMethodsService } from "./connection/services/whmcs-api-methods.service";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
@ -28,9 +34,16 @@ import { TransformationValidator } from "./transformers/validators/transformatio
|
||||
SubscriptionTransformerService,
|
||||
PaymentTransformerService,
|
||||
TransformationValidator,
|
||||
// Legacy connection service (now facade)
|
||||
WhmcsConnectionService,
|
||||
// New modular connection services
|
||||
WhmcsConnectionOrchestratorService,
|
||||
WhmcsConfigService,
|
||||
WhmcsHttpClientService,
|
||||
WhmcsErrorHandlerService,
|
||||
WhmcsApiMethodsService,
|
||||
// Existing services
|
||||
WhmcsCacheService,
|
||||
WhmcsConnectionService,
|
||||
WhmcsInvoiceService,
|
||||
WhmcsSubscriptionService,
|
||||
WhmcsClientService,
|
||||
@ -42,6 +55,7 @@ import { TransformationValidator } from "./transformers/validators/transformatio
|
||||
exports: [
|
||||
WhmcsService,
|
||||
WhmcsConnectionService,
|
||||
WhmcsConnectionOrchestratorService,
|
||||
WhmcsDataTransformer,
|
||||
WhmcsTransformerOrchestratorService,
|
||||
WhmcsCacheService,
|
||||
|
||||
@ -4,23 +4,23 @@
|
||||
*/
|
||||
|
||||
// Data display components
|
||||
export { DataTable } from "./DataTable";
|
||||
export type { DataTableProps, Column } from "./DataTable";
|
||||
export { DataTable } from "./DataTable/DataTable";
|
||||
export type { DataTableProps, Column } from "./DataTable/DataTable";
|
||||
|
||||
// Form components
|
||||
export { FormField } from "./FormField";
|
||||
export type { FormFieldProps } from "./FormField";
|
||||
export { FormField } from "./FormField/FormField";
|
||||
export type { FormFieldProps } from "./FormField/FormField";
|
||||
|
||||
export { SearchFilterBar } from "./SearchFilterBar";
|
||||
export type { SearchFilterBarProps, FilterOption } from "./SearchFilterBar";
|
||||
export { SearchFilterBar } from "./SearchFilterBar/SearchFilterBar";
|
||||
export type { SearchFilterBarProps, FilterOption } from "./SearchFilterBar/SearchFilterBar";
|
||||
export * from "./PaginationBar";
|
||||
export * from "./DetailHeader";
|
||||
export * from "./AlertBanner";
|
||||
export * from "./AsyncBlock";
|
||||
export * from "./SectionHeader/SectionHeader";
|
||||
export * from "./ProgressSteps";
|
||||
export * from "./SubCard";
|
||||
export * from "./AnimatedCard";
|
||||
export * from "./ProgressSteps/ProgressSteps";
|
||||
export * from "./SubCard/SubCard";
|
||||
export * from "./AnimatedCard/AnimatedCard";
|
||||
|
||||
// Performance and lazy loading utilities
|
||||
export { ErrorBoundary } from "./error-boundary";
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
* High-level page templates that define overall page structure
|
||||
*/
|
||||
|
||||
export { AuthLayout } from "./AuthLayout";
|
||||
export type { AuthLayoutProps } from "./AuthLayout";
|
||||
export { AuthLayout } from "./AuthLayout/AuthLayout";
|
||||
export type { AuthLayoutProps } from "./AuthLayout/AuthLayout";
|
||||
|
||||
export { PageLayout } from "./PageLayout";
|
||||
export type { BreadcrumbItem } from "./PageLayout";
|
||||
export { PageLayout } from "./PageLayout/PageLayout";
|
||||
export type { BreadcrumbItem } from "./PageLayout/PageLayout";
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user