customer account/bookings + cancellation email
This commit is contained in:
@@ -3,6 +3,7 @@ import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
import {
|
||||
BookingConflictError,
|
||||
cancelBooking,
|
||||
confirmHold,
|
||||
createHold,
|
||||
expireStaleHolds,
|
||||
@@ -174,6 +175,108 @@ describe("confirmHold", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expireStaleHolds", () => {
|
||||
test("expires only HOLDs whose holdExpiresAt is past", async () => {
|
||||
// Create one fresh hold (10 min default) and manually backdate another.
|
||||
|
||||
Reference in New Issue
Block a user