barsa d943d04754 Refactor environment configuration and enhance order processing logic
- 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.
2025-11-17 10:31:33 +09:00

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("; ");
}