import { createBookingScenario, getDate, getGoogleCalendarCredential, TestData, getOrganizer, getScenarioData, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; import { expectWebhookToHaveBeenCalledWith } from "@calcom/web/test/utils/bookingScenario/expects"; import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; import { describe, vi, test } from "vitest"; import { appStoreMetadata } from "@calcom/app-store/apps.metadata.generated"; import dayjs from "@calcom/dayjs"; import { TimeUnit } from "@calcom/prisma/enums"; import { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; import { calculateMaxStartTime } from "./common"; import { getMeetingSessionsFromRoomName } from "./getMeetingSessionsFromRoomName"; import type { TSendNoShowWebhookPayloadSchema } from "./schema"; import { triggerHostNoShow } from "./triggerHostNoShow"; vi.mock("@calcom/features/tasker/tasks/triggerNoShow/getMeetingSessionsFromRoomName", () => ({ getMeetingSessionsFromRoomName: vi.fn(), })); const timeout = process.env.CI ? 5000 : 20000; const EMPTY_MEETING_SESSIONS = { total_count: 0, data: [], }; describe("Trigger Host No Show:", () => { setupAndTeardown(); test( `Should trigger host no show webhook when no one joined the call`, async () => { const organizer = getOrganizer({ name: "Organizer", email: "organizer@example.com", id: 101, schedules: [TestData.schedules.IstWorkHours], credentials: [getGoogleCalendarCredential()], selectedCalendars: [TestData.selectedCalendars.google], }); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const uidOfBooking = "n5Wv3eHgconAED2j4gcVhP"; const iCalUID = `${uidOfBooking}@Cal.com`; const subscriberUrl = "http://my-webhook.example.com"; const bookingStartTime = `${plus1DateString}T05:00:00.000Z`; await createBookingScenario( getScenarioData({ webhooks: [ { id: "22", userId: organizer.id, eventTriggers: [WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW], subscriberUrl, active: true, eventTypeId: 1, appId: null, time: 5, timeUnit: TimeUnit.MINUTE, }, ], eventTypes: [ { id: 1, slotInterval: 15, length: 15, users: [ { id: 101, }, ], }, ], bookings: [ { id: 222, uid: uidOfBooking, eventTypeId: 1, status: BookingStatus.ACCEPTED, startTime: bookingStartTime, endTime: `${plus1DateString}T05:15:00.000Z`, user: { id: organizer.id }, metadata: { videoCallUrl: "https://existing-daily-video-call-url.example.com", }, references: [ { type: appStoreMetadata.dailyvideo.type, uid: "MOCK_ID", meetingId: "MOCK_ID", meetingPassword: "MOCK_PASS", meetingUrl: "http://mock-dailyvideo.example.com", credentialId: null, }, { type: appStoreMetadata.googlecalendar.type, uid: "MOCK_ID", meetingId: "MOCK_ID", meetingPassword: "MOCK_PASSWORD", meetingUrl: "https://UNUSED_URL", externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", credentialId: undefined, }, ], iCalUID, }, ], organizer, apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], }) ); vi.mocked(getMeetingSessionsFromRoomName).mockResolvedValue(EMPTY_MEETING_SESSIONS); const TEST_WEBHOOK = { id: "22", eventTriggers: [WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW], subscriberUrl, active: true, eventTypeId: 1, appId: null, time: 5, timeUnit: TimeUnit.MINUTE, payloadTemplate: null, secret: null, }; const payload = JSON.stringify({ triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, bookingId: 222, webhook: TEST_WEBHOOK, } satisfies TSendNoShowWebhookPayloadSchema); await triggerHostNoShow(payload); const maxStartTime = calculateMaxStartTime(bookingStartTime as unknown as Date, 5, TimeUnit.MINUTE); const maxStartTimeHumanReadable = dayjs.unix(maxStartTime).format("YYYY-MM-DD HH:mm:ss Z"); await expectWebhookToHaveBeenCalledWith(subscriberUrl, { triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, payload: { title: "Test Booking Title", attendees: [], bookingId: 222, bookingUid: uidOfBooking, participants: [], hostEmail: "organizer@example.com", startTime: `${plus1DateString}T05:00:00.000Z`, endTime: `${plus1DateString}T05:15:00.000Z`, eventType: { id: 1, teamId: null, parentId: null, }, webhook: { ...TEST_WEBHOOK, secret: undefined, active: undefined, eventTypeId: undefined, }, message: `Host with email ${organizer.email} didn't join the call or didn't join before ${maxStartTimeHumanReadable}`, }, }); }, timeout ); test( `Should trigger host no show webhook when host didn't joined the call`, async () => { const organizer = getOrganizer({ name: "Organizer", email: "organizer@example.com", id: 101, schedules: [TestData.schedules.IstWorkHours], credentials: [getGoogleCalendarCredential()], selectedCalendars: [TestData.selectedCalendars.google], }); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const uidOfBooking = "n5Wv3eHgconAED2j4gcVhP"; const iCalUID = `${uidOfBooking}@Cal.com`; const subscriberUrl = "http://my-webhook.example.com"; const bookingStartTime = `${plus1DateString}T05:00:00.000Z`; await createBookingScenario( getScenarioData({ webhooks: [ { id: "22", userId: organizer.id, eventTriggers: [WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW], subscriberUrl, active: true, eventTypeId: 1, appId: null, time: 5, timeUnit: TimeUnit.MINUTE, }, ], eventTypes: [ { id: 1, slotInterval: 15, length: 15, users: [ { id: 101, }, ], }, ], bookings: [ { id: 222, uid: uidOfBooking, eventTypeId: 1, status: BookingStatus.ACCEPTED, startTime: bookingStartTime, endTime: `${plus1DateString}T05:15:00.000Z`, user: { id: organizer.id }, metadata: { videoCallUrl: "https://existing-daily-video-call-url.example.com", }, references: [ { type: appStoreMetadata.dailyvideo.type, uid: "MOCK_ID", meetingId: "MOCK_ID", meetingPassword: "MOCK_PASS", meetingUrl: "http://mock-dailyvideo.example.com", credentialId: null, }, { type: appStoreMetadata.googlecalendar.type, uid: "MOCK_ID", meetingId: "MOCK_ID", meetingPassword: "MOCK_PASSWORD", meetingUrl: "https://UNUSED_URL", externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", credentialId: undefined, }, ], iCalUID, }, ], organizer, apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], }) ); const MOCKED_PARTICIPANTS = [ { user_id: null, participant_id: "MOCK_PARTICIPANT_ID", user_name: "MOCK_USER_NAME", join_time: 0, duration: 15, }, ]; const MOCKED_MEETING_SESSIONS = { total_count: 1, data: [ { id: "MOCK_ID", room: "MOCK_ROOM", start_time: 1234567890, duration: 15, max_participants: 1, // User with id 101 is not in the participants list participants: MOCKED_PARTICIPANTS, }, ], }; vi.mocked(getMeetingSessionsFromRoomName).mockResolvedValue(MOCKED_MEETING_SESSIONS); const TEST_WEBHOOK = { id: "22", eventTriggers: [WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW], subscriberUrl, active: true, eventTypeId: 1, appId: null, time: 5, timeUnit: TimeUnit.MINUTE, payloadTemplate: null, secret: null, }; const payload = JSON.stringify({ triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, bookingId: 222, webhook: TEST_WEBHOOK, } satisfies TSendNoShowWebhookPayloadSchema); await triggerHostNoShow(payload); const maxStartTime = calculateMaxStartTime(bookingStartTime as unknown as Date, 5, TimeUnit.MINUTE); const maxStartTimeHumanReadable = dayjs.unix(maxStartTime).format("YYYY-MM-DD HH:mm:ss Z"); await expectWebhookToHaveBeenCalledWith(subscriberUrl, { triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, payload: { title: "Test Booking Title", attendees: [], bookingId: 222, bookingUid: uidOfBooking, participants: MOCKED_PARTICIPANTS, hostEmail: "organizer@example.com", startTime: `${plus1DateString}T05:00:00.000Z`, endTime: `${plus1DateString}T05:15:00.000Z`, eventType: { id: 1, teamId: null, parentId: null, }, webhook: { ...TEST_WEBHOOK, secret: undefined, active: undefined, eventTypeId: undefined, }, message: `Host with email ${organizer.email} didn't join the call or didn't join before ${maxStartTimeHumanReadable}`, }, }); }, timeout ); test( `Should trigger host no show webhook when host didn't joined the rescheduled call`, async () => { const organizer = getOrganizer({ name: "Organizer", email: "organizer@example.com", id: 101, schedules: [TestData.schedules.IstWorkHours], credentials: [getGoogleCalendarCredential()], selectedCalendars: [TestData.selectedCalendars.google], }); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const uidOfBooking = "j5Wv3eHgconAED2j4gcVhP"; const iCalUID = `${uidOfBooking}@Cal.com`; const subscriberUrl = "http://my-webhook.example.com"; const bookingStartTime = `${plus1DateString}T05:00:00.000Z`; const newUidOfBooking = "k5Wv3eHgconAED2j4gcVhP"; const newiCalUID = `${newUidOfBooking}@Cal.com`; const newBookingStartTime = `${plus1DateString}T05:15:00.000Z`; await createBookingScenario( getScenarioData({ webhooks: [ { id: "23", userId: organizer.id, eventTriggers: [WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW], subscriberUrl, active: true, eventTypeId: 1, appId: null, time: 5, timeUnit: TimeUnit.MINUTE, }, ], eventTypes: [ { id: 1, slotInterval: 15, length: 15, users: [ { id: 101, }, ], }, ], bookings: [ { id: 223, uid: uidOfBooking, eventTypeId: 1, status: BookingStatus.CANCELLED, rescheduled: true, rescheduledBy: organizer.email, startTime: bookingStartTime, endTime: `${plus1DateString}T05:15:00.000Z`, user: { id: organizer.id }, metadata: { videoCallUrl: "https://existing-daily-video-call-url.example.com", }, references: [ { type: appStoreMetadata.dailyvideo.type, uid: "MOCK_ID", meetingId: "MOCK_ID", meetingPassword: "MOCK_PASS", meetingUrl: "http://mock-dailyvideo.example.com", credentialId: null, }, { type: appStoreMetadata.googlecalendar.type, uid: "MOCK_ID", meetingId: "MOCK_ID", meetingPassword: "MOCK_PASSWORD", meetingUrl: "https://UNUSED_URL", externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", credentialId: undefined, }, ], iCalUID, }, { id: 224, uid: newUidOfBooking, eventTypeId: 1, status: BookingStatus.ACCEPTED, startTime: newBookingStartTime, endTime: `${plus1DateString}T05:30:00.000Z`, user: { id: organizer.id }, fromReschedule: uidOfBooking, metadata: { videoCallUrl: "https://existing-daily-video-call-url.example.com", }, references: [ { type: appStoreMetadata.dailyvideo.type, uid: "MOCK_ID", meetingId: "MOCK_ID", meetingPassword: "MOCK_PASS", meetingUrl: "http://mock-dailyvideo.example.com", credentialId: null, }, { type: appStoreMetadata.googlecalendar.type, uid: "MOCK_ID", meetingId: "MOCK_ID", meetingPassword: "MOCK_PASSWORD", meetingUrl: "https://UNUSED_URL", externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", credentialId: undefined, }, ], iCalUID: newiCalUID, }, ], organizer, apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], }) ); const MOCKED_PARTICIPANTS = [ { user_id: null, participant_id: "MOCK_PARTICIPANT_ID", user_name: "MOCK_USER_NAME", join_time: 0, duration: 15, }, ]; const MOCKED_MEETING_SESSIONS = { total_count: 1, data: [ { id: "MOCK_ID", room: "MOCK_ROOM", start_time: 1234567890, duration: 15, max_participants: 1, // User with id 101 is not in the participants list participants: MOCKED_PARTICIPANTS, }, ], }; vi.mocked(getMeetingSessionsFromRoomName).mockResolvedValue(MOCKED_MEETING_SESSIONS); const TEST_WEBHOOK = { id: "23", eventTriggers: [WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW], subscriberUrl, active: true, eventTypeId: 1, appId: null, time: 5, timeUnit: TimeUnit.MINUTE, payloadTemplate: null, secret: null, }; const payload = JSON.stringify({ triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, bookingId: 224, webhook: TEST_WEBHOOK, } satisfies TSendNoShowWebhookPayloadSchema); await triggerHostNoShow(payload); const maxStartTime = calculateMaxStartTime(newBookingStartTime as unknown as Date, 5, TimeUnit.MINUTE); const maxStartTimeHumanReadable = dayjs.unix(maxStartTime).format("YYYY-MM-DD HH:mm:ss Z"); await expectWebhookToHaveBeenCalledWith(subscriberUrl, { triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, payload: { title: "Test Booking Title", attendees: [], bookingId: 224, bookingUid: newUidOfBooking, participants: MOCKED_PARTICIPANTS, hostEmail: "organizer@example.com", startTime: `${plus1DateString}T05:15:00.000Z`, endTime: `${plus1DateString}T05:30:00.000Z`, rescheduledBy: organizer.email, eventType: { id: 1, teamId: null, parentId: null, }, webhook: { ...TEST_WEBHOOK, secret: undefined, active: undefined, eventTypeId: undefined, }, message: `Host with email ${organizer.email} didn't join the call or didn't join before ${maxStartTimeHumanReadable}`, }, }); }, timeout ); });