2025-10-03 14:26:55 +09:00
|
|
|
/**
|
|
|
|
|
* Orders Domain - WHMCS Provider Mapper
|
2025-12-15 17:29:28 +09:00
|
|
|
*
|
2025-10-03 14:26:55 +09:00
|
|
|
* Transforms normalized order data to WHMCS API format.
|
|
|
|
|
*/
|
|
|
|
|
|
2025-12-10 15:22:10 +09:00
|
|
|
import type { OrderDetails, OrderItemDetails } from "../../contract.js";
|
|
|
|
|
import { normalizeBillingCycle } from "../../helpers.js";
|
2025-12-15 17:29:28 +09:00
|
|
|
import { serializeWhmcsKeyValueMap } from "../../../providers/whmcs/utils.js";
|
2025-10-03 14:26:55 +09:00
|
|
|
import {
|
|
|
|
|
type WhmcsOrderItem,
|
|
|
|
|
type WhmcsAddOrderParams,
|
|
|
|
|
type WhmcsAddOrderPayload,
|
2025-12-10 15:22:10 +09:00
|
|
|
} from "./raw.types.js";
|
2025-10-03 14:26:55 +09:00
|
|
|
|
|
|
|
|
export interface OrderItemMappingResult {
|
|
|
|
|
whmcsItems: WhmcsOrderItem[];
|
|
|
|
|
summary: {
|
|
|
|
|
totalItems: number;
|
|
|
|
|
serviceItems: number;
|
|
|
|
|
activationItems: number;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-08 16:31:42 +09:00
|
|
|
* Map a single order item to WHMCS format
|
2025-10-03 14:26:55 +09:00
|
|
|
*/
|
2025-12-15 17:29:28 +09:00
|
|
|
export function mapOrderItemToWhmcs(item: OrderItemDetails, index = 0): WhmcsOrderItem {
|
2025-10-08 16:31:42 +09:00
|
|
|
if (!item.product?.whmcsProductId) {
|
|
|
|
|
throw new Error(`Order item ${index} missing WHMCS product ID`);
|
2025-10-03 14:26:55 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const whmcsItem: WhmcsOrderItem = {
|
2025-10-08 16:31:42 +09:00
|
|
|
productId: item.product.whmcsProductId,
|
|
|
|
|
billingCycle: normalizeBillingCycle(item.billingCycle),
|
|
|
|
|
quantity: item.quantity,
|
2025-10-03 14:26:55 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return whmcsItem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-08 16:31:42 +09:00
|
|
|
* Map order details to WHMCS items format
|
|
|
|
|
* Extracts items from OrderDetails and transforms to WHMCS API format
|
2025-10-03 14:26:55 +09:00
|
|
|
*/
|
2025-12-15 17:29:28 +09:00
|
|
|
export function mapOrderToWhmcsItems(orderDetails: OrderDetails): OrderItemMappingResult {
|
2025-10-08 16:31:42 +09:00
|
|
|
if (!orderDetails.items || orderDetails.items.length === 0) {
|
2025-10-03 14:26:55 +09:00
|
|
|
throw new Error("No order items provided for WHMCS mapping");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const whmcsItems: WhmcsOrderItem[] = [];
|
|
|
|
|
let serviceItems = 0;
|
|
|
|
|
let activationItems = 0;
|
|
|
|
|
|
2025-10-08 16:31:42 +09:00
|
|
|
orderDetails.items.forEach((item, index) => {
|
|
|
|
|
const mapped = mapOrderItemToWhmcs(item, index);
|
2025-10-03 14:26:55 +09:00
|
|
|
whmcsItems.push(mapped);
|
2025-12-15 17:29:28 +09:00
|
|
|
|
2025-10-03 14:26:55 +09:00
|
|
|
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
|
2025-12-15 17:29:28 +09:00
|
|
|
*
|
2025-11-05 18:17:59 +09:00
|
|
|
* WHMCS AddOrder API Documentation:
|
|
|
|
|
* @see https://developers.whmcs.com/api-reference/addorder/
|
2025-12-15 17:29:28 +09:00
|
|
|
*
|
2025-11-05 18:17:59 +09:00
|
|
|
* 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")
|
2025-12-15 17:29:28 +09:00
|
|
|
*
|
2025-11-05 18:17:59 +09:00
|
|
|
* 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
|
2025-12-15 17:29:28 +09:00
|
|
|
*
|
2025-11-05 18:17:59 +09:00
|
|
|
* 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
|
2025-12-15 17:29:28 +09:00
|
|
|
*
|
2025-11-05 18:17:59 +09:00
|
|
|
* 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
|
2025-10-03 14:26:55 +09:00
|
|
|
*/
|
|
|
|
|
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
|
2025-12-15 17:29:28 +09:00
|
|
|
configOptions.push(serializeWhmcsKeyValueMap(item.configOptions));
|
2025-10-03 14:26:55 +09:00
|
|
|
|
2025-12-22 18:59:38 +09:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 14:26:55 +09:00
|
|
|
// Handle custom fields - WHMCS expects base64 encoded serialized arrays
|
2025-12-22 18:59:38 +09:00
|
|
|
customFields.push(serializeWhmcsKeyValueMap(mergedCustomFields));
|
2025-10-03 14:26:55 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-11-17 10:31:33 +09:00
|
|
|
if (params.notes) {
|
|
|
|
|
payload.notes = params.notes;
|
|
|
|
|
}
|
2025-10-03 14:26:55 +09:00
|
|
|
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("; ");
|
|
|
|
|
}
|