import { z } from "zod"; import { countryCodeSchema } from "../common/schema"; const stringOrNull = z.union([z.string(), z.null()]); const booleanLike = z.union([z.boolean(), z.number(), z.string()]); const numberLike = z.union([z.number(), z.string()]); export const customerAddressSchema = z.object({ address1: stringOrNull.optional(), address2: stringOrNull.optional(), city: stringOrNull.optional(), state: stringOrNull.optional(), postcode: stringOrNull.optional(), country: stringOrNull.optional(), countryCode: stringOrNull.optional(), phoneNumber: stringOrNull.optional(), phoneCountryCode: stringOrNull.optional(), }); export const customerEmailPreferencesSchema = z .object({ general: booleanLike.optional(), invoice: booleanLike.optional(), support: booleanLike.optional(), product: booleanLike.optional(), domain: booleanLike.optional(), affiliate: booleanLike.optional(), }) .transform(prefs => { const normalizeBoolean = (input: unknown): boolean | undefined => { if (input === undefined || input === null) return undefined; if (typeof input === "boolean") return input; if (typeof input === "number") return input === 1; if (typeof input === "string") { const normalized = input.trim().toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes"; } return undefined; }; return { general: normalizeBoolean(prefs.general), invoice: normalizeBoolean(prefs.invoice), support: normalizeBoolean(prefs.support), product: normalizeBoolean(prefs.product), domain: normalizeBoolean(prefs.domain), affiliate: normalizeBoolean(prefs.affiliate), }; }); export const customerUserSchema = z .object({ id: numberLike, name: z.string(), email: z.string(), is_owner: booleanLike.optional(), }) .transform(user => ({ id: Number(user.id), name: user.name, email: user.email, isOwner: (() => { const value = user.is_owner; if (value === undefined || value === null) return false; if (typeof value === "boolean") return value; if (typeof value === "number") return value === 1; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes"; } return false; })(), })); const statsRecord = z .record(z.string(), z.union([z.string(), z.number(), z.boolean()])) .optional(); export const customerStatsSchema = statsRecord.transform(stats => { if (!stats) return undefined; const toNumber = (input: unknown): number | undefined => { if (input === undefined || input === null) return undefined; if (typeof input === "number") return input; const parsed = Number.parseInt(String(input).replace(/[^0-9-]/g, ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }; const toString = (input: unknown): string | undefined => { if (input === undefined || input === null) return undefined; return String(input); }; const toBool = (input: unknown): boolean | undefined => { if (input === undefined || input === null) return undefined; if (typeof input === "boolean") return input; if (typeof input === "number") return input === 1; if (typeof input === "string") { const normalized = input.trim().toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes"; } return undefined; }; const normalized: Record = { numDueInvoices: toNumber(stats.numdueinvoices), dueInvoicesBalance: toString(stats.dueinvoicesbalance), numOverdueInvoices: toNumber(stats.numoverdueinvoices), overdueInvoicesBalance: toString(stats.overdueinvoicesbalance), numUnpaidInvoices: toNumber(stats.numunpaidinvoices), unpaidInvoicesAmount: toString(stats.unpaidinvoicesamount), numPaidInvoices: toNumber(stats.numpaidinvoices), paidInvoicesAmount: toString(stats.paidinvoicesamount), creditBalance: toString(stats.creditbalance), inCredit: toBool(stats.incredit), isAffiliate: toBool(stats.isAffiliate), productsNumActive: toNumber(stats.productsnumactive), productsNumTotal: toNumber(stats.productsnumtotal), activeDomains: toNumber(stats.numactivedomains), raw: stats, }; return normalized; }); export const customerSchema = z.object({ id: z.number().int().positive(), clientId: z.number().int().optional(), ownerUserId: z.number().int().nullable().optional(), userId: z.number().int().nullable().optional(), uuid: z.string().nullable().optional(), firstname: z.string().nullable().optional(), lastname: z.string().nullable().optional(), fullname: z.string().nullable().optional(), companyName: z.string().nullable().optional(), email: z.string(), status: z.string().nullable().optional(), language: z.string().nullable().optional(), defaultGateway: z.string().nullable().optional(), defaultPaymentMethodId: z.number().int().nullable().optional(), currencyId: z.number().int().nullable().optional(), currencyCode: z.string().nullable().optional(), taxId: z.string().nullable().optional(), phoneNumber: z.string().nullable().optional(), phoneCountryCode: z.string().nullable().optional(), telephoneNumber: z.string().nullable().optional(), allowSingleSignOn: z.boolean().nullable().optional(), emailVerified: z.boolean().nullable().optional(), marketingEmailsOptIn: z.boolean().nullable().optional(), notes: z.string().nullable().optional(), createdAt: z.string().nullable().optional(), lastLogin: z.string().nullable().optional(), address: customerAddressSchema.nullable().optional(), emailPreferences: customerEmailPreferencesSchema.nullable().optional(), customFields: z.record(z.string(), z.string()).optional(), users: z.array(customerUserSchema).optional(), stats: customerStatsSchema.optional(), raw: z.record(z.string(), z.unknown()).optional(), }); export const addressFormSchema = z.object({ address1: z.string().min(1, "Address line 1 is required").max(200, "Address line 1 is too long").trim(), address2: z.string().max(200, "Address line 2 is too long").trim().optional(), city: z.string().min(1, "City is required").max(100, "City name is too long").trim(), state: z.string().min(1, "State/Prefecture is required").max(100, "State/Prefecture name is too long").trim(), postcode: z.string().min(1, "Postcode is required").max(20, "Postcode is too long").trim(), country: z.string().min(1, "Country is required").max(100, "Country name is too long").trim(), countryCode: countryCodeSchema.optional(), phoneNumber: z.string().optional(), phoneCountryCode: z.string().optional(), }); // Duplicate identifier - remove this // export type AddressFormData = z.infer; const emptyToNull = (value?: string | null) => { if (value === undefined) return undefined; const trimmed = value?.trim(); return trimmed ? trimmed : null; }; export const addressFormToRequest = (form: AddressFormData): CustomerAddress => customerAddressSchema.parse({ address1: emptyToNull(form.address1), address2: emptyToNull(form.address2 ?? null), city: emptyToNull(form.city), state: emptyToNull(form.state), postcode: emptyToNull(form.postcode), country: emptyToNull(form.country), countryCode: emptyToNull(form.countryCode ?? null), phoneNumber: emptyToNull(form.phoneNumber ?? null), phoneCountryCode: emptyToNull(form.phoneCountryCode ?? null), }); export type CustomerAddressSchema = typeof customerAddressSchema; export type CustomerSchema = typeof customerSchema; export type CustomerStatsSchema = typeof customerStatsSchema; export const addressSchema = customerAddressSchema; export type AddressSchema = typeof addressSchema; // ============================================================================ // Inferred Types from Schemas (Schema-First Approach) // ============================================================================ export type CustomerAddress = z.infer; export type CustomerEmailPreferences = z.infer; export type CustomerUser = z.infer; export type CustomerStats = z.infer; export type Customer = z.infer; export type AddressFormData = z.infer; // Type aliases export type Address = CustomerAddress;