fix: v2 filter out ooo days out of slots (#20303)

* refactor: slots input service rely on eventTypeId and set start hours if none set

* fix: filter out ooo slots

* test: ooo days filtered out of slots

* revert changes
This commit is contained in:
Lauris Skraucis
2025-04-02 11:53:52 -03:00
committed by GitHub
parent e76bb37f78
commit 0965fad72a
4 changed files with 196 additions and 71 deletions
@@ -29,6 +29,7 @@ import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-s
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OOORepositoryFixture } from "test/fixtures/repository/ooo.repository.fixture";
import { SelectedSlotsRepositoryFixture } from "test/fixtures/repository/selected-slots.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
@@ -56,6 +57,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
let apiKeyString: string;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let oooRepositoryFixture: OOORepositoryFixture;
const userEmail = `slots-2024-09-04-user-${randomString()}@example.com`;
let user: User;
@@ -80,6 +82,8 @@ describe("Slots 2024-09-04 Endpoints", () => {
let reservedSlot: ReserveSlotOutputData_2024_09_04;
const oooTestUserEmail = `oooTestUser-${randomString()}@cal.com`;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
@@ -107,6 +111,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
oooRepositoryFixture = new OOORepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
@@ -1344,8 +1349,76 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
});
describe("out of office", () => {
let oooTestUser: User;
let oooTestUserEventType: EventType;
let oooEntryId: number;
beforeAll(async () => {
oooTestUser = await userRepositoryFixture.create({
email: oooTestUserEmail,
name: oooTestUserEmail,
username: oooTestUserEmail,
});
const oooUserSchedule: CreateScheduleInput_2024_06_11 = {
name: `slots-2024-09-04-schedule-${randomString()}`,
timeZone: "Europe/Rome",
isDefault: true,
};
// note(Lauris): this creates default schedule monday to friday from 9AM to 5PM in Europe/Rome timezone
await schedulesService.createUserSchedule(oooTestUser.id, oooUserSchedule);
const event = await eventTypesRepositoryFixture.create(
{ title: "frisbee match", slug: `slots-2024-09-04-event-type-${randomString()}`, length: 60 },
oooTestUser.id
);
oooTestUserEventType = event;
});
it("should not returns slots for ooo days", async () => {
const oooStart = new Date("2050-09-06T00:00:00.000Z");
const oooEnd = new Date("2050-09-09T23:59:59.999Z");
const oooEntry = await oooRepositoryFixture.create({
uuid: randomString(),
start: oooStart,
end: oooEnd,
user: { connect: { id: oooTestUser.id } },
toUser: { connect: { id: oooTestUser.id } },
createdAt: new Date(),
reason: {
connect: {
id: 1,
},
},
});
oooEntryId = oooEntry.id;
const response = await request(app.getHttpServer())
.get(`/v2/slots?eventTypeId=${oooTestUserEventType.id}&start=2050-09-05&end=2050-09-09&duration=60`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
.expect(200);
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const slots = responseBody.data;
expect(slots).toBeDefined();
const days = Object.keys(slots);
expect(days.length).toBe(1);
expect(slots).toEqual({
"2050-09-05": expectedSlotsUTC["2050-09-05"],
});
await oooRepositoryFixture.delete(oooEntryId);
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await userRepositoryFixture.deleteByEmail(oooTestUserEmail);
await userRepositoryFixture.deleteByEmail(unrelatedUser.email);
await selectedSlotsRepositoryFixture.deleteByUId(reservedSlot.reservationUid);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
@@ -24,7 +24,7 @@ export class SlotsInputService_2024_09_04 {
}
const isTeamEvent = !!eventType?.teamId;
const startTime = query.start;
const startTime = this.adjustStartTime(query.start);
const endTime = this.adjustEndTime(query.end);
const duration = query.duration;
const eventTypeId = eventType.id;
@@ -82,6 +82,20 @@ export class SlotsInputService_2024_09_04 {
}
}
private adjustStartTime(startTime: string) {
let dateTime = DateTime.fromISO(startTime, { zone: "utc" });
if (dateTime.hour === 0 && dateTime.minute === 0 && dateTime.second === 0) {
dateTime = dateTime.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
}
const ISOStartTime = dateTime.toISO();
if (ISOStartTime === null) {
throw new BadRequestException("Invalid start date");
}
return ISOStartTime;
}
private adjustEndTime(endTime: string) {
let dateTime = DateTime.fromISO(endTime, { zone: "utc" });
if (dateTime.hour === 0 && dateTime.minute === 0 && dateTime.second === 0) {
@@ -16,7 +16,7 @@ import {
import { SelectedSlots } from "@calcom/prisma/client";
type GetAvailableSlots = {
slots: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>;
slots: Record<string, { time: string; attendees?: number; bookingUid?: string; away?: boolean }[]>;
};
@Injectable()
@@ -46,34 +46,37 @@ export class SlotsOutputService_2024_09_04 {
const slots: { [key: string]: (Slot_2024_09_04 | SeatedSlot_2024_09_04)[] } = {};
for (const date in availableSlots.slots) {
slots[date] = availableSlots.slots[date].map((slot) => {
if (!timeZone) {
const availableTimeSlots = availableSlots.slots[date].filter((slot) => !slot.away);
if (availableTimeSlots.length > 0) {
slots[date] = availableTimeSlots.map((slot) => {
if (!timeZone) {
if (!eventType?.seatsPerTimeSlot) {
return this.getAvailableTimeSlot(slot.time);
}
return this.getAvailableTimeSlotSeated(
slot.time,
slot.attendees || 0,
eventType.seatsPerTimeSlot || 0,
slot.bookingUid
);
}
const slotTimezoneAdjusted = DateTime.fromISO(slot.time, { zone: "utc" }).setZone(timeZone).toISO();
if (!slotTimezoneAdjusted) {
throw new BadRequestException(
`Could not adjust timezone for slot ${slot.time} with timezone ${timeZone}`
);
}
if (!eventType?.seatsPerTimeSlot) {
return this.getAvailableTimeSlot(slot.time);
return this.getAvailableTimeSlot(slotTimezoneAdjusted);
}
return this.getAvailableTimeSlotSeated(
slot.time,
slotTimezoneAdjusted,
slot.attendees || 0,
eventType.seatsPerTimeSlot || 0,
slot.bookingUid
);
}
const slotTimezoneAdjusted = DateTime.fromISO(slot.time, { zone: "utc" }).setZone(timeZone).toISO();
if (!slotTimezoneAdjusted) {
throw new BadRequestException(
`Could not adjust timezone for slot ${slot.time} with timezone ${timeZone}`
);
}
if (!eventType?.seatsPerTimeSlot) {
return this.getAvailableTimeSlot(slotTimezoneAdjusted);
}
return this.getAvailableTimeSlotSeated(
slotTimezoneAdjusted,
slot.attendees || 0,
eventType.seatsPerTimeSlot || 0,
slot.bookingUid
);
});
});
}
}
return slots;
@@ -113,56 +116,59 @@ export class SlotsOutputService_2024_09_04 {
const slots = Object.entries(availableSlots.slots).reduce<
Record<string, (RangeSlot_2024_09_04 | SeatedRangeSlot_2024_09_04)[]>
>((acc, [date, slots]) => {
acc[date] = slots.map((slot) => {
if (timeZone) {
const start = DateTime.fromISO(slot.time, { zone: "utc" }).setZone(timeZone).toISO();
if (!start) {
throw new BadRequestException(
`Could not adjust timezone for slot ${slot.time} with timezone ${timeZone}`
const availableTimeSlots = slots.filter((slot) => !slot.away);
if (availableTimeSlots.length > 0) {
acc[date] = availableTimeSlots.map((slot) => {
if (timeZone) {
const start = DateTime.fromISO(slot.time, { zone: "utc" }).setZone(timeZone).toISO();
if (!start) {
throw new BadRequestException(
`Could not adjust timezone for slot ${slot.time} with timezone ${timeZone}`
);
}
const end = DateTime.fromISO(slot.time, { zone: "utc" })
.plus({ minutes: slotDuration })
.setZone(timeZone)
.toISO();
if (!end) {
throw new BadRequestException(
`Could not adjust timezone for slot end time ${slot.time} with timezone ${timeZone}`
);
}
if (!eventType?.seatsPerTimeSlot) {
return this.getAvailableRangeSlot(start, end);
}
return this.getAvailableRangeSlotSeated(
start,
end,
slot.attendees || 0,
eventType.seatsPerTimeSlot ?? undefined,
slot.bookingUid
);
} else {
const start = DateTime.fromISO(slot.time, { zone: "utc" }).toISO();
const end = DateTime.fromISO(slot.time, { zone: "utc" }).plus({ minutes: slotDuration }).toISO();
if (!start || !end) {
throw new BadRequestException(`Could not create UTC time for slot ${slot.time}`);
}
if (!eventType?.seatsPerTimeSlot) {
return this.getAvailableRangeSlot(start, end);
}
return this.getAvailableRangeSlotSeated(
start,
end,
slot.attendees || 0,
eventType.seatsPerTimeSlot ?? undefined,
slot.bookingUid
);
}
const end = DateTime.fromISO(slot.time, { zone: "utc" })
.plus({ minutes: slotDuration })
.setZone(timeZone)
.toISO();
if (!end) {
throw new BadRequestException(
`Could not adjust timezone for slot end time ${slot.time} with timezone ${timeZone}`
);
}
if (!eventType?.seatsPerTimeSlot) {
return this.getAvailableRangeSlot(start, end);
}
return this.getAvailableRangeSlotSeated(
start,
end,
slot.attendees || 0,
eventType.seatsPerTimeSlot ?? undefined,
slot.bookingUid
);
} else {
const start = DateTime.fromISO(slot.time, { zone: "utc" }).toISO();
const end = DateTime.fromISO(slot.time, { zone: "utc" }).plus({ minutes: slotDuration }).toISO();
if (!start || !end) {
throw new BadRequestException(`Could not create UTC time for slot ${slot.time}`);
}
if (!eventType?.seatsPerTimeSlot) {
return this.getAvailableRangeSlot(start, end);
}
return this.getAvailableRangeSlotSeated(
start,
end,
slot.attendees || 0,
eventType.seatsPerTimeSlot ?? undefined,
slot.bookingUid
);
}
});
});
}
return acc;
}, {});
@@ -0,0 +1,32 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TestingModule } from "@nestjs/testing";
import { PlatformOAuthClient, Prisma } from "@prisma/client";
import { CreateOAuthClientInput } from "@calcom/platform-types";
export class OOORepositoryFixture {
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(data: Prisma.OutOfOfficeEntryCreateInput) {
return this.prismaWriteClient.outOfOfficeEntry.create({
data: {
...data,
},
});
}
async delete(id: number) {
return this.prismaWriteClient.outOfOfficeEntry.delete({
where: {
id,
},
});
}
}