216 lines
8.4 KiB
TypeScript
Raw Normal View History

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<string, unknown> = {
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<typeof addressFormSchema>;
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<typeof customerAddressSchema>;
export type CustomerEmailPreferences = z.infer<typeof customerEmailPreferencesSchema>;
export type CustomerUser = z.infer<typeof customerUserSchema>;
export type CustomerStats = z.infer<typeof customerStatsSchema>;
export type Customer = z.infer<typeof customerSchema>;
export type AddressFormData = z.infer<typeof addressFormSchema>;
// Type aliases
export type Address = CustomerAddress;