fix: false 500s from /api/book/event (#23001)

* fix: false 500 from api/book/event

* fix another
This commit is contained in:
Benny Joo
2025-08-11 18:06:26 +01:00
committed by GitHub
parent 76b68bebd7
commit 3133eb5aeb
4 changed files with 234 additions and 88 deletions
+144 -71
View File
@@ -26,11 +26,14 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.bookerUrl).toBe("https://cal.com/user/test-slug");
expect(event.title).toBe("Test Event");
expect(event.startTime).toBe(mockStartTime);
expect(event.endTime).toBe(mockEndTime);
expect(event.additionalNotes).toBe("Some notes");
expect(event).not.toBeNull();
if (event) {
expect(event.bookerUrl).toBe("https://cal.com/user/test-slug");
expect(event.title).toBe("Test Event");
expect(event.startTime).toBe(mockStartTime);
expect(event.endTime).toBe(mockEndTime);
expect(event.additionalNotes).toBe("Some notes");
}
});
it("should create an event with event type details", () => {
@@ -50,11 +53,14 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.type).toBe("test-slug");
expect(event.description).toBe("Test description");
expect(event.eventTypeId).toBe(123);
expect(event.hideCalendarNotes).toBe(true);
expect(event.hideCalendarEventDetails).toBe(false);
expect(event).not.toBeNull();
if (event) {
expect(event.type).toBe("test-slug");
expect(event.description).toBe("Test description");
expect(event.eventTypeId).toBe(123);
expect(event.hideCalendarNotes).toBe(true);
expect(event.hideCalendarEventDetails).toBe(false);
}
});
it("should create an event with organizer details", () => {
@@ -82,17 +88,20 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.organizer).toEqual({
id: 456,
name: "John Doe",
email: "john@example.com",
username: "johndoe",
timeZone: "America/New_York",
language: {
translate: mockTranslate,
locale: "en",
},
});
expect(event).not.toBeNull();
if (event) {
expect(event.organizer).toEqual({
id: 456,
name: "John Doe",
email: "john@example.com",
username: "johndoe",
timeZone: "America/New_York",
language: {
translate: mockTranslate,
locale: "en",
},
});
}
});
it("should handle nameless organizer", () => {
@@ -119,7 +128,10 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.organizer.name).toBe("Nameless");
expect(event).not.toBeNull();
if (event) {
expect(event.organizer.name).toBe("Nameless");
}
});
it("should create an event with attendees", () => {
@@ -158,7 +170,10 @@ describe("CalendarEventBuilder", () => {
.withAttendees(attendees)
.build();
expect(event.attendees).toEqual(attendees);
expect(event).not.toBeNull();
if (event) {
expect(event.attendees).toEqual(attendees);
}
});
it("should create an event with metadata and responses", () => {
@@ -192,10 +207,13 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.additionalNotes).toBe("Some notes");
expect(event.customInputs).toEqual(customInputs);
expect(event.responses).toEqual(responses);
expect(event.userFieldsResponses).toEqual(userFieldsResponses);
expect(event).not.toBeNull();
if (event) {
expect(event.additionalNotes).toBe("Some notes");
expect(event.customInputs).toEqual(customInputs);
expect(event.responses).toEqual(responses);
expect(event.userFieldsResponses).toEqual(userFieldsResponses);
}
});
it("should create an event with location", () => {
@@ -216,8 +234,11 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.location).toBe("Conference Room A");
expect(event.conferenceCredentialId).toBe(789);
expect(event).not.toBeNull();
if (event) {
expect(event.location).toBe("Conference Room A");
expect(event.conferenceCredentialId).toBe(789);
}
});
it("should create an event with destination calendar", () => {
@@ -229,6 +250,8 @@ describe("CalendarEventBuilder", () => {
userId: null,
eventTypeId: null,
credentialId: null,
createdAt: null,
updatedAt: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
};
@@ -247,7 +270,10 @@ describe("CalendarEventBuilder", () => {
.withDestinationCalendar([destinationCalendar])
.build();
expect(event.destinationCalendar).toEqual([destinationCalendar]);
expect(event).not.toBeNull();
if (event) {
expect(event.destinationCalendar).toEqual([destinationCalendar]);
}
});
it("should create an event with identifiers", () => {
@@ -268,8 +294,11 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.iCalUID).toBe("ical-123");
expect(event.iCalSequence).toBe(2);
expect(event).not.toBeNull();
if (event) {
expect(event.iCalUID).toBe("ical-123");
expect(event.iCalSequence).toBe(2);
}
});
it("should create an event with confirmation settings", () => {
@@ -290,8 +319,11 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.requiresConfirmation).toBe(true);
expect(event.oneTimePassword).toBeUndefined();
expect(event).not.toBeNull();
if (event) {
expect(event.requiresConfirmation).toBe(true);
expect(event.oneTimePassword).toBeUndefined();
}
});
it("should set oneTimePassword to null when isConfirmedByDefault is true", () => {
@@ -312,8 +344,11 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.requiresConfirmation).toBe(true);
expect(event.oneTimePassword).toBeNull();
expect(event).not.toBeNull();
if (event) {
expect(event.requiresConfirmation).toBe(true);
expect(event.oneTimePassword).toBeNull();
}
});
it("should create an event with platform variables", () => {
@@ -336,10 +371,13 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.platformClientId).toBe("client-123");
expect(event.platformRescheduleUrl).toBe("https://platform.com/reschedule");
expect(event.platformCancelUrl).toBe("https://platform.com/cancel");
expect(event.platformBookingUrl).toBe("https://platform.com/booking");
expect(event).not.toBeNull();
if (event) {
expect(event.platformClientId).toBe("client-123");
expect(event.platformRescheduleUrl).toBe("https://platform.com/reschedule");
expect(event.platformCancelUrl).toBe("https://platform.com/cancel");
expect(event.platformBookingUrl).toBe("https://platform.com/booking");
}
});
it("should create an event with apps status", () => {
@@ -376,7 +414,10 @@ describe("CalendarEventBuilder", () => {
.withAppsStatus(appsStatus)
.build();
expect(event.appsStatus).toEqual(appsStatus);
expect(event).not.toBeNull();
if (event) {
expect(event.appsStatus).toEqual(appsStatus);
}
});
it("should create an event with video call data", () => {
@@ -401,7 +442,10 @@ describe("CalendarEventBuilder", () => {
.withVideoCallData(videoCallData)
.build();
expect(event.videoCallData).toEqual(videoCallData);
expect(event).not.toBeNull();
if (event) {
expect(event.videoCallData).toEqual(videoCallData);
}
});
it("should create an event with team information", () => {
@@ -435,7 +479,10 @@ describe("CalendarEventBuilder", () => {
.withTeam(team)
.build();
expect(event.team).toEqual(team);
expect(event).not.toBeNull();
if (event) {
expect(event.team).toEqual(team);
}
});
it("should create an event with recurring event information", () => {
@@ -459,7 +506,10 @@ describe("CalendarEventBuilder", () => {
.withRecurring(recurringEvent)
.build();
expect(event.recurringEvent).toEqual(recurringEvent);
expect(event).not.toBeNull();
if (event) {
expect(event.recurringEvent).toEqual(recurringEvent);
}
});
it("should create an event with attendee seat ID", () => {
@@ -477,7 +527,10 @@ describe("CalendarEventBuilder", () => {
.withAttendeeSeatId("seat-123")
.build();
expect(event.attendeeSeatId).toBe("seat-123");
expect(event).not.toBeNull();
if (event) {
expect(event.attendeeSeatId).toBe("seat-123");
}
});
it("should create an event with UID", () => {
@@ -495,7 +548,10 @@ describe("CalendarEventBuilder", () => {
.withUid("booking-uid-123")
.build();
expect(event.uid).toBe("booking-uid-123");
expect(event).not.toBeNull();
if (event) {
expect(event.uid).toBe("booking-uid-123");
}
});
it("should create an event with one-time password", () => {
@@ -513,7 +569,10 @@ describe("CalendarEventBuilder", () => {
.withOneTimePassword("otp123")
.build();
expect(event.oneTimePassword).toBe("otp123");
expect(event).not.toBeNull();
if (event) {
expect(event.oneTimePassword).toBe("otp123");
}
});
it("should create an event with recurring event ID", () => {
@@ -531,9 +590,12 @@ describe("CalendarEventBuilder", () => {
.withRecurringEventId("recurring-123")
.build();
expect(event.existingRecurringEvent).toEqual({
recurringEventId: "recurring-123",
});
expect(event).not.toBeNull();
if (event) {
expect(event.existingRecurringEvent).toEqual({
recurringEventId: "recurring-123",
});
}
});
it("should create a complete calendar event with all properties", () => {
@@ -597,6 +659,8 @@ describe("CalendarEventBuilder", () => {
userId: 1,
eventTypeId: 123,
credentialId: 1,
createdAt: null,
updatedAt: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
},
@@ -644,26 +708,29 @@ describe("CalendarEventBuilder", () => {
.build();
// Test that all properties are set correctly
expect(event.title).toBe("Complete Test Event");
expect(event.type).toBe("complete-test");
expect(event.organizer.name).toBe("John Doe");
expect(event.attendees).toHaveLength(1);
expect(event.location).toBe("Conference Room A");
expect(event.iCalUID).toBe("ical-123");
expect(event.requiresConfirmation).toBe(true);
expect(event.platformClientId).toBe("client-123");
expect(event.appsStatus).toHaveLength(1);
expect(event.videoCallData?.url).toBe("https://meet.example.com/123");
expect(event.team?.name).toBe("Engineering Team");
expect(event.recurringEvent?.count).toBe(5);
expect(event.attendeeSeatId).toBe("seat-123");
expect(event.uid).toBe("booking-uid-123");
expect(event.oneTimePassword).toBe("otp123");
expect(event).not.toBeNull();
if (event) {
expect(event.title).toBe("Complete Test Event");
expect(event.type).toBe("complete-test");
expect(event.organizer.name).toBe("John Doe");
expect(event.attendees).toHaveLength(1);
expect(event.location).toBe("Conference Room A");
expect(event.iCalUID).toBe("ical-123");
expect(event.requiresConfirmation).toBe(true);
expect(event.platformClientId).toBe("client-123");
expect(event.appsStatus).toHaveLength(1);
expect(event.videoCallData?.url).toBe("https://meet.example.com/123");
expect(event.team?.name).toBe("Engineering Team");
expect(event.recurringEvent?.count).toBe(5);
expect(event.attendeeSeatId).toBe("seat-123");
expect(event.uid).toBe("booking-uid-123");
expect(event.oneTimePassword).toBe("otp123");
}
});
it("should throw an error when building without required fields", () => {
it("should return null when building without required fields", () => {
const builder = new CalendarEventBuilder();
expect(() => builder.build()).toThrow("Missing required fields for calendar event");
expect(builder.build()).toBeNull();
});
it("should create an event from an existing event", () => {
@@ -684,8 +751,11 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.title).toBe("Updated Event");
expect(event.type).toBe("existing-type");
expect(event).not.toBeNull();
if (event) {
expect(event.title).toBe("Updated Event");
expect(event.type).toBe("existing-type");
}
});
it("should propagate disableCancelling and disableRescheduling", () => {
@@ -706,7 +776,10 @@ describe("CalendarEventBuilder", () => {
})
.build();
expect(event.disableCancelling).toBe(true);
expect(event.disableRescheduling).toBe(true);
expect(event).not.toBeNull();
if (event) {
expect(event.disableCancelling).toBe(true);
expect(event.disableRescheduling).toBe(true);
}
});
});
+2 -2
View File
@@ -266,7 +266,7 @@ export class CalendarEventBuilder {
return this;
}
build(): CalendarEvent {
build(): CalendarEvent | null {
// Validate required fields
if (
!this.event.startTime ||
@@ -275,7 +275,7 @@ export class CalendarEventBuilder {
!this.event.bookerUrl ||
!this.event.title
) {
throw new Error("Missing required fields for calendar event");
return null;
}
return this.event as CalendarEvent;
@@ -7,6 +7,7 @@ import { contructEmailFromPhoneNumber } from "@calcom/lib/contructEmailFromPhone
import { getBookingWithResponses } from "@calcom/lib/getBooking";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { HttpError } from "@calcom/lib/http-error";
export const getCalEventResponses = ({
bookingFields,
@@ -42,7 +43,10 @@ export const getCalEventResponses = ({
// To set placeholder email for the booking
if (!!!backwardCompatibleResponses.email) {
if (typeof backwardCompatibleResponses["attendeePhoneNumber"] !== "string")
throw new Error("Both Phone and Email are missing");
throw new HttpError({
statusCode: 400,
message: "Both Phone and Email are missing",
});
backwardCompatibleResponses.email = contructEmailFromPhoneNumber(
backwardCompatibleResponses["attendeePhoneNumber"]
@@ -320,17 +320,35 @@ export const buildEventForTeamEventType = async ({
const teamMembers = await Promise.all(teamMemberPromises);
evt = CalendarEventBuilder.fromEvent(evt)
.withDestinationCalendar([...(evt.destinationCalendar ?? []), ...teamDestinationCalendars])
const updatedEvt = CalendarEventBuilder.fromEvent(evt)
?.withDestinationCalendar([...(evt.destinationCalendar ?? []), ...teamDestinationCalendars])
.build();
return CalendarEventBuilder.fromEvent(evt)
.withTeam({
if (!updatedEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with destination calendar due to missing required fields",
});
}
evt = updatedEvt;
const teamEvt = CalendarEventBuilder.fromEvent(evt)
?.withTeam({
members: teamMembers,
name: team?.name || "Nameless",
id: team?.id ?? 0,
})
.build();
if (!teamEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build team event due to missing required fields",
});
}
return teamEvt;
};
function buildTroubleshooterData({
@@ -1168,7 +1186,7 @@ async function handler(
const eventName = getEventName(eventNameObject);
let evt: CalendarEvent = new CalendarEventBuilder()
const builtEvt = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl,
title: eventName,
@@ -1226,20 +1244,44 @@ async function handler(
})
.build();
if (!builtEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build calendar event due to missing required fields",
});
}
let evt: CalendarEvent = builtEvt;
if (input.bookingData.thirdPartyRecurringEventId) {
evt = CalendarEventBuilder.fromEvent(evt)
.withRecurringEventId(input.bookingData.thirdPartyRecurringEventId)
const updatedEvt = CalendarEventBuilder.fromEvent(evt)
?.withRecurringEventId(input.bookingData.thirdPartyRecurringEventId)
.build();
if (!updatedEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with recurring event ID due to missing required fields",
});
}
evt = updatedEvt;
}
if (isTeamEventType) {
evt = await buildEventForTeamEventType({
const teamEvt = await buildEventForTeamEventType({
existingEvent: evt,
schedulingType: eventType.schedulingType,
users,
team: eventType.team,
organizerUser,
});
if (!teamEvt) {
throw new HttpError({ statusCode: 400, message: "Failed to build team event" });
}
evt = teamEvt;
}
// data needed for triggering webhooks
@@ -1357,13 +1399,22 @@ async function handler(
// Rescheduling logic for the original seated event was handled in handleSeats
// We want to use new booking logic for the new time slot
originalRescheduledBooking = null;
evt = CalendarEventBuilder.fromEvent(evt)
.withIdentifiers({
const updatedEvt = CalendarEventBuilder.fromEvent(evt)
?.withIdentifiers({
iCalUID: getICalUID({
attendeeId: bookingSeat?.attendeeId,
}),
})
.build();
if (!updatedEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with new identifiers due to missing required fields",
});
}
evt = updatedEvt;
}
}
@@ -1484,14 +1535,32 @@ async function handler(
}
}
evt = CalendarEventBuilder.fromEvent(evt)
.withUid(booking.uid ?? null)
const updatedEvtWithUid = CalendarEventBuilder.fromEvent(evt)
?.withUid(booking.uid ?? null)
.build();
evt = CalendarEventBuilder.fromEvent(evt)
.withOneTimePassword(booking.oneTimePassword ?? null)
if (!updatedEvtWithUid) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with UID due to missing required fields",
});
}
evt = updatedEvtWithUid;
const updatedEvtWithPassword = CalendarEventBuilder.fromEvent(evt)
?.withOneTimePassword(booking.oneTimePassword ?? null)
.build();
if (!updatedEvtWithPassword) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with one-time password due to missing required fields",
});
}
evt = updatedEvtWithPassword;
if (booking && booking.id && eventType.seatsPerTimeSlot) {
const currentAttendee = booking.attendees.find(
(attendee) =>