199 lines
5.3 KiB
TypeScript
199 lines
5.3 KiB
TypeScript
|
|
/**
|
||
|
|
* Orders Domain - WHMCS Provider Mapper
|
||
|
|
*
|
||
|
|
* Transforms normalized order data to WHMCS API format.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import type { FulfillmentOrderItem } from "../../contract";
|
||
|
|
import {
|
||
|
|
type WhmcsOrderItem,
|
||
|
|
type WhmcsAddOrderParams,
|
||
|
|
type WhmcsAddOrderPayload,
|
||
|
|
whmcsOrderItemSchema,
|
||
|
|
} from "./raw.types";
|
||
|
|
import { z } from "zod";
|
||
|
|
|
||
|
|
const fulfillmentOrderItemSchema = z.object({
|
||
|
|
id: z.string(),
|
||
|
|
orderId: z.string(),
|
||
|
|
quantity: z.number().int().min(1),
|
||
|
|
product: z
|
||
|
|
.object({
|
||
|
|
id: z.string().optional(),
|
||
|
|
sku: z.string().optional(),
|
||
|
|
itemClass: z.string().optional(),
|
||
|
|
whmcsProductId: z.string().min(1),
|
||
|
|
billingCycle: z.string().min(1),
|
||
|
|
})
|
||
|
|
.nullable(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export interface OrderItemMappingResult {
|
||
|
|
whmcsItems: WhmcsOrderItem[];
|
||
|
|
summary: {
|
||
|
|
totalItems: number;
|
||
|
|
serviceItems: number;
|
||
|
|
activationItems: number;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeBillingCycle(cycle: string): WhmcsOrderItem["billingCycle"] {
|
||
|
|
const normalized = cycle.trim().toLowerCase();
|
||
|
|
if (normalized.includes("monthly")) return "monthly";
|
||
|
|
if (normalized.includes("one")) return "onetime";
|
||
|
|
if (normalized.includes("annual")) return "annually";
|
||
|
|
if (normalized.includes("quarter")) return "quarterly";
|
||
|
|
// Default to monthly if unrecognized
|
||
|
|
return "monthly";
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Map a single fulfillment order item to WHMCS format
|
||
|
|
*/
|
||
|
|
export function mapFulfillmentOrderItem(
|
||
|
|
item: FulfillmentOrderItem,
|
||
|
|
index = 0
|
||
|
|
): WhmcsOrderItem {
|
||
|
|
const parsed = fulfillmentOrderItemSchema.parse(item);
|
||
|
|
|
||
|
|
if (!parsed.product) {
|
||
|
|
throw new Error(`Order item ${index} missing product information`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const whmcsItem: WhmcsOrderItem = {
|
||
|
|
productId: parsed.product.whmcsProductId,
|
||
|
|
billingCycle: normalizeBillingCycle(parsed.product.billingCycle),
|
||
|
|
quantity: parsed.quantity,
|
||
|
|
};
|
||
|
|
|
||
|
|
return whmcsItem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Map multiple fulfillment order items to WHMCS format
|
||
|
|
*/
|
||
|
|
export function mapFulfillmentOrderItems(
|
||
|
|
items: FulfillmentOrderItem[]
|
||
|
|
): OrderItemMappingResult {
|
||
|
|
if (!Array.isArray(items) || items.length === 0) {
|
||
|
|
throw new Error("No order items provided for WHMCS mapping");
|
||
|
|
}
|
||
|
|
|
||
|
|
const whmcsItems: WhmcsOrderItem[] = [];
|
||
|
|
let serviceItems = 0;
|
||
|
|
let activationItems = 0;
|
||
|
|
|
||
|
|
items.forEach((item, index) => {
|
||
|
|
const mapped = mapFulfillmentOrderItem(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
|
||
|
|
*/
|
||
|
|
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
|
||
|
|
if (item.configOptions && Object.keys(item.configOptions).length > 0) {
|
||
|
|
const serialized = serializeForWhmcs(item.configOptions);
|
||
|
|
configOptions.push(serialized);
|
||
|
|
} else {
|
||
|
|
configOptions.push(""); // Empty string for items without config options
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle custom fields - WHMCS expects base64 encoded serialized arrays
|
||
|
|
if (item.customFields && Object.keys(item.customFields).length > 0) {
|
||
|
|
const serialized = serializeForWhmcs(item.customFields);
|
||
|
|
customFields.push(serialized);
|
||
|
|
} else {
|
||
|
|
customFields.push(""); // Empty string for items without custom fields
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
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 (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 {
|
||
|
|
const jsonStr = JSON.stringify(data);
|
||
|
|
return Buffer.from(jsonStr).toString("base64");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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("; ");
|
||
|
|
}
|
||
|
|
|