Problem
app/schemas/invoice.schema.js validates invDate and invDueDate as ISO-date strings independently — no cross-field check. An invoice POST or PATCH where invDueDate < invDate (due date BEFORE the issue date) sails through validation. The DB row is created with both bounds intact and no signal to the operator that they wrote bookkeeping nonsense.
This is the same shape as the timeentry teEndedAt >= teStartedAt issue (#129, fixed in #130).
The bulk-create endpoint (POST /v1/invoice/bulk) also lacks the check — anyone could persist a batch of nonsense via the bulk envelope even after a single-entry validation tightens.
Proposed fix
Add a zod .refine() cross-field check to createInvoiceBody and updateInvoiceBody. The bulk schema uses z.array(createInvoiceBody) so it inherits the refinement automatically. Equality stays allowed — "Due on Receipt" / zero-day-net is a real billing term.
Single-bound PATCH (only invDueDate or only invDate) can't be validated at the schema layer without a DB read; leave that on the controller layer for a follow-up, matching how timeentry handled the same edge.
Pin behavior with five new tests covering: inverted single CREATE → 400, equality CREATE → schema-pass, inverted both-bound PATCH → 400, single-bound PATCH → schema-pass, inverted bulk-entry → 400.
Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/
Problem
app/schemas/invoice.schema.jsvalidatesinvDateandinvDueDateas ISO-date strings independently — no cross-field check. An invoice POST or PATCH whereinvDueDate < invDate(due date BEFORE the issue date) sails through validation. The DB row is created with both bounds intact and no signal to the operator that they wrote bookkeeping nonsense.This is the same shape as the timeentry
teEndedAt >= teStartedAtissue (#129, fixed in #130).The bulk-create endpoint (
POST /v1/invoice/bulk) also lacks the check — anyone could persist a batch of nonsense via the bulk envelope even after a single-entry validation tightens.Proposed fix
Add a zod
.refine()cross-field check tocreateInvoiceBodyandupdateInvoiceBody. The bulk schema usesz.array(createInvoiceBody)so it inherits the refinement automatically. Equality stays allowed — "Due on Receipt" / zero-day-net is a real billing term.Single-bound PATCH (only
invDueDateor onlyinvDate) can't be validated at the schema layer without a DB read; leave that on the controller layer for a follow-up, matching how timeentry handled the same edge.Pin behavior with five new tests covering: inverted single CREATE → 400, equality CREATE → schema-pass, inverted both-bound PATCH → 400, single-bound PATCH → schema-pass, inverted bulk-entry → 400.
Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/