2025-10-07 17:38:39 +09:00
|
|
|
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()]);
|
|
|
|
|
|
2025-10-08 10:33:33 +09:00
|
|
|
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(),
|
|
|
|
|
});
|
2025-10-07 17:38:39 +09:00
|
|
|
|
|
|
|
|
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),
|
2025-10-08 10:33:33 +09:00
|
|
|
};
|
2025-10-07 17:38:39 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
})(),
|
2025-10-08 10:33:33 +09:00
|
|
|
}));
|
2025-10-07 17:38:39 +09:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 10:33:33 +09:00
|
|
|
const normalized: Record<string, unknown> = {
|
2025-10-07 17:38:39 +09:00
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-08 10:33:33 +09:00
|
|
|
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(),
|
|
|
|
|
});
|
2025-10-07 17:38:39 +09:00
|
|
|
|
|
|
|
|
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(),
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-08 10:33:33 +09:00
|
|
|
// Duplicate identifier - remove this
|
|
|
|
|
// export type AddressFormData = z.infer<typeof addressFormSchema>;
|
2025-10-07 17:38:39 +09:00
|
|
|
|
|
|
|
|
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;
|
2025-10-08 10:33:33 +09:00
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 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;
|