201 lines
6.0 KiB
TypeScript
Raw Normal View History

/**
* 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;
for (const [index, item] of orderDetails.items.entries()) {
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[] = [];
for (const item of params.items) {
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<string, string> = {
...(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("; ");
}