barsa 5c329bbe96 refactor: update WHMCS mappers and schemas to use numberLike for type safety
- Refactor various mappers in billing, payments, services, and subscriptions to ensure IDs are consistently converted to numbers.
- Update raw types schemas to utilize whmcsNumberLike and whmcsString for improved validation and type safety.
- Enhance the whmcs-utils to include schema exports for better modularity.
2026-02-24 13:56:02 +09:00

126 lines
3.7 KiB
TypeScript

/**
* WHMCS Billing Provider - Mapper
*
* Transforms raw WHMCS invoice data into normalized billing domain types.
*/
import type { Invoice, InvoiceItem } from "../../contract.js";
import { invoiceSchema } from "../../schema.js";
import {
type WhmcsInvoiceRaw,
whmcsInvoiceRawSchema,
type WhmcsInvoiceListItem,
whmcsInvoiceListItemSchema,
whmcsInvoiceItemsRawSchema,
} from "./raw.types.js";
import { parseAmount, formatDate } from "../../../common/providers/whmcs-utils/index.js";
export interface TransformInvoiceOptions {
defaultCurrencyCode?: string;
defaultCurrencySymbol?: string;
}
// Status mapping from WHMCS to domain
const STATUS_MAP: Record<string, Invoice["status"]> = {
draft: "Draft",
pending: "Pending",
"payment pending": "Pending",
paid: "Paid",
unpaid: "Unpaid",
cancelled: "Cancelled",
canceled: "Cancelled",
overdue: "Overdue",
refunded: "Refunded",
collections: "Collections",
};
function mapStatus(status: string): Invoice["status"] {
const normalized = status?.trim().toLowerCase();
if (!normalized) {
throw new Error("Invoice status missing");
}
const mapped = STATUS_MAP[normalized];
if (!mapped) {
throw new Error(`Unsupported WHMCS invoice status: ${status}`);
}
return mapped;
}
function mapItems(rawItems: unknown): InvoiceItem[] {
if (!rawItems) return [];
const parsed = whmcsInvoiceItemsRawSchema.parse(rawItems);
const itemArray = Array.isArray(parsed.item) ? parsed.item : [parsed.item];
return itemArray.map(item => ({
id: Number(item.id),
description: item.description,
amount: parseAmount(item.amount),
quantity: 1,
type: item.type,
serviceId: Number(item.relid) > 0 ? Number(item.relid) : undefined,
}));
}
/**
* Transform raw WHMCS invoice data into normalized Invoice type
*/
export function transformWhmcsInvoice(
rawInvoice: unknown,
options: TransformInvoiceOptions = {}
): Invoice {
const invoicePayload =
rawInvoice && typeof (rawInvoice as { invoiceid?: unknown }).invoiceid !== "undefined"
? whmcsInvoiceRawSchema.parse(rawInvoice)
: normalizeListInvoice(rawInvoice);
const whmcsInvoice = {
...invoicePayload,
invoiceid: invoicePayload.invoiceid ?? invoicePayload.id,
};
const currency = whmcsInvoice.currencycode || options.defaultCurrencyCode || "JPY";
const currencySymbol =
whmcsInvoice.currencyprefix || whmcsInvoice.currencysuffix || options.defaultCurrencySymbol;
// Transform to domain model
const invoice: Invoice = {
id: Number(whmcsInvoice.invoiceid ?? 0),
number: whmcsInvoice.invoicenum || `INV-${whmcsInvoice.invoiceid}`,
status: mapStatus(whmcsInvoice.status),
currency,
currencySymbol,
total: parseAmount(whmcsInvoice.total),
subtotal: parseAmount(whmcsInvoice.subtotal),
tax: parseAmount(whmcsInvoice.tax) + parseAmount(whmcsInvoice.tax2),
issuedAt: formatDate(whmcsInvoice.date || whmcsInvoice.datecreated),
dueDate: formatDate(whmcsInvoice.duedate),
paidDate: formatDate(whmcsInvoice.datepaid),
description: whmcsInvoice.notes || undefined,
items: mapItems(whmcsInvoice.items),
};
// Validate result against domain schema
return invoiceSchema.parse(invoice);
}
/**
* Transform multiple WHMCS invoices
*/
export function transformWhmcsInvoices(
rawInvoices: unknown[],
options: TransformInvoiceOptions = {}
): Invoice[] {
return rawInvoices.map(raw => transformWhmcsInvoice(raw, options));
}
function normalizeListInvoice(rawInvoice: unknown): WhmcsInvoiceRaw & { id?: string | number } {
const listItem: WhmcsInvoiceListItem = whmcsInvoiceListItemSchema.parse(rawInvoice);
const invoiceid = listItem.invoiceid ?? listItem.id;
return {
...listItem,
invoiceid,
};
}