/** * 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 = { 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, }; }