Problem
POST /v1/timeentry and PATCH /v1/timeentry/:id accept a body where teEndedAt is strictly before teStartedAt. The controller's computeMinutes helper notices the inversion and short-circuits to teMinutes = null, but no 400 is returned — the row is created/updated with both bounds intact and the duration column blank. From the client's perspective the request succeeded; from the database's perspective it now has a time entry where the worker clocked out before clocking in.
// app/controllers/timeentrycontroller.js
function computeMinutes(startedAt, endedAt) {
if (!startedAt || !endedAt) return null;
const start = new Date(startedAt).getTime();
const end = new Date(endedAt).getTime();
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
return Math.round((end - start) / 60000);
}
Proposed fix
Add a zod .refine() cross-field check on both createTimeEntryBody and updateTimeEntryBody in app/schemas/timeentry.schema.js. When both bounds are present in the same request body, reject with a 400 if teEndedAt < teStartedAt. Equality (zero-minute entries) stays allowed — that's a valid edge case, not a bug.
The single-bound PATCH case (PATCH with only teEndedAt, validated against the row's existing teStartedAt) can't be enforced at the schema layer without a DB read. Leave that path on the controller layer for a follow-up.
Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/
Problem
POST /v1/timeentryandPATCH /v1/timeentry/:idaccept a body whereteEndedAtis strictly beforeteStartedAt. The controller'scomputeMinuteshelper notices the inversion and short-circuits toteMinutes = null, but no 400 is returned — the row is created/updated with both bounds intact and the duration column blank. From the client's perspective the request succeeded; from the database's perspective it now has a time entry where the worker clocked out before clocking in.Proposed fix
Add a zod
.refine()cross-field check on bothcreateTimeEntryBodyandupdateTimeEntryBodyinapp/schemas/timeentry.schema.js. When both bounds are present in the same request body, reject with a 400 ifteEndedAt < teStartedAt. Equality (zero-minute entries) stays allowed — that's a valid edge case, not a bug.The single-bound PATCH case (
PATCHwith onlyteEndedAt, validated against the row's existingteStartedAt) can't be enforced at the schema layer without a DB read. Leave that path on the controller layer for a follow-up.Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/