190 lines
5.2 KiB
TypeScript

import type { FulfillmentOrderItem } from "@customer-portal/domain/orders";
import {
Providers,
type OrderItemSummary,
} from "@customer-portal/domain/orders";
import {
type WhmcsOrderItem,
type WhmcsAddOrderParams,
type WhmcsAddOrderPayload,
whmcsOrderItemSchema,
} from "@customer-portal/domain/orders/providers/whmcs";
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";
}
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;
}
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 our structured params into the 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 PHP 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("; ");
}