fix: Code Quality improvements to response record endpoint and added unit tests (#22264)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <hariombalhara@gmail.com> --------- Co-authored-by: Claude <noreply@anthropic.com> 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>
This commit is contained in:
co-authored by
hariom@cal.com <hariombalhara@gmail.com>
Claude
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
cf392562da
commit
05e1ad0c91
+1
-4
@@ -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()
|
||||
|
||||
+3
-4
@@ -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;
|
||||
|
||||
@@ -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<InternalGetSlotsQuery> {
|
||||
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<InternalGetSlotsQueryWithRouting> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>(SlotsService_2024_09_04);
|
||||
slotsInputService = module.get<SlotsInputService_2024_09_04>(SlotsInputService_2024_09_04);
|
||||
eventTypesRepository = module.get<EventTypesRepository_2024_06_14>(EventTypesRepository_2024_06_14);
|
||||
availableSlotsService = module.get<AvailableSlotsService>(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: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user