2025-10-03 14:26:55 +09:00
|
|
|
/**
|
|
|
|
|
* WHMCS Subscriptions Provider - Mapper
|
|
|
|
|
*
|
|
|
|
|
* Transforms raw WHMCS product/service data into normalized subscription types.
|
|
|
|
|
*/
|
|
|
|
|
|
2025-10-08 18:35:05 +09:00
|
|
|
import type { Subscription, SubscriptionStatus, SubscriptionCycle } from "../../contract";
|
|
|
|
|
import { subscriptionSchema } from "../../schema";
|
2025-10-03 14:26:55 +09:00
|
|
|
import {
|
|
|
|
|
type WhmcsProductRaw,
|
|
|
|
|
whmcsProductRawSchema,
|
|
|
|
|
whmcsCustomFieldsContainerSchema,
|
2025-10-08 18:35:05 +09:00
|
|
|
} from "./raw.types";
|
2025-10-03 14:26:55 +09:00
|
|
|
|
|
|
|
|
export interface TransformSubscriptionOptions {
|
|
|
|
|
defaultCurrencyCode?: string;
|
|
|
|
|
defaultCurrencySymbol?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Status mapping
|
|
|
|
|
const STATUS_MAP: Record<string, SubscriptionStatus> = {
|
|
|
|
|
active: "Active",
|
|
|
|
|
inactive: "Inactive",
|
|
|
|
|
pending: "Pending",
|
|
|
|
|
cancelled: "Cancelled",
|
|
|
|
|
canceled: "Cancelled",
|
|
|
|
|
terminated: "Terminated",
|
|
|
|
|
completed: "Completed",
|
|
|
|
|
suspended: "Suspended",
|
|
|
|
|
fraud: "Cancelled",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Cycle mapping
|
|
|
|
|
const CYCLE_MAP: Record<string, SubscriptionCycle> = {
|
|
|
|
|
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 {
|
|
|
|
|
if (!status) return "Cancelled";
|
|
|
|
|
const mapped = STATUS_MAP[status.trim().toLowerCase()];
|
|
|
|
|
return mapped ?? "Cancelled";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mapCycle(cycle?: string | null): SubscriptionCycle {
|
|
|
|
|
if (!cycle) return "One-time";
|
|
|
|
|
const normalized = cycle.trim().toLowerCase().replace(/[_\s-]+/g, " ");
|
|
|
|
|
return CYCLE_MAP[normalized] ?? "One-time";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractCustomFields(raw: unknown): Record<string, string> | 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<Record<string, string>>((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));
|
|
|
|
|
}
|