- 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.
366 lines
11 KiB
TypeScript
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");
|
|
}
|
|
}
|