From 602fd6879fef8e7f89b3f771daa63d118443eabe Mon Sep 17 00:00:00 2001 From: Pritam <113875582+Pritam-sd-dev@users.noreply.github.com> Date: Thu, 21 Mar 2024 00:37:48 +0530 Subject: [PATCH] fix: Broken reschedule link in email created by workflow for team event (#13893) * add reschedule link if event if type team * fix type errors * fix type errors * change team.name to team.slug * fix type_errors * fix type-errors * fix type-errors * fetch team from db * make rescheduleLink dynamic (temp_changes) * fix type-error * add org support * fix type errors * remove old org code * use /reschedule/uid instead * remove not needed changes * implement feedback * code clean up --------- Co-authored-by: CarinaWolli Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- .../bookings/lib/handleBookingRequested.ts | 3 + .../bookings/lib/handleConfirmation.ts | 3 + .../workflows/api/scheduleEmailReminders.ts | 17 ++- .../ee/workflows/api/scheduleSMSReminders.ts | 36 +++--- .../ee/workflows/lib/getWorkflowReminders.ts | 111 ++++++++++-------- .../lib/reminders/emailReminderManager.ts | 4 +- .../lib/reminders/smsReminderManager.ts | 5 +- .../lib/reminders/templates/customTemplate.ts | 6 +- packages/lib/payment/getBooking.ts | 54 ++++++++- .../viewer/bookings/confirm.handler.ts | 19 +++ 10 files changed, 182 insertions(+), 76 deletions(-) diff --git a/packages/features/bookings/lib/handleBookingRequested.ts b/packages/features/bookings/lib/handleBookingRequested.ts index 397e7ba2c0..579b2fd237 100644 --- a/packages/features/bookings/lib/handleBookingRequested.ts +++ b/packages/features/bookings/lib/handleBookingRequested.ts @@ -16,6 +16,9 @@ export async function handleBookingRequested(args: { evt: CalendarEvent; booking: { eventType: { + team?: { + parentId: number | null; + } | null; currency: string; description: string | null; id: number; diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 14bc68817d..748773613c 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -41,6 +41,9 @@ export async function handleConfirmation(args: { requiresConfirmation: boolean; metadata?: Prisma.JsonValue; title: string; + team?: { + parentId: number | null; + } | null; teamId?: number | null; parentId?: number | null; workflows?: { diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 7f862e14a1..25596d1bec 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -4,6 +4,7 @@ import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; +import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import logger from "@calcom/lib/logger"; import { defaultHandler } from "@calcom/lib/server"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; @@ -161,6 +162,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { booking: reminder.booking, }); + const organizerOrganizationProfile = await prisma.profile.findFirst({ + where: { + userId: reminder.booking.user?.id, + }, + }); + + const organizerOrganizationId = organizerOrganizationProfile?.organizationId; + + const bookerUrl = await getBookerBaseUrl( + reminder.booking.eventType?.team?.parentId ?? organizerOrganizationId ?? null + ); + const variables: VariablesType = { eventName: reminder.booking.eventType?.title || "", organizerName: reminder.booking.user?.name || "", @@ -173,8 +186,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { additionalNotes: reminder.booking.description, responses: responses, meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl, - cancelLink: `/booking/${reminder.booking.uid}?cancel=true`, - rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`, + cancelLink: `${bookerUrl}/booking/${reminder.booking.uid}?cancel=true`, + rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}`, }; const emailLocale = locale || "en"; const emailSubject = customTemplate( diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index ef10656b40..3faf361ce7 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; +import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import { defaultHandler } from "@calcom/lib/server"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; @@ -10,6 +11,8 @@ import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/pri import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { getSenderId } from "../lib/alphanumericSenderIdSupport"; +import type { PartialWorkflowReminder } from "../lib/getWorkflowReminders"; +import { select } from "../lib/getWorkflowReminders"; import * as twilio from "../lib/reminders/providers/twilioProvider"; import type { VariablesType } from "../lib/reminders/templates/customTemplate"; import customTemplate from "../lib/reminders/templates/customTemplate"; @@ -33,7 +36,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { }); //find all unscheduled SMS reminders - const unscheduledReminders = await prisma.workflowReminder.findMany({ + const unscheduledReminders = (await prisma.workflowReminder.findMany({ where: { method: WorkflowMethods.SMS, scheduled: false, @@ -41,17 +44,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { lte: dayjs().add(7, "day").toISOString(), }, }, - include: { - workflowStep: true, - booking: { - include: { - eventType: true, - user: true, - attendees: true, - }, - }, - }, - }); + select, + })) as PartialWorkflowReminder[]; if (!unscheduledReminders.length) { res.json({ ok: true }); @@ -91,7 +85,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { ? reminder.booking?.attendees[0].locale : reminder.booking?.user?.locale; - let message: string | null = reminder.workflowStep.reminderBody; + let message: string | null = reminder.workflowStep.reminderBody || null; if (reminder.workflowStep.reminderBody) { const { responses } = getCalEventResponses({ @@ -99,6 +93,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { booking: reminder.booking, }); + const organizerOrganizationProfile = await prisma.profile.findFirst({ + where: { + userId: reminder.booking.user?.id, + }, + }); + + const organizerOrganizationId = organizerOrganizationProfile?.organizationId; + + const bookerUrl = await getBookerBaseUrl( + reminder.booking.eventType?.team?.parentId ?? organizerOrganizationId ?? null + ); + const variables: VariablesType = { eventName: reminder.booking?.eventType?.title, organizerName: reminder.booking?.user?.name || "", @@ -111,8 +117,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { additionalNotes: reminder.booking?.description, responses: responses, meetingUrl: bookingMetadataSchema.parse(reminder.booking?.metadata || {})?.videoCallUrl, - cancelLink: `/booking/${reminder.booking.uid}?cancel=true`, - rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`, + cancelLink: `${bookerUrl}/booking/${reminder.booking.uid}?cancel=true`, + rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}`, }; const customMessage = customTemplate( reminder.workflowStep.reminderBody || "", diff --git a/packages/features/ee/workflows/lib/getWorkflowReminders.ts b/packages/features/ee/workflows/lib/getWorkflowReminders.ts index b459a89bb4..4c88206dbd 100644 --- a/packages/features/ee/workflows/lib/getWorkflowReminders.ts +++ b/packages/features/ee/workflows/lib/getWorkflowReminders.ts @@ -24,7 +24,10 @@ type PartialBooking = | "uid" | "attendees" | "userPrimaryEmail" - > & { eventType: Partial | null } & { user: Partial | null }) + | "smsReminderNumber" + > & { eventType: (Partial & { team: { parentId?: number } }) | null } & { + user: Partial | null; + }) | null; export type PartialWorkflowReminder = Pick< @@ -104,6 +107,62 @@ export async function getAllRemindersToCancel(): Promise { const whereFilter: Prisma.WorkflowReminderWhereInput = { method: WorkflowMethods.EMAIL, @@ -114,56 +173,6 @@ export async function getAllUnscheduledReminders(): Promise additionalNotes: evt.additionalNotes, responses: evt.responses, meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl, - cancelLink: `/booking/${evt.uid}?cancel=true`, - rescheduleLink: `/${evt.organizer.username}/${evt.eventType.slug}?rescheduleUid=${evt.uid}`, + cancelLink: `${evt.bookerUrl}/booking/${evt.uid}?cancel=true`, + rescheduleLink: `${evt.bookerUrl}/reschedule/${evt.uid}`, }; const locale = diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 176c8ca8fc..a555d74dff 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -34,6 +34,7 @@ export type AttendeeInBookingInfo = { export type BookingInfo = { uid?: string | null; + bookerUrl?: string; attendees: AttendeeInBookingInfo[]; organizer: { language: { locale: string }; @@ -153,8 +154,8 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => { additionalNotes: evt.additionalNotes, responses: evt.responses, meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl, - cancelLink: `/booking/${evt.uid}?cancel=true`, - rescheduleLink: `/${evt.organizer.username}/${evt.eventType.slug}?rescheduleUid=${evt.uid}`, + cancelLink: `${evt.bookerUrl}/booking/${evt.uid}?cancel=true`, + rescheduleLink: `${evt.bookerUrl}/reschedule/${evt.uid}`, }; const customMessage = customTemplate(smsMessage, variables, locale, evt.organizer.timeFormat); smsMessage = customMessage.text; diff --git a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts index bb36f646c5..86c5ddcb26 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts @@ -1,7 +1,7 @@ import { guessEventLocationType } from "@calcom/app-store/locations"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; -import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; +import { APP_NAME } from "@calcom/lib/constants"; import { TimeFormat } from "@calcom/lib/timeFormat"; import type { CalEventResponses } from "@calcom/types/Calendar"; @@ -43,8 +43,8 @@ const customTemplate = ( locationString = guessEventLocationType(locationString)?.label || locationString; } - const cancelLink = variables.cancelLink ? `${WEBAPP_URL}${variables.cancelLink}` : ""; - const rescheduleLink = variables.rescheduleLink ? `${WEBAPP_URL}${variables.rescheduleLink}` : ""; + const cancelLink = variables.cancelLink ?? ""; + const rescheduleLink = variables.rescheduleLink ?? ""; const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; diff --git a/packages/lib/payment/getBooking.ts b/packages/lib/payment/getBooking.ts index 62ecb36580..bbd4af216c 100644 --- a/packages/lib/payment/getBooking.ts +++ b/packages/lib/payment/getBooking.ts @@ -8,6 +8,8 @@ import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/crede import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { getBookerBaseUrl } from "../getBookerUrl/server"; + async function getEventType(id: number) { return prisma.eventType.findUnique({ where: { @@ -29,7 +31,44 @@ export async function getBooking(bookingId: number) { select: { ...bookingMinimalSelect, responses: true, - eventType: true, + eventType: { + select: { + currency: true, + description: true, + id: true, + length: true, + price: true, + requiresConfirmation: true, + metadata: true, + title: true, + teamId: true, + parentId: true, + slug: true, + workflows: { + select: { + workflow: { + select: { + id: true, + userId: true, + name: true, + steps: true, + position: true, + teamId: true, + time: true, + timeUnit: true, + trigger: true, + }, + }, + }, + }, + bookingFields: true, + team: { + select: { + parentId: true, + }, + }, + }, + }, metadata: true, smsReminderNumber: true, location: true, @@ -82,11 +121,24 @@ export async function getBooking(bookingId: number) { }; }); + const organizerOrganizationProfile = await prisma.profile.findFirst({ + where: { + userId: booking.userId ?? undefined, + }, + }); + + const organizerOrganizationId = organizerOrganizationProfile?.organizationId; + + const bookerUrl = await getBookerBaseUrl( + booking.eventType?.team?.parentId ?? organizerOrganizationId ?? null + ); + const attendeesList = await Promise.all(attendeesListPromises); const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar; const evt: CalendarEvent = { type: booking?.eventType?.slug as string, title: booking.title, + bookerUrl, description: booking.description || undefined, startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index dbf0f449a8..5d632951c4 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -7,6 +7,7 @@ import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirma import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; +import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { getTranslation } from "@calcom/lib/server"; import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; @@ -66,6 +67,11 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { bookingFields: true, disableGuests: true, metadata: true, + team: { + select: { + parentId: true, + }, + }, workflows: { include: { workflow: { @@ -154,12 +160,25 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { }; }); + const organizerOrganizationProfile = await prisma.profile.findFirst({ + where: { + userId: user.id, + }, + }); + + const organizerOrganizationId = organizerOrganizationProfile?.organizationId; + + const bookerUrl = await getBookerBaseUrl( + booking.eventType?.team?.parentId ?? organizerOrganizationId ?? null + ); + const attendeesList = await Promise.all(attendeesListPromises); const evt: CalendarEvent = { type: booking?.eventType?.slug as string, title: booking.title, description: booking.description, + bookerUrl, // TODO: Remove the usage of `bookingFields` in computing responses. We can do that by storing `label` with the response. Also, this would allow us to correctly show the label for a field even after the Event Type has been deleted. ...getCalEventResponses({ bookingFields: booking.eventType?.bookingFields ?? null,