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/providers"; import * as Providers from "@customer-portal/domain/orders/providers"; import { whmcsAddOrderResponseSchema, whmcsAcceptOrderResponseSchema, } from "@customer-portal/domain/orders/providers"; 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 { 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 { 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; // 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 | null> { try { // Note: The HTTP client throws errors automatically if result === "error" const response = (await this.connection.getOrders({ id: orderId.toString(), })) as Record; return (response.orders as { order?: Record[] })?.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 { 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 { 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; } 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)); } }