fix: preserve seatsPerTimeSlot during partial event type updates (#25450)

* fix: preserve seatsPerTimeSlot during partial event type updates

When doing a partial update on the PATCH /v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId} endpoint, providing a partial body would reset seatsPerTimeSlot back to null because the transformSeatsApiToInternal function was always called even when seats was undefined.

This fix ensures that the seat options are only transformed when the seats field is explicitly provided in the update request, preserving existing values during partial updates.

Also adds e2e test to verify partial updates preserve seatsPerTimeSlot.

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: handle TypeScript union type narrowing for seatsPerTimeSlot

When seats is not provided in a partial update, the transformed body
may not have seatsPerTimeSlot property. This fix uses 'in' operator
to safely check for the property before accessing it.

Also fixes the e2e test to properly narrow the union type for seats
using type guards.

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* refactor: simplify seatsPerTimeSlot handling per review feedback

Per supalarry's suggestion, instead of using 'in' operator checks in
multiple validation calls, we now return {seatsPerTimeSlot: undefined}
when seats is not provided. This keeps the validation code unchanged
and only requires modifying one line in the transformation.

Co-Authored-By: morgan@cal.com <morgan@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Morgan
2025-12-02 18:38:01 +00:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 8b6c6273ee
commit d15281160f
2 changed files with 65 additions and 1 deletions
@@ -252,7 +252,7 @@ export class InputEventTypesService_2024_06_14 {
requiresConfirmationWillBlockSlot:
confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined,
eventTypeColor: this.transformInputEventTypeColor(color),
...this.transformInputSeatOptions(seats),
...(seats ? this.transformInputSeatOptions(seats) : { seatsPerTimeSlot: undefined }),
eventName: customName,
useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail,
...maxActiveBookingsPerBooker,
@@ -1230,6 +1230,70 @@ describe("Organizations Event Types Endpoints", () => {
});
});
it("should preserve seatsPerTimeSlot when doing partial update", async () => {
const createBody: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation with seats",
slug: `organizations-event-types-seats-${randomString()}`,
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
schedulingType: "COLLECTIVE",
hosts: [
{
userId: teammate1.id,
},
],
seats: {
seatsPerTimeSlot: 5,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
};
const createResponse = await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
.send(createBody)
.expect(201);
const createdEventType: TeamEventTypeOutput_2024_06_14 = createResponse.body.data;
const createdSeats = createdEventType.seats;
expect(createdSeats).toBeDefined();
expect(createdSeats && "seatsPerTimeSlot" in createdSeats).toBe(true);
if (createdSeats && "seatsPerTimeSlot" in createdSeats) {
expect(createdSeats.seatsPerTimeSlot).toEqual(5);
expect(createdSeats.showAttendeeInfo).toEqual(true);
expect(createdSeats.showAvailabilityCount).toEqual(true);
}
// Now do a partial update that only changes a different field (not seats)
const updateBody: UpdateTeamEventTypeInput_2024_06_14 = {
bookingRequiresAuthentication: false,
};
const updateResponse = await request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${createdEventType.id}`)
.send(updateBody)
.expect(200);
const updatedEventType: TeamEventTypeOutput_2024_06_14 = updateResponse.body.data;
// Verify that seatsPerTimeSlot is preserved and not reset to null
const updatedSeats = updatedEventType.seats;
expect(updatedSeats).toBeDefined();
expect(updatedSeats && "seatsPerTimeSlot" in updatedSeats).toBe(true);
if (updatedSeats && "seatsPerTimeSlot" in updatedSeats) {
expect(updatedSeats.seatsPerTimeSlot).toEqual(5);
expect(updatedSeats.showAttendeeInfo).toEqual(true);
expect(updatedSeats.showAvailabilityCount).toEqual(true);
}
expect(updatedEventType.bookingRequiresAuthentication).toEqual(false);
});
function evaluateHost(expected: Host, received: Host | undefined) {
expect(expected.userId).toEqual(received?.userId);
expect(expected.mandatory).toEqual(received?.mandatory);