- Updated SF_PUBSUB_NUM_REQUESTED in environment configuration to improve flow control. - Enhanced CatalogCdcSubscriber and OrderCdcSubscriber to utilize a dynamic numRequested value for subscriptions, improving event handling. - Removed deprecated WHMCS API access key configurations from WhmcsConfigService to streamline integration. - Improved error handling and logging in various services for better operational insights. - Refactored currency service to centralize fallback currency logic, ensuring consistent currency handling across the application.
213 lines
6.0 KiB
TypeScript
213 lines
6.0 KiB
TypeScript
/**
|
|
* Orders Domain - WHMCS Provider Mapper
|
|
*
|
|
* Transforms normalized order data to WHMCS API format.
|
|
*/
|
|
|
|
import type { OrderDetails, OrderItemDetails } from "../../contract";
|
|
import { normalizeBillingCycle } from "../../helpers";
|
|
import {
|
|
type WhmcsOrderItem,
|
|
type WhmcsAddOrderParams,
|
|
type WhmcsAddOrderPayload,
|
|
} from "./raw.types";
|
|
|
|
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, string>): 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("; ");
|
|
}
|