Skip to content

Commit d8a87e9

Browse files
authored
fix: v2 seated bookings (calcom#23514)
* fix: email manage link missing seat uid * fix: toggle seated booking attendees * fix: email manage link seat uid * chore: update platform libraries
1 parent 86dcd6f commit d8a87e9

7 files changed

Lines changed: 193 additions & 80 deletions

File tree

apps/api/v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@axiomhq/winston": "^1.2.0",
3939
"@calcom/platform-constants": "*",
4040
"@calcom/platform-enums": "*",
41-
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.344",
41+
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.346",
4242
"@calcom/platform-types": "*",
4343
"@calcom/platform-utils": "*",
4444
"@calcom/prisma": "*",

apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/booking-fields.e2e-spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
117117
slug: seatedEventTypeSlug,
118118
length: 60,
119119
seatsPerTimeSlot: 3,
120+
seatsShowAttendees: true,
120121
},
121122
user.id
122123
);

apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
5353
let apiKeyString: string;
5454

5555
let seatedEventTypeId: number;
56+
let seatedEventTypeIdAttendeesDisabledId: number;
5657

5758
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
59+
const seatedEventSlugAttendeesDisabled = `seated-bookings-event-type-attendees-disabled-${randomString()}`;
5860

5961
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
6062
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
@@ -121,6 +123,28 @@ describe("Bookings Endpoints 2024-08-13", () => {
121123
);
122124
seatedEventTypeId = seatedEvent.id;
123125

126+
const seatedEventAttendeesDisabled = await eventTypesRepositoryFixture.create(
127+
{
128+
title: `seated-bookings-2024-08-13-event-type-attendees-disabled-${randomString()}`,
129+
slug: seatedEventSlugAttendeesDisabled,
130+
length: 60,
131+
seatsPerTimeSlot: 5,
132+
seatsShowAttendees: false,
133+
seatsShowAvailabilityCount: false,
134+
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
135+
metadata: {
136+
disableStandardEmails: {
137+
all: {
138+
attendee: true,
139+
host: true,
140+
},
141+
},
142+
},
143+
},
144+
user.id
145+
);
146+
seatedEventTypeIdAttendeesDisabledId = seatedEventAttendeesDisabled.id;
147+
124148
app = moduleRef.createNestApplication();
125149
bootstrap(app as NestExpressApplication);
126150

@@ -414,6 +438,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
414438
});
415439
});
416440

441+
it("should book an event type with attendees disabled", async () => {
442+
const body: CreateBookingInput_2024_08_13 = {
443+
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
444+
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
445+
attendee: {
446+
name: nameAttendeeOne,
447+
email: emailAttendeeOne,
448+
timeZone: "Europe/Rome",
449+
language: "it",
450+
},
451+
bookingFieldsResponses: {
452+
codingLanguage: "TypeScript",
453+
},
454+
metadata: {
455+
userId: "100",
456+
},
457+
};
458+
459+
return request(app.getHttpServer())
460+
.post("/v2/bookings")
461+
.send(body)
462+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
463+
.expect(201)
464+
.then(async (response) => {
465+
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
466+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
467+
expect(responseBody.data).toBeDefined();
468+
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
469+
470+
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
471+
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
472+
expect(data.seatUid).toBeDefined();
473+
expect(data.id).toBeDefined();
474+
expect(data.uid).toBeDefined();
475+
expect(data.hosts[0].id).toEqual(user.id);
476+
expect(data.status).toEqual("accepted");
477+
expect(data.start).toEqual(body.start);
478+
expect(data.end).toEqual(
479+
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
480+
);
481+
expect(data.duration).toEqual(60);
482+
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
483+
expect(data.eventType).toEqual({
484+
id: seatedEventTypeIdAttendeesDisabledId,
485+
slug: seatedEventSlugAttendeesDisabled,
486+
});
487+
expect(data.attendees.length).toEqual(0);
488+
expect(data.location).toBeDefined();
489+
expect(data.absentHost).toEqual(false);
490+
} else {
491+
throw new Error(
492+
"Invalid response data - expected seated booking but received non array response"
493+
);
494+
}
495+
});
496+
});
497+
417498
describe("cancel seated booking", () => {
418499
describe("cancel seated booking as attendee", () => {
419500
it("should cancel seated booking", async () => {

apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -576,10 +576,13 @@ export class BookingsService_2024_08_13 {
576576
return this.outputService.getOutputRecurringBooking(booking);
577577
}
578578
if (isRecurring && isSeated) {
579-
return this.outputService.getOutputRecurringSeatedBooking(booking);
579+
return this.outputService.getOutputRecurringSeatedBooking(
580+
booking,
581+
!!booking.eventType?.seatsShowAttendees
582+
);
580583
}
581584
if (isSeated) {
582-
return this.outputService.getOutputSeatedBooking(booking);
585+
return this.outputService.getOutputSeatedBooking(booking, !!booking.eventType?.seatsShowAttendees);
583586
}
584587
return this.outputService.getOutputBooking(booking);
585588
}
@@ -591,7 +594,10 @@ export class BookingsService_2024_08_13 {
591594
const ids = recurringBooking.map((booking) => booking.id);
592595
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
593596
if (isRecurringSeated) {
594-
return this.outputService.getOutputRecurringSeatedBookings(ids);
597+
return this.outputService.getOutputRecurringSeatedBookings(
598+
ids,
599+
!!recurringBooking[0].eventType?.seatsShowAttendees
600+
);
595601
}
596602

597603
return this.outputService.getOutputRecurringBookings(ids);
@@ -657,9 +663,19 @@ export class BookingsService_2024_08_13 {
657663
if (isRecurring && !isSeated) {
658664
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
659665
} else if (isRecurring && isSeated) {
660-
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
666+
formattedBookings.push(
667+
this.outputService.getOutputRecurringSeatedBooking(
668+
formatted,
669+
!!formatted.eventType?.seatsShowAttendees
670+
)
671+
);
661672
} else if (isSeated) {
662-
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted));
673+
formattedBookings.push(
674+
await this.outputService.getOutputSeatedBooking(
675+
formatted,
676+
!!formatted.eventType?.seatsShowAttendees
677+
)
678+
);
663679
} else {
664680
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
665681
}

apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts

Lines changed: 77 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type DatabaseBooking = Booking & {
6868
eventType: {
6969
id: number;
7070
slug: string;
71+
seatsShowAttendees?: boolean | null;
7172
} | null;
7273
attendees: {
7374
name: string;
@@ -167,6 +168,7 @@ export class OutputBookingsService_2024_08_13 {
167168
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
168169
if (databaseMetadata === null) return {};
169170

171+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
170172
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
171173

172174
return userDefinedMetadata;
@@ -266,11 +268,14 @@ export class OutputBookingsService_2024_08_13 {
266268
databaseBooking: DatabaseBooking,
267269
seatUid: string
268270
): Promise<CreateSeatedBookingOutput_2024_08_13> {
269-
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking);
271+
const getSeatedBookingOutput = await this.getOutputSeatedBooking(
272+
databaseBooking,
273+
!!databaseBooking.eventType?.seatsShowAttendees
274+
);
270275
return { ...getSeatedBookingOutput, seatUid };
271276
}
272277

273-
async getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
278+
async getOutputSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
274279
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
275280
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
276281
const duration = dateEnd.diff(dateStart, "minutes").minutes;
@@ -317,41 +322,43 @@ export class OutputBookingsService_2024_08_13 {
317322
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
318323

319324
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
320-
parsed.attendees = databaseBooking.attendees.map((attendee) => {
321-
const { responses } = safeParse(
322-
seatedBookingDataSchema,
323-
attendee.bookingSeat?.data,
324-
defaultSeatedBookingData,
325-
false
326-
);
327-
328-
const attendeeData = {
329-
name: attendee.name,
330-
email: attendee.email,
331-
timeZone: attendee.timeZone,
332-
language: attendee.locale,
333-
absent: !!attendee.noShow,
334-
seatUid: attendee.bookingSeat?.referenceUid,
335-
bookingFieldsResponses: {},
336-
};
337-
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
338-
attendeeParsed.bookingFieldsResponses = responses || {};
339-
attendeeParsed.metadata = safeParse(
340-
seatedBookingMetadataSchema,
341-
attendee.bookingSeat?.metadata,
342-
defaultSeatedBookingMetadata,
343-
false
344-
);
345-
// note(Lauris): as of now email is not returned for privacy
346-
delete attendeeParsed.bookingFieldsResponses.email;
347-
348-
return attendeeParsed;
349-
});
325+
parsed.attendees = showAttendees
326+
? databaseBooking.attendees.map((attendee) => {
327+
const { responses } = safeParse(
328+
seatedBookingDataSchema,
329+
attendee.bookingSeat?.data,
330+
defaultSeatedBookingData,
331+
false
332+
);
333+
334+
const attendeeData = {
335+
name: attendee.name,
336+
email: attendee.email,
337+
timeZone: attendee.timeZone,
338+
language: attendee.locale,
339+
absent: !!attendee.noShow,
340+
seatUid: attendee.bookingSeat?.referenceUid,
341+
bookingFieldsResponses: {},
342+
};
343+
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
344+
attendeeParsed.bookingFieldsResponses = responses || {};
345+
attendeeParsed.metadata = safeParse(
346+
seatedBookingMetadataSchema,
347+
attendee.bookingSeat?.metadata,
348+
defaultSeatedBookingMetadata,
349+
false
350+
);
351+
// note(Lauris): as of now email is not returned for privacy
352+
delete attendeeParsed.bookingFieldsResponses.email;
353+
354+
return attendeeParsed;
355+
})
356+
: [];
350357

351358
return parsed;
352359
}
353360

354-
async getOutputRecurringSeatedBookings(bookingsIds: number[]) {
361+
async getOutputRecurringSeatedBookings(bookingsIds: number[], showAttendees: boolean) {
355362
const transformed = [];
356363

357364
for (const bookingId of bookingsIds) {
@@ -361,7 +368,7 @@ export class OutputBookingsService_2024_08_13 {
361368
throw new Error(`Booking with id=${bookingId} was not found in the database`);
362369
}
363370

364-
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking));
371+
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking, showAttendees));
365372
}
366373

367374
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
@@ -386,11 +393,14 @@ export class OutputBookingsService_2024_08_13 {
386393
databaseBooking: DatabaseBooking,
387394
seatUid: string
388395
): CreateRecurringSeatedBookingOutput_2024_08_13 {
389-
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(databaseBooking);
396+
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(
397+
databaseBooking,
398+
!!databaseBooking.eventType?.seatsShowAttendees
399+
);
390400
return { ...getRecurringSeatedBookingOutput, seatUid };
391401
}
392402

393-
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking) {
403+
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
394404
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
395405
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
396406
const duration = dateEnd.diff(dateStart, "minutes").minutes;
@@ -430,35 +440,37 @@ export class OutputBookingsService_2024_08_13 {
430440
});
431441

432442
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
433-
parsed.attendees = databaseBooking.attendees.map((attendee) => {
434-
const { responses } = safeParse(
435-
seatedBookingDataSchema,
436-
attendee.bookingSeat?.data,
437-
defaultSeatedBookingData,
438-
false
439-
);
440-
441-
const attendeeData = {
442-
name: attendee.name,
443-
email: attendee.email,
444-
timeZone: attendee.timeZone,
445-
language: attendee.locale,
446-
absent: !!attendee.noShow,
447-
seatUid: attendee.bookingSeat?.referenceUid,
448-
bookingFieldsResponses: {},
449-
};
450-
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
451-
attendeeParsed.bookingFieldsResponses = responses || {};
452-
attendeeParsed.metadata = safeParse(
453-
seatedBookingMetadataSchema,
454-
attendee.bookingSeat?.metadata,
455-
defaultSeatedBookingMetadata,
456-
false
457-
);
458-
// note(Lauris): as of now email is not returned for privacy
459-
delete attendeeParsed.bookingFieldsResponses.email;
460-
return attendeeParsed;
461-
});
443+
parsed.attendees = showAttendees
444+
? databaseBooking.attendees.map((attendee) => {
445+
const { responses } = safeParse(
446+
seatedBookingDataSchema,
447+
attendee.bookingSeat?.data,
448+
defaultSeatedBookingData,
449+
false
450+
);
451+
452+
const attendeeData = {
453+
name: attendee.name,
454+
email: attendee.email,
455+
timeZone: attendee.timeZone,
456+
language: attendee.locale,
457+
absent: !!attendee.noShow,
458+
seatUid: attendee.bookingSeat?.referenceUid,
459+
bookingFieldsResponses: {},
460+
};
461+
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
462+
attendeeParsed.bookingFieldsResponses = responses || {};
463+
attendeeParsed.metadata = safeParse(
464+
seatedBookingMetadataSchema,
465+
attendee.bookingSeat?.metadata,
466+
defaultSeatedBookingMetadata,
467+
false
468+
);
469+
// note(Lauris): as of now email is not returned for privacy
470+
delete attendeeParsed.bookingFieldsResponses.email;
471+
return attendeeParsed;
472+
})
473+
: [];
462474

463475
return parsed;
464476
}

0 commit comments

Comments
 (0)