/** * Orders Domain - WHMCS Provider Mapper * * Transforms normalized order data to WHMCS API format. */ import type { OrderDetails, OrderItemDetails } from "../../contract.js"; import { normalizeBillingCycle } from "../../helpers.js"; import { serializeWhmcsKeyValueMap } from "../../../common/providers/whmcs-utils/index.js"; import { type WhmcsOrderItem, type WhmcsAddOrderParams, type WhmcsAddOrderPayload, } from "./raw.types.js"; export interface OrderItemMappingResult { whmcsItems: WhmcsOrderItem[]; summary: { totalItems: number; serviceItems: number; activationItems: number; }; } /** * Map a single order item to WHMCS format */ export function mapOrderItemToWhmcs(item: OrderItemDetails, index = 0): WhmcsOrderItem { if (!item.product?.whmcsProductId) { throw new Error(`Order item ${index} missing WHMCS product ID`); } const whmcsItem: WhmcsOrderItem = { productId: item.product.whmcsProductId, billingCycle: normalizeBillingCycle(item.billingCycle), quantity: item.quantity, }; return whmcsItem; } /** * Map order details to WHMCS items format * Extracts items from OrderDetails and transforms to WHMCS API format */ export function mapOrderToWhmcsItems(orderDetails: OrderDetails): OrderItemMappingResult { if (!orderDetails.items || orderDetails.items.length === 0) { throw new Error("No order items provided for WHMCS mapping"); } const whmcsItems: WhmcsOrderItem[] = []; let serviceItems = 0; let activationItems = 0; orderDetails.items.forEach((item, index) => { const mapped = mapOrderItemToWhmcs(item, index); whmcsItems.push(mapped); if (mapped.billingCycle === "monthly") { serviceItems++; } else if (mapped.billingCycle === "onetime") { activationItems++; } }); return { whmcsItems, summary: { totalItems: whmcsItems.length, serviceItems, activationItems, }, }; } /** * Build WHMCS AddOrder API payload from parameters * Converts structured params into WHMCS API array format * * WHMCS AddOrder API Documentation: * @see https://developers.whmcs.com/api-reference/addorder/ * * Required Parameters: * - clientid (int): The client ID * - paymentmethod (string): Payment method (e.g. "stripe", "paypal", "mailin") * - pid (int[]): Array of product IDs * - qty (int[]): Array of product quantities (REQUIRED! Without this, no products are added) * - billingcycle (string[]): Array of billing cycles (e.g. "monthly", "onetime") * * Optional Parameters: * - promocode (string): Promotion code to apply * - noinvoice (bool): Don't create invoice * - noinvoiceemail (bool): Don't send invoice email * - noemail (bool): Don't send order confirmation email * - configoptions (string[]): Base64 encoded serialized arrays of config options * - customfields (string[]): Base64 encoded serialized arrays of custom fields * * Response Fields: * - result: "success" or "error" * - orderid: The created order ID * - serviceids: Comma-separated service IDs created * - addonids: Comma-separated addon IDs created * - domainids: Comma-separated domain IDs created * - invoiceid: The invoice ID created * * Common Errors: * - "No items added to cart so order cannot proceed" - Missing or invalid qty parameter * - "Client ID Not Found" - Invalid client ID * - "Invalid Payment Method" - Payment method not recognized */ export function buildWhmcsAddOrderPayload(params: WhmcsAddOrderParams): WhmcsAddOrderPayload { const pids: string[] = []; const billingCycles: string[] = []; const quantities: number[] = []; const domains: string[] = []; const configOptions: string[] = []; const customFields: string[] = []; params.items.forEach(item => { pids.push(item.productId); billingCycles.push(item.billingCycle); quantities.push(item.quantity); domains.push(item.domain || ""); // Domain/hostname (phone number for SIM) // Handle config options - WHMCS expects base64 encoded serialized arrays configOptions.push(serializeWhmcsKeyValueMap(item.configOptions)); // Build custom fields - include item-level fields plus order-level OpportunityId const mergedCustomFields: Record = { ...(item.customFields ?? {}), }; // Inject OpportunityId into each item's custom fields for lifecycle tracking // This links the WHMCS service back to the Salesforce Opportunity if (params.sfOpportunityId) { mergedCustomFields["OpportunityId"] = params.sfOpportunityId; } // Handle custom fields - WHMCS expects base64 encoded serialized arrays customFields.push(serializeWhmcsKeyValueMap(mergedCustomFields)); }); const payload: WhmcsAddOrderPayload = { clientid: params.clientId, paymentmethod: params.paymentMethod, pid: pids, billingcycle: billingCycles, qty: quantities, }; // Add domain array if any items have domains if (domains.some(d => d !== "")) { payload.domain = domains; } // Add optional fields if (params.promoCode) { payload.promocode = params.promoCode; } if (params.noinvoice !== undefined) { payload.noinvoice = params.noinvoice; } if (params.noinvoiceemail !== undefined) { payload.noinvoiceemail = params.noinvoiceemail; } if (params.noemail !== undefined) { payload.noemail = params.noemail; } if (params.notes) { payload.notes = params.notes; } if (configOptions.some(opt => opt !== "")) { payload.configoptions = configOptions; } if (customFields.some(field => field !== "")) { payload.customfields = customFields; } return payload; } /** * Create order notes with Salesforce tracking information */ export function createOrderNotes(sfOrderId: string, additionalNotes?: string): string { const notes: string[] = []; // Always include Salesforce Order ID for tracking notes.push(`sfOrderId=${sfOrderId}`); // Add provisioning timestamp notes.push(`provisionedAt=${new Date().toISOString()}`); // Add additional notes if provided if (additionalNotes) { notes.push(additionalNotes); } return notes.join("; "); }