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.
This commit is contained in:
parent
7bc4c14b4c
commit
5c329bbe96
@ -276,7 +276,7 @@ export class WhmcsInvoiceService {
|
||||
},
|
||||
});
|
||||
|
||||
const totalItems = response.totalresults || 0;
|
||||
const totalItems = Number(response.totalresults) || 0;
|
||||
const totalPages = Math.ceil(totalItems / limit);
|
||||
|
||||
return {
|
||||
@ -353,7 +353,7 @@ export class WhmcsInvoiceService {
|
||||
});
|
||||
|
||||
return {
|
||||
id: response.invoiceid,
|
||||
id: Number(response.invoiceid),
|
||||
number: `INV-${response.invoiceid}`,
|
||||
total: params.amount,
|
||||
status: response.status,
|
||||
|
||||
@ -54,12 +54,12 @@ function mapItems(rawItems: unknown): InvoiceItem[] {
|
||||
const itemArray = Array.isArray(parsed.item) ? parsed.item : [parsed.item];
|
||||
|
||||
return itemArray.map(item => ({
|
||||
id: item.id,
|
||||
id: Number(item.id),
|
||||
description: item.description,
|
||||
amount: parseAmount(item.amount),
|
||||
quantity: 1,
|
||||
type: item.type,
|
||||
serviceId: typeof item.relid === "number" && item.relid > 0 ? item.relid : undefined,
|
||||
serviceId: Number(item.relid) > 0 ? Number(item.relid) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ export function transformWhmcsInvoice(
|
||||
|
||||
// Transform to domain model
|
||||
const invoice: Invoice = {
|
||||
id: whmcsInvoice.invoiceid ?? 0,
|
||||
id: Number(whmcsInvoice.invoiceid ?? 0),
|
||||
number: whmcsInvoice.invoicenum || `INV-${whmcsInvoice.invoiceid}`,
|
||||
status: mapStatus(whmcsInvoice.status),
|
||||
currency,
|
||||
@ -115,7 +115,7 @@ export function transformWhmcsInvoices(
|
||||
return rawInvoices.map(raw => transformWhmcsInvoice(raw, options));
|
||||
}
|
||||
|
||||
function normalizeListInvoice(rawInvoice: unknown): WhmcsInvoiceRaw & { id?: number } {
|
||||
function normalizeListInvoice(rawInvoice: unknown): WhmcsInvoiceRaw & { id?: string | number } {
|
||||
const listItem: WhmcsInvoiceListItem = whmcsInvoiceListItemSchema.parse(rawInvoice);
|
||||
const invoiceid = listItem.invoiceid ?? listItem.id;
|
||||
return {
|
||||
|
||||
@ -9,6 +9,10 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
whmcsString as s,
|
||||
whmcsNumberLike as numberLike,
|
||||
} from "../../../common/providers/whmcs-utils/index.js";
|
||||
|
||||
// ============================================================================
|
||||
// Request Parameter Types
|
||||
@ -102,12 +106,12 @@ export interface WhmcsCapturePaymentParams {
|
||||
|
||||
// Raw WHMCS Invoice Item
|
||||
export const whmcsInvoiceItemRawSchema = z.object({
|
||||
id: z.number(),
|
||||
type: z.string(),
|
||||
relid: z.number(),
|
||||
description: z.string(),
|
||||
amount: z.union([z.string(), z.number()]),
|
||||
taxed: z.number().optional(),
|
||||
id: numberLike,
|
||||
type: s,
|
||||
relid: numberLike,
|
||||
description: s,
|
||||
amount: numberLike,
|
||||
taxed: numberLike.optional(),
|
||||
});
|
||||
|
||||
export type WhmcsInvoiceItemRaw = z.infer<typeof whmcsInvoiceItemRawSchema>;
|
||||
@ -121,53 +125,53 @@ export type WhmcsInvoiceItemsRaw = z.infer<typeof whmcsInvoiceItemsRawSchema>;
|
||||
|
||||
const whmcsInvoiceCommonSchema = z
|
||||
.object({
|
||||
invoicenum: z.string().optional(),
|
||||
userid: z.number(),
|
||||
date: z.string(),
|
||||
duedate: z.string(),
|
||||
subtotal: z.string(),
|
||||
credit: z.string(),
|
||||
tax: z.string(),
|
||||
tax2: z.string(),
|
||||
total: z.string(),
|
||||
balance: z.string().optional(),
|
||||
status: z.string(),
|
||||
paymentmethod: z.string(),
|
||||
notes: z.string().optional(),
|
||||
invoicenum: s.optional(),
|
||||
userid: numberLike,
|
||||
date: s,
|
||||
duedate: s,
|
||||
subtotal: s,
|
||||
credit: s,
|
||||
tax: s,
|
||||
tax2: s,
|
||||
total: s,
|
||||
balance: s.optional(),
|
||||
status: s,
|
||||
paymentmethod: s,
|
||||
notes: s.optional(),
|
||||
ccgateway: z.boolean().optional(),
|
||||
items: whmcsInvoiceItemsRawSchema.optional(),
|
||||
transactions: z.unknown().optional(),
|
||||
clientid: z.number().optional(),
|
||||
datecreated: z.string().optional(),
|
||||
paymentmethodname: z.string().optional(),
|
||||
currencyprefix: z.string().optional(),
|
||||
currencysuffix: z.string().optional(),
|
||||
lastcaptureattempt: z.string().optional(),
|
||||
last_capture_attempt: z.string().optional(),
|
||||
datepaid: z.string().optional(),
|
||||
date_refunded: z.string().optional(),
|
||||
date_cancelled: z.string().optional(),
|
||||
created_at: z.string().optional(),
|
||||
updated_at: z.string().optional(),
|
||||
taxrate: z.string().optional(),
|
||||
taxrate2: z.string().optional(),
|
||||
firstname: z.string().optional(),
|
||||
lastname: z.string().optional(),
|
||||
companyname: z.string().optional(),
|
||||
currencycode: z.string().optional(),
|
||||
clientid: numberLike.optional(),
|
||||
datecreated: s.optional(),
|
||||
paymentmethodname: s.optional(),
|
||||
currencyprefix: s.optional(),
|
||||
currencysuffix: s.optional(),
|
||||
lastcaptureattempt: s.optional(),
|
||||
last_capture_attempt: s.optional(),
|
||||
datepaid: s.optional(),
|
||||
date_refunded: s.optional(),
|
||||
date_cancelled: s.optional(),
|
||||
created_at: s.optional(),
|
||||
updated_at: s.optional(),
|
||||
taxrate: s.optional(),
|
||||
taxrate2: s.optional(),
|
||||
firstname: s.optional(),
|
||||
lastname: s.optional(),
|
||||
companyname: s.optional(),
|
||||
currencycode: s.optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const whmcsInvoiceListItemSchema = whmcsInvoiceCommonSchema.extend({
|
||||
id: z.number(),
|
||||
invoiceid: z.number().optional(),
|
||||
id: numberLike,
|
||||
invoiceid: numberLike.optional(),
|
||||
});
|
||||
|
||||
// Raw WHMCS Invoice (detailed GetInvoice response)
|
||||
export const whmcsInvoiceRawSchema = whmcsInvoiceCommonSchema.extend({
|
||||
invoiceid: z.number(),
|
||||
id: z.number().optional(),
|
||||
balance: z.string().optional(),
|
||||
invoiceid: numberLike,
|
||||
id: numberLike.optional(),
|
||||
balance: s.optional(),
|
||||
});
|
||||
|
||||
export type WhmcsInvoiceRaw = z.infer<typeof whmcsInvoiceRawSchema>;
|
||||
@ -184,9 +188,9 @@ export const whmcsInvoiceListResponseSchema = z.object({
|
||||
invoices: z.object({
|
||||
invoice: z.array(whmcsInvoiceListItemSchema),
|
||||
}),
|
||||
totalresults: z.number(),
|
||||
numreturned: z.number(),
|
||||
startnumber: z.number(),
|
||||
totalresults: numberLike,
|
||||
numreturned: numberLike,
|
||||
startnumber: numberLike,
|
||||
});
|
||||
|
||||
export type WhmcsInvoiceListResponse = z.infer<typeof whmcsInvoiceListResponseSchema>;
|
||||
@ -214,9 +218,9 @@ export type WhmcsInvoiceResponse = z.infer<typeof whmcsInvoiceResponseSchema>;
|
||||
*/
|
||||
export const whmcsCreateInvoiceResponseSchema = z.object({
|
||||
result: z.enum(["success", "error"]),
|
||||
invoiceid: z.number(),
|
||||
status: z.string(),
|
||||
message: z.string().optional(),
|
||||
invoiceid: numberLike,
|
||||
status: s,
|
||||
message: s.optional(),
|
||||
});
|
||||
|
||||
export type WhmcsCreateInvoiceResponse = z.infer<typeof whmcsCreateInvoiceResponseSchema>;
|
||||
@ -230,9 +234,9 @@ export type WhmcsCreateInvoiceResponse = z.infer<typeof whmcsCreateInvoiceRespon
|
||||
*/
|
||||
export const whmcsUpdateInvoiceResponseSchema = z.object({
|
||||
result: z.enum(["success", "error"]),
|
||||
invoiceid: z.number(),
|
||||
status: z.string(),
|
||||
message: z.string().optional(),
|
||||
invoiceid: numberLike,
|
||||
status: s,
|
||||
message: s.optional(),
|
||||
});
|
||||
|
||||
export type WhmcsUpdateInvoiceResponse = z.infer<typeof whmcsUpdateInvoiceResponseSchema>;
|
||||
@ -246,13 +250,13 @@ export type WhmcsUpdateInvoiceResponse = z.infer<typeof whmcsUpdateInvoiceRespon
|
||||
*/
|
||||
export const whmcsCapturePaymentResponseSchema = z.object({
|
||||
result: z.enum(["success", "error"]),
|
||||
invoiceid: z.number(),
|
||||
status: z.string(),
|
||||
transactionid: z.string().optional(),
|
||||
amount: z.number().optional(),
|
||||
fees: z.number().optional(),
|
||||
message: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
invoiceid: numberLike,
|
||||
status: s,
|
||||
transactionid: s.optional(),
|
||||
amount: numberLike.optional(),
|
||||
fees: numberLike.optional(),
|
||||
message: s.optional(),
|
||||
error: s.optional(),
|
||||
});
|
||||
|
||||
export type WhmcsCapturePaymentResponse = z.infer<typeof whmcsCapturePaymentResponseSchema>;
|
||||
@ -265,12 +269,12 @@ export type WhmcsCapturePaymentResponse = z.infer<typeof whmcsCapturePaymentResp
|
||||
* WHMCS Currency schema
|
||||
*/
|
||||
export const whmcsCurrencySchema = z.object({
|
||||
id: z.number(),
|
||||
code: z.string(),
|
||||
prefix: z.string(),
|
||||
suffix: z.string(),
|
||||
format: z.string(),
|
||||
rate: z.string(),
|
||||
id: numberLike,
|
||||
code: s,
|
||||
prefix: s,
|
||||
suffix: s,
|
||||
format: s,
|
||||
rate: s,
|
||||
});
|
||||
|
||||
export type WhmcsCurrency = z.infer<typeof whmcsCurrencySchema>;
|
||||
|
||||
@ -10,3 +10,4 @@ export * from "./parsing.js";
|
||||
export * from "./normalize.js";
|
||||
export * from "./custom-fields.js";
|
||||
export * from "./php-serialize.js";
|
||||
export * from "./schema.js";
|
||||
|
||||
28
packages/domain/common/providers/whmcs-utils/schema.ts
Normal file
28
packages/domain/common/providers/whmcs-utils/schema.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* WHMCS Zod Schema Primitives (domain-internal)
|
||||
*
|
||||
* Coercing schema helpers for WHMCS API responses.
|
||||
* WHMCS (PHP) is loosely typed — fields documented as strings may arrive
|
||||
* as numbers (and vice-versa). These primitives absorb that inconsistency
|
||||
* at the parsing boundary so the rest of the codebase sees clean types.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Coercing string — accepts string or number, always outputs string.
|
||||
* Use for any WHMCS response field that should be a string.
|
||||
*/
|
||||
export const whmcsString = z.coerce.string();
|
||||
|
||||
/**
|
||||
* Accepts number or string (e.g. "123"), keeps the raw union type.
|
||||
* Use when downstream code handles both types (e.g. IDs you'll parse later).
|
||||
*/
|
||||
export const whmcsNumberLike = z.union([z.number(), z.string()]);
|
||||
|
||||
/**
|
||||
* Accepts boolean, number (0/1), or string ("true"/"false"/etc).
|
||||
* Use for WHMCS boolean flags that arrive in varying formats.
|
||||
*/
|
||||
export const whmcsBooleanLike = z.union([z.boolean(), z.number(), z.string()]);
|
||||
@ -43,7 +43,7 @@ export function transformWhmcsPaymentMethod(raw: unknown): PaymentMethod {
|
||||
const whmcs = whmcsPaymentMethodRawSchema.parse(raw);
|
||||
|
||||
const paymentMethod: PaymentMethod = {
|
||||
id: whmcs.id,
|
||||
id: Number(whmcs.id),
|
||||
type: mapPaymentMethodType(whmcs.payment_type || whmcs.type || "manual"),
|
||||
description: whmcs.description,
|
||||
gatewayName: whmcs.gateway_name || whmcs.gateway,
|
||||
|
||||
@ -113,7 +113,7 @@ export function transformWhmcsCatalogProductsResponse(
|
||||
|
||||
return {
|
||||
id: String(product.pid),
|
||||
groupId: product.gid,
|
||||
groupId: Number(product.gid),
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
module: product.module,
|
||||
|
||||
@ -5,26 +5,30 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
whmcsString as s,
|
||||
whmcsNumberLike as numberLike,
|
||||
} from "../../../common/providers/whmcs-utils/index.js";
|
||||
|
||||
// ============================================================================
|
||||
// WHMCS Catalog Product Pricing Cycle
|
||||
// ============================================================================
|
||||
|
||||
const whmcsCatalogProductPricingCycleSchema = z.object({
|
||||
prefix: z.string(),
|
||||
suffix: z.string(),
|
||||
msetupfee: z.string(),
|
||||
qsetupfee: z.string(),
|
||||
ssetupfee: z.string(),
|
||||
asetupfee: z.string(),
|
||||
bsetupfee: z.string(),
|
||||
tsetupfee: z.string(),
|
||||
monthly: z.string(),
|
||||
quarterly: z.string(),
|
||||
semiannually: z.string(),
|
||||
annually: z.string(),
|
||||
biennially: z.string(),
|
||||
triennially: z.string(),
|
||||
prefix: s,
|
||||
suffix: s,
|
||||
msetupfee: s,
|
||||
qsetupfee: s,
|
||||
ssetupfee: s,
|
||||
asetupfee: s,
|
||||
bsetupfee: s,
|
||||
tsetupfee: s,
|
||||
monthly: s,
|
||||
quarterly: s,
|
||||
semiannually: s,
|
||||
annually: s,
|
||||
biennially: s,
|
||||
triennially: s,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
@ -32,12 +36,12 @@ const whmcsCatalogProductPricingCycleSchema = z.object({
|
||||
// ============================================================================
|
||||
|
||||
const whmcsCatalogProductSchema = z.object({
|
||||
pid: z.number(),
|
||||
gid: z.number(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
module: z.string(),
|
||||
paytype: z.string(),
|
||||
pid: numberLike,
|
||||
gid: numberLike,
|
||||
name: s,
|
||||
description: s,
|
||||
module: s,
|
||||
paytype: s,
|
||||
pricing: z.record(z.string(), whmcsCatalogProductPricingCycleSchema),
|
||||
});
|
||||
|
||||
@ -54,7 +58,7 @@ export const whmcsCatalogProductListResponseSchema = z.object({
|
||||
products: z.object({
|
||||
product: z.array(whmcsCatalogProductSchema),
|
||||
}),
|
||||
totalresults: z.number(),
|
||||
totalresults: numberLike,
|
||||
});
|
||||
|
||||
export type WhmcsCatalogProductListResponse = z.infer<typeof whmcsCatalogProductListResponseSchema>;
|
||||
|
||||
@ -8,6 +8,11 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
whmcsString as s,
|
||||
whmcsNumberLike as numberLike,
|
||||
} from "../../../common/providers/whmcs-utils/index.js";
|
||||
|
||||
const normalizeRequiredNumber = z.preprocess(value => {
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
@ -73,10 +78,10 @@ export const whmcsCustomFieldsContainerSchema = z.object({
|
||||
});
|
||||
|
||||
export const whmcsConfigOptionSchema = z.object({
|
||||
id: z.union([z.string(), z.number()]).optional(),
|
||||
option: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
value: z.string().optional(),
|
||||
id: numberLike.optional(),
|
||||
option: s.optional(),
|
||||
type: s.optional(),
|
||||
value: s.optional(),
|
||||
});
|
||||
|
||||
export const whmcsConfigOptionsContainerSchema = z.object({
|
||||
@ -92,20 +97,20 @@ export const whmcsProductRawSchema = z.object({
|
||||
pid: normalizeOptionalNumber,
|
||||
orderid: normalizeOptionalNumber,
|
||||
ordernumber: optionalStringField(),
|
||||
regdate: z.string(),
|
||||
name: z.string(),
|
||||
translated_name: z.string().optional(),
|
||||
groupname: z.string().optional(),
|
||||
translated_groupname: z.string().optional(),
|
||||
regdate: s,
|
||||
name: s,
|
||||
translated_name: s.optional(),
|
||||
groupname: s.optional(),
|
||||
translated_groupname: s.optional(),
|
||||
domain: optionalStringField(),
|
||||
dedicatedip: optionalStringField(),
|
||||
serverid: normalizeOptionalNumber,
|
||||
servername: optionalStringField(),
|
||||
serverip: optionalStringField(),
|
||||
serverhostname: optionalStringField(),
|
||||
suspensionreason: z.string().optional(),
|
||||
suspensionreason: s.optional(),
|
||||
promoid: normalizeOptionalNumber,
|
||||
subscriptionid: z.string().optional(),
|
||||
subscriptionid: s.optional(),
|
||||
overideautosuspend: optionalStringField(),
|
||||
overidesuspenduntil: optionalStringField(),
|
||||
ns1: optionalStringField(),
|
||||
@ -113,29 +118,29 @@ export const whmcsProductRawSchema = z.object({
|
||||
assignedips: optionalStringField(),
|
||||
|
||||
// Pricing
|
||||
firstpaymentamount: z.union([z.string(), z.number()]).optional(),
|
||||
amount: z.union([z.string(), z.number()]).optional(),
|
||||
recurringamount: z.union([z.string(), z.number()]).optional(),
|
||||
billingcycle: z.string().optional(),
|
||||
paymentmethod: z.string().optional(),
|
||||
paymentmethodname: z.string().optional(),
|
||||
firstpaymentamount: numberLike.optional(),
|
||||
amount: numberLike.optional(),
|
||||
recurringamount: numberLike.optional(),
|
||||
billingcycle: s.optional(),
|
||||
paymentmethod: s.optional(),
|
||||
paymentmethodname: s.optional(),
|
||||
|
||||
// Dates
|
||||
nextduedate: z.string().optional(),
|
||||
nextinvoicedate: z.string().optional(),
|
||||
nextduedate: s.optional(),
|
||||
nextinvoicedate: s.optional(),
|
||||
|
||||
// Status
|
||||
status: z.string(),
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
status: s,
|
||||
username: s.optional(),
|
||||
password: s.optional(),
|
||||
|
||||
// Notes
|
||||
notes: z.string().optional(),
|
||||
notes: s.optional(),
|
||||
diskusage: normalizeOptionalNumber,
|
||||
disklimit: normalizeOptionalNumber,
|
||||
bwusage: normalizeOptionalNumber,
|
||||
bwlimit: normalizeOptionalNumber,
|
||||
lastupdate: z.string().optional(),
|
||||
lastupdate: s.optional(),
|
||||
|
||||
// Custom fields
|
||||
customfields: whmcsCustomFieldsContainerSchema.optional(),
|
||||
@ -144,10 +149,10 @@ export const whmcsProductRawSchema = z.object({
|
||||
// Pricing details
|
||||
pricing: z
|
||||
.object({
|
||||
amount: z.union([z.string(), z.number()]).optional(),
|
||||
currency: z.string().optional(),
|
||||
currencyprefix: z.string().optional(),
|
||||
currencysuffix: z.string().optional(),
|
||||
amount: numberLike.optional(),
|
||||
currency: s.optional(),
|
||||
currencyprefix: s.optional(),
|
||||
currencysuffix: s.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
@ -176,12 +181,12 @@ const whmcsProductContainerSchema = z.object({
|
||||
|
||||
export const whmcsProductListResponseSchema = z.object({
|
||||
result: z.enum(["success", "error"]).optional(),
|
||||
message: z.string().optional(),
|
||||
clientid: z.union([z.number(), z.string()]).optional(),
|
||||
serviceid: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
pid: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
domain: z.string().nullable().optional(),
|
||||
totalresults: z.union([z.number(), z.string()]).optional(),
|
||||
message: s.optional(),
|
||||
clientid: numberLike.optional(),
|
||||
serviceid: z.union([numberLike, z.null()]).optional(),
|
||||
pid: z.union([numberLike, z.null()]).optional(),
|
||||
domain: s.nullable().optional(),
|
||||
totalresults: numberLike.optional(),
|
||||
startnumber: normalizeOptionalNumber,
|
||||
numreturned: normalizeOptionalNumber,
|
||||
products: z.preprocess(value => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user