/** * WHMCS Subscriptions Provider - Mapper * * Transforms raw WHMCS product/service data into normalized subscription types. */ import type { Subscription, SubscriptionStatus, SubscriptionCycle, SubscriptionList, } from "../../contract.js"; import { subscriptionSchema, subscriptionListSchema, subscriptionStatusSchema, } from "../../schema.js"; import { type WhmcsProductRaw, whmcsProductRawSchema, whmcsCustomFieldsContainerSchema, whmcsProductListResponseSchema, } from "./raw.types.js"; import { parseAmount, formatDate, normalizeStatus, normalizeCycle, } from "../../../providers/whmcs/utils.js"; export interface TransformSubscriptionOptions { defaultCurrencyCode?: string; defaultCurrencySymbol?: string; } export interface TransformSubscriptionListResponseOptions extends TransformSubscriptionOptions { status?: string; onItemError?: (error: unknown, product: WhmcsProductRaw) => void; } // Status mapping const STATUS_MAP: Record = { active: "Active", inactive: "Inactive", pending: "Pending", cancelled: "Cancelled", canceled: "Cancelled", terminated: "Terminated", completed: "Completed", suspended: "Suspended", fraud: "Cancelled", }; // Cycle mapping const CYCLE_MAP: Record = { monthly: "Monthly", annually: "Annually", annual: "Annually", yearly: "Annually", quarterly: "Quarterly", "semi annually": "Semi-Annually", semiannually: "Semi-Annually", "semi-annually": "Semi-Annually", biennially: "Biennially", triennially: "Triennially", "one time": "One-time", onetime: "One-time", "one-time": "One-time", "one time fee": "One-time", free: "Free", }; function mapStatus(status?: string | null): SubscriptionStatus { return normalizeStatus(status ?? undefined, STATUS_MAP, "Cancelled"); } function mapCycle(cycle?: string | null): SubscriptionCycle { return normalizeCycle(cycle ?? undefined, CYCLE_MAP, "One-time"); } function extractCustomFields(raw: unknown): Record | undefined { if (!raw) return undefined; const container = whmcsCustomFieldsContainerSchema.safeParse(raw); if (!container.success) return undefined; const customfield = container.data.customfield; const fieldsArray = Array.isArray(customfield) ? customfield : [customfield]; const entries = fieldsArray.reduce>((acc, field) => { if (field?.name && field.value) { acc[field.name] = field.value; } return acc; }, {}); return Object.keys(entries).length > 0 ? entries : undefined; } /** * Transform raw WHMCS product/service into normalized Subscription */ export function transformWhmcsSubscription( rawProduct: unknown, options: TransformSubscriptionOptions = {} ): Subscription { // Validate raw data const product = whmcsProductRawSchema.parse(rawProduct); // Extract currency info const currency = product.pricing?.currency || options.defaultCurrencyCode || "JPY"; const currencySymbol = product.pricing?.currencyprefix || product.pricing?.currencysuffix || options.defaultCurrencySymbol; // Determine amount const amount = parseAmount( product.amount || product.recurringamount || product.pricing?.amount || product.firstpaymentamount || 0 ); // Transform to domain model const subscription: Subscription = { id: product.id, serviceId: product.serviceid || product.id, productName: product.name || product.translated_name || "Unknown Product", domain: product.domain || undefined, cycle: mapCycle(product.billingcycle), status: mapStatus(product.status), nextDue: formatDate(product.nextduedate || product.nextinvoicedate), amount, currency, currencySymbol, registrationDate: formatDate(product.regdate) || new Date().toISOString(), notes: product.notes || undefined, customFields: extractCustomFields(product.customfields), orderNumber: product.ordernumber || undefined, groupName: product.groupname || product.translated_groupname || undefined, paymentMethod: product.paymentmethodname || product.paymentmethod || undefined, serverName: product.servername || product.serverhostname || undefined, }; // Validate against domain schema return subscriptionSchema.parse(subscription); } /** * Transform multiple WHMCS subscriptions */ export function transformWhmcsSubscriptions( rawProducts: unknown[], options: TransformSubscriptionOptions = {} ): Subscription[] { return rawProducts.map(raw => transformWhmcsSubscription(raw, options)); } export function transformWhmcsSubscriptionListResponse( response: unknown, options: TransformSubscriptionListResponseOptions = {} ): SubscriptionList { const parsed = whmcsProductListResponseSchema.parse(response); const { status, onItemError, ...subscriptionOptions } = options; if (parsed.result === "error") { const message = parsed.message || "WHMCS GetClientsProducts returned error"; throw new Error(message); } const productContainer = parsed.products?.product; const products = Array.isArray(productContainer) ? productContainer : productContainer ? [productContainer] : []; const subscriptions: Subscription[] = []; for (const product of products) { try { const subscription = transformWhmcsSubscription(product, subscriptionOptions); subscriptions.push(subscription); } catch (error) { onItemError?.(error, product); } } const totalResultsRaw = parsed.totalresults; const totalResults = typeof totalResultsRaw === "number" ? totalResultsRaw : typeof totalResultsRaw === "string" ? Number.parseInt(totalResultsRaw, 10) : subscriptions.length; if (status) { const normalizedStatus = subscriptionStatusSchema.parse(status); const filtered = subscriptions.filter(sub => sub.status === normalizedStatus); return subscriptionListSchema.parse({ subscriptions: filtered, totalCount: filtered.length, }); } return subscriptionListSchema.parse({ subscriptions, totalCount: Number.isFinite(totalResults) ? totalResults : subscriptions.length, }); } export function filterSubscriptionsByStatus( list: SubscriptionList, status: string ): SubscriptionList { const normalizedStatus = subscriptionStatusSchema.parse(status); const filtered = list.subscriptions.filter(sub => sub.status === normalizedStatus); return subscriptionListSchema.parse({ subscriptions: filtered, totalCount: filtered.length, }); }