Files
touchbase/test/reminders.test.ts
2026-05-03 09:43:10 -04:00

131 lines
4.5 KiB
TypeScript

// Tests for the reminder *handler* — the pg-boss producer side requires a
// running boss instance and is exercised via the worker smoke test, not here.
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "@/generated/prisma/client";
import { confirmHold, createHold } from "@/lib/booking";
import { handleReminderJob } from "@/lib/reminders";
import { resetEmailTransport } from "@/lib/email";
import { seed, type SeedResult } from "@/lib/seed";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const db = new PrismaClient({ adapter });
const MAILPIT = "http://localhost:8025";
let fx: SeedResult;
let serviceId: string;
let therapistA: string;
let roomA: 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;
roomA = fx.rooms[0].id;
customerId = fx.customers[0].id;
process.env.SMTP_HOST = "localhost";
process.env.SMTP_PORT = "1025";
process.env.SMTP_FROM = "TouchBase <noreply@touchbase.local>";
resetEmailTransport();
});
afterAll(async () => {
await db.$disconnect();
});
beforeEach(async () => {
await db.notification.deleteMany();
await db.booking.deleteMany();
await fetch(`${MAILPIT}/api/v1/messages`, { method: "DELETE" });
});
const D = (iso: string) => new Date(iso);
async function makeConfirmedBooking(startsAt: Date) {
const hold = await createHold(db, {
customerId,
serviceId,
therapistId: therapistA,
roomId: roomA,
startsAt,
});
await confirmHold(db, hold.id);
return hold;
}
describe("handleReminderJob", () => {
test("sends and writes a Notification when CONFIRMED + future + not-yet-reminded", async () => {
const future = new Date(Date.now() + 24 * 3600 * 1000);
const hold = await makeConfirmedBooking(future);
const result = await handleReminderJob(db, { bookingId: hold.id });
expect(result.sent).toBe(true);
const noti = await db.notification.findFirst({
where: { bookingId: hold.id, template: "booking_reminder" },
});
expect(noti?.status).toBe("sent");
expect(noti?.userId).toBe(customerId);
const res = await fetch(`${MAILPIT}/api/v1/messages`);
const json = (await res.json()) as { messages: { Subject: string }[] };
expect(json.messages).toHaveLength(1);
expect(json.messages[0].Subject).toMatch(/Reminder:/);
});
test("dedupes — second call after a sent reminder is a no-op", async () => {
const future = new Date(Date.now() + 24 * 3600 * 1000);
const hold = await makeConfirmedBooking(future);
await handleReminderJob(db, { bookingId: hold.id });
const second = await handleReminderJob(db, { bookingId: hold.id });
expect(second.sent).toBe(false);
expect(second.reason).toBe("already_sent");
const count = await db.notification.count({
where: { bookingId: hold.id, template: "booking_reminder" },
});
expect(count).toBe(1);
});
test("skips CANCELLED bookings", async () => {
const future = new Date(Date.now() + 24 * 3600 * 1000);
const hold = await makeConfirmedBooking(future);
await db.booking.update({
where: { id: hold.id },
data: { status: "CANCELLED", cancelledAt: new Date() },
});
const result = await handleReminderJob(db, { bookingId: hold.id });
expect(result.sent).toBe(false);
expect(result.reason).toBe("status:CANCELLED");
});
test("skips bookings whose startsAt is already past", async () => {
const past = D("2026-04-01T14:00:00Z");
const hold = await makeConfirmedBooking(past);
const result = await handleReminderJob(db, { bookingId: hold.id });
expect(result.sent).toBe(false);
expect(result.reason).toBe("past");
});
test("returns not_found for unknown bookingId", async () => {
const result = await handleReminderJob(db, { bookingId: "no-such" });
expect(result.sent).toBe(false);
expect(result.reason).toBe("not_found");
});
test("skips HOLD bookings (only CONFIRMED reminds)", async () => {
const future = new Date(Date.now() + 24 * 3600 * 1000);
const hold = await createHold(db, {
customerId,
serviceId,
therapistId: therapistA,
roomId: roomA,
startsAt: future,
});
const result = await handleReminderJob(db, { bookingId: hold.id });
expect(result.sent).toBe(false);
expect(result.reason).toBe("status:HOLD");
});
});