fix: v2 slots timeZone parameter (#18488)
* fix: v2 slots timeZone parameter * test: v2 slots
This commit is contained in:
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],
|
||||
})
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
Reference in new issue
Block a user