Skip to content

Commit

Permalink
chore: slots input service
Browse files Browse the repository at this point in the history
  • Loading branch information
supalarry committed Jan 21, 2025
1 parent fff2435 commit dfb2f84
Show file tree
Hide file tree
Showing 6 changed files with 646 additions and 2,417 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ export class SlotsController_2024_09_04 {
description: `
There are 3 ways to get available slots:
1. By event type id. Example '/api/v2/slots/available?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'
1. By event type id. Event type id can be of user and team event types. Example '/api/v2/slots/available?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'
2. By event type slug + username. Example '/api/v2/slots/available?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'
3. By usernames (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/api/v2/slots/available?usernames=alice,bob&username=bob&start=2050-09-05&end=2050-09-06&duration=60'
3. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/api/v2/slots/available?usernames=alice,bob&username=bob&start=2050-09-05&end=2050-09-06&duration=60'
All of them require "start" and "end" query parameters which define the time range for which available slots should be checked.
Optional parameters are:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { DateTime } from "luxon";

import { dynamicEvent } from "@calcom/platform-libraries";
import { GetSlotsInput_2024_09_04 } from "@calcom/platform-types";

@Injectable()
export class SlotsInputService_2024_09_04 {
constructor(
private readonly eventTypeRepository: EventTypesRepository_2024_06_14,
private readonly usersRepository: UsersRepository
) {}

async transformGetSlotsQuery(query: GetSlotsInput_2024_09_04) {
const eventType = await this.getEventType(query);
if (!eventType) {
throw new NotFoundException(`Event Type not found`);
}
const isTeamEvent = !!eventType?.teamId;

const startTime = query.start;
const endTime = this.adjustEndTime(query.end);
const duration = query.duration;
const eventTypeId = eventType.id;
const eventTypeSlug = eventType.slug;
const usernameList = "usernames" in query ? query.usernames : [];
const timeZone = query.timeZone;

return {
isTeamEvent,
startTime,
endTime,
duration,
eventTypeId,
eventTypeSlug,
usernameList,
timeZone,
};
}

private async getEventType(input: GetSlotsInput_2024_09_04) {
if ("eventTypeId" in input) {
return this.eventTypeRepository.getEventTypeById(input.eventTypeId);
}
if ("eventTypeSlug" in input) {
const user = await this.usersRepository.findByUsername(input.username);
if (!user) {
throw new NotFoundException(`User with username ${input.username} not found`);
}
return this.eventTypeRepository.getUserEventTypeBySlug(user.id, input.eventTypeSlug);
}

return input.duration ? { ...dynamicEvent, length: input.duration } : dynamicEvent;
}

private adjustEndTime(endTime: string) {
let dateTime = DateTime.fromISO(endTime, { zone: "utc" });
if (dateTime.hour === 0 && dateTime.minute === 0 && dateTime.second === 0) {
dateTime = dateTime.set({ hour: 23, minute: 59, second: 59 });
}

return dateTime.toISO();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { SlotsInputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slot-input.service";
import { SlotsOutputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots-output.service";
import { SlotsRepository_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.repository";
import { UsersRepository } from "@/modules/users/users.repository";
Expand All @@ -24,67 +25,31 @@ export class SlotsService_2024_09_04 {
constructor(
private readonly eventTypeRepository: EventTypesRepository_2024_06_14,
private readonly slotsRepository: SlotsRepository_2024_09_04,
private readonly usersRepository: UsersRepository,
private readonly slotsOutputService: SlotsOutputService_2024_09_04
private readonly slotsOutputService: SlotsOutputService_2024_09_04,
private readonly slotsInputService: SlotsInputService_2024_09_04
) {}

async getReservedSlot(uid: string) {
const slot = await this.slotsRepository.getByUid(uid);
if (!slot) {
return null;
}
return this.slotsOutputService.getOutputSlot(slot);
}

async getAvailableSlots(query: GetSlotsInput_2024_09_04) {
const eventType = await this.getEventType(query);
if (!eventType) {
throw new NotFoundException(`Event Type not found`);
}
const isTeamEvent = !!eventType?.teamId;

const startTime = query.start;
const endTime = this.adjustEndTime(query.end);
const duration = query.duration;
const eventTypeId = eventType.id;
const eventTypeSlug = eventType.slug;
const usernameList = "usernames" in query ? query.usernames : [];
const timeZone = query.timeZone;
const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query);

const availableSlots: { slots: { [key: string]: { time: string }[] } } = await getAvailableSlots({
input: {
startTime,
endTime,
duration,
eventTypeId,
eventTypeSlug,
usernameList,
timeZone,
isTeamEvent,
...queryTransformed,
},
ctx: {},
});

const formatted = await this.slotsOutputService.getOutputSlots(
availableSlots,
duration,
eventTypeId,
queryTransformed.duration,
queryTransformed.eventTypeId,
query.format,
timeZone
queryTransformed.timeZone
);

return formatted;
}

adjustEndTime(endTime: string) {
let dateTime = DateTime.fromISO(endTime, { zone: "utc" });
if (dateTime.hour === 0 && dateTime.minute === 0 && dateTime.second === 0) {
dateTime = dateTime.set({ hour: 23, minute: 59, second: 59 });
}

return dateTime.toISO();
}

async reserveSlot(input: ReserveSlotInput_2024_09_04) {
const eventType = await this.eventTypeRepository.getEventTypeWithHosts(input.eventTypeId);
if (!eventType) {
Expand Down Expand Up @@ -161,6 +126,14 @@ export class SlotsService_2024_09_04 {
return this.slotsOutputService.getOutputReservedSlot(slot, input.reservationDuration);
}

async getReservedSlot(uid: string) {
const slot = await this.slotsRepository.getByUid(uid);
if (!slot) {
return null;
}
return this.slotsOutputService.getOutputSlot(slot);
}

async updateReservedSlot(input: ReserveSlotInput_2024_09_04, uid: string) {
const dbSlot = await this.slotsRepository.getByUid(uid);
if (!dbSlot) {
Expand Down Expand Up @@ -245,19 +218,4 @@ export class SlotsService_2024_09_04 {
async deleteReservedSlot(uid: string) {
return this.slotsRepository.deleteSlot(uid);
}

async getEventType(input: GetSlotsInput_2024_09_04) {
if ("eventTypeId" in input) {
return this.eventTypeRepository.getEventTypeById(input.eventTypeId);
}
if ("eventTypeSlug" in input) {
const user = await this.usersRepository.findByUsername(input.username);
if (!user) {
throw new NotFoundException(`User with username ${input.username} not found`);
}
return this.eventTypeRepository.getUserEventTypeBySlug(user.id, input.eventTypeSlug);
}

return input.duration ? { ...dynamicEvent, length: input.duration } : dynamicEvent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_0
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SlotsController_2024_09_04 } from "@/modules/slots/slots-2024-09-04/controllers/slots.controller";
import { SlotsInputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slot-input.service";
import { SlotsOutputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots-output.service";
import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service";
import { SlotsRepository_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.repository";
Expand All @@ -14,6 +15,7 @@ import { Module } from "@nestjs/common";
SlotsRepository_2024_09_04,
SlotsService_2024_09_04,
UsersRepository,
SlotsInputService_2024_09_04,
SlotsOutputService_2024_09_04,
],
controllers: [SlotsController_2024_09_04],
Expand Down
2 changes: 1 addition & 1 deletion apps/api/v2/swagger/documentation.json
Original file line number Diff line number Diff line change
Expand Up @@ -5271,7 +5271,7 @@
"get": {
"operationId": "SlotsController_2024_09_04_getAvailableSlots",
"summary": "Find out when is an event type ready to be booked.",
"description": "\n There are 3 ways to get available slots:\n \n 1. By event type id. Example '/api/v2/slots/available?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/api/v2/slots/available?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By usernames (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/api/v2/slots/available?usernames=alice,bob&username=bob&start=2050-09-05&end=2050-09-06&duration=60'\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ",
"description": "\n There are 3 ways to get available slots:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/api/v2/slots/available?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/api/v2/slots/available?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/api/v2/slots/available?usernames=alice,bob&username=bob&start=2050-09-05&end=2050-09-06&duration=60'\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ",
"parameters": [
{
"name": "cal-api-version",
Expand Down
Loading

0 comments on commit dfb2f84

Please sign in to comment.