fix: v2 slots timeZone parameter (#18488)

* fix: v2 slots timeZone parameter

* test: v2 slots
This commit is contained in:
Lauris Skraucis authored and GitHub committed 2025-01-07 15:24:20 +05:30
1 parent 408aee5851
commit 9159c591ff
10 files changed
+638 -66

No files matched your search

@@ -0,0 +1,490 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SlotsModule } from "@/modules/slots/slots.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
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";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomNumber } from "test/utils/randomNumber";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
const expectedSlotsUTC = {
slots: {
"2050-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" },
{ time: "2050-09-05T12:00:00.000Z" },
{ time: "2050-09-05T13:00:00.000Z" },
{ time: "2050-09-05T14:00:00.000Z" },
],
"2050-09-06": [
{ time: "2050-09-06T07:00:00.000Z" },
{ time: "2050-09-06T08:00:00.000Z" },
{ time: "2050-09-06T09:00:00.000Z" },
{ time: "2050-09-06T10:00:00.000Z" },
{ time: "2050-09-06T11:00:00.000Z" },
{ time: "2050-09-06T12:00:00.000Z" },
{ time: "2050-09-06T13:00:00.000Z" },
{ time: "2050-09-06T14:00:00.000Z" },
],
"2050-09-07": [
{ time: "2050-09-07T07:00:00.000Z" },
{ time: "2050-09-07T08:00:00.000Z" },
{ time: "2050-09-07T09:00:00.000Z" },
{ time: "2050-09-07T10:00:00.000Z" },
{ time: "2050-09-07T11:00:00.000Z" },
{ time: "2050-09-07T12:00:00.000Z" },
{ time: "2050-09-07T13:00:00.000Z" },
{ time: "2050-09-07T14:00:00.000Z" },
],
"2050-09-08": [
{ time: "2050-09-08T07:00:00.000Z" },
{ time: "2050-09-08T08:00:00.000Z" },
{ time: "2050-09-08T09:00:00.000Z" },
{ time: "2050-09-08T10:00:00.000Z" },
{ time: "2050-09-08T11:00:00.000Z" },
{ time: "2050-09-08T12:00:00.000Z" },
{ time: "2050-09-08T13:00:00.000Z" },
{ time: "2050-09-08T14:00:00.000Z" },
],
"2050-09-09": [
{ time: "2050-09-09T07:00:00.000Z" },
{ time: "2050-09-09T08:00:00.000Z" },
{ time: "2050-09-09T09:00:00.000Z" },
{ time: "2050-09-09T10:00:00.000Z" },
{ time: "2050-09-09T11:00:00.000Z" },
{ time: "2050-09-09T12:00:00.000Z" },
{ time: "2050-09-09T13:00:00.000Z" },
{ time: "2050-09-09T14:00:00.000Z" },
],
},
};
const expectedSlotsRome = {
slots: {
"2050-09-05": [
{ time: "2050-09-05T09:00:00.000+02:00" },
{ time: "2050-09-05T10:00:00.000+02:00" },
{ time: "2050-09-05T11:00:00.000+02:00" },
{ time: "2050-09-05T12:00:00.000+02:00" },
{ time: "2050-09-05T13:00:00.000+02:00" },
{ time: "2050-09-05T14:00:00.000+02:00" },
{ time: "2050-09-05T15:00:00.000+02:00" },
{ time: "2050-09-05T16:00:00.000+02:00" },
],
"2050-09-06": [
{ time: "2050-09-06T09:00:00.000+02:00" },
{ time: "2050-09-06T10:00:00.000+02:00" },
{ time: "2050-09-06T11:00:00.000+02:00" },
{ time: "2050-09-06T12:00:00.000+02:00" },
{ time: "2050-09-06T13:00:00.000+02:00" },
{ time: "2050-09-06T14:00:00.000+02:00" },
{ time: "2050-09-06T15:00:00.000+02:00" },
{ time: "2050-09-06T16:00:00.000+02:00" },
],
"2050-09-07": [
{ time: "2050-09-07T09:00:00.000+02:00" },
{ time: "2050-09-07T10:00:00.000+02:00" },
{ time: "2050-09-07T11:00:00.000+02:00" },
{ time: "2050-09-07T12:00:00.000+02:00" },
{ time: "2050-09-07T13:00:00.000+02:00" },
{ time: "2050-09-07T14:00:00.000+02:00" },
{ time: "2050-09-07T15:00:00.000+02:00" },
{ time: "2050-09-07T16:00:00.000+02:00" },
],
"2050-09-08": [
{ time: "2050-09-08T09:00:00.000+02:00" },
{ time: "2050-09-08T10:00:00.000+02:00" },
{ time: "2050-09-08T11:00:00.000+02:00" },
{ time: "2050-09-08T12:00:00.000+02:00" },
{ time: "2050-09-08T13:00:00.000+02:00" },
{ time: "2050-09-08T14:00:00.000+02:00" },
{ time: "2050-09-08T15:00:00.000+02:00" },
{ time: "2050-09-08T16:00:00.000+02:00" },
],
"2050-09-09": [
{ time: "2050-09-09T09:00:00.000+02:00" },
{ time: "2050-09-09T10:00:00.000+02:00" },
{ time: "2050-09-09T11:00:00.000+02:00" },
{ time: "2050-09-09T12:00:00.000+02:00" },
{ time: "2050-09-09T13:00:00.000+02:00" },
{ time: "2050-09-09T14:00:00.000+02:00" },
{ time: "2050-09-09T15:00:00.000+02:00" },
{ time: "2050-09-09T16:00:00.000+02:00" },
],
},
};
const expectedSlotsUTCRange = {
slots: {
"2050-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" },
{ 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" },
],
"2050-09-06": [
{ startTime: "2050-09-06T07:00:00.000Z", endTime: "2050-09-06T08:00:00.000Z" },
{ startTime: "2050-09-06T08:00:00.000Z", endTime: "2050-09-06T09:00:00.000Z" },
{ startTime: "2050-09-06T09:00:00.000Z", endTime: "2050-09-06T10:00:00.000Z" },
{ startTime: "2050-09-06T10:00:00.000Z", endTime: "2050-09-06T11:00:00.000Z" },
{ startTime: "2050-09-06T11:00:00.000Z", endTime: "2050-09-06T12:00:00.000Z" },
{ startTime: "2050-09-06T12:00:00.000Z", endTime: "2050-09-06T13:00:00.000Z" },
{ startTime: "2050-09-06T13:00:00.000Z", endTime: "2050-09-06T14:00:00.000Z" },
{ startTime: "2050-09-06T14:00:00.000Z", endTime: "2050-09-06T15:00:00.000Z" },
],
"2050-09-07": [
{ startTime: "2050-09-07T07:00:00.000Z", endTime: "2050-09-07T08:00:00.000Z" },
{ startTime: "2050-09-07T08:00:00.000Z", endTime: "2050-09-07T09:00:00.000Z" },
{ startTime: "2050-09-07T09:00:00.000Z", endTime: "2050-09-07T10:00:00.000Z" },
{ startTime: "2050-09-07T10:00:00.000Z", endTime: "2050-09-07T11:00:00.000Z" },
{ startTime: "2050-09-07T11:00:00.000Z", endTime: "2050-09-07T12:00:00.000Z" },
{ startTime: "2050-09-07T12:00:00.000Z", endTime: "2050-09-07T13:00:00.000Z" },
{ startTime: "2050-09-07T13:00:00.000Z", endTime: "2050-09-07T14:00:00.000Z" },
{ startTime: "2050-09-07T14:00:00.000Z", endTime: "2050-09-07T15:00:00.000Z" },
],
"2050-09-08": [
{ startTime: "2050-09-08T07:00:00.000Z", endTime: "2050-09-08T08:00:00.000Z" },
{ startTime: "2050-09-08T08:00:00.000Z", endTime: "2050-09-08T09:00:00.000Z" },
{ startTime: "2050-09-08T09:00:00.000Z", endTime: "2050-09-08T10:00:00.000Z" },
{ startTime: "2050-09-08T10:00:00.000Z", endTime: "2050-09-08T11:00:00.000Z" },
{ startTime: "2050-09-08T11:00:00.000Z", endTime: "2050-09-08T12:00:00.000Z" },
{ startTime: "2050-09-08T12:00:00.000Z", endTime: "2050-09-08T13:00:00.000Z" },
{ startTime: "2050-09-08T13:00:00.000Z", endTime: "2050-09-08T14:00:00.000Z" },
{ startTime: "2050-09-08T14:00:00.000Z", endTime: "2050-09-08T15:00:00.000Z" },
],
"2050-09-09": [
{ startTime: "2050-09-09T07:00:00.000Z", endTime: "2050-09-09T08:00:00.000Z" },
{ startTime: "2050-09-09T08:00:00.000Z", endTime: "2050-09-09T09:00:00.000Z" },
{ startTime: "2050-09-09T09:00:00.000Z", endTime: "2050-09-09T10:00:00.000Z" },
{ startTime: "2050-09-09T10:00:00.000Z", endTime: "2050-09-09T11:00:00.000Z" },
{ startTime: "2050-09-09T11:00:00.000Z", endTime: "2050-09-09T12:00:00.000Z" },
{ startTime: "2050-09-09T12:00:00.000Z", endTime: "2050-09-09T13:00:00.000Z" },
{ startTime: "2050-09-09T13:00:00.000Z", endTime: "2050-09-09T14:00:00.000Z" },
{ startTime: "2050-09-09T14:00:00.000Z", endTime: "2050-09-09T15:00:00.000Z" },
],
},
};
const expectedSlotsRomeRange = {
slots: {
"2050-09-05": [
{ startTime: "2050-09-05T09:00:00.000+02:00", endTime: "2050-09-05T10:00:00.000+02:00" },
{ startTime: "2050-09-05T10:00:00.000+02:00", endTime: "2050-09-05T11:00:00.000+02:00" },
{ startTime: "2050-09-05T11:00:00.000+02:00", endTime: "2050-09-05T12:00:00.000+02:00" },
{ startTime: "2050-09-05T12:00:00.000+02:00", endTime: "2050-09-05T13:00:00.000+02:00" },
{ startTime: "2050-09-05T13:00:00.000+02:00", endTime: "2050-09-05T14:00:00.000+02:00" },
{ startTime: "2050-09-05T14:00:00.000+02:00", endTime: "2050-09-05T15:00:00.000+02:00" },
{ startTime: "2050-09-05T15:00:00.000+02:00", endTime: "2050-09-05T16:00:00.000+02:00" },
{ startTime: "2050-09-05T16:00:00.000+02:00", endTime: "2050-09-05T17:00:00.000+02:00" },
],
"2050-09-06": [
{ startTime: "2050-09-06T09:00:00.000+02:00", endTime: "2050-09-06T10:00:00.000+02:00" },
{ startTime: "2050-09-06T10:00:00.000+02:00", endTime: "2050-09-06T11:00:00.000+02:00" },
{ startTime: "2050-09-06T11:00:00.000+02:00", endTime: "2050-09-06T12:00:00.000+02:00" },
{ startTime: "2050-09-06T12:00:00.000+02:00", endTime: "2050-09-06T13:00:00.000+02:00" },
{ startTime: "2050-09-06T13:00:00.000+02:00", endTime: "2050-09-06T14:00:00.000+02:00" },
{ startTime: "2050-09-06T14:00:00.000+02:00", endTime: "2050-09-06T15:00:00.000+02:00" },
{ startTime: "2050-09-06T15:00:00.000+02:00", endTime: "2050-09-06T16:00:00.000+02:00" },
{ startTime: "2050-09-06T16:00:00.000+02:00", endTime: "2050-09-06T17:00:00.000+02:00" },
],
"2050-09-07": [
{ startTime: "2050-09-07T09:00:00.000+02:00", endTime: "2050-09-07T10:00:00.000+02:00" },
{ startTime: "2050-09-07T10:00:00.000+02:00", endTime: "2050-09-07T11:00:00.000+02:00" },
{ startTime: "2050-09-07T11:00:00.000+02:00", endTime: "2050-09-07T12:00:00.000+02:00" },
{ startTime: "2050-09-07T12:00:00.000+02:00", endTime: "2050-09-07T13:00:00.000+02:00" },
{ startTime: "2050-09-07T13:00:00.000+02:00", endTime: "2050-09-07T14:00:00.000+02:00" },
{ startTime: "2050-09-07T14:00:00.000+02:00", endTime: "2050-09-07T15:00:00.000+02:00" },
{ startTime: "2050-09-07T15:00:00.000+02:00", endTime: "2050-09-07T16:00:00.000+02:00" },
{ startTime: "2050-09-07T16:00:00.000+02:00", endTime: "2050-09-07T17:00:00.000+02:00" },
],
"2050-09-08": [
{ startTime: "2050-09-08T09:00:00.000+02:00", endTime: "2050-09-08T10:00:00.000+02:00" },
{ startTime: "2050-09-08T10:00:00.000+02:00", endTime: "2050-09-08T11:00:00.000+02:00" },
{ startTime: "2050-09-08T11:00:00.000+02:00", endTime: "2050-09-08T12:00:00.000+02:00" },
{ startTime: "2050-09-08T12:00:00.000+02:00", endTime: "2050-09-08T13:00:00.000+02:00" },
{ startTime: "2050-09-08T13:00:00.000+02:00", endTime: "2050-09-08T14:00:00.000+02:00" },
{ startTime: "2050-09-08T14:00:00.000+02:00", endTime: "2050-09-08T15:00:00.000+02:00" },
{ startTime: "2050-09-08T15:00:00.000+02:00", endTime: "2050-09-08T16:00:00.000+02:00" },
{ startTime: "2050-09-08T16:00:00.000+02:00", endTime: "2050-09-08T17:00:00.000+02:00" },
],
"2050-09-09": [
{ startTime: "2050-09-09T09:00:00.000+02:00", endTime: "2050-09-09T10:00:00.000+02:00" },
{ startTime: "2050-09-09T10:00:00.000+02:00", endTime: "2050-09-09T11:00:00.000+02:00" },
{ startTime: "2050-09-09T11:00:00.000+02:00", endTime: "2050-09-09T12:00:00.000+02:00" },
{ startTime: "2050-09-09T12:00:00.000+02:00", endTime: "2050-09-09T13:00:00.000+02:00" },
{ startTime: "2050-09-09T13:00:00.000+02:00", endTime: "2050-09-09T14:00:00.000+02:00" },
{ startTime: "2050-09-09T14:00:00.000+02:00", endTime: "2050-09-09T15:00:00.000+02:00" },
{ startTime: "2050-09-09T15:00:00.000+02:00", endTime: "2050-09-09T16:00:00.000+02:00" },
{ startTime: "2050-09-09T16:00:00.000+02:00", endTime: "2050-09-09T17:00:00.000+02:00" },
],
},
};
describe("Slots Endpoints", () => {
describe("Individual user slots", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let schedulesService: SchedulesService_2024_06_11;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let selectedSlotsRepositoryFixture: SelectedSlotsRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
const userEmail = `slots-${randomNumber()}-controller-e2e@api.com`;
const userName = "bob";
let user: User;
let eventTypeId: number;
let eventTypeSlug: string;
let reservedSlotUid: string;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [
AppModule,
PrismaModule,
UsersModule,
TokensModule,
SchedulesModule_2024_06_11,
SlotsModule,
],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_06_11>(SchedulesService_2024_06_11);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
selectedSlotsRepositoryFixture = new SelectedSlotsRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
name: "bob slot",
username: userName,
});
// nxte(Lauris): this creates default schedule monday to friday from 9AM to 5PM in Europe/Rome timezone
await schedulesService.createUserDefaultSchedule(user.id, "Europe/Rome");
const event = await eventTypesRepositoryFixture.create(
{ title: "frisbee match", slug: "frisbee-match", length: 60 },
user.id
);
eventTypeId = event.id;
eventTypeSlug = event.slug;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should get slots in UTC by event type id", async () => {
return request(app.getHttpServer())
.get(`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10`)
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
status: SUCCESS_STATUS,
data: expectedSlotsUTC,
});
});
});
it("should get slots in specified time zone by event type id", async () => {
return request(app.getHttpServer())
.get(
`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10&timeZone=Europe/Rome`
)
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
status: SUCCESS_STATUS,
data: expectedSlotsRome,
});
});
});
it("should get slots in UTC by event type id in range format", async () => {
return request(app.getHttpServer())
.get(
`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10&slotFormat=range`
)
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
status: SUCCESS_STATUS,
data: expectedSlotsUTCRange,
});
});
});
it("should get slots in specified time zone by event type id in range format", async () => {
return request(app.getHttpServer())
.get(
`/api/v2/slots/available?eventTypeId=${eventTypeId}&startTime=2050-09-05&endTime=2050-09-10&timeZone=Europe/Rome&slotFormat=range`
)
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
status: SUCCESS_STATUS,
data: expectedSlotsRomeRange,
});
});
});
it("should get slots in UTC by event type slug", async () => {
return request(app.getHttpServer())
.get(
`/api/v2/slots/available?eventTypeSlug=${eventTypeSlug}&usernameList[]=${userName}&startTime=2050-09-05&endTime=2050-09-10`
)
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
status: SUCCESS_STATUS,
data: expectedSlotsUTC,
});
});
});
it("should get slots in specified time zone by event type slug", async () => {
return request(app.getHttpServer())
.get(
`/api/v2/slots/available?eventTypeSlug=${eventTypeSlug}&usernameList[]=${userName}&startTime=2050-09-05&endTime=2050-09-10&timeZone=Europe/Rome`
)
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
status: SUCCESS_STATUS,
data: expectedSlotsRome,
});
});
});
it("should reserve a slot and it should not appear in available slots", async () => {
const slotStartTime = "2050-09-05T10:00:00.000Z";
const reserveResponse = await request(app.getHttpServer())
.post(`/api/v2/slots/reserve`)
.send({
eventTypeId,
slotUtcStartDate: slotStartTime,
slotUtcEndDate: "2050-09-05T11:00:00.000Z",
})
.expect(201);
const reserveResponseBody = reserveResponse.body;
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
const uid: string = reserveResponseBody.data;
expect(uid).toBeDefined();
if (!uid) {
throw new Error("Reserved slot uid is undefined");
}
reservedSlotUid = uid;
const response = await request(app.getHttpServer())
.get(`/api/v2/slots/available?eventTypeId=${eventTypeId}&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 = expectedSlotsUTC.slots["2050-09-05"].filter(
(slot) => slot.time !== slotStartTime
);
expect(slots).toEqual({
slots: { ...expectedSlotsUTC.slots, "2050-09-05": expectedSlotsUTC2050_09_05 },
});
});
it("should delete reserved slot", async () => {
await request(app.getHttpServer())
.delete(`/api/v2/slots/selected-slot?uid=${reservedSlotUid}`)
.expect(200);
});
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({
uid: `booking-uid-${eventTypeId}`,
title: "booking title",
startTime,
endTime: "2050-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
metadata: {},
responses: {
name: "tester",
email: "tester@example.com",
guests: [],
},
user: {
connect: {
id: user.id,
},
},
});
const response = await request(app.getHttpServer())
.get(`/api/v2/slots/available?eventTypeId=${eventTypeId}&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 = expectedSlotsUTC.slots["2050-09-05"].filter(
(slot) => slot.time !== startTime
);
expect(slots).toEqual({
slots: { ...expectedSlotsUTC.slots, "2050-09-05": expectedSlotsUTC2050_09_05 },
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await selectedSlotsRepositoryFixture.deleteByUId(reservedSlotUid);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
await app.close();
});
});
});
@@ -1,11 +1,11 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { SlotsOutputService } from "@/modules/slots/services/slots-output.service";
import { SlotsService } from "@/modules/slots/services/slots.service";
import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common";
import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SlotFormat } from "@calcom/platform-enums";
import { getAvailableSlots } from "@calcom/platform-libraries";
import type { AvailableSlotsType } from "@calcom/platform-libraries";
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
@@ -17,7 +17,10 @@ import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
})
@DocsTags("Slots")
export class SlotsController {
constructor(private readonly slotsService: SlotsService) {}
constructor(
private readonly slotsService: SlotsService,
private readonly slotsOutputService: SlotsOutputService
) {}
@Post("/reserve")
@ApiCreatedResponse({
@@ -159,19 +162,17 @@ export class SlotsController {
},
});
const transformedSlots =
query.slotFormat === SlotFormat.Range
? await this.slotsService.formatSlots(
availableSlots,
query.duration,
query.eventTypeId,
query.slotFormat
)
: availableSlots.slots;
const { slots } = await this.slotsOutputService.getOutputSlots(
availableSlots,
query.duration,
query.eventTypeId,
query.slotFormat,
query.timeZone
);
return {
data: {
slots: transformedSlots,
slots,
},
status: SUCCESS_STATUS,
};
@@ -0,0 +1,96 @@
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { Injectable, BadRequestException } from "@nestjs/common";
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 }[]> };
@Injectable()
export class SlotsOutputService {
constructor(private readonly eventTypesRepository: EventTypesRepository_2024_04_15) {}
async getOutputSlots(
availableSlots: TimeSlots,
duration?: number,
eventTypeId?: number,
slotFormat?: SlotFormat,
timeZone?: string
): Promise<TimeSlots | RangeSlots> {
if (!slotFormat) {
return timeZone ? this.setTimeZone(availableSlots, timeZone) : availableSlots;
}
const formattedSlots = await this.formatSlots(availableSlots, duration, eventTypeId, slotFormat);
return timeZone ? this.setTimeZoneRange(formattedSlots, timeZone) : formattedSlots;
}
private setTimeZone(slots: TimeSlots, timeZone: string): TimeSlots {
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",
}));
return acc;
}, {} as Record<string, { time: string }[]>);
return { slots: formattedSlots };
}
private setTimeZoneRange(slots: RangeSlots, timeZone: string): RangeSlots {
const formattedSlots = Object.entries(slots.slots).reduce((acc, [date, daySlots]) => {
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",
}));
return acc;
}, {} as Record<string, { startTime: string; endTime: string }[]>);
return { slots: formattedSlots };
}
private async formatSlots(
availableSlots: TimeSlots,
duration?: number,
eventTypeId?: number,
slotFormat?: SlotFormat
): Promise<RangeSlots> {
if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) {
throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'");
}
const slotDuration = await this.getDuration(duration, eventTypeId);
const slots = Object.entries(availableSlots.slots).reduce<
Record<string, { startTime: string; endTime: string }[]>
>((acc, [date, slots]) => {
acc[date] = (slots as { time: string }[]).map((slot) => {
const startTime = new Date(slot.time);
const endTime = new Date(startTime.getTime() + slotDuration * 60000);
return {
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
};
});
return acc;
}, {});
return { slots };
}
private async getDuration(duration?: number, eventTypeId?: number): Promise<number> {
if (duration) {
return duration;
}
if (eventTypeId) {
const eventType = await this.eventTypesRepository.getEventTypeWithDuration(eventTypeId);
if (!eventType) {
throw new Error("Event type not found");
}
return eventType.length;
}
throw new Error("duration or eventTypeId is required");
}
}
@@ -1,6 +1,7 @@
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { SlotsRepository } from "@/modules/slots/slots.repository";
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
import { DateTime } from "luxon";
import { v4 as uuid } from "uuid";
import { SlotFormat } from "@calcom/platform-enums";
@@ -55,51 +56,4 @@ export class SlotsService {
const event = await this.eventTypeRepo.getEventTypeById(eventTypeId);
return !!event?.teamId;
}
async getEventTypeWithDuration(eventTypeId: number) {
return await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
}
async getDuration(duration?: number, eventTypeId?: number): Promise<number> {
if (duration) {
return duration;
}
if (eventTypeId) {
const eventType = await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
if (!eventType) {
throw new Error("Event type not found");
}
return eventType.length;
}
throw new Error("duration or eventTypeId is required");
}
async formatSlots(
availableSlots: { slots: Record<string, { time: string }[]> },
duration?: number,
eventTypeId?: number,
slotFormat?: SlotFormat
): Promise<Record<string, { startTime: string; endTime: string }[]>> {
if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) {
throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'");
}
const slotDuration = await this.getDuration(duration, eventTypeId);
return Object.entries(availableSlots.slots).reduce<
Record<string, { startTime: string; endTime: string }[]>
>((acc, [date, slots]) => {
acc[date] = (slots as { time: string }[]).map((slot) => {
const startTime = new Date(slot.time);
const endTime = new Date(startTime.getTime() + slotDuration * 60000);
return {
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
};
});
return acc;
}, {});
}
}
@@ -1,13 +1,14 @@
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SlotsController } from "@/modules/slots/controllers/slots.controller";
import { SlotsOutputService } from "@/modules/slots/services/slots-output.service";
import { SlotsService } from "@/modules/slots/services/slots.service";
import { SlotsRepository } from "@/modules/slots/slots.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, EventTypesModule_2024_04_15],
providers: [SlotsRepository, SlotsService],
providers: [SlotsRepository, SlotsService, SlotsOutputService],
controllers: [SlotsController],
exports: [SlotsService],
})
+3 -2
View File
@@ -5139,7 +5139,7 @@
"name": "eventTypeSlug",
"required": false,
"in": "query",
"description": "Slug of the event type for which slots are being fetched.",
"description": "Slug of the event type for which slots are being fetched. If event slug is provided then username must be provided too as query parameter `usernameList[]=username`",
"schema": {
"type": "string"
}
@@ -5148,7 +5148,8 @@
"name": "usernameList",
"required": false,
"in": "query",
"description": "Only for dynamic events - list of usernames for which slots are being fetched.",
"description": "Only if eventTypeSlug is provided or for dynamic events - list of usernames for which slots are being fetched.",
"example": "usernameList[]=bob",
"schema": {
"type": "array",
"items": {
@@ -38,6 +38,11 @@ export class EventTypesRepositoryFixture {
return this.prismaWriteClient.eventType.create({
data: {
...data,
users: {
connect: {
id: userId,
},
},
owner: {
connect: {
id: userId,
@@ -0,0 +1,18 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TestingModule } from "@nestjs/testing";
import { SelectedSlots } from "@prisma/client";
export class SelectedSlotsRepositoryFixture {
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 deleteByUId(uid: SelectedSlots["uid"]) {
return this.prismaWriteClient.selectedSlots.deleteMany({ where: { uid } });
}
}
+3 -2
View File
@@ -4879,7 +4879,7 @@
"name": "eventTypeSlug",
"required": false,
"in": "query",
"description": "Slug of the event type for which slots are being fetched.",
"description": "Slug of the event type for which slots are being fetched. If event slug is provided then username must be provided too as query parameter `usernameList[]=username`",
"schema": {
"type": "string"
}
@@ -4888,7 +4888,8 @@
"name": "usernameList",
"required": false,
"in": "query",
"description": "Only for dynamic events - list of usernames for which slots are being fetched.",
"description": "Only if eventTypeSlug is provided or for dynamic events - list of usernames for which slots are being fetched.",
"example": "usernameList[]=bob",
"schema": {
"type": "array",
"items": {
+7 -2
View File
@@ -37,7 +37,10 @@ export class GetAvailableSlotsInput {
@IsString()
@IsOptional()
@ApiPropertyOptional({ description: "Slug of the event type for which slots are being fetched." })
@ApiPropertyOptional({
description:
"Slug of the event type for which slots are being fetched. If event slug is provided then username must be provided too as query parameter `usernameList[]=username`",
})
eventTypeSlug?: string;
@IsArray()
@@ -45,7 +48,9 @@ export class GetAvailableSlotsInput {
@IsOptional()
@ApiPropertyOptional({
type: [String],
description: "Only for dynamic events - list of usernames for which slots are being fetched.",
description:
"Only if eventTypeSlug is provided or for dynamic events - list of usernames for which slots are being fetched.",
example: "usernameList[]=bob",
})
usernameList?: string[];