/** * 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 { 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 configOptions: string[] = []; const customFields: string[] = []; params.items.forEach(item => { pids.push(item.productId); billingCycles.push(item.billingCycle); quantities.push(item.quantity); // Handle config options - WHMCS expects base64 encoded serialized arrays configOptions.push(serializeForWhmcs(item.configOptions)); // Handle custom fields - WHMCS expects base64 encoded serialized arrays customFields.push(serializeForWhmcs(item.customFields)); }); const payload: WhmcsAddOrderPayload = { clientid: params.clientId, paymentmethod: params.paymentMethod, pid: pids, billingcycle: billingCycles, qty: quantities, }; // 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; } /** * Serialize object for WHMCS API * WHMCS expects base64-encoded serialized data */ function serializeForWhmcs(data?: Record): string { if (!data || Object.keys(data).length === 0) { return ""; } const entries = Object.entries(data).map(([key, value]) => { const safeKey = key ?? ""; const safeValue = value ?? ""; return ( `s:${Buffer.byteLength(safeKey, "utf8")}:"${escapePhpString(safeKey)}";` + `s:${Buffer.byteLength(safeValue, "utf8")}:"${escapePhpString(safeValue)}";` ); }); const serialized = `a:${entries.length}:{${entries.join("")}}`; return Buffer.from(serialized).toString("base64"); } function escapePhpString(value: string): string { return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } /** * 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("; "); }