From 36f9b5035d6c552d2a6eb1beebd2ffb80caf78c8 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 10:13:39 -0500 Subject: [PATCH] fix(schema): default invPaid to false so bulk-create doesn't trip NOT NULL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same drift class as #265 (custState) and #277 (custCompanyName et al.), but with a twist: the single-create controller already filled in the missing-invPaid case with \`if (payload.invPaid === undefined) payload.invPaid = false;\`, so the bug only manifested in the bulk path, which routes through \`makeBulkCreateIndirect\` and doesn't have entity-specific defaults. A bulk POST like { invoices: [{ invCustId: 5, invDate: "2026-01-01", invDueDate: "2026-02-01" }] } passed the schema (invPaid was \`.optional()\`), and then tripped "null value in column invPaid violates not-null constraint" at the postgres INSERT — surfacing as a 500 instead of a clean accepted row. Switching the schema to \`z.boolean().default(false)\` fills the value at the validator boundary, so both paths now get \`invPaid: false\` for free. The single-create controller's explicit default becomes dead defense but stays harmless. 760 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/schemas/invoice.schema.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/schemas/invoice.schema.js b/app/schemas/invoice.schema.js index 450d643..86b1d13 100644 --- a/app/schemas/invoice.schema.js +++ b/app/schemas/invoice.schema.js @@ -35,7 +35,16 @@ const createInvoiceBody = z.object({ invCustId: z.coerce.number().int().positive(), invDate: isoDate, invDueDate: isoDate, - invPaid: z.boolean().optional(), + // invPaid is `boolean NOT NULL` in the DB (no DB-level default). + // The single-create controller already short-circuits a missing + // value to `false`, but the bulk-create path (`makeBulkCreateIndirect`) + // doesn't know to do that, so a bulk POST without invPaid landed + // as "null value in column invPaid violates not-null constraint" + // at the postgres layer — surfacing as a 500 instead of a clean + // accepted row. Switching from `.optional()` to `.default(false)` + // fills the value at the validator boundary, which both paths + // consume via `validate.body()` (see app/middleware/validate.js). + invPaid: z.boolean().default(false), }).strict({ message: 'Unexpected field in body. Whitelist: invCustId, invDate, invDueDate, invPaid.', }).refine(refineDueDateAfterIssue, DUE_BEFORE_ISSUE);