Prisma truncates a JS Date used in a WHERE filter on a @db.Date column to its UTC date part. Under TZ=Europe/Prague a local-midnight boundary (new Date(y,m,d) = 22:00/23:00Z of the previous day) therefore filtered as the PREVIOUS calendar date. Same class as the dashboard fix; full sweep of every @db.Date filter in the codebase. New shared helper utcMidnightOfLocalDay() in src/utils/date.ts. Fixed (all previously off by one day): - attendance.service: getStatus today+month windows; getWorkfund (last day of each month was double-counted across months); getPrintData (monthly print included prev month's last day); listAttendance (admin month view included prev month's last day AND dropped the selected month's last day); createAttendance duplicate/overlap validation (checked only the PREVIOUS day - same-day duplicates were never caught, neighbors falsely rejected) - invoice-alerts: the 'splatnost za 3 dny' advance alert had NEVER fired (due==today+3 was outside the fetched window); window computation extracted as computeAlertWindow() + pure tests - invoices.service: month list/totals filter dropped invoices issued on the month's last day; getInvoiceStats month/year bounds; markOverdueInvoices boundary; auto paid_date could store yesterday during 00:00-02:00 - received-invoices auto paid_date, issued-orders default order_date: same night-window write hazard, now via the helper - attendance.schema: shift_date hardened to isoDateString (bare YYYY-MM-DD) +9 regression tests (month boundaries, same-day duplicate, last-day-of-month invoice in list+totals, alert window). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
51 lines
2.3 KiB
TypeScript
51 lines
2.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { computeAlertWindow } from "../services/invoice-alerts";
|
|
import { utcMidnightOfLocalDay } from "../utils/date";
|
|
|
|
// Pure tests — no DB rows are touched. due_date is @db.Date: Prisma truncates
|
|
// a Date used in a WHERE filter to its UTC date part, so the alert window
|
|
// boundaries must be UTC midnights of the LOCAL calendar day. The old code
|
|
// used local midnights (= 22:00/23:00 UTC of the PREVIOUS day), which shifted
|
|
// the [today, today+3] due_date window a day early — the "splatnost za 3 dny"
|
|
// advance alert (due == today+3) could then never fire. All assertions below
|
|
// are timezone-independent (inputs are built from local components).
|
|
|
|
describe("utcMidnightOfLocalDay", () => {
|
|
it("preserves the LOCAL calendar day shortly after local midnight (the night window)", () => {
|
|
// 00:30 local on 2098-07-01 — in Prague (UTC+2) this instant is
|
|
// 2098-06-30T22:30Z, i.e. its UTC date part is the PREVIOUS day, which is
|
|
// exactly what Prisma would truncate a bare Date to.
|
|
const justAfterMidnight = new Date(2098, 6, 1, 0, 30, 0);
|
|
expect(utcMidnightOfLocalDay(justAfterMidnight).getTime()).toBe(
|
|
Date.UTC(2098, 6, 1),
|
|
);
|
|
});
|
|
|
|
it("returns UTC midnight of the local day for a mid-day instant", () => {
|
|
const noon = new Date(2098, 0, 15, 12, 0, 0);
|
|
expect(utcMidnightOfLocalDay(noon).getTime()).toBe(Date.UTC(2098, 0, 15));
|
|
});
|
|
});
|
|
|
|
describe("computeAlertWindow", () => {
|
|
it("builds [today, today+3] as UTC midnights of the LOCAL day, with matching strings", () => {
|
|
// 00:30 local — the window where the old local-midnight computation
|
|
// produced yesterday's UTC date and the whole window slid a day early.
|
|
const now = new Date(2098, 6, 1, 0, 30, 0);
|
|
const w = computeAlertWindow(now);
|
|
|
|
expect(w.today.getTime()).toBe(Date.UTC(2098, 6, 1));
|
|
expect(w.in3days.getTime()).toBe(Date.UTC(2098, 6, 4));
|
|
expect(w.todayStr).toBe("2098-07-01");
|
|
expect(w.in3daysStr).toBe("2098-07-04");
|
|
});
|
|
|
|
it("rolls the +3-day bound over a month boundary", () => {
|
|
const now = new Date(2098, 0, 30, 8, 0, 0); // 2098-01-30
|
|
const w = computeAlertWindow(now);
|
|
|
|
expect(w.in3days.getTime()).toBe(Date.UTC(2098, 1, 2)); // 2098-02-02
|
|
expect(w.in3daysStr).toBe("2098-02-02");
|
|
});
|
|
});
|