- Adjusted TypeScript settings in tsconfig files for better alignment with ESNext standards. - Updated pnpm-lock.yaml to reflect dependency changes and improve package management. - Cleaned up Dockerfiles for both BFF and Portal applications to enhance build processes. - Modified import statements across various modules to include file extensions for consistency. - Removed outdated SHA256 files for backend and frontend tarballs to streamline project structure. - Enhanced health check mechanisms in Dockerfiles for improved application startup reliability.
271 lines
8.9 KiB
TypeScript
271 lines
8.9 KiB
TypeScript
import { Injectable, Inject } from "@nestjs/common";
|
|
import { Logger } from "nestjs-pino";
|
|
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
|
|
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
|
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js";
|
|
|
|
import type {
|
|
WhmcsOrderItem,
|
|
WhmcsAddOrderParams,
|
|
WhmcsAddOrderResponse,
|
|
WhmcsOrderResult,
|
|
} from "@customer-portal/domain/orders";
|
|
import {
|
|
Providers,
|
|
whmcsAddOrderResponseSchema,
|
|
whmcsAcceptOrderResponseSchema,
|
|
} from "@customer-portal/domain/orders";
|
|
|
|
export type { WhmcsOrderItem, WhmcsAddOrderParams, WhmcsOrderResult };
|
|
|
|
@Injectable()
|
|
export class WhmcsOrderService {
|
|
constructor(
|
|
private readonly connection: WhmcsConnectionOrchestratorService,
|
|
@Inject(Logger) private readonly logger: Logger
|
|
) {}
|
|
|
|
/**
|
|
* Create order in WHMCS using AddOrder API
|
|
* Maps Salesforce OrderItems to WHMCS products
|
|
*
|
|
* WHMCS API Response Structure:
|
|
* Success: { orderid, productids, serviceids, addonids, domainids, invoiceid }
|
|
* Error: Thrown by HTTP client before returning
|
|
*/
|
|
async addOrder(params: WhmcsAddOrderParams): Promise<WhmcsOrderResult> {
|
|
this.logger.log("Creating WHMCS order", {
|
|
clientId: params.clientId,
|
|
itemCount: params.items.length,
|
|
sfOrderId: params.sfOrderId,
|
|
hasPromoCode: Boolean(params.promoCode),
|
|
});
|
|
|
|
try {
|
|
// Build WHMCS AddOrder payload
|
|
const addOrderPayload = this.buildAddOrderPayload(params);
|
|
|
|
this.logger.debug("Built WHMCS AddOrder payload", {
|
|
clientId: params.clientId,
|
|
productCount: Array.isArray(addOrderPayload.pid) ? addOrderPayload.pid.length : 0,
|
|
pids: addOrderPayload.pid,
|
|
quantities: addOrderPayload.qty, // CRITICAL: Must be included for products to be added
|
|
billingCycles: addOrderPayload.billingcycle,
|
|
hasConfigOptions: Boolean(addOrderPayload.configoptions),
|
|
hasCustomFields: Boolean(addOrderPayload.customfields),
|
|
promoCode: addOrderPayload.promocode,
|
|
paymentMethod: addOrderPayload.paymentmethod,
|
|
});
|
|
|
|
// Call WHMCS AddOrder API
|
|
// Note: The HTTP client throws errors automatically if result === "error"
|
|
// So we only get here if the request was successful
|
|
const response = (await this.connection.addOrder(addOrderPayload)) as WhmcsAddOrderResponse;
|
|
|
|
// Log the full response for debugging
|
|
this.logger.debug("WHMCS AddOrder response", {
|
|
response,
|
|
clientId: params.clientId,
|
|
sfOrderId: params.sfOrderId,
|
|
});
|
|
|
|
const parsedResponse = whmcsAddOrderResponseSchema.safeParse(response);
|
|
if (!parsedResponse.success) {
|
|
this.logger.error("WHMCS AddOrder response failed validation", {
|
|
clientId: params.clientId,
|
|
sfOrderId: params.sfOrderId,
|
|
issues: parsedResponse.error.flatten(),
|
|
rawResponse: response,
|
|
});
|
|
throw new WhmcsOperationException("WHMCS AddOrder response was invalid", {
|
|
response,
|
|
});
|
|
}
|
|
|
|
const normalizedResult = this.toWhmcsOrderResult(parsedResponse.data);
|
|
|
|
this.logger.log("WHMCS order created successfully", {
|
|
orderId: normalizedResult.orderId,
|
|
invoiceId: normalizedResult.invoiceId,
|
|
serviceIds: normalizedResult.serviceIds,
|
|
addonIds: normalizedResult.addonIds,
|
|
domainIds: normalizedResult.domainIds,
|
|
clientId: params.clientId,
|
|
sfOrderId: params.sfOrderId,
|
|
});
|
|
|
|
return normalizedResult;
|
|
} catch (error) {
|
|
// Enhanced error logging with full context
|
|
this.logger.error("Failed to create WHMCS order", {
|
|
error: getErrorMessage(error),
|
|
errorType: error?.constructor?.name,
|
|
clientId: params.clientId,
|
|
sfOrderId: params.sfOrderId,
|
|
itemCount: params.items.length,
|
|
// Include first 100 chars of error stack for debugging
|
|
errorStack: error instanceof Error ? error.stack?.substring(0, 100) : undefined,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accept/provision order in WHMCS using AcceptOrder API
|
|
* This activates services and creates subscriptions
|
|
*
|
|
* WHMCS API Response Structure:
|
|
* Success: { orderid, invoiceid, serviceids, addonids, domainids }
|
|
* Error: Thrown by HTTP client before returning
|
|
*/
|
|
async acceptOrder(orderId: number, sfOrderId?: string): Promise<void> {
|
|
this.logger.log("Accepting WHMCS order", {
|
|
orderId,
|
|
sfOrderId,
|
|
});
|
|
|
|
try {
|
|
// Call WHMCS AcceptOrder API
|
|
// Note: The HTTP client throws errors automatically if result === "error"
|
|
// So we only get here if the request was successful
|
|
const response = (await this.connection.acceptOrder(orderId)) as Record<string, unknown>;
|
|
|
|
// Log the full response for debugging
|
|
this.logger.debug("WHMCS AcceptOrder response", {
|
|
response,
|
|
orderId,
|
|
sfOrderId,
|
|
});
|
|
|
|
const parsedResponse = whmcsAcceptOrderResponseSchema.safeParse(response);
|
|
if (!parsedResponse.success) {
|
|
this.logger.error("WHMCS AcceptOrder response failed validation", {
|
|
orderId,
|
|
sfOrderId,
|
|
issues: parsedResponse.error.flatten(),
|
|
rawResponse: response,
|
|
});
|
|
throw new WhmcsOperationException("WHMCS AcceptOrder response was invalid", {
|
|
response,
|
|
});
|
|
}
|
|
|
|
this.logger.log("WHMCS order accepted successfully", {
|
|
orderId,
|
|
invoiceId: parsedResponse.data.invoiceid,
|
|
sfOrderId,
|
|
});
|
|
} catch (error) {
|
|
// Enhanced error logging with full context
|
|
this.logger.error("Failed to accept WHMCS order", {
|
|
error: getErrorMessage(error),
|
|
errorType: error?.constructor?.name,
|
|
orderId,
|
|
sfOrderId,
|
|
// Include first 100 chars of error stack for debugging
|
|
errorStack: error instanceof Error ? error.stack?.substring(0, 100) : undefined,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get order details from WHMCS
|
|
*/
|
|
async getOrderDetails(orderId: number): Promise<Record<string, unknown> | null> {
|
|
try {
|
|
// Note: The HTTP client throws errors automatically if result === "error"
|
|
const response = (await this.connection.getOrders({
|
|
id: orderId.toString(),
|
|
})) as Record<string, unknown>;
|
|
|
|
return (response.orders as { order?: Record<string, unknown>[] })?.order?.[0] || null;
|
|
} catch (error) {
|
|
this.logger.error("Failed to get WHMCS order details", {
|
|
error: getErrorMessage(error),
|
|
errorType: error?.constructor?.name,
|
|
orderId,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if client has valid payment method
|
|
*/
|
|
async hasPaymentMethod(clientId: number): Promise<boolean> {
|
|
try {
|
|
const response = await this.connection.getPaymentMethods({
|
|
clientid: clientId,
|
|
});
|
|
|
|
const methods = Array.isArray(response.paymethods) ? response.paymethods : [];
|
|
const hasValidMethod = methods.length > 0;
|
|
|
|
this.logger.log("Payment method check completed", {
|
|
clientId,
|
|
hasPaymentMethod: hasValidMethod,
|
|
methodCount: methods.length,
|
|
});
|
|
|
|
return hasValidMethod;
|
|
} catch (error) {
|
|
this.logger.error("Failed to check payment methods", {
|
|
error: getErrorMessage(error),
|
|
clientId,
|
|
});
|
|
// Don't throw - return false to indicate no payment method
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build WHMCS AddOrder payload from our parameters
|
|
* Following official WHMCS API documentation format
|
|
*
|
|
* Delegates to shared mapper function from integration package
|
|
*/
|
|
private buildAddOrderPayload(params: WhmcsAddOrderParams): Record<string, unknown> {
|
|
const payload = Providers.Whmcs.buildWhmcsAddOrderPayload(params);
|
|
|
|
this.logger.debug("Built WHMCS AddOrder payload", {
|
|
clientId: params.clientId,
|
|
productCount: params.items.length,
|
|
pids: payload.pid,
|
|
billingCycles: payload.billingcycle,
|
|
hasConfigOptions: !!payload.configoptions,
|
|
hasCustomFields: !!payload.customfields,
|
|
});
|
|
|
|
return payload as Record<string, unknown>;
|
|
}
|
|
|
|
private toWhmcsOrderResult(response: WhmcsAddOrderResponse): WhmcsOrderResult {
|
|
const orderId = parseInt(String(response.orderid), 10);
|
|
if (!orderId || Number.isNaN(orderId)) {
|
|
throw new WhmcsOperationException("WHMCS AddOrder did not return valid order ID", {
|
|
response,
|
|
});
|
|
}
|
|
|
|
return {
|
|
orderId,
|
|
invoiceId: response.invoiceid ? parseInt(String(response.invoiceid), 10) : undefined,
|
|
serviceIds: this.parseDelimitedIds(response.serviceids),
|
|
addonIds: this.parseDelimitedIds(response.addonids),
|
|
domainIds: this.parseDelimitedIds(response.domainids),
|
|
};
|
|
}
|
|
|
|
private parseDelimitedIds(value?: string): number[] {
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
return value
|
|
.toString()
|
|
.split(",")
|
|
.map(entry => parseInt(entry.trim(), 10))
|
|
.filter(id => !Number.isNaN(id));
|
|
}
|
|
}
|