barsa 9e27380069 Update TypeScript configurations, improve module imports, and clean up Dockerfiles
- 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.
2025-12-10 16:08:34 +09:00

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));
}
}