fix: `videoCallUrl` not updating when rescheduling with a broken Calendar integration (#11923)
This commit is contained in:
parent
7a014761dc
commit
d12a5c5883
|
@ -166,6 +166,7 @@
|
|||
"env-cmd": "^10.1.0",
|
||||
"module-alias": "^2.2.2",
|
||||
"msw": "^0.42.3",
|
||||
"node-html-parser": "^6.1.10",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
|
|
|
@ -2,7 +2,7 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store";
|
|||
import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n";
|
||||
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { BookingReference, Attendee } from "@prisma/client";
|
||||
import type { BookingReference, Attendee, Booking } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { WebhookTriggerEvents } from "@prisma/client";
|
||||
import type Stripe from "stripe";
|
||||
|
@ -102,7 +102,7 @@ export type InputEventType = {
|
|||
schedule?: InputUser["schedules"][number];
|
||||
} & Partial<Omit<Prisma.EventTypeCreateInput, "users" | "schedule">>;
|
||||
|
||||
type InputBooking = {
|
||||
type WhiteListedBookingProps = {
|
||||
id?: number;
|
||||
uid?: string;
|
||||
userId?: number;
|
||||
|
@ -118,6 +118,8 @@ type InputBooking = {
|
|||
})[];
|
||||
};
|
||||
|
||||
type InputBooking = Partial<Omit<Booking, keyof WhiteListedBookingProps>> & WhiteListedBookingProps;
|
||||
|
||||
export const Timezones = {
|
||||
"+5:30": "Asia/Kolkata",
|
||||
"+6:00": "Asia/Dhaka",
|
||||
|
@ -1203,3 +1205,35 @@ export const enum BookingLocations {
|
|||
CalVideo = "integrations:daily",
|
||||
ZoomVideo = "integrations:zoom",
|
||||
}
|
||||
|
||||
const getMockAppStatus = ({
|
||||
slug,
|
||||
failures,
|
||||
success,
|
||||
}: {
|
||||
slug: string;
|
||||
failures: number;
|
||||
success: number;
|
||||
}) => {
|
||||
const foundEntry = Object.entries(appStoreMetadata).find(([, app]) => {
|
||||
return app.slug === slug;
|
||||
});
|
||||
if (!foundEntry) {
|
||||
throw new Error("App not found for the slug");
|
||||
}
|
||||
const foundApp = foundEntry[1];
|
||||
return {
|
||||
appName: foundApp.slug,
|
||||
type: foundApp.type,
|
||||
failures,
|
||||
success,
|
||||
errors: [],
|
||||
};
|
||||
};
|
||||
export const getMockFailingAppStatus = ({ slug }: { slug: string }) => {
|
||||
return getMockAppStatus({ slug, failures: 1, success: 0 });
|
||||
};
|
||||
|
||||
export const getMockPassingAppStatus = ({ slug }: { slug: string }) => {
|
||||
return getMockAppStatus({ slug, failures: 0, success: 1 });
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalendar } from "@prisma/client";
|
||||
import { parse } from "node-html-parser";
|
||||
import ical from "node-ical";
|
||||
import { expect } from "vitest";
|
||||
import "vitest-fetch-mock";
|
||||
|
@ -8,6 +9,7 @@ import "vitest-fetch-mock";
|
|||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { AppsStatus } from "@calcom/types/Calendar";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
|
@ -19,14 +21,14 @@ declare global {
|
|||
interface Matchers<R> {
|
||||
toHaveEmail(
|
||||
expectedEmail: {
|
||||
//TODO: Support email HTML parsing to target specific elements
|
||||
htmlToContain?: string;
|
||||
title?: string;
|
||||
to: string;
|
||||
noIcs?: true;
|
||||
ics?: {
|
||||
filename: string;
|
||||
iCalUID: string;
|
||||
};
|
||||
appsStatus?: AppsStatus[];
|
||||
},
|
||||
to: string
|
||||
): R;
|
||||
|
@ -38,21 +40,23 @@ expect.extend({
|
|||
toHaveEmail(
|
||||
emails: Fixtures["emails"],
|
||||
expectedEmail: {
|
||||
//TODO: Support email HTML parsing to target specific elements
|
||||
htmlToContain?: string;
|
||||
title?: string;
|
||||
to: string;
|
||||
ics: {
|
||||
filename: string;
|
||||
iCalUID: string;
|
||||
};
|
||||
noIcs: true;
|
||||
appsStatus: AppsStatus[];
|
||||
},
|
||||
to: string
|
||||
) {
|
||||
const { isNot } = this;
|
||||
const testEmail = emails.get().find((email) => email.to.includes(to));
|
||||
const emailsToLog = emails
|
||||
.get()
|
||||
.map((email) => ({ to: email.to, html: email.html, ics: email.icalEvent }));
|
||||
|
||||
if (!testEmail) {
|
||||
logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog }));
|
||||
return {
|
||||
|
@ -63,48 +67,93 @@ expect.extend({
|
|||
const ics = testEmail.icalEvent;
|
||||
const icsObject = ics?.content ? ical.sync.parseICS(ics?.content) : null;
|
||||
|
||||
let isHtmlContained = true;
|
||||
let isToAddressExpected = true;
|
||||
const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true;
|
||||
const isIcsUIDExpected = expectedEmail.ics
|
||||
? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null)
|
||||
: true;
|
||||
const emailDom = parse(testEmail.html);
|
||||
|
||||
if (expectedEmail.htmlToContain) {
|
||||
isHtmlContained = testEmail.html.includes(expectedEmail.htmlToContain);
|
||||
const actualEmailContent = {
|
||||
title: emailDom.querySelector("title")?.innerText,
|
||||
subject: emailDom.querySelector("subject")?.innerText,
|
||||
};
|
||||
|
||||
const expectedEmailContent = {
|
||||
title: expectedEmail.title,
|
||||
};
|
||||
|
||||
const isEmailContentMatched = this.equals(
|
||||
actualEmailContent,
|
||||
expect.objectContaining(expectedEmailContent)
|
||||
);
|
||||
|
||||
if (!isEmailContentMatched) {
|
||||
logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog }));
|
||||
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Email content ${isNot ? "is" : "is not"} matching`,
|
||||
actual: actualEmailContent,
|
||||
expected: expectedEmailContent,
|
||||
};
|
||||
}
|
||||
|
||||
isToAddressExpected = expectedEmail.to === testEmail.to;
|
||||
|
||||
if (!isHtmlContained || !isToAddressExpected) {
|
||||
if (!isToAddressExpected) {
|
||||
logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog }));
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `To address ${isNot ? "is" : "is not"} matching`,
|
||||
actual: testEmail.to,
|
||||
expected: expectedEmail.to,
|
||||
};
|
||||
}
|
||||
|
||||
if (!expectedEmail.noIcs && !isIcsFilenameExpected) {
|
||||
return {
|
||||
pass: false,
|
||||
actual: ics?.filename,
|
||||
expected: expectedEmail.ics.filename,
|
||||
message: () => `ICS Filename ${isNot ? "is" : "is not"} matching`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!expectedEmail.noIcs && !isIcsUIDExpected) {
|
||||
return {
|
||||
pass: false,
|
||||
actual: JSON.stringify(icsObject),
|
||||
expected: expectedEmail.ics.iCalUID,
|
||||
message: () => `Expected ICS UID ${isNot ? "is" : "isn't"} present in actual`,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectedEmail.appsStatus) {
|
||||
const actualAppsStatus = emailDom.querySelectorAll('[data-testid="appsStatus"] li').map((li) => {
|
||||
return li.innerText.trim();
|
||||
});
|
||||
const expectedAppStatus = expectedEmail.appsStatus.map((appStatus) => {
|
||||
if (appStatus.success && !appStatus.failures) {
|
||||
return `${appStatus.appName} ✅`;
|
||||
}
|
||||
return `${appStatus.appName} ❌`;
|
||||
});
|
||||
|
||||
const isAppsStatusCorrect = this.equals(actualAppsStatus, expectedAppStatus);
|
||||
|
||||
if (!isAppsStatusCorrect) {
|
||||
return {
|
||||
pass: false,
|
||||
actual: actualAppsStatus,
|
||||
expected: expectedAppStatus,
|
||||
message: () => `AppsStatus ${isNot ? "is" : "isn't"} matching`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pass:
|
||||
isHtmlContained &&
|
||||
isToAddressExpected &&
|
||||
(expectedEmail.noIcs ? true : isIcsFilenameExpected && isIcsUIDExpected),
|
||||
message: () => {
|
||||
if (!isHtmlContained) {
|
||||
return `Email HTML is not as expected. Expected:"${expectedEmail.htmlToContain}" isn't contained in "${testEmail.html}"`;
|
||||
}
|
||||
|
||||
if (!isToAddressExpected) {
|
||||
return `Email To address is not as expected. Expected:${expectedEmail.to} isn't equal to ${testEmail.to}`;
|
||||
}
|
||||
|
||||
if (!isIcsFilenameExpected) {
|
||||
return `ICS Filename is not as expected. Expected:${expectedEmail.ics.filename} isn't equal to ${ics?.filename}`;
|
||||
}
|
||||
|
||||
if (!isIcsUIDExpected) {
|
||||
return `ICS UID is not as expected. Expected:${
|
||||
expectedEmail.ics.iCalUID
|
||||
} isn't present in ${JSON.stringify(icsObject)}`;
|
||||
}
|
||||
throw new Error("Unknown error");
|
||||
},
|
||||
pass: true,
|
||||
message: () => `Email ${isNot ? "is" : "isn't"} correct`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -139,9 +188,10 @@ export function expectWebhookToHaveBeenCalledWith(
|
|||
const parsedBody = JSON.parse((body as string) || "{}");
|
||||
|
||||
expect(parsedBody.triggerEvent).toBe(data.triggerEvent);
|
||||
|
||||
if (parsedBody.payload.metadata?.videoCallUrl) {
|
||||
parsedBody.payload.metadata.videoCallUrl = parsedBody.payload.metadata.videoCallUrl
|
||||
? parsedBody.payload.metadata.videoCallUrl.replace(/\/video\/[a-zA-Z0-9]{22}/, "/video/DYNAMIC_UID")
|
||||
? parsedBody.payload.metadata.videoCallUrl
|
||||
: parsedBody.payload.metadata.videoCallUrl;
|
||||
}
|
||||
if (data.payload) {
|
||||
|
@ -195,7 +245,7 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
title: "confirmed_event_type_subject",
|
||||
to: `${organizer.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -207,7 +257,7 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
title: "confirmed_event_type_subject",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -221,7 +271,7 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
otherTeamMembers.forEach((otherTeamMember) => {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
title: "confirmed_event_type_subject",
|
||||
// Don't know why but organizer and team members of the eventType don'thave their name here like Booker
|
||||
to: `${otherTeamMember.email}`,
|
||||
ics: {
|
||||
|
@ -238,7 +288,7 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
guests.forEach((guest) => {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
title: "confirmed_event_type_subject",
|
||||
to: `${guest.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -261,7 +311,7 @@ export function expectBrokenIntegrationEmails({
|
|||
// Broken Integration email is only sent to the Organizer
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>broken_integration</title>",
|
||||
title: "broken_integration",
|
||||
to: `${organizer.email}`,
|
||||
// No ics goes in case of broken integration email it seems
|
||||
// ics: {
|
||||
|
@ -274,7 +324,7 @@ export function expectBrokenIntegrationEmails({
|
|||
|
||||
// expect(emails).toHaveEmail(
|
||||
// {
|
||||
// htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
// title: "confirmed_event_type_subject",
|
||||
// to: `${booker.name} <${booker.email}>`,
|
||||
// },
|
||||
// `${booker.name} <${booker.email}>`
|
||||
|
@ -294,7 +344,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>broken_integration</title>",
|
||||
title: "broken_integration",
|
||||
to: `${organizer.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -306,7 +356,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>calendar_event_creation_failure_subject</title>",
|
||||
title: "calendar_event_creation_failure_subject",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -322,27 +372,30 @@ export function expectSuccessfulBookingRescheduledEmails({
|
|||
organizer,
|
||||
booker,
|
||||
iCalUID,
|
||||
appsStatus,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string };
|
||||
booker: { email: string; name: string };
|
||||
iCalUID: string;
|
||||
appsStatus: AppsStatus[];
|
||||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>event_type_has_been_rescheduled_on_time_date</title>",
|
||||
title: "event_type_has_been_rescheduled_on_time_date",
|
||||
to: `${organizer.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID,
|
||||
},
|
||||
appsStatus,
|
||||
},
|
||||
`${organizer.email}`
|
||||
);
|
||||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>event_type_has_been_rescheduled_on_time_date</title>",
|
||||
title: "event_type_has_been_rescheduled_on_time_date",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
|
@ -362,7 +415,7 @@ export function expectAwaitingPaymentEmails({
|
|||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>awaiting_payment_subject</title>",
|
||||
title: "awaiting_payment_subject",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
noIcs: true,
|
||||
},
|
||||
|
@ -381,7 +434,7 @@ export function expectBookingRequestedEmails({
|
|||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>event_awaiting_approval_subject</title>",
|
||||
title: "event_awaiting_approval_subject",
|
||||
to: `${organizer.email}`,
|
||||
noIcs: true,
|
||||
},
|
||||
|
@ -390,7 +443,7 @@ export function expectBookingRequestedEmails({
|
|||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>booking_submitted_subject</title>",
|
||||
title: "booking_submitted_subject",
|
||||
to: `${booker.email}`,
|
||||
noIcs: true,
|
||||
},
|
||||
|
@ -509,6 +562,7 @@ export function expectBookingRescheduledWebhookToHaveBeenFired({
|
|||
location,
|
||||
subscriberUrl,
|
||||
videoCallUrl,
|
||||
payload,
|
||||
}: {
|
||||
organizer: { email: string; name: string };
|
||||
booker: { email: string; name: string };
|
||||
|
@ -516,10 +570,12 @@ export function expectBookingRescheduledWebhookToHaveBeenFired({
|
|||
location: string;
|
||||
paidEvent?: boolean;
|
||||
videoCallUrl?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
}) {
|
||||
expectWebhookToHaveBeenCalledWith(subscriberUrl, {
|
||||
triggerEvent: "BOOKING_RESCHEDULED",
|
||||
payload: {
|
||||
...payload,
|
||||
metadata: {
|
||||
...(videoCallUrl ? { videoCallUrl } : null),
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TFunction } from "next-i18next";
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
|
@ -11,7 +11,7 @@ export const AppsStatus = (props: { calEvent: CalendarEvent; t: TFunction }) =>
|
|||
<Info
|
||||
label={t("apps_status")}
|
||||
description={
|
||||
<ul style={{ lineHeight: "24px" }}>
|
||||
<ul style={{ lineHeight: "24px" }} data-testid="appsStatus">
|
||||
{props.calEvent.appsStatus.map((status) => (
|
||||
<li key={status.type} style={{ fontWeight: 400 }}>
|
||||
{status.appName}{" "}
|
||||
|
|
|
@ -1431,7 +1431,7 @@ async function handler(
|
|||
metadata.hangoutLink = updatedEvent.hangoutLink;
|
||||
metadata.conferenceData = updatedEvent.conferenceData;
|
||||
metadata.entryPoints = updatedEvent.entryPoints;
|
||||
handleAppsStatus(results, newBooking);
|
||||
evt.appsStatus = handleAppsStatus(results, newBooking);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2105,7 +2105,7 @@ async function handler(
|
|||
booking: (Booking & { appsStatus?: AppsStatus[] }) | null
|
||||
) {
|
||||
// Taking care of apps status
|
||||
const resultStatus: AppsStatus[] = results.map((app) => ({
|
||||
let resultStatus: AppsStatus[] = results.map((app) => ({
|
||||
appName: app.appName,
|
||||
type: app.type,
|
||||
success: app.success ? 1 : 0,
|
||||
|
@ -2118,8 +2118,7 @@ async function handler(
|
|||
if (booking !== null) {
|
||||
booking.appsStatus = resultStatus;
|
||||
}
|
||||
evt.appsStatus = resultStatus;
|
||||
return;
|
||||
return resultStatus;
|
||||
}
|
||||
// From down here we can assume reqAppsStatus is not undefined anymore
|
||||
// Other status exist, so this is the last booking of a series,
|
||||
|
@ -2134,7 +2133,8 @@ async function handler(
|
|||
}
|
||||
return prev;
|
||||
}, {} as { [key: string]: AppsStatus });
|
||||
evt.appsStatus = Object.values(calcAppsStatus);
|
||||
resultStatus = Object.values(calcAppsStatus);
|
||||
return resultStatus;
|
||||
}
|
||||
|
||||
let videoCallUrl;
|
||||
|
@ -2174,44 +2174,42 @@ async function handler(
|
|||
|
||||
results = updateManager.results;
|
||||
referencesToCreate = updateManager.referencesToCreate;
|
||||
if (results.length > 0 && results.some((res) => !res.success)) {
|
||||
const isThereAnIntegrationError = results && results.some((res) => !res.success);
|
||||
if (isThereAnIntegrationError) {
|
||||
const error = {
|
||||
errorCode: "BookingReschedulingMeetingFailed",
|
||||
message: "Booking Rescheduling failed",
|
||||
};
|
||||
|
||||
loggerWithEventDetails.error(`Booking ${organizerUser.name} failed`, safeStringify({ error, results }));
|
||||
loggerWithEventDetails.error(
|
||||
`EventManager.create failure in some of the integrations ${organizerUser.username}`,
|
||||
safeStringify({ error, results })
|
||||
);
|
||||
} else {
|
||||
const metadata: AdditionalInformation = {};
|
||||
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
||||
|
||||
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
||||
? calendarResult?.updatedEvent[0]?.iCalUID
|
||||
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
||||
}
|
||||
|
||||
if (results.length) {
|
||||
// TODO: Handle created event metadata more elegantly
|
||||
const [updatedEvent] = Array.isArray(results[0].updatedEvent)
|
||||
? results[0].updatedEvent
|
||||
: [results[0].updatedEvent];
|
||||
if (updatedEvent) {
|
||||
metadata.hangoutLink = updatedEvent.hangoutLink;
|
||||
metadata.conferenceData = updatedEvent.conferenceData;
|
||||
metadata.entryPoints = updatedEvent.entryPoints;
|
||||
handleAppsStatus(results, booking);
|
||||
videoCallUrl = metadata.hangoutLink || videoCallUrl || updatedEvent?.url;
|
||||
}
|
||||
}
|
||||
if (noEmail !== true && isConfirmedByDefault) {
|
||||
const copyEvent = cloneDeep(evt);
|
||||
loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation");
|
||||
await sendRescheduledEmails({
|
||||
...copyEvent,
|
||||
additionalInformation: metadata,
|
||||
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
|
||||
});
|
||||
}
|
||||
const { metadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({
|
||||
results,
|
||||
});
|
||||
|
||||
videoCallUrl = _videoCallUrl;
|
||||
evt.appsStatus = handleAppsStatus(results, booking);
|
||||
|
||||
// If there is an integration error, we don't send successful rescheduling email, instead broken integration email should be sent that are handled by either CalendarManager or videoClient
|
||||
if (noEmail !== true && isConfirmedByDefault && !isThereAnIntegrationError) {
|
||||
const copyEvent = cloneDeep(evt);
|
||||
loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation");
|
||||
await sendRescheduledEmails({
|
||||
...copyEvent,
|
||||
additionalInformation: metadata,
|
||||
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
|
||||
});
|
||||
}
|
||||
// If it's not a reschedule, doesn't require confirmation and there's no price,
|
||||
// Create a booking
|
||||
|
@ -2234,7 +2232,7 @@ async function handler(
|
|||
};
|
||||
|
||||
loggerWithEventDetails.error(
|
||||
`Failure in creating events in some of the integrations ${organizerUser.username} failed`,
|
||||
`EventManager.create failure in some of the integrations ${organizerUser.username}`,
|
||||
safeStringify({ error, results })
|
||||
);
|
||||
} else {
|
||||
|
@ -2296,7 +2294,7 @@ async function handler(
|
|||
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
||||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||
handleAppsStatus(results, booking);
|
||||
evt.appsStatus = handleAppsStatus(results, booking);
|
||||
videoCallUrl =
|
||||
metadata.hangoutLink || organizerOrFirstDynamicGroupMemberDefaultLocationUrl || videoCallUrl;
|
||||
}
|
||||
|
@ -2374,6 +2372,7 @@ async function handler(
|
|||
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const webhookData = {
|
||||
...evt,
|
||||
...eventTypeInfo,
|
||||
|
@ -2558,6 +2557,31 @@ async function handler(
|
|||
|
||||
export default handler;
|
||||
|
||||
function getVideoCallDetails({
|
||||
results,
|
||||
}: {
|
||||
results: EventResult<AdditionalInformation & { url?: string | undefined; iCalUID?: string | undefined }>[];
|
||||
}) {
|
||||
const firstVideoResult = results.find((result) => result.type.includes("_video"));
|
||||
const metadata: AdditionalInformation = {};
|
||||
let updatedVideoEvent = null;
|
||||
|
||||
if (firstVideoResult && firstVideoResult.success) {
|
||||
updatedVideoEvent = Array.isArray(firstVideoResult.updatedEvent)
|
||||
? firstVideoResult.updatedEvent[0]
|
||||
: firstVideoResult.updatedEvent;
|
||||
|
||||
if (updatedVideoEvent) {
|
||||
metadata.hangoutLink = updatedVideoEvent.hangoutLink;
|
||||
metadata.conferenceData = updatedVideoEvent.conferenceData;
|
||||
metadata.entryPoints = updatedVideoEvent.entryPoints;
|
||||
}
|
||||
}
|
||||
const videoCallUrl = metadata.hangoutLink || updatedVideoEvent?.url;
|
||||
|
||||
return { videoCallUrl, metadata, updatedVideoEvent };
|
||||
}
|
||||
|
||||
function getRequiresConfirmationFlags({
|
||||
eventType,
|
||||
bookingStartTime,
|
||||
|
|
|
@ -206,7 +206,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -353,7 +353,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -499,7 +499,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -760,7 +760,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -1447,7 +1447,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -1883,7 +1883,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
paidEvent: true,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -21,6 +21,8 @@ import {
|
|||
BookingLocations,
|
||||
getMockBookingReference,
|
||||
getMockBookingAttendee,
|
||||
getMockFailingAppStatus,
|
||||
getMockPassingAppStatus,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
|
@ -103,6 +105,9 @@ describe("handleNewBooking", () => {
|
|||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
metadata: {
|
||||
videoCallUrl: "https://existing-daily-video-call-url.example.com",
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
|
@ -254,6 +259,10 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
emails,
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
appsStatus: [
|
||||
getMockPassingAppStatus({ slug: appStoreMetadata.dailyvideo.slug }),
|
||||
getMockPassingAppStatus({ slug: appStoreMetadata.googlecalendar.slug }),
|
||||
],
|
||||
});
|
||||
|
||||
expectBookingRescheduledWebhookToHaveBeenFired({
|
||||
|
@ -261,7 +270,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -464,7 +473,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -525,6 +534,9 @@ describe("handleNewBooking", () => {
|
|||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
metadata: {
|
||||
videoCallUrl: "https://existing-daily-video-call-url.example.com",
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
|
@ -551,6 +563,9 @@ describe("handleNewBooking", () => {
|
|||
);
|
||||
|
||||
const _calendarMock = mockCalendarToCrashOnUpdateEvent("googlecalendar");
|
||||
const _videoMock = mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
|
@ -559,7 +574,7 @@ describe("handleNewBooking", () => {
|
|||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "New York" },
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -577,16 +592,27 @@ describe("handleNewBooking", () => {
|
|||
},
|
||||
to: {
|
||||
description: "",
|
||||
location: "New York",
|
||||
location: "integrations:daily",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
metadata: {
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking?.uid}`,
|
||||
},
|
||||
responses: expect.objectContaining({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
}),
|
||||
// Booking References still use the original booking's references - Not sure how intentional it is.
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
// A reference is still created in case of event creation failure, with nullish values. Not sure what's the purpose for this.
|
||||
|
@ -607,8 +633,18 @@ describe("handleNewBooking", () => {
|
|||
expectBookingRescheduledWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: "New York",
|
||||
location: "integrations:daily",
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
payload: {
|
||||
uid: createdBooking.uid,
|
||||
appsStatus: [
|
||||
expect.objectContaining(getMockPassingAppStatus({ slug: appStoreMetadata.dailyvideo.slug })),
|
||||
expect.objectContaining(
|
||||
getMockFailingAppStatus({ slug: appStoreMetadata.googlecalendar.slug })
|
||||
),
|
||||
],
|
||||
},
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking?.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -1048,7 +1084,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -1497,12 +1533,13 @@ describe("handleNewBooking", () => {
|
|||
emails,
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
});
|
||||
|
||||
expectBookingRescheduledWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
|
|
@ -225,7 +225,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -537,7 +537,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
@ -854,7 +854,7 @@ describe("handleNewBooking", () => {
|
|||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -4576,6 +4576,7 @@ __metadata:
|
|||
next-i18next: ^13.2.2
|
||||
next-seo: ^6.0.0
|
||||
next-themes: ^0.2.0
|
||||
node-html-parser: ^6.1.10
|
||||
nodemailer: ^6.7.8
|
||||
otplib: ^12.0.1
|
||||
postcss: ^8.4.18
|
||||
|
@ -18514,6 +18515,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-select@npm:^5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "css-select@npm:5.1.0"
|
||||
dependencies:
|
||||
boolbase: ^1.0.0
|
||||
css-what: ^6.1.0
|
||||
domhandler: ^5.0.2
|
||||
domutils: ^3.0.1
|
||||
nth-check: ^2.0.1
|
||||
checksum: 2772c049b188d3b8a8159907192e926e11824aea525b8282981f72ba3f349cf9ecd523fdf7734875ee2cb772246c22117fc062da105b6d59afe8dcd5c99c9bda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-to-react-native@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "css-to-react-native@npm:3.0.0"
|
||||
|
@ -18532,7 +18546,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-what@npm:^6.0.1":
|
||||
"css-what@npm:^6.0.1, css-what@npm:^6.1.0":
|
||||
version: 6.1.0
|
||||
resolution: "css-what@npm:6.1.0"
|
||||
checksum: b975e547e1e90b79625918f84e67db5d33d896e6de846c9b584094e529f0c63e2ab85ee33b9daffd05bff3a146a1916bec664e18bb76dd5f66cbff9fc13b2bbe
|
||||
|
@ -29541,6 +29555,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-html-parser@npm:^6.1.10":
|
||||
version: 6.1.10
|
||||
resolution: "node-html-parser@npm:6.1.10"
|
||||
dependencies:
|
||||
css-select: ^5.1.0
|
||||
he: 1.2.0
|
||||
checksum: 927f6a38b3b1cbc042bce609e24fb594d3b1e0f1067ffb416a925fa5a699e907be31980f349e094d55bab706dc16a71958b08f8dcdab62faf7b12013f29442bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-ical@npm:^0.16.1":
|
||||
version: 0.16.1
|
||||
resolution: "node-ical@npm:0.16.1"
|
||||
|
|
Loading…
Reference in New Issue
Block a user