diff --git a/packages/shared/src/schemas/account.schema.ts b/packages/shared/src/schemas/account.schema.ts index 0f991ae..8eee5ea 100644 --- a/packages/shared/src/schemas/account.schema.ts +++ b/packages/shared/src/schemas/account.schema.ts @@ -1,5 +1,10 @@ import { z } from 'zod' +/** Coerce empty strings to undefined — solves HTML form inputs sending '' for blank optional fields */ +function opt(schema: T) { + return z.preprocess((v) => (v === '' ? undefined : v), schema.optional()) as z.ZodEffects> +} + export const BillingMode = z.enum(['consolidated', 'split']) export type BillingMode = z.infer @@ -11,8 +16,8 @@ export type TaxExemptStatus = z.infer export const AccountCreateSchema = z.object({ name: z.string().min(1).max(255), - email: z.string().email().optional(), - phone: z.string().max(50).optional(), + email: opt(z.string().email()), + phone: opt(z.string().max(50)), address: z .object({ street: z.string().optional(), @@ -22,7 +27,7 @@ export const AccountCreateSchema = z.object({ }) .optional(), billingMode: BillingMode.default('consolidated'), - notes: z.string().optional(), + notes: opt(z.string()), }) export type AccountCreateInput = z.infer @@ -33,11 +38,11 @@ export const MemberCreateSchema = z.object({ accountId: z.string().uuid(), firstName: z.string().min(1).max(100), lastName: z.string().min(1).max(100), - dateOfBirth: z.string().date().optional(), + dateOfBirth: opt(z.string().date()), isMinor: z.boolean().optional(), - email: z.string().email().optional(), - phone: z.string().max(50).optional(), - notes: z.string().optional(), + email: opt(z.string().email()), + phone: opt(z.string().max(50)), + notes: opt(z.string()), }) export type MemberCreateInput = z.infer @@ -56,14 +61,14 @@ export type IdentifierType = z.infer export const MemberIdentifierCreateSchema = z.object({ memberId: z.string().uuid(), type: IdentifierType, - label: z.string().max(100).optional(), + label: opt(z.string().max(100)), value: z.string().min(1).max(255), - issuingAuthority: z.string().max(255).optional(), - issuedDate: z.string().date().optional(), - expiresAt: z.string().date().optional(), - imageFrontUrl: z.string().max(500).optional(), - imageBackUrl: z.string().max(500).optional(), - notes: z.string().optional(), + issuingAuthority: opt(z.string().max(255)), + issuedDate: opt(z.string().date()), + expiresAt: opt(z.string().date()), + imageFront: opt(z.string()), + imageBack: opt(z.string()), + notes: opt(z.string()), isPrimary: z.boolean().default(false), }) export type MemberIdentifierCreateInput = z.infer @@ -91,8 +96,8 @@ export const PaymentMethodCreateSchema = z.object({ accountId: z.string().uuid(), processor: PaymentProcessor, processorPaymentMethodId: z.string().min(1).max(255), - cardBrand: z.string().max(50).optional(), - lastFour: z.string().length(4).optional(), + cardBrand: opt(z.string().max(50)), + lastFour: opt(z.string().length(4)), expMonth: z.number().int().min(1).max(12).optional(), expYear: z.number().int().min(2000).max(2100).optional(), isDefault: z.boolean().default(false), @@ -110,19 +115,19 @@ export type PaymentMethodUpdateInput = z.infer export const TaxExemptionCreateSchema = z.object({ accountId: z.string().uuid(), certificateNumber: z.string().min(1).max(255), - certificateType: z.string().max(100).optional(), - issuingState: z.string().length(2).optional(), - expiresAt: z.string().date().optional(), - notes: z.string().optional(), + certificateType: opt(z.string().max(100)), + issuingState: opt(z.string().length(2)), + expiresAt: opt(z.string().date()), + notes: opt(z.string()), }) export type TaxExemptionCreateInput = z.infer export const TaxExemptionUpdateSchema = z.object({ - certificateNumber: z.string().min(1).max(255).optional(), - certificateType: z.string().max(100).optional(), - issuingState: z.string().length(2).optional(), - expiresAt: z.string().date().optional(), - notes: z.string().optional(), + certificateNumber: opt(z.string().min(1).max(255)), + certificateType: opt(z.string().max(100)), + issuingState: opt(z.string().length(2)), + expiresAt: opt(z.string().date()), + notes: opt(z.string()), }) export type TaxExemptionUpdateInput = z.infer @@ -131,14 +136,14 @@ export type TaxExemptionUpdateInput = z.infer export const LookupCreateSchema = z.object({ name: z.string().min(1).max(100), slug: z.string().min(1).max(100).regex(/^[a-z0-9_]+$/, 'Slug must be lowercase alphanumeric with underscores'), - description: z.string().optional(), + description: opt(z.string()), sortOrder: z.number().int().default(0), }) export type LookupCreateInput = z.infer export const LookupUpdateSchema = z.object({ - name: z.string().min(1).max(100).optional(), - description: z.string().optional(), + name: opt(z.string().min(1).max(100)), + description: opt(z.string()), sortOrder: z.number().int().optional(), isActive: z.boolean().optional(), }) diff --git a/packages/shared/src/schemas/inventory.schema.ts b/packages/shared/src/schemas/inventory.schema.ts index 168ed36..96d882b 100644 --- a/packages/shared/src/schemas/inventory.schema.ts +++ b/packages/shared/src/schemas/inventory.schema.ts @@ -1,9 +1,14 @@ import { z } from 'zod' +/** Coerce empty strings to undefined — solves HTML form inputs sending '' for blank optional fields */ +function opt(schema: T) { + return z.preprocess((v) => (v === '' ? undefined : v), schema.optional()) as z.ZodEffects> +} + export const CategoryCreateSchema = z.object({ name: z.string().min(1).max(255), - description: z.string().optional(), - parentId: z.string().uuid().optional(), + description: opt(z.string()), + parentId: opt(z.string().uuid()), sortOrder: z.number().int().default(0), }) export type CategoryCreateInput = z.infer @@ -13,13 +18,13 @@ export type CategoryUpdateInput = z.infer export const SupplierCreateSchema = z.object({ name: z.string().min(1).max(255), - contactName: z.string().max(255).optional(), - email: z.string().email().optional(), - phone: z.string().max(50).optional(), - website: z.string().max(255).optional(), - accountNumber: z.string().max(100).optional(), - paymentTerms: z.string().max(100).optional(), - notes: z.string().optional(), + contactName: opt(z.string().max(255)), + email: opt(z.string().email()), + phone: opt(z.string().max(50)), + website: opt(z.string().max(255)), + accountNumber: opt(z.string().max(100)), + paymentTerms: opt(z.string().max(100)), + notes: opt(z.string()), }) export type SupplierCreateInput = z.infer @@ -45,14 +50,14 @@ export const ItemCondition = z.string().min(1).max(100) export const UnitStatus = z.string().min(1).max(100) export const ProductCreateSchema = z.object({ - sku: z.string().max(100).optional(), - upc: z.string().max(100).optional(), + sku: opt(z.string().max(100)), + upc: opt(z.string().max(100)), name: z.string().min(1).max(255), - description: z.string().optional(), - brand: z.string().max(255).optional(), - model: z.string().max(255).optional(), - categoryId: z.string().uuid().optional(), - locationId: z.string().uuid().optional(), + description: opt(z.string()), + brand: opt(z.string().max(255)), + model: opt(z.string().max(255)), + categoryId: opt(z.string().uuid()), + locationId: opt(z.string().uuid()), isSerialized: z.boolean().default(false), isRental: z.boolean().default(false), isDualUseRepair: z.boolean().default(false), @@ -73,13 +78,13 @@ export const ProductSearchSchema = z.object({ export const InventoryUnitCreateSchema = z.object({ productId: z.string().uuid(), - locationId: z.string().uuid().optional(), - serialNumber: z.string().max(255).optional(), + locationId: opt(z.string().uuid()), + serialNumber: opt(z.string().max(255)), condition: ItemCondition.default('new'), status: UnitStatus.default('available'), - purchaseDate: z.string().date().optional(), + purchaseDate: opt(z.string().date()), purchaseCost: z.number().min(0).optional(), - notes: z.string().optional(), + notes: opt(z.string()), }) export type InventoryUnitCreateInput = z.infer