2026-05-01 18:59:19 -04:00
|
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
|
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
|
|
|
import { PrismaClient } from "@/generated/prisma/client";
|
|
|
|
|
import {
|
|
|
|
|
BookingConflictError,
|
2026-05-02 09:07:19 -04:00
|
|
|
cancelBooking,
|
2026-05-01 18:59:19 -04:00
|
|
|
confirmHold,
|
|
|
|
|
createHold,
|
|
|
|
|
expireStaleHolds,
|
2026-05-02 14:05:30 -04:00
|
|
|
markComplete,
|
|
|
|
|
markNoShow,
|
|
|
|
|
rescheduleBooking,
|
2026-05-01 18:59:19 -04:00
|
|
|
} from "@/lib/booking";
|
|
|
|
|
import { seed, type SeedResult } from "@/lib/seed";
|
|
|
|
|
|
|
|
|
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
|
|
|
|
const db = new PrismaClient({ adapter });
|
|
|
|
|
|
|
|
|
|
let fx: SeedResult;
|
|
|
|
|
let serviceId: string;
|
|
|
|
|
let therapistA: string;
|
|
|
|
|
let therapistB: string;
|
|
|
|
|
let roomA: string;
|
|
|
|
|
let roomB: string;
|
|
|
|
|
let customerId: string;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
fx = await seed(db);
|
|
|
|
|
serviceId = fx.services.find((s) => s.name === "60-minute Swedish")!.id;
|
|
|
|
|
therapistA = fx.therapists[0].id;
|
|
|
|
|
therapistB = fx.therapists[1].id;
|
|
|
|
|
roomA = fx.rooms[0].id;
|
|
|
|
|
roomB = fx.rooms[1].id;
|
|
|
|
|
customerId = fx.customers[0].id;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await db.$disconnect();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
await db.booking.deleteMany();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const D = (iso: string) => new Date(iso);
|
|
|
|
|
|
|
|
|
|
describe("createHold", () => {
|
|
|
|
|
test("inserts a HOLD with computed endsAt and roomReleasedAt", async () => {
|
|
|
|
|
const startsAt = D("2026-05-05T14:00:00Z");
|
|
|
|
|
const hold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt,
|
|
|
|
|
});
|
|
|
|
|
// 60-min Swedish, 15-min buffer
|
|
|
|
|
expect(hold.endsAt).toEqual(D("2026-05-05T15:00:00Z"));
|
|
|
|
|
expect(hold.roomReleasedAt).toEqual(D("2026-05-05T15:15:00Z"));
|
|
|
|
|
expect(hold.priceCents).toBe(9000);
|
|
|
|
|
expect(hold.depositCents).toBe(2000);
|
|
|
|
|
|
|
|
|
|
const row = await db.booking.findUnique({ where: { id: hold.id } });
|
|
|
|
|
expect(row?.status).toBe("HOLD");
|
|
|
|
|
expect(row?.holdExpiresAt).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("conflicting createHold (same therapist, overlapping) throws BookingConflictError", async () => {
|
|
|
|
|
await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await expect(
|
|
|
|
|
createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA, // same therapist
|
|
|
|
|
roomId: roomB, // different room
|
|
|
|
|
startsAt: D("2026-05-05T14:30:00Z"),
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toBeInstanceOf(BookingConflictError);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("conflicting createHold (same room within buffer) throws BookingConflictError", async () => {
|
|
|
|
|
await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
// Next slot after 11:00 is 11:15 because of the buffer.
|
|
|
|
|
await expect(
|
|
|
|
|
createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistB,
|
|
|
|
|
roomId: roomA, // same room
|
|
|
|
|
startsAt: D("2026-05-05T15:00:00Z"), // exactly at end — within buffer
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toBeInstanceOf(BookingConflictError);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("non-overlapping createHold succeeds after the room buffer", async () => {
|
|
|
|
|
await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
const hold2 = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistB,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T15:15:00Z"), // exactly when buffer ends
|
|
|
|
|
});
|
|
|
|
|
expect(hold2.id).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("inactive service is rejected", async () => {
|
|
|
|
|
await db.service.update({
|
|
|
|
|
where: { id: serviceId },
|
|
|
|
|
data: { active: false },
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
await expect(
|
|
|
|
|
createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toThrow(/inactive/);
|
|
|
|
|
} finally {
|
|
|
|
|
await db.service.update({
|
|
|
|
|
where: { id: serviceId },
|
|
|
|
|
data: { active: true },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("custom holdMinutes is honored", async () => {
|
|
|
|
|
const before = Date.now();
|
|
|
|
|
const hold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
holdMinutes: 30,
|
|
|
|
|
});
|
|
|
|
|
const expected = before + 30 * 60_000;
|
|
|
|
|
// Allow 5 seconds of drift for the test execution time.
|
|
|
|
|
expect(hold.holdExpiresAt.getTime()).toBeGreaterThanOrEqual(expected - 5000);
|
|
|
|
|
expect(hold.holdExpiresAt.getTime()).toBeLessThanOrEqual(expected + 5000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("confirmHold", () => {
|
|
|
|
|
test("flips a HOLD to CONFIRMED and clears holdExpiresAt", async () => {
|
|
|
|
|
const hold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await confirmHold(db, hold.id);
|
|
|
|
|
const row = await db.booking.findUnique({ where: { id: hold.id } });
|
|
|
|
|
expect(row?.status).toBe("CONFIRMED");
|
|
|
|
|
expect(row?.holdExpiresAt).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-02 09:07:19 -04:00
|
|
|
describe("cancelBooking", () => {
|
|
|
|
|
async function makeBooking(status: "HOLD" | "CONFIRMED" | "CANCELLED" | "COMPLETED" | "NO_SHOW" = "CONFIRMED") {
|
|
|
|
|
const hold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
if (status !== "HOLD") {
|
|
|
|
|
await db.booking.update({
|
|
|
|
|
where: { id: hold.id },
|
|
|
|
|
data: { status, holdExpiresAt: null },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return hold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("cancels a CONFIRMED booking and records audit fields", async () => {
|
|
|
|
|
const hold = await makeBooking("CONFIRMED");
|
|
|
|
|
const result = await cancelBooking(db, {
|
|
|
|
|
bookingId: hold.id,
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
reason: "test cancel",
|
|
|
|
|
});
|
|
|
|
|
expect(result).toEqual({ kind: "cancelled", alreadyCancelled: false });
|
|
|
|
|
|
|
|
|
|
const row = await db.booking.findUnique({ where: { id: hold.id } });
|
|
|
|
|
expect(row?.status).toBe("CANCELLED");
|
|
|
|
|
expect(row?.cancelledBy).toBe(customerId);
|
|
|
|
|
expect(row?.cancelReason).toBe("test cancel");
|
|
|
|
|
expect(row?.cancelledAt).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("cancels a HOLD booking", async () => {
|
|
|
|
|
const hold = await makeBooking("HOLD");
|
|
|
|
|
const result = await cancelBooking(db, {
|
|
|
|
|
bookingId: hold.id,
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
});
|
|
|
|
|
expect(result.kind).toBe("cancelled");
|
|
|
|
|
const row = await db.booking.findUnique({ where: { id: hold.id } });
|
|
|
|
|
expect(row?.status).toBe("CANCELLED");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("is idempotent — cancelling an already-CANCELLED booking returns alreadyCancelled", async () => {
|
|
|
|
|
const hold = await makeBooking("CANCELLED");
|
|
|
|
|
const result = await cancelBooking(db, {
|
|
|
|
|
bookingId: hold.id,
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
});
|
|
|
|
|
expect(result).toEqual({ kind: "cancelled", alreadyCancelled: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("rejects cancelling a COMPLETED booking", async () => {
|
|
|
|
|
const hold = await makeBooking("COMPLETED");
|
|
|
|
|
const result = await cancelBooking(db, {
|
|
|
|
|
bookingId: hold.id,
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
});
|
|
|
|
|
expect(result).toEqual({ kind: "rejected", reason: "completed" });
|
|
|
|
|
const row = await db.booking.findUnique({ where: { id: hold.id } });
|
|
|
|
|
expect(row?.status).toBe("COMPLETED"); // unchanged
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("rejects cancelling a NO_SHOW booking", async () => {
|
|
|
|
|
const hold = await makeBooking("NO_SHOW");
|
|
|
|
|
const result = await cancelBooking(db, {
|
|
|
|
|
bookingId: hold.id,
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
});
|
|
|
|
|
expect(result).toEqual({ kind: "rejected", reason: "no_show" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("after cancelling, the slot becomes bookable again (DB exclusion no longer blocks)", async () => {
|
|
|
|
|
const hold = await makeBooking("CONFIRMED");
|
|
|
|
|
await cancelBooking(db, {
|
|
|
|
|
bookingId: hold.id,
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
});
|
|
|
|
|
// Same therapist + same time should now succeed
|
|
|
|
|
const newHold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
expect(newHold.id).toBeTruthy();
|
|
|
|
|
expect(newHold.id).not.toBe(hold.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("throws on unknown bookingId", async () => {
|
|
|
|
|
await expect(
|
|
|
|
|
cancelBooking(db, {
|
|
|
|
|
bookingId: "no-such-booking",
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toThrow(/not found/);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-01 18:59:19 -04:00
|
|
|
describe("expireStaleHolds", () => {
|
|
|
|
|
test("expires only HOLDs whose holdExpiresAt is past", async () => {
|
|
|
|
|
// Create one fresh hold (10 min default) and manually backdate another.
|
|
|
|
|
const fresh = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
const stale = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistB,
|
|
|
|
|
roomId: roomB,
|
|
|
|
|
startsAt: D("2026-05-05T16:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await db.booking.update({
|
|
|
|
|
where: { id: stale.id },
|
|
|
|
|
data: { holdExpiresAt: new Date(Date.now() - 60_000) },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const count = await expireStaleHolds(db);
|
|
|
|
|
expect(count).toBe(1);
|
|
|
|
|
|
|
|
|
|
const freshRow = await db.booking.findUnique({ where: { id: fresh.id } });
|
|
|
|
|
const staleRow = await db.booking.findUnique({ where: { id: stale.id } });
|
|
|
|
|
expect(freshRow?.status).toBe("HOLD");
|
|
|
|
|
expect(staleRow?.status).toBe("CANCELLED");
|
|
|
|
|
expect(staleRow?.cancelReason).toBe("hold expired");
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-05-02 14:05:30 -04:00
|
|
|
|
|
|
|
|
describe("rescheduleBooking", () => {
|
|
|
|
|
test("cancels old + creates new at the new time, atomically", async () => {
|
|
|
|
|
const original = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await confirmHold(db, original.id);
|
|
|
|
|
|
|
|
|
|
const result = await rescheduleBooking(db, {
|
|
|
|
|
oldBookingId: original.id,
|
|
|
|
|
newStartsAt: D("2026-05-06T14:00:00Z"),
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
});
|
|
|
|
|
expect(result.oldBookingId).toBe(original.id);
|
|
|
|
|
expect(result.newBookingId).not.toBe(original.id);
|
|
|
|
|
|
|
|
|
|
const oldRow = await db.booking.findUnique({ where: { id: original.id } });
|
|
|
|
|
const newRow = await db.booking.findUnique({ where: { id: result.newBookingId } });
|
|
|
|
|
expect(oldRow?.status).toBe("CANCELLED");
|
|
|
|
|
expect(oldRow?.cancelReason).toBe("rescheduled");
|
|
|
|
|
expect(newRow?.status).toBe("CONFIRMED");
|
|
|
|
|
expect(newRow?.startsAt).toEqual(D("2026-05-06T14:00:00Z"));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("rolls back if the new slot conflicts (old booking stays CONFIRMED)", async () => {
|
|
|
|
|
// Block the target slot with a CONFIRMED booking on therapistA at 11:00.
|
|
|
|
|
const blocker = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-06T15:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await confirmHold(db, blocker.id);
|
|
|
|
|
|
|
|
|
|
const original = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await confirmHold(db, original.id);
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
rescheduleBooking(db, {
|
|
|
|
|
oldBookingId: original.id,
|
|
|
|
|
newStartsAt: D("2026-05-06T15:00:00Z"), // collides with blocker
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toBeInstanceOf(BookingConflictError);
|
|
|
|
|
|
|
|
|
|
// Both bookings unchanged
|
|
|
|
|
const orig = await db.booking.findUnique({ where: { id: original.id } });
|
|
|
|
|
const blk = await db.booking.findUnique({ where: { id: blocker.id } });
|
|
|
|
|
expect(orig?.status).toBe("CONFIRMED");
|
|
|
|
|
expect(blk?.status).toBe("CONFIRMED");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("rejects rescheduling a CANCELLED booking", async () => {
|
|
|
|
|
const original = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await cancelBooking(db, {
|
|
|
|
|
bookingId: original.id,
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
});
|
|
|
|
|
await expect(
|
|
|
|
|
rescheduleBooking(db, {
|
|
|
|
|
oldBookingId: original.id,
|
|
|
|
|
newStartsAt: D("2026-05-06T14:00:00Z"),
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toThrow(/Cannot reschedule/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("can change therapist/room/service on reschedule", async () => {
|
|
|
|
|
const original = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await confirmHold(db, original.id);
|
|
|
|
|
|
|
|
|
|
const result = await rescheduleBooking(db, {
|
|
|
|
|
oldBookingId: original.id,
|
|
|
|
|
newStartsAt: D("2026-05-06T14:00:00Z"),
|
|
|
|
|
newTherapistId: therapistB,
|
|
|
|
|
newRoomId: roomB,
|
|
|
|
|
cancelledByUserId: customerId,
|
|
|
|
|
});
|
|
|
|
|
const newRow = await db.booking.findUnique({ where: { id: result.newBookingId } });
|
|
|
|
|
expect(newRow?.therapistId).toBe(therapistB);
|
|
|
|
|
expect(newRow?.roomId).toBe(roomB);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("markComplete / markNoShow", () => {
|
|
|
|
|
test("markComplete: CONFIRMED → COMPLETED", async () => {
|
|
|
|
|
const hold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await confirmHold(db, hold.id);
|
|
|
|
|
const result = await markComplete(db, hold.id);
|
|
|
|
|
expect(result.ok).toBe(true);
|
|
|
|
|
const row = await db.booking.findUnique({ where: { id: hold.id } });
|
|
|
|
|
expect(row?.status).toBe("COMPLETED");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("markComplete is idempotent", async () => {
|
|
|
|
|
const hold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await confirmHold(db, hold.id);
|
|
|
|
|
await markComplete(db, hold.id);
|
|
|
|
|
const second = await markComplete(db, hold.id);
|
|
|
|
|
expect(second.ok).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("markComplete rejects HOLD bookings", async () => {
|
|
|
|
|
const hold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
const result = await markComplete(db, hold.id);
|
|
|
|
|
expect(result.ok).toBe(false);
|
|
|
|
|
if (!result.ok) expect(result.reason).toMatch(/HOLD/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("markNoShow: CONFIRMED → NO_SHOW", async () => {
|
|
|
|
|
const hold = await createHold(db, {
|
|
|
|
|
customerId,
|
|
|
|
|
serviceId,
|
|
|
|
|
therapistId: therapistA,
|
|
|
|
|
roomId: roomA,
|
|
|
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
|
|
|
|
});
|
|
|
|
|
await confirmHold(db, hold.id);
|
|
|
|
|
const result = await markNoShow(db, hold.id);
|
|
|
|
|
expect(result.ok).toBe(true);
|
|
|
|
|
const row = await db.booking.findUnique({ where: { id: hold.id } });
|
|
|
|
|
expect(row?.status).toBe("NO_SHOW");
|
|
|
|
|
});
|
|
|
|
|
});
|