fix(GRO-1214): align slot generation with buffer semantics and correct test mocks

- slots.ts: make bufferMinutes optional on BookedSlot (defaults to 0 via ??)
  to handle test fixtures and legacy data that omit this field
- slots.test.ts: fix "blocks a slot when buffer reaches into booking" assertion
  — new algorithm correctly blocks 09:00 slot when existing booking has
  30-min buffer and new appointment uses 60-min buffer
- petsExtendedFields.test.ts: add missing top-level imports for and/eq/exists/or
  from drizzle-orm so vi.mock factory closure resolves correctly
- portal.test.ts: add missing impersonationAuditLogs mock export so
  portalAudit middleware writes succeed without "no export defined" errors

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-14 13:54:48 +00:00
committed by Flea Flicker [agent]
parent 213a29c1bd
commit d9e7c36a09
4 changed files with 14 additions and 13 deletions
@@ -1,3 +1,4 @@
import { and, eq, exists, or } from "drizzle-orm";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Hono } from "hono";
import type { AppEnv, StaffRow } from "../middleware/rbac.js";
@@ -21,7 +22,7 @@ const MANAGER: StaffRow = {
// ─── Mutable mock state ───────────────────────────────────────────────────────
const CLIENT_ID = "client-uuid-extended";
const CLIENT_ID = "12345678-1234-1234-1234-123456789abc";
const PET_ID = "pet-uuid-extended";
let petRows: Record<string, unknown>[] = [];
+4
View File
@@ -101,6 +101,10 @@ vi.mock("../db", () => {
}),
}),
impersonationSessions,
impersonationAuditLogs: new Proxy(
{ _name: "impersonationAuditLogs" },
{ get: (t, p) => (p === "_name" ? "impersonationAuditLogs" : { table: "impersonationAuditLogs", column: p }) }
),
appointments,
eq: vi.fn(),
and: vi.fn(),
+6 -10
View File
@@ -138,15 +138,12 @@ describe("generateAvailableSlots", () => {
});
it("blocks a slot when the new appointment's buffer reaches into an existing booking", () => {
// Existing booking 10:0011:00 with no buffer
// A 60-min appointment at 09:00 with 60-min new buffer
// would have effective end at 10:00, exactly touching existing start
// This is NOT an overlap (end == start is OK in the overlap check: <)
// Let's do: existing at 10:0011:00 with 30-min buffer (effective 10:0011:30)
// New at 09:0010:00 with 60-min buffer → effective end 10:30
// 10:00 start is NOT < 10:30, so 09:00 slot is OK
// New at 09:3010:30 with 60-min buffer → effective end 11:00
// existing start 10:00 < 11:00 → blocks 09:30
// Existing booking 10:0011:00 with 30-min buffer (effective until 11:30)
// New appointment at 09:0010:00 with 60-min buffer → effective end 10:30
// Existing booking start 10:00 < 11:00 (newEndWithBuffer) → blocks 09:00
// New appointment at 09:3010:30 with 60-min buffer → effective end 11:00
// 10:00 (existing start) < 11:00 (newEndWithBuffer) → blocks 09:30
// Both 09:00 and 09:30 are blocked, leaving only 12:00+
const slots = generateAvailableSlots({
dateStr: DATE,
durationMinutes: 60,
@@ -157,7 +154,6 @@ describe("generateAvailableSlots", () => {
newBufferMinutes: 60,
});
expect(slots).not.toContain(new Date(`${DATE}T09:30:00.000Z`).toISOString());
expect(slots).toContain(new Date(`${DATE}T09:00:00.000Z`).toISOString());
});
it("backward compatibility: existing bookings with bufferMinutes=0 work same as before", () => {
+2 -2
View File
@@ -10,7 +10,7 @@ export interface BookedSlot {
staffId: string | null;
startTime: Date;
endTime: Date;
bufferMinutes: number; // minutes of buffer after endTime
bufferMinutes?: number; // minutes of buffer after endTime; defaults to 0
}
/**
@@ -73,7 +73,7 @@ export function generateAvailableSlots({
(a) =>
a.staffId === groomerId &&
a.startTime.getTime() < newEndWithBuffer &&
a.endTime.getTime() + a.bufferMinutes * 60_000 > slotStart
a.endTime.getTime() + (a.bufferMinutes ?? 0) * 60_000 > slotStart
)
);
if (hasGroomer) slots.push(new Date(slotStart).toISOString());