diff --git a/apps/api/v2/src/modules/slots/controllers/slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/controllers/slots.controller.e2e-spec.ts index 4a975d5066..0d85f96cea 100644 --- a/apps/api/v2/src/modules/slots/controllers/slots.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/slots/controllers/slots.controller.e2e-spec.ts @@ -12,6 +12,8 @@ import { NestExpressApplication } from "@nestjs/platform-express"; import { Test } from "@nestjs/testing"; import { User } from "@prisma/client"; import * as request from "supertest"; +import { AttendeeRepositoryFixture } from "test/fixtures/repository/attendee.repository.fixture"; +import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture"; import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; import { SelectedSlotsRepositoryFixture } from "test/fixtures/repository/selected-slots.repository.fixture"; @@ -250,6 +252,8 @@ describe("Slots Endpoints", () => { let eventTypesRepositoryFixture: EventTypesRepositoryFixture; let selectedSlotsRepositoryFixture: SelectedSlotsRepositoryFixture; let bookingsRepositoryFixture: BookingsRepositoryFixture; + let bookingSeatsRepositoryFixture: BookingSeatRepositoryFixture; + let attendeesRepositoryFixture: AttendeeRepositoryFixture; const userEmail = `slots-${randomString()}-controller-e2e@api.com`; const userName = "bob"; @@ -258,6 +262,9 @@ describe("Slots Endpoints", () => { let eventTypeSlug: string; let reservedSlotUid: string; + const seatedEventTypeSlug = "peer-coding-seated"; + let seatedEventTypeId: number; + beforeAll(async () => { const moduleRef = await withApiAuth( userEmail, @@ -283,6 +290,8 @@ describe("Slots Endpoints", () => { eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); selectedSlotsRepositoryFixture = new SelectedSlotsRepositoryFixture(moduleRef); bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + bookingSeatsRepositoryFixture = new BookingSeatRepositoryFixture(moduleRef); + attendeesRepositoryFixture = new AttendeeRepositoryFixture(moduleRef); user = await userRepositoryFixture.create({ email: userEmail, @@ -291,7 +300,7 @@ describe("Slots Endpoints", () => { }); // nxte(Lauris): this creates default schedule monday to friday from 9AM to 5PM in Europe/Rome timezone - const userSchedule = await schedulesService.createUserSchedule(user.id, { + await schedulesService.createUserSchedule(user.id, { name: `slots-schedule-${randomString()}-slots.controller.e2e-spec`, timeZone: "Europe/Rome", isDefault: true, @@ -306,9 +315,24 @@ describe("Slots Endpoints", () => { }, user.id ); + eventTypeId = eventType.id; eventTypeSlug = eventType.slug; + const seatedEvent = await eventTypesRepositoryFixture.create( + { + title: `slots-event-type-seated-${randomString()}-slots.controller.e2e-spec`, + slug: `slots-event-type-seated-${randomString()}-slots.controller.e2e-spec`, + length: 60, + seatsPerTimeSlot: 5, + seatsShowAttendees: true, + seatsShowAvailabilityCount: true, + locations: [{ type: "inPerson", address: "via 10, rome, italy" }], + }, + user.id + ); + seatedEventTypeId = seatedEvent.id; + app = moduleRef.createNestApplication(); bootstrap(app as NestExpressApplication); @@ -445,7 +469,7 @@ describe("Slots Endpoints", () => { it("should do a booking and slot should not be available at that time", async () => { const startTime = "2050-09-05T11:00:00.000Z"; - await bookingsRepositoryFixture.create({ + const booking = await bookingsRepositoryFixture.create({ uid: `booking-uid-${eventTypeId}`, title: "booking title", startTime, @@ -486,6 +510,179 @@ describe("Slots Endpoints", () => { expect(slots).toEqual({ slots: { ...expectedSlotsUTC.slots, "2050-09-05": expectedSlotsUTC2050_09_05 }, }); + + await bookingsRepositoryFixture.deleteById(booking.id); + }); + + it("should do a booking for seated event and slot should show attendees count and bookingUid", async () => { + const startTime = "2050-09-05T11:00:00.000Z"; + const booking = await bookingsRepositoryFixture.create({ + uid: `booking-uid-${seatedEventTypeId}`, + title: "booking title", + startTime, + endTime: "2050-09-05T12:00:00.000Z", + eventType: { + connect: { + id: seatedEventTypeId, + }, + }, + metadata: {}, + responses: { + name: "tester", + email: "tester@example.com", + guests: [], + }, + user: { + connect: { + id: user.id, + }, + }, + }); + + const attendee = await attendeesRepositoryFixture.create({ + name: "tester", + email: "tester@example.com", + timeZone: "Europe/London", + booking: { + connect: { + id: booking.id, + }, + }, + }); + + bookingSeatsRepositoryFixture.create({ + referenceUid: "100", + data: {}, + booking: { + connect: { + id: booking.id, + }, + }, + attendee: { + connect: { + id: attendee.id, + }, + }, + }); + + const response = await request(app.getHttpServer()) + .get( + `/api/v2/slots/available?eventTypeId=${seatedEventTypeId}&startTime=2050-09-05&endTime=2050-09-10` + ) + .expect(200); + + const responseBody = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const slots = responseBody.data; + + expect(slots).toBeDefined(); + const days = Object.keys(slots.slots); + expect(days.length).toEqual(5); + + const expectedSlotsUTC2050_09_05 = [ + { time: "2050-09-05T07:00:00.000Z" }, + { time: "2050-09-05T08:00:00.000Z" }, + { time: "2050-09-05T09:00:00.000Z" }, + { time: "2050-09-05T10:00:00.000Z" }, + { time: "2050-09-05T11:00:00.000Z", attendees: 1, bookingUid: booking.uid }, + { time: "2050-09-05T12:00:00.000Z" }, + { time: "2050-09-05T13:00:00.000Z" }, + { time: "2050-09-05T14:00:00.000Z" }, + ]; + + expect(slots).toEqual({ + slots: { ...expectedSlotsUTC.slots, "2050-09-05": expectedSlotsUTC2050_09_05 }, + }); + + await bookingsRepositoryFixture.deleteById(booking.id); + }); + + it("should do a booking for seated event and slot should show attendees count and bookingUid in range format", async () => { + const startTime = "2050-09-05T11:00:00.000Z"; + const booking = await bookingsRepositoryFixture.create({ + uid: `booking-uid-${seatedEventTypeId}`, + title: "booking title", + startTime, + endTime: "2050-09-05T12:00:00.000Z", + eventType: { + connect: { + id: seatedEventTypeId, + }, + }, + metadata: {}, + responses: { + name: "tester", + email: "tester@example.com", + guests: [], + }, + user: { + connect: { + id: user.id, + }, + }, + }); + + const attendee = await attendeesRepositoryFixture.create({ + name: "tester", + email: "tester@example.com", + timeZone: "Europe/London", + booking: { + connect: { + id: booking.id, + }, + }, + }); + + bookingSeatsRepositoryFixture.create({ + referenceUid: "100", + data: {}, + booking: { + connect: { + id: booking.id, + }, + }, + attendee: { + connect: { + id: attendee.id, + }, + }, + }); + + const response = await request(app.getHttpServer()) + .get( + `/api/v2/slots/available?eventTypeId=${seatedEventTypeId}&startTime=2050-09-05&endTime=2050-09-10&slotFormat=range` + ) + .expect(200); + + const responseBody = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const slots = responseBody.data; + + expect(slots).toBeDefined(); + const days = Object.keys(slots.slots); + expect(days.length).toEqual(5); + + const expectedSlotsUTC2050_09_05 = [ + { startTime: "2050-09-05T07:00:00.000Z", endTime: "2050-09-05T08:00:00.000Z" }, + { startTime: "2050-09-05T08:00:00.000Z", endTime: "2050-09-05T09:00:00.000Z" }, + { startTime: "2050-09-05T09:00:00.000Z", endTime: "2050-09-05T10:00:00.000Z" }, + { startTime: "2050-09-05T10:00:00.000Z", endTime: "2050-09-05T11:00:00.000Z" }, + { + startTime: "2050-09-05T11:00:00.000Z", + endTime: "2050-09-05T12:00:00.000Z", + attendees: 1, + bookingUid: booking.uid, + }, + { startTime: "2050-09-05T12:00:00.000Z", endTime: "2050-09-05T13:00:00.000Z" }, + { startTime: "2050-09-05T13:00:00.000Z", endTime: "2050-09-05T14:00:00.000Z" }, + { startTime: "2050-09-05T14:00:00.000Z", endTime: "2050-09-05T15:00:00.000Z" }, + ]; + + expect(slots).toEqual({ + slots: { ...expectedSlotsUTCRange.slots, "2050-09-05": expectedSlotsUTC2050_09_05 }, + }); + + await bookingsRepositoryFixture.deleteById(booking.id); }); afterAll(async () => { diff --git a/apps/api/v2/src/modules/slots/services/slots-output.service.ts b/apps/api/v2/src/modules/slots/services/slots-output.service.ts index 6bfb7261ac..5b00dfea70 100644 --- a/apps/api/v2/src/modules/slots/services/slots-output.service.ts +++ b/apps/api/v2/src/modules/slots/services/slots-output.service.ts @@ -4,8 +4,10 @@ import { DateTime } from "luxon"; import { SlotFormat } from "@calcom/platform-enums"; -type TimeSlots = { slots: Record }; -type RangeSlots = { slots: Record }; +type TimeSlots = { slots: Record }; +type RangeSlots = { + slots: Record; +}; @Injectable() export class SlotsOutputService { @@ -30,6 +32,8 @@ export class SlotsOutputService { const formattedSlots = Object.entries(slots.slots).reduce((acc, [date, daySlots]) => { acc[date] = daySlots.map((slot) => ({ time: DateTime.fromISO(slot.time).setZone(timeZone).toISO() || "unknown-time", + ...(slot.attendees ? { attendees: slot.attendees } : {}), + ...(slot.bookingUid ? { bookingUid: slot.bookingUid } : {}), })); return acc; }, {} as Record); @@ -42,6 +46,8 @@ export class SlotsOutputService { acc[date] = daySlots.map((slot) => ({ startTime: DateTime.fromISO(slot.startTime).setZone(timeZone).toISO() || "unknown-start-time", endTime: DateTime.fromISO(slot.endTime).setZone(timeZone).toISO() || "unknown-end-time", + ...(slot.attendees ? { attendees: slot.attendees } : {}), + ...(slot.bookingUid ? { bookingUid: slot.bookingUid } : {}), })); return acc; }, {} as Record); @@ -62,14 +68,16 @@ export class SlotsOutputService { const slotDuration = await this.getDuration(duration, eventTypeId); const slots = Object.entries(availableSlots.slots).reduce< - Record + Record >((acc, [date, slots]) => { - acc[date] = (slots as { time: string }[]).map((slot) => { + acc[date] = (slots as { time: string; attendees?: number; bookingUid?: string }[]).map((slot) => { const startTime = new Date(slot.time); const endTime = new Date(startTime.getTime() + slotDuration * 60000); return { startTime: startTime.toISOString(), endTime: endTime.toISOString(), + ...(slot.attendees ? { attendees: slot.attendees } : {}), + ...(slot.bookingUid ? { bookingUid: slot.bookingUid } : {}), }; }); return acc; diff --git a/apps/api/v2/test/fixtures/repository/attendee.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/attendee.repository.fixture.ts new file mode 100644 index 0000000000..56b373f68a --- /dev/null +++ b/apps/api/v2/test/fixtures/repository/attendee.repository.fixture.ts @@ -0,0 +1,19 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TestingModule } from "@nestjs/testing"; + +import { Prisma } from "@calcom/prisma/client"; + +export class AttendeeRepositoryFixture { + private prismaReadClient: PrismaReadService["prisma"]; + private prismaWriteClient: PrismaWriteService["prisma"]; + + constructor(private readonly module: TestingModule) { + this.prismaReadClient = module.get(PrismaReadService).prisma; + this.prismaWriteClient = module.get(PrismaWriteService).prisma; + } + + async create(attendee: Prisma.AttendeeCreateInput) { + return this.prismaWriteClient.attendee.create({ data: attendee }); + } +} diff --git a/apps/api/v2/test/fixtures/repository/booking-seat.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/booking-seat.repository.fixture.ts new file mode 100644 index 0000000000..776eb282d4 --- /dev/null +++ b/apps/api/v2/test/fixtures/repository/booking-seat.repository.fixture.ts @@ -0,0 +1,19 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TestingModule } from "@nestjs/testing"; + +import { Prisma } from "@calcom/prisma/client"; + +export class BookingSeatRepositoryFixture { + private prismaReadClient: PrismaReadService["prisma"]; + private prismaWriteClient: PrismaWriteService["prisma"]; + + constructor(private readonly module: TestingModule) { + this.prismaReadClient = module.get(PrismaReadService).prisma; + this.prismaWriteClient = module.get(PrismaWriteService).prisma; + } + + async create(bookingSeat: Prisma.BookingSeatCreateInput) { + return this.prismaWriteClient.bookingSeat.create({ data: bookingSeat }); + } +}