import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { cleanupTestBusiness, createTestBusiness, createTestCustomer, createTestStaff, prisma, } from "@/services/scheduling/__tests__/fixtures"; import type { TestBusiness } from "@/services/scheduling/__tests__/fixtures"; import { AppointmentStatus, CalendarSyncStatus } from "@/generated/prisma/enums"; import { encryptToken } from "@/lib/security/token-encryption"; import { MockCalendarProvider } from "../mock-calendar.provider"; import { runInitialSync, syncAppointmentSafely } from "../calendar-sync.service"; /** * Exercises the real sync policy (create/update/cancel/duplicate * prevention/reassignment) against a real database, with a * `MockCalendarProvider` standing in for Google — see that file's own * header for why (the brief: "Do not call real Google Calendar APIs in * automated tests"). */ describe("calendar-sync.service (integration)", () => { let business: TestBusiness; let staffId: string; let customerId: string; let provider: MockCalendarProvider; beforeAll(() => { process.env.CALENDAR_TOKEN_ENCRYPTION_KEY = "test-key-not-for-production-use"; }); beforeEach(async () => { business = await createTestBusiness(); const staff = await createTestStaff(business); staffId = staff.staffId; const customer = await createTestCustomer(business); customerId = customer.customerId; provider = new MockCalendarProvider(); }); afterEach(async () => { await cleanupTestBusiness(business); }); async function connectCalendar(): Promise { const connection = await prisma.staffCalendarConnection.create({ data: { staffId, externalAccountId: "mock-account", accountEmail: "barber@example.com", encryptedAccessToken: encryptToken("mock-access-token"), encryptedRefreshToken: encryptToken("mock-refresh-token"), tokenExpiresAt: new Date(Date.now() + 3600_000), selectedCalendarId: "primary", selectedCalendarName: "Primary Calendar", }, }); return connection.id; } async function createAppointment(status: AppointmentStatus, overrides: { staffId?: string } = {}) { return prisma.appointment.create({ data: { businessId: business.businessId, staffId: overrides.staffId ?? staffId, customerId, status, startAt: new Date(Date.now() + 24 * 60 * 60 * 1000), endAt: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60_000), services: { create: [{ serviceId: business.haircutServiceId, priceAtBooking: 20, durationAtBookingMinutes: 30 }] }, }, }); } it("creates a remote event for a CONFIRMED appointment", async () => { await connectCalendar(); const appointment = await createAppointment(AppointmentStatus.CONFIRMED); await syncAppointmentSafely(prisma, appointment.id, provider); const event = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: appointment.id } }); expect(event?.status).toBe(CalendarSyncStatus.SYNCED); expect(provider.events.size).toBe(1); const [providerEvent] = [...provider.events.values()]; expect(providerEvent?.summary).toContain("Test Customer"); }); it("does not sync a PENDING appointment", async () => { await connectCalendar(); const appointment = await createAppointment(AppointmentStatus.PENDING); await syncAppointmentSafely(prisma, appointment.id, provider); const event = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: appointment.id } }); expect(event).toBeNull(); expect(provider.events.size).toBe(0); }); it("is idempotent — syncing an already-synced appointment updates the same remote event rather than creating a second one", async () => { await connectCalendar(); const appointment = await createAppointment(AppointmentStatus.CONFIRMED); await syncAppointmentSafely(prisma, appointment.id, provider); await syncAppointmentSafely(prisma, appointment.id, provider); const events = await prisma.appointmentCalendarEvent.findMany({ where: { appointmentId: appointment.id } }); expect(events).toHaveLength(1); expect(provider.events.size).toBe(1); }); it("deletes the remote event when the appointment is cancelled", async () => { await connectCalendar(); const appointment = await createAppointment(AppointmentStatus.CONFIRMED); await syncAppointmentSafely(prisma, appointment.id, provider); expect(provider.events.size).toBe(1); await prisma.appointment.update({ where: { id: appointment.id }, data: { status: AppointmentStatus.CANCELLED } }); await syncAppointmentSafely(prisma, appointment.id, provider); expect(provider.events.size).toBe(0); const event = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: appointment.id } }); expect(event?.status).toBe(CalendarSyncStatus.CANCELLED); }); it("cancels the previous staff member's event on reassignment and creates one under the new staff member's connection", async () => { await connectCalendar(); const otherStaff = await createTestStaff(business); const otherConnection = await prisma.staffCalendarConnection.create({ data: { staffId: otherStaff.staffId, externalAccountId: "mock-account-2", accountEmail: "other-barber@example.com", encryptedAccessToken: encryptToken("mock-access-token-2"), encryptedRefreshToken: encryptToken("mock-refresh-token-2"), tokenExpiresAt: new Date(Date.now() + 3600_000), selectedCalendarId: "primary", selectedCalendarName: "Primary Calendar", }, }); const appointment = await createAppointment(AppointmentStatus.CONFIRMED); await syncAppointmentSafely(prisma, appointment.id, provider); expect(provider.events.size).toBe(1); await prisma.appointment.update({ where: { id: appointment.id }, data: { staffId: otherStaff.staffId } }); await syncAppointmentSafely(prisma, appointment.id, provider); const originalConnectionEvent = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: appointment.id, connection: { staffId } }, }); expect(originalConnectionEvent?.status).toBe(CalendarSyncStatus.CANCELLED); const newConnectionEvent = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: appointment.id, connectionId: otherConnection.id }, }); expect(newConnectionEvent?.status).toBe(CalendarSyncStatus.SYNCED); await prisma.staffCalendarConnection.delete({ where: { id: otherConnection.id } }).catch(() => {}); }); it("runInitialSync syncs every future non-pending, non-cancelled appointment for a newly connected staff member", async () => { await connectCalendar(); const confirmed = await createAppointment(AppointmentStatus.CONFIRMED); const pending = await createAppointment(AppointmentStatus.PENDING); const cancelled = await createAppointment(AppointmentStatus.CANCELLED); const result = await runInitialSync(prisma, staffId, provider); expect(result.synced).toBe(1); expect(provider.events.size).toBe(1); const syncedEvent = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: confirmed.id } }); expect(syncedEvent).toBeTruthy(); const pendingEvent = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: pending.id } }); expect(pendingEvent).toBeNull(); const cancelledEvent = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: cancelled.id } }); expect(cancelledEvent).toBeNull(); }); it("does nothing when no calendar connection exists for the staff member", async () => { const appointment = await createAppointment(AppointmentStatus.CONFIRMED); await syncAppointmentSafely(prisma, appointment.id, provider); const event = await prisma.appointmentCalendarEvent.findFirst({ where: { appointmentId: appointment.id } }); expect(event).toBeNull(); expect(provider.events.size).toBe(0); }); });