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 <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
Pritam
2024-03-20 19:07:48 +00:00
committed by GitHub
co-authored by CarinaWolli Carina Wollendorfer Udit Takkar
parent 6ea13b7c9a
commit 602fd6879f
10 changed files with 182 additions and 76 deletions
@@ -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;
@@ -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?: {
@@ -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(
@@ -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 || "",
@@ -24,7 +24,10 @@ type PartialBooking =
| "uid"
| "attendees"
| "userPrimaryEmail"
> & { eventType: Partial<EventType> | null } & { user: Partial<User> | null })
| "smsReminderNumber"
> & { eventType: (Partial<EventType> & { team: { parentId?: number } }) | null } & {
user: Partial<User> | null;
})
| null;
export type PartialWorkflowReminder = Pick<
@@ -104,6 +107,62 @@ export async function getAllRemindersToCancel(): Promise<RemindersToCancelType[]
return remindersToCancel;
}
export const select: Prisma.WorkflowReminderSelect = {
id: true,
scheduledDate: true,
isMandatoryReminder: true,
workflowStep: {
select: {
action: true,
sendTo: true,
reminderBody: true,
emailSubject: true,
template: true,
sender: true,
includeCalendarEvent: true,
},
},
booking: {
select: {
startTime: true,
endTime: true,
location: true,
description: true,
smsReminderNumber: true,
userPrimaryEmail: true,
user: {
select: {
email: true,
name: true,
timeZone: true,
locale: true,
username: true,
timeFormat: true,
hideBranding: true,
},
},
metadata: true,
uid: true,
customInputs: true,
responses: true,
attendees: true,
eventType: {
select: {
bookingFields: true,
title: true,
slug: true,
recurringEvent: true,
team: {
select: {
parentId: true,
},
},
},
},
},
},
};
export async function getAllUnscheduledReminders(): Promise<PartialWorkflowReminder[]> {
const whereFilter: Prisma.WorkflowReminderWhereInput = {
method: WorkflowMethods.EMAIL,
@@ -114,56 +173,6 @@ export async function getAllUnscheduledReminders(): Promise<PartialWorkflowRemin
OR: [{ cancelled: false }, { cancelled: null }],
};
const select: Prisma.WorkflowReminderSelect = {
id: true,
scheduledDate: true,
isMandatoryReminder: true,
workflowStep: {
select: {
action: true,
sendTo: true,
reminderBody: true,
emailSubject: true,
template: true,
sender: true,
includeCalendarEvent: true,
},
},
booking: {
select: {
startTime: true,
endTime: true,
location: true,
description: true,
userPrimaryEmail: true,
user: {
select: {
email: true,
name: true,
timeZone: true,
locale: true,
username: true,
timeFormat: true,
hideBranding: true,
},
},
metadata: true,
uid: true,
customInputs: true,
responses: true,
attendees: true,
eventType: {
select: {
bookingFields: true,
title: true,
slug: true,
recurringEvent: true,
},
},
},
},
};
const unscheduledReminders = (await getWorkflowReminders(whereFilter, select)) as PartialWorkflowReminder[];
return unscheduledReminders;
@@ -198,8 +198,8 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
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 =
@@ -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;
@@ -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;
+53 -1
View File
@@ -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(),
@@ -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,