Booking Engine
Architecture: Six focused services with the BookingService/SchedulingService overlap resolved by giving SchedulingService exactly one job, the transactional double-booking guard , while BookingService owns the create/reschedule/cancel workflow around it. 59 unit tests, 24 integration tests
This commit is contained in:
28 files changed
+4853
-15
No files matched your search
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Weekday } from "@/generated/prisma/enums";
|
||||
import {
|
||||
addDays,
|
||||
calendarDateOf,
|
||||
compareCalendarDates,
|
||||
eachCalendarDate,
|
||||
isAfterCalendarDate,
|
||||
isBeforeCalendarDate,
|
||||
minutesFromMidnight,
|
||||
todayIn,
|
||||
wallClockToInstant,
|
||||
weekdayOf,
|
||||
} from "../business-time";
|
||||
|
||||
describe("weekdayOf", () => {
|
||||
it("maps calendar dates to the correct Weekday", () => {
|
||||
// 2026-08-03 is a Monday.
|
||||
expect(weekdayOf("2026-08-03")).toBe(Weekday.MONDAY);
|
||||
expect(weekdayOf("2026-08-04")).toBe(Weekday.TUESDAY);
|
||||
expect(weekdayOf("2026-08-08")).toBe(Weekday.SATURDAY);
|
||||
expect(weekdayOf("2026-08-09")).toBe(Weekday.SUNDAY);
|
||||
});
|
||||
});
|
||||
|
||||
describe("wallClockToInstant / minutesFromMidnight round-trip", () => {
|
||||
it("round-trips a plain wall-clock time in a fixed-offset zone", () => {
|
||||
const instant = wallClockToInstant("2026-01-15", 9 * 60 + 30, "Europe/London");
|
||||
expect(minutesFromMidnight(instant, "Europe/London")).toBe(9 * 60 + 30);
|
||||
expect(calendarDateOf(instant, "Europe/London")).toBe("2026-01-15");
|
||||
});
|
||||
|
||||
it("produces the documented UTC instant for a known GMT (winter) date", () => {
|
||||
// 2026-01-15 is outside BST — Europe/London is UTC+0.
|
||||
const instant = wallClockToInstant("2026-01-15", 10 * 60, "Europe/London");
|
||||
expect(instant.toISOString()).toBe("2026-01-15T10:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DST correctness (Europe/London BST transition, last Sunday of March 2026)", () => {
|
||||
it("the same wall-clock time yields UTC instants shifted by the DST offset change", () => {
|
||||
// Before the 2026-03-29 transition: Europe/London is GMT (UTC+0).
|
||||
const beforeTransition = wallClockToInstant("2026-03-28", 10 * 60, "Europe/London");
|
||||
expect(beforeTransition.toISOString()).toBe("2026-03-28T10:00:00.000Z");
|
||||
|
||||
// After the transition: Europe/London is BST (UTC+1).
|
||||
const afterTransition = wallClockToInstant("2026-03-30", 10 * 60, "Europe/London");
|
||||
expect(afterTransition.toISOString()).toBe("2026-03-30T09:00:00.000Z");
|
||||
|
||||
// A naive "always UTC+0" implementation would put these exactly 48h
|
||||
// apart; DST-correct math puts them 47h apart because a wall-clock
|
||||
// hour was skipped.
|
||||
const diffHours = (afterTransition.getTime() - beforeTransition.getTime()) / 3_600_000;
|
||||
expect(diffHours).toBe(47);
|
||||
});
|
||||
|
||||
it("minutesFromMidnight recovers the same local wall-clock time on both sides of the transition", () => {
|
||||
const before = wallClockToInstant("2026-03-28", 10 * 60, "Europe/London");
|
||||
const after = wallClockToInstant("2026-03-30", 10 * 60, "Europe/London");
|
||||
expect(minutesFromMidnight(before, "Europe/London")).toBe(10 * 60);
|
||||
expect(minutesFromMidnight(after, "Europe/London")).toBe(10 * 60);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calendar date arithmetic", () => {
|
||||
it("addDays is pure calendar-date math, independent of any zone", () => {
|
||||
expect(addDays("2026-02-27", 1)).toBe("2026-02-28");
|
||||
expect(addDays("2026-02-28", 1)).toBe("2026-03-01"); // 2026 is not a leap year
|
||||
expect(addDays("2026-12-31", 1)).toBe("2027-01-01");
|
||||
});
|
||||
|
||||
it("compares calendar dates correctly", () => {
|
||||
expect(compareCalendarDates("2026-01-01", "2026-01-02")).toBeLessThan(0);
|
||||
expect(compareCalendarDates("2026-01-02", "2026-01-01")).toBeGreaterThan(0);
|
||||
expect(compareCalendarDates("2026-01-01", "2026-01-01")).toBe(0);
|
||||
expect(isBeforeCalendarDate("2026-01-01", "2026-01-02")).toBe(true);
|
||||
expect(isAfterCalendarDate("2026-01-02", "2026-01-01")).toBe(true);
|
||||
});
|
||||
|
||||
it("eachCalendarDate yields an inclusive range and nothing when from > to", () => {
|
||||
expect([...eachCalendarDate("2026-01-01", "2026-01-03")]).toEqual([
|
||||
"2026-01-01",
|
||||
"2026-01-02",
|
||||
"2026-01-03",
|
||||
]);
|
||||
expect([...eachCalendarDate("2026-01-03", "2026-01-01")]).toEqual([]);
|
||||
});
|
||||
|
||||
it("eachCalendarDate guards against absurdly large ranges", () => {
|
||||
expect(() => [...eachCalendarDate("2026-01-01", "2030-01-01")]).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("todayIn", () => {
|
||||
it("returns a well-formed calendar date", () => {
|
||||
expect(todayIn("Europe/London")).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user