Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: v2 slots not returning attendees and bookingUid info #18858

Merged
merged 9 commits into from
Feb 12, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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()}[email protected]`;
const userName = "bob";
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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: "[email protected]",
guests: [],
},
user: {
connect: {
id: user.id,
},
},
});

const attendee = await attendeesRepositoryFixture.create({
name: "tester",
email: "[email protected]",
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: "[email protected]",
guests: [],
},
user: {
connect: {
id: user.id,
},
},
});

const attendee = await attendeesRepositoryFixture.create({
name: "tester",
email: "[email protected]",
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 () => {
Expand Down
16 changes: 12 additions & 4 deletions apps/api/v2/src/modules/slots/services/slots-output.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { DateTime } from "luxon";

import { SlotFormat } from "@calcom/platform-enums";

type TimeSlots = { slots: Record<string, { time: string }[]> };
type RangeSlots = { slots: Record<string, { startTime: string; endTime: string }[]> };
type TimeSlots = { slots: Record<string, { time: string; attendees?: number; bookingUid?: string }[]> };
type RangeSlots = {
slots: Record<string, { startTime: string; endTime: string; attendees?: number; bookingUid?: string }[]>;
};

@Injectable()
export class SlotsOutputService {
Expand All @@ -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<string, { time: string }[]>);
Expand All @@ -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<string, { startTime: string; endTime: string }[]>);
Expand All @@ -62,14 +68,16 @@ export class SlotsOutputService {
const slotDuration = await this.getDuration(duration, eventTypeId);

const slots = Object.entries(availableSlots.slots).reduce<
Record<string, { startTime: string; endTime: string }[]>
Record<string, { startTime: string; endTime: string; attendees?: number; bookingUid?: string }[]>
>((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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 });
}
}
Original file line number Diff line number Diff line change
@@ -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 });
}
}
Loading