barsa 0a3d5b1e3c Refactor WHMCS service methods and improve type handling
- Updated WHMCS service methods to return normalized product types, enhancing type consistency across services.
- Refactored product retrieval logic in WhmcsPaymentService and WhmcsService to streamline data handling.
- Removed deprecated utility functions and optimized custom field handling in WHMCS-related services.
- Enhanced error handling in subscription processing to improve reliability and clarity.
- Cleaned up imports and improved overall code organization for better maintainability.
2025-10-29 18:59:17 +09:00

137 lines
4.1 KiB
TypeScript

/**
* 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<T extends string>(
status: string | null | undefined,
statusMap: Record<string, T>,
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<T extends string>(
cycle: string | null | undefined,
cycleMap: Record<string, T>,
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<string, unknown> =>
typeof value === "object" && value !== null;
const normalizeCustomFieldEntries = (value: unknown): Array<Record<string, unknown>> => {
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<string, string> {
if (!customFields) return {};
if (isObject(customFields) && !Array.isArray(customFields) && !("customfield" in customFields)) {
return Object.entries(customFields).reduce<Record<string, string>>((acc, [key, value]) => {
if (typeof value === "string") {
const trimmedKey = key.trim();
if (trimmedKey) acc[trimmedKey] = value;
}
return acc;
}, {});
}
const map: Record<string, string> = {};
for (const entry of normalizeCustomFieldEntries(customFields)) {
const id =
"id" in entry && entry.id !== undefined && entry.id !== null
? String(entry.id).trim()
: 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 : String(rawValue);
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;
}