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:
co-authored by
CarinaWolli
Carina Wollendorfer
Udit Takkar
parent
6ea13b7c9a
commit
602fd6879f
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user