From 04b49acbb02e1cebfa5dc423ac9c7f7181a48355 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Thu, 21 Nov 2024 22:21:19 +0530 Subject: [PATCH] chore: add rescheduled by property (#17631) * refactor: add rescheduledBy * chore * fix: bug * test: add test for rescheduled booking * fix: test --- .../tasker/tasks/triggerNoShow/common.ts | 27 +++ .../tasker/tasks/triggerNoShow/getBooking.ts | 1 + .../tasks/triggerNoShow/triggerGuestNoShow.ts | 6 +- .../triggerNoShow/triggerHostNoShow.test.ts | 204 ++++++++++++++++++ .../tasks/triggerNoShow/triggerHostNoShow.ts | 3 +- 5 files changed, 238 insertions(+), 3 deletions(-) diff --git a/packages/features/tasker/tasks/triggerNoShow/common.ts b/packages/features/tasker/tasks/triggerNoShow/common.ts index a9d3518517..8fab17c7d3 100644 --- a/packages/features/tasker/tasks/triggerNoShow/common.ts +++ b/packages/features/tasker/tasks/triggerNoShow/common.ts @@ -2,6 +2,7 @@ import dayjs from "@calcom/dayjs"; import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; +import prisma from "@calcom/prisma"; import type { TimeUnit } from "@calcom/prisma/enums"; import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums"; @@ -10,6 +11,13 @@ import { getMeetingSessionsFromRoomName } from "./getMeetingSessionsFromRoomName import type { TWebhook, TTriggerNoShowPayloadSchema } from "./schema"; import { ZSendNoShowWebhookPayloadSchema } from "./schema"; +type OriginalRescheduledBooking = + | { + rescheduledBy?: string | null; + } + | null + | undefined; + export type Host = { id: number; email: string; @@ -50,6 +58,7 @@ export function sendWebhookPayload( triggerEvent: WebhookTriggerEvents, booking: Booking, maxStartTime: number, + originalRescheduledBooking?: OriginalRescheduledBooking, hostEmail?: string ): Promise { const maxStartTimeHumanReadable = dayjs.unix(maxStartTime).format("YYYY-MM-DD HH:mm:ss Z"); @@ -67,6 +76,7 @@ export function sendWebhookPayload( attendees: booking.attendees, endTime: booking.endTime, ...(!!hostEmail ? { hostEmail } : {}), + ...(originalRescheduledBooking ? { rescheduledBy: originalRescheduledBooking.rescheduledBy } : {}), eventType: { ...booking.eventType, id: booking.eventTypeId, @@ -114,10 +124,26 @@ export const prepareNoShowTrigger = async ( hostsThatJoinedTheCall: Host[]; numberOfHostsThatJoined: number; didGuestJoinTheCall: boolean; + originalRescheduledBooking?: OriginalRescheduledBooking; } | void> => { const { bookingId, webhook } = ZSendNoShowWebhookPayloadSchema.parse(JSON.parse(payload)); const booking = await getBooking(bookingId); + let originalRescheduledBooking = null; + + if (booking.fromReschedule) { + originalRescheduledBooking = await prisma.booking.findFirst({ + where: { + uid: booking.fromReschedule, + status: { + in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED, BookingStatus.PENDING], + }, + }, + select: { + rescheduledBy: true, + }, + }); + } if (booking.status !== BookingStatus.ACCEPTED) { log.debug( @@ -173,5 +199,6 @@ export const prepareNoShowTrigger = async ( numberOfHostsThatJoined, webhook, didGuestJoinTheCall, + originalRescheduledBooking, }; }; diff --git a/packages/features/tasker/tasks/triggerNoShow/getBooking.ts b/packages/features/tasker/tasks/triggerNoShow/getBooking.ts index b5889f992b..fcef2cdc68 100644 --- a/packages/features/tasker/tasks/triggerNoShow/getBooking.ts +++ b/packages/features/tasker/tasks/triggerNoShow/getBooking.ts @@ -18,6 +18,7 @@ export const getBooking = async (bookingId: number) => { isRecorded: true, eventTypeId: true, references: true, + fromReschedule: true, eventType: { select: { id: true, diff --git a/packages/features/tasker/tasks/triggerNoShow/triggerGuestNoShow.ts b/packages/features/tasker/tasks/triggerNoShow/triggerGuestNoShow.ts index 826af8206c..ff8aa3a2cc 100644 --- a/packages/features/tasker/tasks/triggerNoShow/triggerGuestNoShow.ts +++ b/packages/features/tasker/tasks/triggerNoShow/triggerGuestNoShow.ts @@ -30,7 +30,8 @@ export async function triggerGuestNoShow(payload: string): Promise { const result = await prepareNoShowTrigger(payload); if (!result) return; - const { webhook, booking, hostsThatJoinedTheCall, didGuestJoinTheCall } = result; + const { webhook, booking, hostsThatJoinedTheCall, didGuestJoinTheCall, originalRescheduledBooking } = + result; const maxStartTime = calculateMaxStartTime(booking.startTime, webhook.time, webhook.timeUnit); @@ -39,7 +40,8 @@ export async function triggerGuestNoShow(payload: string): Promise { webhook, WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, booking, - maxStartTime + maxStartTime, + originalRescheduledBooking ); await markAllGuestNoshowInBooking({ bookingId: booking.id, hostsThatJoinedTheCall }); diff --git a/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.test.ts b/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.test.ts index a4c7c15dc0..9e17c17c14 100644 --- a/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.test.ts +++ b/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.test.ts @@ -7,6 +7,7 @@ import { 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"; @@ -40,6 +41,8 @@ const EMPTY_MEETING_SESSIONS = { }; describe("Trigger Host No Show:", () => { + setupAndTeardown(); + test( `Should trigger host no show webhook when no one joined the call`, async () => { @@ -337,4 +340,205 @@ describe("Trigger Host No Show:", () => { }, 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_MEETING_SESSIONS = { + total_count: 1, + data: [ + { + id: "MOCK_ID", + room: "MOCK_ROOM", + start_time: "MOCK_START_TIME", + duration: 15, + max_participants: 1, + // User with id 101 is not in the participants list + participants: [ + { + user_id: null, + participant_id: "MOCK_PARTICIPANT_ID", + user_name: "MOCK_USER_NAME", + join_time: 0, + duration: 15, + }, + ], + }, + ], + }; + + 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, + 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 + ); }); diff --git a/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.ts b/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.ts index dee91eb04b..156cab9027 100644 --- a/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.ts +++ b/packages/features/tasker/tasks/triggerNoShow/triggerHostNoShow.ts @@ -37,7 +37,7 @@ export async function triggerHostNoShow(payload: string): Promise { const result = await prepareNoShowTrigger(payload); if (!result) return; - const { booking, webhook, hostsThatDidntJoinTheCall } = result; + const { booking, webhook, hostsThatDidntJoinTheCall, originalRescheduledBooking } = result; const maxStartTime = calculateMaxStartTime(booking.startTime, webhook.time, webhook.timeUnit); @@ -47,6 +47,7 @@ export async function triggerHostNoShow(payload: string): Promise { WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, booking, maxStartTime, + originalRescheduledBooking, host.email ); });