From 05e1ad0c91d0856caa6fe759fbd40cdd018d3ed8 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Fri, 18 Jul 2025 16:26:53 +0530 Subject: [PATCH] fix: Code Quality improvements to response record endpoint and added unit tests (#22264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove unnecessary logs and fix documentation * refactor: extract GetSlotsInputWithRouting type and eliminate code duplication - Move GetSlotsInputWithRouting_2024_09_04 type to platform-types package for reuse - Refactor slots service to eliminate duplicate error handling logic - Fix TypeScript errors in slots service tests by adding missing type property - Update test expectations to match new implementation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * refactor: improve type safety in slots service - Export explicit types from slots-input.service for transformed queries - Replace 'any' type with proper TransformedSlotsQuery union type - Re-implement fetchAndFormatSlots abstraction to eliminate code duplication - Revert unrelated console.log in router.controller.ts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * refactor: simplify slots service implementation - Remove intermediate variable assignment in getAvailableSlotsWithRouting - Update test to match simplified routing parameters structure * test: add comprehensive error handling and edge case tests for slots service - Add error scenario tests for NotFoundException, invalid time range, and generic errors - Add edge case tests for null/undefined parameters and empty arrays - Improve test coverage for getAvailableSlotsWithRouting method - Mock SlotsInputService properly to enable isolated unit testing * 📝 CodeRabbit Chat: Rename TransformedGetSlotsQuery types to InternalGetSlotsQuery in slot services * fix: correct import path for AvailableSlotsService in slots service test - Fix import path from '@/lib/services/AvailableSlots' to '@/lib/services/available-slots.service' - Resolves unit test failure due to case sensitivity/naming mismatch - All API v2 tests now pass (9 test suites, 142 tests) Co-Authored-By: hariom@cal.com --------- Co-authored-by: Claude Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../create-routing-form-response.output.ts | 5 +- .../shared-routing-form-response.service.ts | 7 +- .../services/slots-input.service.ts | 48 ++- .../services/slots.service.spec.ts | 397 ++++++++++++++++++ .../services/slots.service.ts | 37 +- apps/api/v2/swagger/documentation.json | 2 +- docs/api-reference/v2/openapi.json | 2 +- .../inputs/get-slots-input.pipe.ts | 7 + .../inputs/get-slots.input.ts | 23 +- 9 files changed, 477 insertions(+), 51 deletions(-) create mode 100644 apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.spec.ts diff --git a/apps/api/v2/src/modules/organizations/routing-forms/outputs/create-routing-form-response.output.ts b/apps/api/v2/src/modules/organizations/routing-forms/outputs/create-routing-form-response.output.ts index e61d13ba3c..dc00f54757 100644 --- a/apps/api/v2/src/modules/organizations/routing-forms/outputs/create-routing-form-response.output.ts +++ b/apps/api/v2/src/modules/organizations/routing-forms/outputs/create-routing-form-response.output.ts @@ -86,11 +86,10 @@ export class CreateRoutingFormResponseOutputData { eventTypeId?: number; @ValidateNested() - @ApiProperty({ type: Routing }) @Type(() => Routing) @ApiPropertyOptional({ type: Routing, - description: "The routing information.", + description: "The routing information that could be passed as is to the booking API.", example: { eventTypeId: 123, routing: { @@ -100,8 +99,6 @@ export class CreateRoutingFormResponseOutputData { }, }, }) - @ValidateNested() - @Type(() => Routing) routing?: Routing; @IsString() diff --git a/apps/api/v2/src/modules/organizations/routing-forms/services/shared-routing-form-response.service.ts b/apps/api/v2/src/modules/organizations/routing-forms/services/shared-routing-form-response.service.ts index d19e5666ae..4daab24e29 100644 --- a/apps/api/v2/src/modules/organizations/routing-forms/services/shared-routing-form-response.service.ts +++ b/apps/api/v2/src/modules/organizations/routing-forms/services/shared-routing-form-response.service.ts @@ -55,15 +55,14 @@ export class SharedRoutingFormResponseService { // Extract event type information from the routed URL const { eventTypeId, crmParams } = await this.extractEventTypeAndCrmParams(user.id, redirectUrl); - const paramsForGetAvailableSlots = { + // Get available slots using the slots service with CRM parameters + const slots = await this.slotsService.getAvailableSlotsWithRouting({ type: ById_2024_09_04_type, eventTypeId, ...slotsQuery, ...crmParams, - } as const; + }); - // Get available slots using the slots service with CRM parameters - const slots = await this.slotsService.getAvailableSlots(paramsForGetAvailableSlots); const teamMemberIds = crmParams.routedTeamMemberIds ?? []; const teamMemberEmail = crmParams.teamMemberEmail ?? undefined; const skipContactOwner = crmParams.skipContactOwner ?? undefined; diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts index 9cc326376c..5d5a5b90f0 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts @@ -13,11 +13,32 @@ import { ByUsernameAndEventTypeSlug_2024_09_04, ByTeamSlugAndEventTypeSlug_2024_09_04, GetSlotsInput_2024_09_04, + GetSlotsInputWithRouting_2024_09_04, ById_2024_09_04_type, ByUsernameAndEventTypeSlug_2024_09_04_type, ByTeamSlugAndEventTypeSlug_2024_09_04_type, } from "@calcom/platform-types"; +export type InternalGetSlotsQuery = { + isTeamEvent: boolean; + startTime: string; + endTime: string; + duration?: number; + eventTypeId: number; + eventTypeSlug: string; + usernameList: string[]; + timeZone: string | undefined; + orgSlug: string | null | undefined; + rescheduleUid: string | null; +}; + +export type InternalGetSlotsQueryWithRouting = InternalGetSlotsQuery & { + routedTeamMemberIds: number[] | null; + skipContactOwner: boolean; + teamMemberEmail: string | null; + routingFormResponseId: number | undefined; +}; + @Injectable() export class SlotsInputService_2024_09_04 { constructor( @@ -30,7 +51,7 @@ export class SlotsInputService_2024_09_04 { private readonly teamsEventTypesRepository: TeamsEventTypesRepository ) {} - async transformGetSlotsQuery(query: GetSlotsInput_2024_09_04) { + async transformGetSlotsQuery(query: GetSlotsInput_2024_09_04): Promise { const eventType = await this.getEventType(query); if (!eventType) { throw new NotFoundException(`Event Type not found`); @@ -46,10 +67,6 @@ export class SlotsInputService_2024_09_04 { const timeZone = query.timeZone; const orgSlug = "organizationSlug" in query ? query.organizationSlug : null; const rescheduleUid = query.bookingUidToReschedule || null; - const routedTeamMemberIds = query.routedTeamMemberIds || null; - const skipContactOwner = query.skipContactOwner || false; - const teamMemberEmail = query.teamMemberEmail || null; - const routingFormResponseId = query.routingFormResponseId || null; return { isTeamEvent, @@ -62,10 +79,23 @@ export class SlotsInputService_2024_09_04 { timeZone, orgSlug, rescheduleUid, - routedTeamMemberIds, - skipContactOwner, - teamMemberEmail, - routingFormResponseId, + }; + } + + async transformRoutingGetSlotsQuery( + query: GetSlotsInputWithRouting_2024_09_04 + ): Promise { + const { routedTeamMemberIds, skipContactOwner, teamMemberEmail, routingFormResponseId, ...baseQuery } = + query; + + const baseTransformation = await this.transformGetSlotsQuery(baseQuery); + + return { + ...baseTransformation, + routedTeamMemberIds: routedTeamMemberIds || null, + skipContactOwner: skipContactOwner || false, + teamMemberEmail: teamMemberEmail || null, + routingFormResponseId: routingFormResponseId ?? undefined, }; } diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.spec.ts new file mode 100644 index 0000000000..2ffc759c9b --- /dev/null +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.spec.ts @@ -0,0 +1,397 @@ +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { AvailableSlotsService } from "@/lib/services/available-slots.service"; +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { MembershipsService } from "@/modules/memberships/services/memberships.service"; +import { SlotsInputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots-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 { TeamsRepository } from "@/modules/teams/teams/teams.repository"; +import { BadRequestException, NotFoundException } from "@nestjs/common"; +import { Test, TestingModule } from "@nestjs/testing"; + +import { SlotsService_2024_09_04 } from "./slots.service"; + +describe("SlotsService_2024_09_04", () => { + let service: SlotsService_2024_09_04; + let eventTypesRepository: EventTypesRepository_2024_06_14; + let availableSlotsService: AvailableSlotsService; + let slotsInputService: SlotsInputService_2024_09_04; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SlotsService_2024_09_04, + { + provide: SlotsInputService_2024_09_04, + useValue: { + transformGetSlotsQuery: jest.fn(), + transformRoutingGetSlotsQuery: jest.fn(), + }, + }, + { + provide: EventTypesRepository_2024_06_14, + useValue: { + getEventTypeById: jest.fn(), + }, + }, + { + provide: SlotsRepository_2024_09_04, + useValue: {}, + }, + { + provide: SlotsOutputService_2024_09_04, + useValue: { + getAvailableSlots: jest.fn().mockResolvedValue({ + slots: { + "2024-01-15": [{ time: "2024-01-15T10:00:00.000Z" }, { time: "2024-01-15T11:00:00.000Z" }], + }, + }), + }, + }, + { + provide: AvailableSlotsService, + useValue: { + getAvailableSlots: jest.fn(), + }, + }, + { + provide: MembershipsService, + useValue: {}, + }, + { + provide: MembershipsRepository, + useValue: {}, + }, + { + provide: TeamsRepository, + useValue: {}, + }, + ], + }).compile(); + + service = module.get(SlotsService_2024_09_04); + slotsInputService = module.get(SlotsInputService_2024_09_04); + eventTypesRepository = module.get(EventTypesRepository_2024_06_14); + availableSlotsService = module.get(AvailableSlotsService); + + jest.clearAllMocks(); + }); + + describe("getAvailableSlotsWithRouting", () => { + const sharedTestData = { + eventTypeId: 123, + start: "2024-01-15T00:00:00.000Z", + end: "2024-01-16T00:00:00.000Z", + timeZone: "America/New_York", + mockEventType: { + id: 123, + slug: "test-event", + teamId: null, // Not a team event + length: 30, + }, + mockSlotsResponse: { + slots: {}, + }, + baseInputQuery: { + type: "byEventTypeId" as const, + isTeamEvent: false, + start: "2024-01-15T00:00:00.000Z", + end: "2024-01-16T00:00:00.000Z", + duration: undefined, + eventTypeId: 123, + eventTypeSlug: "test-event", + usernameList: [], + timeZone: "America/New_York", + orgSlug: null, + rescheduleUid: null, + }, + routingParams: { + routedTeamMemberIds: [456, 789], + skipContactOwner: true, + teamMemberEmail: "test@example.com", + routingFormResponseId: 999, + }, + }; + + beforeEach(() => { + // Setup shared mocks + (eventTypesRepository.getEventTypeById as jest.Mock).mockResolvedValue(sharedTestData.mockEventType); + (availableSlotsService.getAvailableSlots as jest.Mock).mockResolvedValue( + sharedTestData.mockSlotsResponse + ); + }); + + it("should call getAvailableSlots with correct routing parameters", async () => { + const inputQuery = { + ...sharedTestData.baseInputQuery, + ...sharedTestData.routingParams, + }; + + // Mock the transform method to return the expected transformed query + const transformedQuery = { + isTeamEvent: false, + startTime: "2024-01-15T00:00:00.000Z", + endTime: "2024-01-16T23:59:59.000Z", + eventTypeId: 123, + eventTypeSlug: "test-event", + usernameList: [], + timeZone: "America/New_York", + orgSlug: null, + rescheduleUid: null, + ...sharedTestData.routingParams, + }; + (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); + + await service.getAvailableSlotsWithRouting({ + ...inputQuery, + }); + + const { start: _1, end: _2, type: _3, ...queryWithoutStartEndAndType } = sharedTestData.baseInputQuery; + + expect(availableSlotsService.getAvailableSlots).toHaveBeenCalledWith({ + input: { + ...queryWithoutStartEndAndType, + startTime: "2024-01-15T00:00:00.000Z", + endTime: "2024-01-16T23:59:59.000Z", + ...sharedTestData.routingParams, + }, + ctx: {}, + }); + }); + }); + + describe("getAvailableSlotsWithRouting - Error Scenarios", () => { + const baseInputQuery = { + type: "byEventTypeId" as const, + eventTypeId: 123, + start: "2024-01-15T00:00:00.000Z", + end: "2024-01-16T00:00:00.000Z", + timeZone: "America/New_York", + }; + + it("should handle when event type is not found", async () => { + const notFoundError = new NotFoundException("Event Type not found"); + (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockRejectedValue(notFoundError); + + await expect( + service.getAvailableSlotsWithRouting({ + ...baseInputQuery, + teamMemberEmail: "test@example.com", + }) + ).rejects.toThrow(NotFoundException); + + expect(slotsInputService.transformRoutingGetSlotsQuery).toHaveBeenCalledWith({ + ...baseInputQuery, + teamMemberEmail: "test@example.com", + }); + }); + + it("should handle invalid time range error and throw BadRequestException", async () => { + const transformedQuery = { + isTeamEvent: false, + startTime: "2024-01-16T00:00:00.000Z", + endTime: "2024-01-15T23:59:59.000Z", + eventTypeId: 123, + eventTypeSlug: "test-event", + usernameList: [], + timeZone: "America/New_York", + orgSlug: null, + rescheduleUid: null, + routedTeamMemberIds: null, + skipContactOwner: false, + teamMemberEmail: null, + routingFormResponseId: undefined, + }; + + (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); + (availableSlotsService.getAvailableSlots as jest.Mock).mockRejectedValue( + new Error("Invalid time range given - start time must be before end time") + ); + + await expect(service.getAvailableSlotsWithRouting(baseInputQuery)).rejects.toThrow(BadRequestException); + + await expect(service.getAvailableSlotsWithRouting(baseInputQuery)).rejects.toThrow( + "Invalid time range given - check the 'start' and 'end' query parameters." + ); + }); + + it("should re-throw non-time-range errors without modification", async () => { + const genericError = new Error("Database connection failed"); + const transformedQuery = { + isTeamEvent: false, + startTime: "2024-01-15T00:00:00.000Z", + endTime: "2024-01-16T23:59:59.000Z", + eventTypeId: 123, + eventTypeSlug: "test-event", + usernameList: [], + timeZone: "America/New_York", + orgSlug: null, + rescheduleUid: null, + routedTeamMemberIds: null, + skipContactOwner: false, + teamMemberEmail: null, + routingFormResponseId: undefined, + }; + + (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); + (availableSlotsService.getAvailableSlots as jest.Mock).mockRejectedValue(genericError); + + await expect(service.getAvailableSlotsWithRouting(baseInputQuery)).rejects.toThrow(genericError); + }); + + it("should handle non-Error objects thrown by dependencies", async () => { + const transformedQuery = { + isTeamEvent: false, + startTime: "2024-01-15T00:00:00.000Z", + endTime: "2024-01-16T23:59:59.000Z", + eventTypeId: 123, + eventTypeSlug: "test-event", + usernameList: [], + timeZone: "America/New_York", + orgSlug: null, + rescheduleUid: null, + routedTeamMemberIds: null, + skipContactOwner: false, + teamMemberEmail: null, + routingFormResponseId: undefined, + }; + + (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); + const nonErrorObject = { message: "Something went wrong", code: "UNKNOWN_ERROR" }; + (availableSlotsService.getAvailableSlots as jest.Mock).mockRejectedValue(nonErrorObject); + + await expect(service.getAvailableSlotsWithRouting(baseInputQuery)).rejects.toEqual(nonErrorObject); + }); + }); + + describe("getAvailableSlotsWithRouting - Edge Cases", () => { + it("should handle null and undefined routing parameters", async () => { + const input = { + type: "byEventTypeId" as const, + eventTypeId: 123, + start: "2024-01-15T00:00:00.000Z", + end: "2024-01-16T00:00:00.000Z", + timeZone: "America/New_York", + teamMemberEmail: undefined, + routedTeamMemberIds: undefined, + skipContactOwner: undefined, + routingFormResponseId: undefined, + }; + + const transformedQuery = { + isTeamEvent: false, + startTime: "2024-01-15T00:00:00.000Z", + endTime: "2024-01-16T23:59:59.000Z", + eventTypeId: 123, + eventTypeSlug: "test-event", + usernameList: [], + timeZone: "America/New_York", + orgSlug: null, + rescheduleUid: null, + teamMemberEmail: null, + routedTeamMemberIds: null, + skipContactOwner: false, + routingFormResponseId: undefined, + }; + + (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); + (availableSlotsService.getAvailableSlots as jest.Mock).mockResolvedValue({ slots: {} }); + + await service.getAvailableSlotsWithRouting(input); + + expect(availableSlotsService.getAvailableSlots).toHaveBeenCalledWith({ + input: expect.objectContaining({ + teamMemberEmail: null, + routedTeamMemberIds: null, + skipContactOwner: false, + routingFormResponseId: undefined, + }), + ctx: {}, + }); + }); + + it("should handle empty routedTeamMemberIds array", async () => { + const input = { + type: "byEventTypeId" as const, + eventTypeId: 123, + start: "2024-01-15T00:00:00.000Z", + end: "2024-01-16T00:00:00.000Z", + timeZone: "America/New_York", + routedTeamMemberIds: [], + }; + + const transformedQuery = { + isTeamEvent: false, + startTime: "2024-01-15T00:00:00.000Z", + endTime: "2024-01-16T23:59:59.000Z", + eventTypeId: 123, + eventTypeSlug: "test-event", + usernameList: [], + timeZone: "America/New_York", + orgSlug: null, + rescheduleUid: null, + routedTeamMemberIds: [], + skipContactOwner: false, + teamMemberEmail: null, + routingFormResponseId: undefined, + }; + + (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); + (availableSlotsService.getAvailableSlots as jest.Mock).mockResolvedValue({ slots: {} }); + + await service.getAvailableSlotsWithRouting(input); + + expect(availableSlotsService.getAvailableSlots).toHaveBeenCalledWith({ + input: expect.objectContaining({ + routedTeamMemberIds: [], + }), + ctx: {}, + }); + }); + + it("should handle all routing parameters being provided", async () => { + const input = { + type: "byEventTypeId" as const, + eventTypeId: 123, + start: "2024-01-15T00:00:00.000Z", + end: "2024-01-16T00:00:00.000Z", + timeZone: "America/New_York", + routedTeamMemberIds: [1, 2, 3], + skipContactOwner: true, + teamMemberEmail: "team@example.com", + routingFormResponseId: 999, + }; + + const transformedQuery = { + isTeamEvent: false, + startTime: "2024-01-15T00:00:00.000Z", + endTime: "2024-01-16T23:59:59.000Z", + eventTypeId: 123, + eventTypeSlug: "test-event", + usernameList: [], + timeZone: "America/New_York", + orgSlug: null, + rescheduleUid: null, + routedTeamMemberIds: [1, 2, 3], + skipContactOwner: true, + teamMemberEmail: "team@example.com", + routingFormResponseId: 999, + }; + + (slotsInputService.transformRoutingGetSlotsQuery as jest.Mock).mockResolvedValue(transformedQuery); + (availableSlotsService.getAvailableSlots as jest.Mock).mockResolvedValue({ slots: {} }); + + await service.getAvailableSlotsWithRouting(input); + + expect(availableSlotsService.getAvailableSlots).toHaveBeenCalledWith({ + input: expect.objectContaining({ + routedTeamMemberIds: [1, 2, 3], + skipContactOwner: true, + teamMemberEmail: "team@example.com", + routingFormResponseId: 999, + }), + ctx: {}, + }); + }); + }); +}); diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts index 40ae4b3570..190c9d73bc 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts @@ -3,7 +3,11 @@ import { AvailableSlotsService } from "@/lib/services/available-slots.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { MembershipsService } from "@/modules/memberships/services/memberships.service"; import { TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service"; -import { SlotsInputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots-input.service"; +import { + SlotsInputService_2024_09_04, + InternalGetSlotsQuery, + InternalGetSlotsQueryWithRouting, +} from "@/modules/slots/slots-2024-09-04/services/slots-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 { TeamsRepository } from "@/modules/teams/teams/teams.repository"; @@ -18,8 +22,13 @@ import { import { DateTime } from "luxon"; import { z } from "zod"; -import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types"; -import { Booking, EventType } from "@calcom/prisma/client"; +import { SlotFormat } from "@calcom/platform-enums"; +import { + GetSlotsInput_2024_09_04, + GetSlotsInputWithRouting_2024_09_04, + ReserveSlotInput_2024_09_04, +} from "@calcom/platform-types"; +import { EventType } from "@calcom/prisma/client"; const eventTypeMetadataSchema = z .object({ @@ -29,6 +38,7 @@ const eventTypeMetadataSchema = z const DEFAULT_RESERVATION_DURATION = 5; +type InternalSlotsQuery = InternalGetSlotsQuery | InternalGetSlotsQueryWithRouting; @Injectable() export class SlotsService_2024_09_04 { constructor( @@ -42,21 +52,18 @@ export class SlotsService_2024_09_04 { private readonly availableSlotsService: AvailableSlotsService ) {} - async getAvailableSlots(query: GetSlotsInput_2024_09_04) { + private async fetchAndFormatSlots(queryTransformed: InternalSlotsQuery, format?: SlotFormat) { try { - const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query); const availableSlots: TimeSlots = await this.availableSlotsService.getAvailableSlots({ - input: { - ...queryTransformed, - routingFormResponseId: queryTransformed.routingFormResponseId ?? undefined, - }, + input: queryTransformed, ctx: {}, }); + const formatted = await this.slotsOutputService.getAvailableSlots( availableSlots, queryTransformed.eventTypeId, queryTransformed.duration, - query.format, + format, queryTransformed.timeZone ); @@ -73,6 +80,16 @@ export class SlotsService_2024_09_04 { } } + async getAvailableSlots(query: GetSlotsInput_2024_09_04) { + const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query); + return this.fetchAndFormatSlots(queryTransformed, query.format); + } + + async getAvailableSlotsWithRouting(query: GetSlotsInputWithRouting_2024_09_04) { + const queryTransformed = await this.slotsInputService.transformRoutingGetSlotsQuery(query); + return this.fetchAndFormatSlots(queryTransformed, query.format); + } + async reserveSlot(input: ReserveSlotInput_2024_09_04, authUserId?: number) { if (input.reservationDuration && !authUserId) { throw new UnauthorizedException( diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 1dadbea1ef..0afd2f8209 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -25010,7 +25010,7 @@ "example": 123 }, "routing": { - "description": "The routing information.", + "description": "The routing information that could be passed as is to the booking API.", "example": { "eventTypeId": 123, "routing": { diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 1dadbea1ef..0afd2f8209 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -25010,7 +25010,7 @@ "example": 123 }, "routing": { - "description": "The routing information.", + "description": "The routing information that could be passed as is to the booking API.", "example": { "eventTypeId": 123, "routing": { diff --git a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots-input.pipe.ts b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots-input.pipe.ts index 6b5175e391..080f08724d 100644 --- a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots-input.pipe.ts +++ b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots-input.pipe.ts @@ -17,6 +17,13 @@ export type GetSlotsInput_2024_09_04 = | ByTeamSlugAndEventTypeSlug_2024_09_04 | ByUsernames_2024_09_04; +export type GetSlotsInputWithRouting_2024_09_04 = GetSlotsInput_2024_09_04 & { + teamMemberEmail?: string; + routingFormResponseId?: number; + routedTeamMemberIds?: number[]; + skipContactOwner?: boolean; +}; + @Injectable() export class GetSlotsInputPipe implements PipeTransform { // note(Lauris): we need empty constructor otherwise v2 can't be started due to error: diff --git a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts index e611ad8749..e22265fc31 100644 --- a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts +++ b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts @@ -1,4 +1,4 @@ -import { ApiProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Transform } from "class-transformer"; import { IsDateString, @@ -9,7 +9,6 @@ import { IsArray, ArrayMinSize, IsEnum, - IsBoolean, } from "class-validator"; import { SlotFormat } from "@calcom/platform-enums"; @@ -89,26 +88,6 @@ export class GetAvailableSlotsInput_2024_09_04 { example: "abc123def456", }) bookingUidToReschedule?: string; - - @IsString() - @IsOptional() - @ApiHideProperty() - teamMemberEmail?: string; - - @IsNumber() - @IsOptional() - @ApiHideProperty() - routingFormResponseId?: number; - - @IsArray() - @IsOptional() - @ApiHideProperty() - routedTeamMemberIds?: number[]; - - @IsBoolean() - @IsOptional() - @ApiHideProperty() - skipContactOwner?: boolean; } export const ById_2024_09_04_type = "byEventTypeId";