From d15281160fdd06f8b17bc55d8ff19347ffd69ee6 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Tue, 2 Dec 2025 20:38:01 +0200 Subject: [PATCH] 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 * 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 * 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 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../services/input-event-types.service.ts | 2 +- ...-member-team-admin-event-types.e2e-spec.ts | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index c904901e89..879ba789a3 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -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, diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts index db232ff21f..5558717d14 100644 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts @@ -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);