Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccc814eb54 | ||
|
|
2ee0ec11d6 | ||
|
|
3223beb9ac | ||
|
|
c1bcfcfa3d | ||
|
|
83aea7d28c |
@@ -13,16 +13,18 @@ const schemaMembershipRequiredParams = z.object({
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export const membershipCreateBodySchema = Membership.partial({
|
||||
accepted: true,
|
||||
role: true,
|
||||
disableImpersonation: true,
|
||||
}).transform((v) => ({
|
||||
accepted: false,
|
||||
role: MembershipRole.MEMBER,
|
||||
disableImpersonation: false,
|
||||
...v,
|
||||
}));
|
||||
export const membershipCreateBodySchema = Membership.omit({ id: true })
|
||||
.partial({
|
||||
accepted: true,
|
||||
role: true,
|
||||
disableImpersonation: true,
|
||||
})
|
||||
.transform((v) => ({
|
||||
accepted: false,
|
||||
role: MembershipRole.MEMBER,
|
||||
disableImpersonation: false,
|
||||
...v,
|
||||
}));
|
||||
|
||||
export const membershipEditBodySchema = Membership.omit({
|
||||
/** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.2.5",
|
||||
"version": "3.2.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -34,13 +34,17 @@ const triggerWebhook = async ({
|
||||
booking: {
|
||||
userId: number | undefined;
|
||||
eventTypeId: number | null;
|
||||
eventTypeParentId: number | null | undefined;
|
||||
teamId?: number | null;
|
||||
};
|
||||
}) => {
|
||||
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
|
||||
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
||||
|
||||
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
|
||||
|
||||
const subscriberOptions = {
|
||||
userId: booking.userId,
|
||||
userId: triggerForUser ? booking.userId : null,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
triggerEvent: eventTrigger,
|
||||
teamId: booking.teamId,
|
||||
@@ -183,6 +187,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
booking: {
|
||||
userId: booking?.user?.id,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
eventTypeParentId: booking.eventType?.parentId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -158,9 +158,10 @@ test.describe("pro user", () => {
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
additionalGuests.forEach(async (email) => {
|
||||
const promises = additionalGuests.map(async (email) => {
|
||||
await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
test("Time slots should be reserved when selected", async ({ context, page }) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
.trim();
|
||||
return (
|
||||
<AppCard
|
||||
returnTo={WEBAPP_URL + pathname}
|
||||
returnTo={WEBAPP_URL + pathname + "?tabName=apps"}
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
@@ -45,7 +45,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
switchChecked={requirePayment}
|
||||
switchOnClick={(enabled) => {
|
||||
setRequirePayment(enabled);
|
||||
}}>
|
||||
}}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<>
|
||||
{recurringEventDefined && (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
|
||||
@@ -121,18 +121,21 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
|
||||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
|
||||
(ref) => !!credentialsMap.get(ref.type)
|
||||
);
|
||||
|
||||
const promises = bookingRefsFiltered.map(async (bookingRef) => {
|
||||
if (!bookingRef.uid) return;
|
||||
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
});
|
||||
try {
|
||||
bookingRefsFiltered.forEach(async (bookingRef) => {
|
||||
if (bookingRef.uid) {
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
// FIXME: error logging - non-Error type errors are currently discarded
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
|
||||
@@ -121,17 +121,19 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
|
||||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
|
||||
(ref) => !!credentialsMap.get(ref.type)
|
||||
);
|
||||
|
||||
const promises = bookingRefsFiltered.map(async (bookingRef) => {
|
||||
if (!bookingRef.uid) return;
|
||||
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
});
|
||||
try {
|
||||
bookingRefsFiltered.forEach(async (bookingRef) => {
|
||||
if (bookingRef.uid) {
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
|
||||
@@ -22,8 +22,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
try {
|
||||
const where: Prisma.BookingWhereInput = {};
|
||||
if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
|
||||
else where.userId = validKey.userId;
|
||||
if (validKey.teamId) {
|
||||
where.eventType = {
|
||||
OR: [{ teamId: validKey.teamId }, { parent: { teamId: validKey.teamId } }],
|
||||
};
|
||||
} else {
|
||||
where.userId = validKey.userId;
|
||||
}
|
||||
|
||||
const bookings = await prisma.booking.findMany({
|
||||
take: 3,
|
||||
where,
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function scheduleTrigger(
|
||||
) {
|
||||
try {
|
||||
//schedule job to call subscriber url at the end of meeting
|
||||
// FIXME: in-process scheduling - job will vanish on server crash / restart
|
||||
const job = schedule.scheduleJob(
|
||||
`${subscriber.appId}_${subscriber.id}`,
|
||||
booking.endTime,
|
||||
@@ -57,38 +58,39 @@ export async function cancelScheduledJobs(
|
||||
appId?: string | null,
|
||||
isReschedule?: boolean
|
||||
) {
|
||||
try {
|
||||
let scheduledJobs = booking.scheduledJobs || [];
|
||||
if (!booking.scheduledJobs) return;
|
||||
|
||||
if (booking.scheduledJobs) {
|
||||
booking.scheduledJobs.forEach(async (scheduledJob) => {
|
||||
if (appId) {
|
||||
if (scheduledJob.startsWith(appId)) {
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
|
||||
}
|
||||
} else {
|
||||
//if no specific appId given, delete all scheduled jobs of booking
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = [];
|
||||
let scheduledJobs = booking.scheduledJobs || [];
|
||||
const promises = booking.scheduledJobs.map(async (scheduledJob) => {
|
||||
if (appId) {
|
||||
if (scheduledJob.startsWith(appId)) {
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
|
||||
}
|
||||
} else {
|
||||
//if no specific appId given, delete all scheduled jobs of booking
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = [];
|
||||
}
|
||||
|
||||
if (!isReschedule) {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: booking.uid,
|
||||
},
|
||||
data: {
|
||||
scheduledJobs: scheduledJobs,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!isReschedule) {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: booking.uid,
|
||||
},
|
||||
data: {
|
||||
scheduledJobs: scheduledJobs,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.error("Error cancelling scheduled jobs", error);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const libraries = [
|
||||
},
|
||||
];
|
||||
|
||||
libraries.forEach(async (lib) => {
|
||||
const promises = libraries.map(async (lib) => {
|
||||
await build({
|
||||
build: {
|
||||
outDir: `./dist/${lib.fileName}`,
|
||||
@@ -29,3 +29,4 @@ libraries.forEach(async (lib) => {
|
||||
},
|
||||
});
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
@@ -157,8 +157,10 @@ async function handler(req: CustomRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
const triggerForUser = !teamId || (teamId && bookingToDelete.eventType?.parentId);
|
||||
|
||||
const subscriberOptions = {
|
||||
userId: bookingToDelete.userId,
|
||||
userId: triggerForUser ? bookingToDelete.userId : null,
|
||||
eventTypeId: bookingToDelete.eventTypeId as number,
|
||||
triggerEvent: eventTrigger,
|
||||
teamId,
|
||||
@@ -428,9 +430,9 @@ async function handler(req: CustomRequest) {
|
||||
bookingToDelete.recurringEventId &&
|
||||
allRemainingBookings
|
||||
) {
|
||||
bookingToDelete.user.credentials
|
||||
const promises = bookingToDelete.user.credentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.forEach(async (credential) => {
|
||||
.map(async (credential) => {
|
||||
const calendar = await getCalendar(credential);
|
||||
for (const updBooking of updatedBookings) {
|
||||
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
|
||||
@@ -441,6 +443,13 @@ async function handler(req: CustomRequest) {
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
|
||||
}
|
||||
@@ -601,11 +610,13 @@ async function handler(req: CustomRequest) {
|
||||
});
|
||||
|
||||
// delete scheduled jobs of cancelled bookings
|
||||
// FIXME: async calls into ether
|
||||
updatedBookings.forEach((booking) => {
|
||||
cancelScheduledJobs(booking);
|
||||
});
|
||||
|
||||
//Workflows - cancel all reminders for cancelled bookings
|
||||
// FIXME: async calls into ether
|
||||
updatedBookings.forEach((booking) => {
|
||||
booking.workflowReminders.forEach((reminder) => {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
@@ -620,11 +631,14 @@ async function handler(req: CustomRequest) {
|
||||
|
||||
const prismaPromises: Promise<unknown>[] = [bookingReferenceDeletes];
|
||||
|
||||
// @TODO: find a way in the future if a promise fails don't stop the rest of the promises
|
||||
// Also if emails fails try to requeue them
|
||||
try {
|
||||
await Promise.all(prismaPromises.concat(apiDeletes));
|
||||
const settled = await Promise.allSettled(prismaPromises.concat(apiDeletes));
|
||||
const rejected = settled.filter(({ status }) => status === "rejected") as PromiseRejectedResult[];
|
||||
if (rejected.length) {
|
||||
throw new Error(`Reasons: ${rejected.map(({ reason }) => reason)}`);
|
||||
}
|
||||
|
||||
// TODO: if emails fail try to requeue them
|
||||
await sendCancelledEmails(evt, { eventName: bookingToDelete?.eventType?.eventName });
|
||||
} catch (error) {
|
||||
console.error("Error deleting event", error);
|
||||
|
||||
@@ -293,14 +293,16 @@ export async function handleConfirmation(args: {
|
||||
},
|
||||
});
|
||||
|
||||
const triggerForUser = !teamId || (teamId && booking.eventType?.parentId);
|
||||
|
||||
const subscribersBookingCreated = await getWebhooks({
|
||||
userId: booking.userId,
|
||||
userId: triggerForUser ? booking.userId : null,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
||||
teamId,
|
||||
});
|
||||
const subscribersMeetingEnded = await getWebhooks({
|
||||
userId: booking.userId,
|
||||
userId: triggerForUser ? booking.userId : null,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
teamId: booking.eventType?.teamId,
|
||||
|
||||
@@ -1126,8 +1126,10 @@ async function handler(
|
||||
|
||||
const teamId = await getTeamIdFromEventType({ eventType });
|
||||
|
||||
const triggerForUser = !teamId || (teamId && eventType.parentId);
|
||||
|
||||
const subscriberOptions: GetSubscriberOptions = {
|
||||
userId: organizerUser.id,
|
||||
userId: triggerForUser ? organizerUser.id : null,
|
||||
eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
||||
teamId,
|
||||
@@ -1140,7 +1142,7 @@ async function handler(
|
||||
subscriberOptions.triggerEvent = eventTrigger;
|
||||
|
||||
const subscriberOptionsMeetingEnded = {
|
||||
userId: organizerUser.id,
|
||||
userId: triggerForUser ? organizerUser.id : null,
|
||||
eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
teamId,
|
||||
|
||||
@@ -10,16 +10,15 @@ export type GetSubscriberOptions = {
|
||||
};
|
||||
|
||||
const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient = defaultPrisma) => {
|
||||
const userId = options.teamId ? 0 : options.userId ?? 0;
|
||||
const userId = options.userId ?? 0;
|
||||
const eventTypeId = options.eventTypeId ?? 0;
|
||||
const teamId = options.teamId ?? 0;
|
||||
|
||||
// if we have userId and teamId it is a managed event type and should trigger for team and user
|
||||
const allWebhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
eventTypeId,
|
||||
|
||||
@@ -30,7 +30,9 @@ export type WebhookDataType = CalendarEvent &
|
||||
downloadLink?: string;
|
||||
};
|
||||
|
||||
function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: string }): string {
|
||||
function getZapierPayload(
|
||||
data: CalendarEvent & EventTypeInfo & { status?: string; createdAt: string }
|
||||
): string {
|
||||
const attendees = data.attendees.map((attendee) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
@@ -69,6 +71,7 @@ function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: strin
|
||||
length: data.length,
|
||||
},
|
||||
attendees: attendees,
|
||||
createdAt: data.createdAt,
|
||||
};
|
||||
return JSON.stringify(body);
|
||||
}
|
||||
@@ -112,7 +115,7 @@ const sendPayload = async (
|
||||
|
||||
/* Zapier id is hardcoded in the DB, we send the raw data for this case */
|
||||
if (appId === "zapier") {
|
||||
body = getZapierPayload(data);
|
||||
body = getZapierPayload({ ...data, createdAt });
|
||||
} else if (template) {
|
||||
body = applyTemplate(template, { ...data, triggerEvent, createdAt }, contentType);
|
||||
} else {
|
||||
|
||||
@@ -102,168 +102,172 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" });
|
||||
}
|
||||
|
||||
if (bookingToReschedule) {
|
||||
let event: Partial<EventType> = {};
|
||||
if (bookingToReschedule.eventTypeId) {
|
||||
event = await prisma.eventType.findFirstOrThrow({
|
||||
select: {
|
||||
title: true,
|
||||
users: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingToReschedule.eventTypeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.booking.update({
|
||||
if (!bookingToReschedule) return;
|
||||
|
||||
let event: Partial<EventType> = {};
|
||||
if (bookingToReschedule.eventTypeId) {
|
||||
event = await prisma.eventType.findFirstOrThrow({
|
||||
select: {
|
||||
title: true,
|
||||
users: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingToReschedule.id,
|
||||
},
|
||||
data: {
|
||||
rescheduled: true,
|
||||
cancellationReason,
|
||||
status: BookingStatus.CANCELLED,
|
||||
updatedAt: dayjs().toISOString(),
|
||||
id: bookingToReschedule.eventTypeId,
|
||||
},
|
||||
});
|
||||
|
||||
// delete scheduled jobs of previous booking
|
||||
cancelScheduledJobs(bookingToReschedule);
|
||||
|
||||
//cancel workflow reminders of previous booking
|
||||
bookingToReschedule.workflowReminders.forEach((reminder) => {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
||||
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
|
||||
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
|
||||
}
|
||||
});
|
||||
|
||||
const [mainAttendee] = bookingToReschedule.attendees;
|
||||
// @NOTE: Should we assume attendees language?
|
||||
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
|
||||
const usersToPeopleType = (
|
||||
users: PersonAttendeeCommonFields[],
|
||||
selectedLanguage: TFunction
|
||||
): Person[] => {
|
||||
return users?.map((user) => {
|
||||
return {
|
||||
email: user.email || "",
|
||||
name: user.name || "",
|
||||
username: user?.username || "",
|
||||
language: { translate: selectedLanguage, locale: user.locale || "en" },
|
||||
timeZone: user?.timeZone,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const userTranslation = await getTranslation(user.locale ?? "en", "common");
|
||||
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
||||
|
||||
const builder = new CalendarEventBuilder();
|
||||
builder.init({
|
||||
title: bookingToReschedule.title,
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
startTime: bookingToReschedule.startTime.toISOString(),
|
||||
endTime: bookingToReschedule.endTime.toISOString(),
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
organizer: userAsPeopleType,
|
||||
});
|
||||
|
||||
const director = new CalendarEventDirector();
|
||||
director.setBuilder(builder);
|
||||
director.setExistingBooking(bookingToReschedule);
|
||||
cancellationReason && director.setCancellationReason(cancellationReason);
|
||||
if (event) {
|
||||
await director.buildForRescheduleEmail();
|
||||
} else {
|
||||
await director.buildWithoutEventTypeForRescheduleEmail();
|
||||
}
|
||||
|
||||
// Handling calendar and videos cancellation
|
||||
// This can set previous time as available, until virtual calendar is done
|
||||
const credentials = await getUsersCredentials(user.id);
|
||||
const credentialsMap = new Map();
|
||||
credentials.forEach((credential) => {
|
||||
credentialsMap.set(credential.type, credential);
|
||||
});
|
||||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter((ref) =>
|
||||
credentialsMap.has(ref.type)
|
||||
);
|
||||
bookingRefsFiltered.forEach(async (bookingRef) => {
|
||||
if (bookingRef.uid) {
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent, bookingRef.externalCalendarId);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Send emails
|
||||
await sendRequestRescheduleEmail(builder.calendarEvent, {
|
||||
rescheduleLink: builder.rescheduleLink,
|
||||
});
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
title: bookingToReschedule?.title,
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
description: bookingToReschedule?.description || "",
|
||||
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
|
||||
...getCalEventResponses({
|
||||
booking: bookingToReschedule,
|
||||
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
|
||||
}),
|
||||
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
||||
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
||||
organizer: userAsPeopleType,
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
uid: bookingToReschedule?.uid,
|
||||
location: bookingToReschedule?.location,
|
||||
destinationCalendar:
|
||||
bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
||||
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
||||
};
|
||||
|
||||
// Send webhook
|
||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
||||
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: bookingToReschedule.eventType?.teamId ?? null },
|
||||
parentId: bookingToReschedule?.eventType?.parentId ?? null,
|
||||
},
|
||||
});
|
||||
// Send Webhook call if hooked to BOOKING.CANCELLED
|
||||
const subscriberOptions = {
|
||||
userId: bookingToReschedule.userId,
|
||||
eventTypeId: bookingToReschedule.eventTypeId as number,
|
||||
triggerEvent: eventTrigger,
|
||||
teamId,
|
||||
};
|
||||
const webhooks = await getWebhooks(subscriberOptions);
|
||||
const promises = webhooks.map((webhook) =>
|
||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
||||
...evt,
|
||||
smsReminderNumber: bookingToReschedule.smsReminderNumber || undefined,
|
||||
}).catch((e) => {
|
||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: bookingToReschedule.id,
|
||||
},
|
||||
data: {
|
||||
rescheduled: true,
|
||||
cancellationReason,
|
||||
status: BookingStatus.CANCELLED,
|
||||
updatedAt: dayjs().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
// delete scheduled jobs of previous booking
|
||||
// FIXME: async fn off into the ether
|
||||
cancelScheduledJobs(bookingToReschedule);
|
||||
|
||||
//cancel workflow reminders of previous booking
|
||||
// FIXME: more async fns off into the ether
|
||||
bookingToReschedule.workflowReminders.forEach((reminder) => {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
||||
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
|
||||
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
|
||||
}
|
||||
});
|
||||
|
||||
const [mainAttendee] = bookingToReschedule.attendees;
|
||||
// @NOTE: Should we assume attendees language?
|
||||
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
|
||||
const usersToPeopleType = (users: PersonAttendeeCommonFields[], selectedLanguage: TFunction): Person[] => {
|
||||
return users?.map((user) => {
|
||||
return {
|
||||
email: user.email || "",
|
||||
name: user.name || "",
|
||||
username: user?.username || "",
|
||||
language: { translate: selectedLanguage, locale: user.locale || "en" },
|
||||
timeZone: user?.timeZone,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const userTranslation = await getTranslation(user.locale ?? "en", "common");
|
||||
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
||||
|
||||
const builder = new CalendarEventBuilder();
|
||||
builder.init({
|
||||
title: bookingToReschedule.title,
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
startTime: bookingToReschedule.startTime.toISOString(),
|
||||
endTime: bookingToReschedule.endTime.toISOString(),
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
organizer: userAsPeopleType,
|
||||
});
|
||||
|
||||
const director = new CalendarEventDirector();
|
||||
director.setBuilder(builder);
|
||||
director.setExistingBooking(bookingToReschedule);
|
||||
cancellationReason && director.setCancellationReason(cancellationReason);
|
||||
if (event) {
|
||||
await director.buildForRescheduleEmail();
|
||||
} else {
|
||||
await director.buildWithoutEventTypeForRescheduleEmail();
|
||||
}
|
||||
|
||||
// Handling calendar and videos cancellation
|
||||
// This can set previous time as available, until virtual calendar is done
|
||||
const credentials = await getUsersCredentials(user.id);
|
||||
const credentialsMap = new Map();
|
||||
credentials.forEach((credential) => {
|
||||
credentialsMap.set(credential.type, credential);
|
||||
});
|
||||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter((ref) =>
|
||||
credentialsMap.has(ref.type)
|
||||
);
|
||||
|
||||
// FIXME: error-handling
|
||||
await Promise.allSettled(
|
||||
bookingRefsFiltered.map(async (bookingRef) => {
|
||||
if (!bookingRef.uid) return;
|
||||
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent, bookingRef.externalCalendarId);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Send emails
|
||||
await sendRequestRescheduleEmail(builder.calendarEvent, {
|
||||
rescheduleLink: builder.rescheduleLink,
|
||||
});
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
title: bookingToReschedule?.title,
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
description: bookingToReschedule?.description || "",
|
||||
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
|
||||
...getCalEventResponses({
|
||||
booking: bookingToReschedule,
|
||||
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
|
||||
}),
|
||||
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
||||
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
||||
organizer: userAsPeopleType,
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
uid: bookingToReschedule?.uid,
|
||||
location: bookingToReschedule?.location,
|
||||
destinationCalendar: bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
||||
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
||||
};
|
||||
|
||||
// Send webhook
|
||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
||||
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: bookingToReschedule.eventType?.teamId ?? null },
|
||||
parentId: bookingToReschedule?.eventType?.parentId ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const triggerForUser = !teamId || (teamId && bookingToReschedule.eventType?.parentId);
|
||||
|
||||
// Send Webhook call if hooked to BOOKING.CANCELLED
|
||||
const subscriberOptions = {
|
||||
userId: triggerForUser ? bookingToReschedule.userId : null,
|
||||
eventTypeId: bookingToReschedule.eventTypeId as number,
|
||||
triggerEvent: eventTrigger,
|
||||
teamId,
|
||||
};
|
||||
const webhooks = await getWebhooks(subscriberOptions);
|
||||
const promises = webhooks.map((webhook) =>
|
||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
||||
...evt,
|
||||
smsReminderNumber: bookingToReschedule.smsReminderNumber || undefined,
|
||||
}).catch((e) => {
|
||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
@@ -264,13 +264,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
steps.forEach(async (step) => {
|
||||
const promiseSteps = steps.map(async (step) => {
|
||||
if (
|
||||
step.action !== WorkflowActions.SMS_ATTENDEE &&
|
||||
step.action !== WorkflowActions.WHATSAPP_ATTENDEE
|
||||
) {
|
||||
//as we do not have attendees phone number (user is notified about that when setting this action)
|
||||
bookingsForReminders.forEach(async (booking) => {
|
||||
const promiseScheduleReminders = bookingsForReminders.map(async (booking) => {
|
||||
const defaultLocale = "en";
|
||||
const bookingInfo = {
|
||||
uid: booking.uid,
|
||||
@@ -367,29 +367,28 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
);
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseScheduleReminders);
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseSteps);
|
||||
}
|
||||
//create all workflow - eventtypes relationships
|
||||
activeOnEventTypes.forEach(async (eventType) => {
|
||||
await ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: {
|
||||
workflowId: id,
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (eventType.children.length) {
|
||||
eventType.children.forEach(async (chEventType) => {
|
||||
await ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: {
|
||||
workflowId: id,
|
||||
eventTypeId: chEventType.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
await ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: activeOnEventTypes.map((eventType) => ({
|
||||
workflowId: id,
|
||||
eventTypeId: eventType.id,
|
||||
})),
|
||||
});
|
||||
await Promise.all(
|
||||
activeOnEventTypes.map((eventType) =>
|
||||
ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: eventType.children.map((chEventType) => ({
|
||||
workflowId: id,
|
||||
eventTypeId: chEventType.id,
|
||||
})),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
userWorkflow.steps.map(async (oldStep) => {
|
||||
@@ -465,11 +464,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
});
|
||||
|
||||
//cancel all workflow reminders from steps that were edited
|
||||
remindersToUpdate.forEach(async (reminder) => {
|
||||
// FIXME: async calls into ether
|
||||
remindersToUpdate.forEach((reminder) => {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
||||
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
|
||||
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
|
||||
}
|
||||
});
|
||||
const eventTypesToUpdateReminders = activeOn.filter((eventTypeId) => {
|
||||
@@ -497,7 +499,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
bookingsOfEventTypes.forEach(async (booking) => {
|
||||
const promiseScheduleReminders = bookingsOfEventTypes.map(async (booking) => {
|
||||
const defaultLocale = "en";
|
||||
const bookingInfo = {
|
||||
uid: booking.uid,
|
||||
@@ -594,6 +596,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
);
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseScheduleReminders);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -617,7 +620,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
return activeEventType;
|
||||
}
|
||||
});
|
||||
addedSteps.forEach(async (step) => {
|
||||
const promiseAddedSteps = addedSteps.map(async (step) => {
|
||||
if (step) {
|
||||
const { senderName, ...newStep } = step;
|
||||
newStep.sender = getSender({
|
||||
@@ -749,6 +752,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseAddedSteps);
|
||||
}
|
||||
|
||||
//update trigger, name, time, timeUnit
|
||||
|
||||
Reference in New Issue
Block a user