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:
+73
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user