feat(deps): bump zod 3→4 + @hookform/resolvers 3→5

Resolved 65 type errors across the codebase via these v4 migration
patterns:

- `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth
  routes + central error handler).
- `z.record(value)` now requires explicit key type: `z.record(z.string(),
  value)`. Updated 7 sites across templates / forms / saved-views /
  website-inquiries.
- `.refine(check, msgFn)` second-arg shape changed — now requires an
  `{ error: (issue) => ... }` object form. Updated
  `mergeFieldsSchema` in document-templates validator.
- `.transform(...).default(...)` chains: v4 enforces default value type
  matches transform OUTPUT. Reordered to `.default(...).transform(...)`
  in list-query / company-memberships handlers.
- `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures
  using `z.input<typeof schema>` (kept for caller flexibility around
  defaults) now re-parse via `schema.parse(data)` to recover the
  post-coercion shape Drizzle needs. Done in berth-reservations service.
  Invoice service narrows `lineItems` locally with a typed cast since
  re-parsing would double-validate.
- `.optional().transform(...)` no longer propagates the optional marker
  through v4's new ZodPipe. Moved `.optional()` to the END of chain in
  `optionalDesiredDimSchema` (interests) and documents list query
  (folderId, signatureOnly).
- ZodIssue subtype shapes simplified: `received` removed from
  invalid_type, `type` renamed to `origin` on too_small. Test fixtures
  updated.
- @hookform/resolvers v5 splits Resolver into 3-generic form (Input,
  Context, Output). useForm calls in 6 forms (client, yacht, berth,
  interest, expense, invoices-new-page) now pass explicit generics:
  `useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`.

Verified: tsc clean (0 errors), vitest 1293/1293 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 18:29:03 +02:00
parent d3960af340
commit acf878f997
24 changed files with 131 additions and 90 deletions

View File

@@ -11,14 +11,17 @@ import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';
import type { z } from 'zod';
import { createPendingSchema } from '@/lib/validators/reservations';
import type {
createPendingSchema,
ActivateInput,
EndReservationInput,
CancelInput,
ListReservationsInput,
} from '@/lib/validators/reservations';
// Use z.input so callers (including tests) can omit fields with
// `.default()` like `tenureType`. The service re-parses below to get
// the post-coercion shape Drizzle expects (Date, defaulted tenureType).
type CreatePendingInput = z.input<typeof createPendingSchema>;
export type { BerthReservation };
@@ -110,18 +113,23 @@ export async function createPending(
data.clientId,
);
// Re-parse to apply coercions/defaults locally — Drizzle's .values()
// wants the post-coercion shape (Date, defaulted enum), and v4's
// z.input is too loose to satisfy that.
const parsed = createPendingSchema.parse(data);
const [reservation] = await db
.insert(berthReservations)
.values({
portId,
berthId: data.berthId,
clientId: data.clientId,
yachtId: data.yachtId,
interestId: data.interestId ?? null,
berthId: parsed.berthId,
clientId: parsed.clientId,
yachtId: parsed.yachtId,
interestId: parsed.interestId ?? null,
status: 'pending',
startDate: data.startDate,
tenureType: data.tenureType ?? 'permanent',
notes: data.notes ?? null,
startDate: parsed.startDate,
tenureType: parsed.tenureType,
notes: parsed.notes ?? null,
createdBy: meta.userId,
})
.returning();

View File

@@ -248,8 +248,12 @@ export async function createInvoice(portId: string, data: CreateInvoiceInput, me
const invoiceNumber = await generateInvoiceNumber(portId, tx);
// Calculate subtotal from line items
const lineItemsData = data.lineItems ?? [];
// Calculate subtotal from line items. The `z.coerce.number()` in
// the schema makes the parsed value a number at runtime — narrow
// the post-parse shape locally so v4's stricter input typing
// (unknown for coerced fields) doesn't leak into arithmetic.
type ParsedLineItem = { quantity: number; unitPrice: number; description: string };
const lineItemsData = (data.lineItems ?? []) as ParsedLineItem[];
const subtotal = lineItemsData.reduce((sum, li) => sum + (li.quantity ?? 1) * li.unitPrice, 0);
// BR-042: net10 discount - read from systemSettings
@@ -429,9 +433,12 @@ export async function updateInvoice(
}
if (data.kind !== undefined) updateData.kind = data.kind;
// Recalculate totals if line items changed
// Recalculate totals if line items changed (see createInvoice for
// the ParsedLineItem narrowing rationale — same coerced-number
// story applies on the update path).
if (data.lineItems !== undefined) {
const lineItemsData = data.lineItems;
type ParsedLineItem = { quantity: number; unitPrice: number; description: string };
const lineItemsData = data.lineItems as ParsedLineItem[];
const subtotal = lineItemsData.reduce(
(sum, li) => sum + (li.quantity ?? 1) * li.unitPrice,
0,