Assist_Design/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts
T. Narantuya 0a387275ff Refactor address handling in AuthService and SignupDto, and enhance order processing with address verification
- Updated AuthService to directly access address fields and added support for address line 2.
- Introduced AddressDto in SignupDto for structured address validation.
- Modified OrdersController to utilize CreateOrderDto for improved type safety.
- Enhanced OrderBuilder to include address snapshot functionality during order creation.
- Updated UsersService to handle address updates and added new methods in WHMCS service for client updates.
- Improved address confirmation logic in AddressConfirmation component for internet orders.
2025-08-29 13:26:57 +09:00

366 lines
11 KiB
TypeScript

import { getErrorMessage } from "../../../common/utils/error.util";
import { Logger } from "nestjs-pino";
import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import {
WhmcsApiResponse,
WhmcsErrorResponse,
WhmcsInvoicesResponse,
WhmcsInvoiceResponse,
WhmcsProductsResponse,
WhmcsClientResponse,
WhmcsSsoResponse,
WhmcsValidateLoginResponse,
WhmcsAddClientResponse,
WhmcsCatalogProductsResponse,
WhmcsPayMethodsResponse,
WhmcsAddPayMethodResponse,
WhmcsPaymentGatewaysResponse,
WhmcsGetInvoicesParams,
WhmcsGetClientsProductsParams,
WhmcsCreateSsoTokenParams,
WhmcsValidateLoginParams,
WhmcsAddClientParams,
WhmcsGetPayMethodsParams,
WhmcsAddPayMethodParams,
} from "../types/whmcs-api.types";
export interface WhmcsApiConfig {
baseUrl: string;
identifier: string;
secret: string;
timeout?: number;
retryAttempts?: number;
retryDelay?: number;
}
@Injectable()
export class WhmcsConnectionService {
private readonly config: WhmcsApiConfig;
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly configService: ConfigService
) {
this.config = {
baseUrl: this.configService.get<string>("WHMCS_BASE_URL", ""),
identifier: this.configService.get<string>("WHMCS_API_IDENTIFIER", ""),
secret: this.configService.get<string>("WHMCS_API_SECRET", ""),
timeout: this.configService.get<number>("WHMCS_API_TIMEOUT", 30000),
retryAttempts: this.configService.get<number>("WHMCS_API_RETRY_ATTEMPTS", 3),
retryDelay: this.configService.get<number>("WHMCS_API_RETRY_DELAY", 1000),
};
this.validateConfig();
}
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`;
const requestParams = {
action,
username: this.config.identifier,
password: this.config.secret,
responsetype: "json",
...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),
});
const formData = new URLSearchParams(requestParams);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Customer-Portal/1.0",
},
body: formData,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const responseText = await response.text();
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),
});
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;
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") {
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", "token", "key"];
sensitiveFields.forEach(field => {
if (sanitized[field]) {
sanitized[field] = "[REDACTED]";
}
});
return sanitized;
}
// ==========================================
// PUBLIC API 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;
}
}
/**
* Check if WHMCS service is available
*/
async isAvailable(): Promise<boolean> {
try {
return await this.healthCheck();
} catch {
return false;
}
}
/**
* 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;
}
}
// ==========================================
// CLIENT API METHODS
// ==========================================
async getClientDetails(clientId: number): Promise<WhmcsClientResponse> {
return this.makeRequest<WhmcsClientResponse>("GetClientsDetails", {
clientid: clientId,
});
}
async getClientDetailsByEmail(email: string): Promise<WhmcsClientResponse> {
return this.makeRequest<WhmcsClientResponse>("GetClientsDetails", {
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);
}
async addClient(params: WhmcsAddClientParams): Promise<WhmcsAddClientResponse> {
return this.makeRequest<WhmcsAddClientResponse>("AddClient", params);
}
// ==========================================
// INVOICE API METHODS
// ==========================================
async getInvoices(params: WhmcsGetInvoicesParams): Promise<WhmcsInvoicesResponse> {
return this.makeRequest<WhmcsInvoicesResponse>("GetInvoices", params);
}
async getInvoice(invoiceId: number): Promise<WhmcsInvoiceResponse> {
return this.makeRequest<WhmcsInvoiceResponse>("GetInvoice", {
invoiceid: invoiceId,
});
}
// ==========================================
// PRODUCT/SERVICE API METHODS
// ==========================================
async getClientsProducts(params: WhmcsGetClientsProductsParams): Promise<WhmcsProductsResponse> {
return this.makeRequest<WhmcsProductsResponse>("GetClientsProducts", params);
}
async getProducts(): Promise<WhmcsCatalogProductsResponse> {
return this.makeRequest<WhmcsCatalogProductsResponse>("GetProducts");
}
// ==========================================
// SSO API METHODS
// ==========================================
async createSsoToken(params: WhmcsCreateSsoTokenParams): Promise<WhmcsSsoResponse> {
return this.makeRequest<WhmcsSsoResponse>("CreateSsoToken", params);
}
// ==========================================
// PAYMENT METHOD API METHODS
// ==========================================
async getPayMethods(params: WhmcsGetPayMethodsParams): Promise<WhmcsPayMethodsResponse> {
return this.makeRequest<WhmcsPayMethodsResponse>("GetPayMethods", params);
}
async addPayMethod(params: WhmcsAddPayMethodParams): Promise<WhmcsAddPayMethodResponse> {
return this.makeRequest<WhmcsAddPayMethodResponse>("AddPayMethod", params);
}
// ==========================================
// PAYMENT GATEWAY API METHODS
// ==========================================
async getPaymentGateways(): Promise<WhmcsPaymentGatewaysResponse> {
return this.makeRequest<WhmcsPaymentGatewaysResponse>("GetPaymentMethods");
}
}