/** * Shared WHMCS Provider Utilities * Single source of truth for WHMCS data parsing * * Raw API types are source of truth - no fallbacks or variations expected. */ /** * Parse amount from WHMCS API response * WHMCS returns amounts as strings or numbers */ export function parseAmount(amount: string | number | undefined): number { if (typeof amount === "number") return amount; if (!amount) return 0; const cleaned = String(amount).replace(/[^\d.-]/g, ""); const parsed = Number.parseFloat(cleaned); return Number.isNaN(parsed) ? 0 : parsed; } /** * Format date from WHMCS API to ISO string * Returns undefined if input is invalid */ export function formatDate(input?: string | null): string | undefined { if (!input) return undefined; const date = new Date(input); if (Number.isNaN(date.getTime())) return undefined; return date.toISOString(); } /** * Normalize status using provided status map * Generic helper for consistent status mapping */ export function normalizeStatus( status: string | null | undefined, statusMap: Record, defaultStatus: T ): T { if (!status) return defaultStatus; const mapped = statusMap[status.trim().toLowerCase()]; return mapped ?? defaultStatus; } /** * Normalize billing cycle using provided cycle map * Generic helper for consistent cycle mapping */ export function normalizeCycle( cycle: string | null | undefined, cycleMap: Record, defaultCycle: T ): T { if (!cycle) return defaultCycle; const normalized = cycle .trim() .toLowerCase() .replace(/[_\s-]+/g, " "); return cycleMap[normalized] ?? defaultCycle; } const isObject = (value: unknown): value is Record => typeof value === "object" && value !== null; const normalizeCustomFieldEntries = (value: unknown): Array> => { if (Array.isArray(value)) return value.filter(isObject); if (isObject(value) && "customfield" in value) { const custom = (value as { customfield?: unknown }).customfield; if (Array.isArray(custom)) return custom.filter(isObject); if (isObject(custom)) return [custom]; return []; } return []; }; /** * Build a lightweight map of WHMCS custom field identifiers to values. * Accepts the documented WHMCS response shapes (array or { customfield }). */ export function getCustomFieldsMap(customFields: unknown): Record { if (!customFields) return {}; if (isObject(customFields) && !Array.isArray(customFields) && !("customfield" in customFields)) { return Object.entries(customFields).reduce>((acc, [key, value]) => { if (typeof value === "string") { const trimmedKey = key.trim(); if (trimmedKey) acc[trimmedKey] = value; } return acc; }, {}); } const map: Record = {}; for (const entry of normalizeCustomFieldEntries(customFields)) { const idRaw = "id" in entry ? entry.id : undefined; const id = typeof idRaw === "string" ? idRaw.trim() : typeof idRaw === "number" ? String(idRaw) : undefined; const name = "name" in entry && typeof entry.name === "string" ? entry.name.trim() : undefined; const rawValue = "value" in entry ? entry.value : undefined; if (rawValue === undefined || rawValue === null) continue; const value = typeof rawValue === "string" ? rawValue : typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : undefined; if (!value) continue; if (id) map[id] = value; if (name) map[name] = value; } return map; } /** * Retrieve a custom field value by numeric id or name. */ export function getCustomFieldValue( customFields: unknown, key: string | number ): string | undefined { if (key === undefined || key === null) return undefined; const map = getCustomFieldsMap(customFields); const primary = map[String(key)]; if (primary !== undefined) return primary; if (typeof key === "string") { const numeric = Number.parseInt(key, 10); if (!Number.isNaN(numeric)) { const numericValue = map[String(numeric)]; if (numericValue !== undefined) return numericValue; } } return undefined; }