Commit 2533bf6
#130 added a zod `.refine()` on createTimeEntryBody and
updateTimeEntryBody that rejects teEndedAt < teStartedAt when both
bounds are in the request body. The PR called out the
single-bound PATCH case as intentionally NOT covered there: a
PATCH that supplies only teEndedAt (or only teStartedAt) merges
against the row's existing value, which the schema layer can't
see without a DB read. Said:
> The single-bound PATCH case (only teEndedAt, validated against
> the row's existing teStartedAt) can't be enforced at the schema
> layer without a DB read. Leave that on the controller layer
> for a follow-up.
This is that follow-up. After the controller computes
`mergedStart` / `mergedEnd` from the body + existing row, check
whether the merged range is inverted; reject with 400 instead of
silently dropping `teMinutes` to null (the previous behaviour,
which the PR review confirmed had real-world bookkeeping cost:
"clocked out before clocked in" rows look correct in their two
timestamp columns but have an empty duration).
Factored the check into a small `isInvertedRange(startedAt,
endedAt)` helper, exposed via `_internals` for unit testing.
Equality (zero-minute "Due on Receipt"-style entries) stays
allowed. Unparseable input returns false from the helper because
computeMinutes' own NaN guard already null-tags those rows;
flagging them at this layer too would be double-counting a
problem we'd 400 on elsewhere.
Four new unit tests on the helper cover the four classes:
missing-bound (false), happy-path including equality (false),
inverted (true), unparseable (false).
Co-authored-by: Aaron K. Clark <akclark@thenetwerk.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 53b408a commit 2533bf6
2 files changed
Lines changed: 91 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
33 | 56 | | |
34 | 57 | | |
35 | 58 | | |
| |||
236 | 259 | | |
237 | 260 | | |
238 | 261 | | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
243 | 281 | | |
244 | 282 | | |
245 | 283 | | |
| |||
414 | 452 | | |
415 | 453 | | |
416 | 454 | | |
417 | | - | |
| 455 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
190 | 238 | | |
191 | 239 | | |
192 | 240 | | |
| |||
0 commit comments