refactor: convert RoutingFormResponseRepository to use dependency injection pattern (#22408)

* refactor: convert RoutingFormResponseRepository to use dependency injection pattern

- Add constructor with PrismaClient dependency injection
- Convert all 5 static methods to instance methods using this.prismaClient
- Convert private static helper method generateCreateFormResponseData to instance method
- Update all usage sites to use two-step instantiation pattern:
  const formResponseRepo = new RoutingFormResponseRepository(prisma); formResponseRepo.method(...)
- Update test files to use proper mock implementation with vi.mocked pattern
- Reuse repository instances within same function scope where multiple calls are made
- Follow same dependency injection pattern as UserRepository, TeamRepository, SelectedSlotsRepository, BookingRepository, and EventTypeRepository

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: resolve inconsistent dependency injection in AvailableSlotsService

- Use two-step instantiation pattern for UserRepository calls
- Continue fixing remaining repository instantiation inconsistencies

Co-Authored-By: morgan@cal.com <morgan@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot]
2025-07-11 10:03:25 +00:00
committed by GitHub
co-authored by morgan@cal.com Morgan
parent 1c9778e667
commit c9a47dd0ce
6 changed files with 64 additions and 41 deletions
@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import { beforeEach, describe, it, expect, vi } from "vitest";
import { onSubmissionOfFormResponse } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils";
import { getResponseToStore } from "@calcom/app-store/routing-forms/lib/getResponseToStore";
@@ -8,6 +8,11 @@ import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/for
import { queuedResponseHandler } from "../route";
vi.mock("@calcom/lib/server/repository/formResponse");
const mockRoutingFormResponseRepository = {
getQueuedFormResponseFromId: vi.fn(),
recordFormResponse: vi.fn(),
};
vi.mock("@calcom/app-store/routing-forms/lib/getSerializableForm");
vi.mock("@calcom/app-store/routing-forms/lib/getResponseToStore");
vi.mock("@calcom/app-store/routing-forms/lib/formSubmissionUtils");
@@ -43,8 +48,14 @@ const mockQueuedFormResponse = {
};
describe("queuedResponseHandler", () => {
beforeEach(() => {
vi.mocked(RoutingFormResponseRepository).mockImplementation(
() => mockRoutingFormResponseRepository as any
);
});
it("should process a queued form response", async () => {
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
vi.mocked(mockRoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
mockQueuedFormResponse
);
@@ -90,7 +101,7 @@ describe("queuedResponseHandler", () => {
team: null,
} as unknown as Awaited<ReturnType<typeof onSubmissionOfFormResponse>>);
vi.mocked(RoutingFormResponseRepository.recordFormResponse).mockResolvedValue({
vi.mocked(mockRoutingFormResponseRepository.recordFormResponse).mockResolvedValue({
id: "mock-form-id",
name: "Test Form",
description: "Test Form Description",
@@ -102,7 +113,7 @@ describe("queuedResponseHandler", () => {
},
team: null,
chosenRouteId: "mock-chosen-route-id",
} as unknown as Awaited<ReturnType<typeof RoutingFormResponseRepository.recordFormResponse>>);
} as unknown as Awaited<ReturnType<typeof mockRoutingFormResponseRepository.recordFormResponse>>);
const response = await queuedResponseHandler({
queuedFormResponseId: "1",
@@ -115,7 +126,7 @@ describe("queuedResponseHandler", () => {
});
it("if no queued form response is found, should return early", async () => {
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(null);
vi.mocked(mockRoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(null);
const response = await queuedResponseHandler({
queuedFormResponseId: "1",
params: {},
@@ -127,7 +138,7 @@ describe("queuedResponseHandler", () => {
});
it("should throw if form has no fields", async () => {
vi.mocked(RoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
vi.mocked(mockRoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(
mockQueuedFormResponse
);
vi.mocked(getSerializableForm).mockResolvedValue({
@@ -8,6 +8,7 @@ import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSeri
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse";
import prisma from "@calcom/prisma";
import { defaultResponderForAppDir } from "../../defaultResponderForAppDir";
@@ -23,10 +24,10 @@ export const queuedResponseHandler = async ({
queuedFormResponseId: string;
params: Record<string, string | string[]>;
}) => {
const formResponseRepo = new RoutingFormResponseRepository(prisma);
// Get the queued response
const queuedFormResponse = await RoutingFormResponseRepository.getQueuedFormResponseFromId(
queuedFormResponseId
);
const queuedFormResponse = await formResponseRepo.getQueuedFormResponseFromId(queuedFormResponseId);
if (!queuedFormResponse) {
return {
@@ -48,7 +49,7 @@ export const queuedResponseHandler = async ({
fieldsResponses: params,
});
const formResponse = await RoutingFormResponseRepository.recordFormResponse({
const formResponse = await formResponseRepo.recordFormResponse({
formId: queuedFormResponse.formId,
queuedFormResponseId: queuedFormResponse.id,
// We record new response here as that might be different from the queued response depending on if the user changed something in b/w before clicking CTA and that something wasn't prerendered
@@ -17,12 +17,12 @@ vi.mock("@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic", () => ({
findTeamMembersMatchingAttributeLogic: vi.fn(),
}));
vi.mock("@calcom/lib/server/repository/formResponse", () => ({
RoutingFormResponseRepository: {
recordQueuedFormResponse: vi.fn(),
recordFormResponse: vi.fn(),
},
}));
vi.mock("@calcom/lib/server/repository/formResponse");
const mockRoutingFormResponseRepository = {
recordQueuedFormResponse: vi.fn(),
recordFormResponse: vi.fn(),
};
vi.mock("./crmRouting/routerGetCrmContactOwnerEmail", () => ({
default: vi.fn(),
@@ -107,6 +107,9 @@ const mockResponse: z.infer<typeof ZResponseInputSchema>["response"] = {
describe("handleResponse", () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(RoutingFormResponseRepository).mockImplementation(
() => mockRoutingFormResponseRepository as any
);
});
it("should throw a TRPCError for missing required fields", async () => {
@@ -150,7 +153,7 @@ describe("handleResponse", () => {
createdAt: new Date(),
updatedAt: new Date(),
};
vi.mocked(RoutingFormResponseRepository.recordFormResponse).mockResolvedValue(dbFormResponse);
vi.mocked(mockRoutingFormResponseRepository.recordFormResponse).mockResolvedValue(dbFormResponse);
const result = await handleResponse({
response: mockResponse,
@@ -161,7 +164,7 @@ describe("handleResponse", () => {
isPreview: false,
});
expect(RoutingFormResponseRepository.recordFormResponse).toHaveBeenCalledWith({
expect(mockRoutingFormResponseRepository.recordFormResponse).toHaveBeenCalledWith({
formId: mockForm.id,
response: mockResponse,
chosenRouteId: null,
@@ -185,7 +188,7 @@ describe("handleResponse", () => {
createdAt: new Date(),
updatedAt: new Date(),
};
vi.mocked(RoutingFormResponseRepository.recordQueuedFormResponse).mockResolvedValue(queuedResponse);
vi.mocked(mockRoutingFormResponseRepository.recordQueuedFormResponse).mockResolvedValue(queuedResponse);
const result = await handleResponse({
response: mockResponse,
@@ -197,12 +200,12 @@ describe("handleResponse", () => {
queueFormResponse: true,
});
expect(RoutingFormResponseRepository.recordQueuedFormResponse).toHaveBeenCalledWith({
expect(mockRoutingFormResponseRepository.recordQueuedFormResponse).toHaveBeenCalledWith({
formId: mockForm.id,
response: mockResponse,
chosenRouteId: null,
});
expect(RoutingFormResponseRepository.recordFormResponse).not.toHaveBeenCalled();
expect(mockRoutingFormResponseRepository.recordFormResponse).not.toHaveBeenCalled();
expect(onSubmissionOfFormResponse).not.toHaveBeenCalled();
expect(result.queuedFormResponse).toEqual(queuedResponse);
expect(result.formResponse).toBeNull();
@@ -219,7 +222,7 @@ describe("handleResponse", () => {
isPreview: true,
});
expect(RoutingFormResponseRepository.recordFormResponse).not.toHaveBeenCalled();
expect(mockRoutingFormResponseRepository.recordFormResponse).not.toHaveBeenCalled();
expect(onSubmissionOfFormResponse).not.toHaveBeenCalled();
expect(result.isPreview).toBe(true);
expect(result.formResponse).toBeDefined();
@@ -237,7 +240,7 @@ describe("handleResponse", () => {
queueFormResponse: true,
});
expect(RoutingFormResponseRepository.recordQueuedFormResponse).not.toHaveBeenCalled();
expect(mockRoutingFormResponseRepository.recordQueuedFormResponse).not.toHaveBeenCalled();
expect(result.isPreview).toBe(true);
expect(result.queuedFormResponse).toBeDefined();
expect(result.queuedFormResponse?.id).toBe("00000000-0000-0000-0000-000000000000");
@@ -7,6 +7,7 @@ import { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeam
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse";
import prisma from "@calcom/prisma";
import type { ZResponseInputSchema } from "@calcom/trpc/server/routers/viewer/routing-forms/response.schema";
import { TRPCError } from "@trpc/server";
@@ -161,15 +162,16 @@ const _handleResponse = async ({
}
let dbFormResponse, queuedFormResponse;
if (!isPreview) {
const formResponseRepo = new RoutingFormResponseRepository(prisma);
if (queueFormResponse) {
queuedFormResponse = await RoutingFormResponseRepository.recordQueuedFormResponse({
queuedFormResponse = await formResponseRepo.recordQueuedFormResponse({
formId: form.id,
response,
chosenRouteId,
});
dbFormResponse = null;
} else {
dbFormResponse = await RoutingFormResponseRepository.recordFormResponse({
dbFormResponse = await formResponseRepo.recordFormResponse({
formId: form.id,
response,
chosenRouteId,
+14 -12
View File
@@ -1,4 +1,4 @@
import prisma from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
interface RecordFormResponseInput {
@@ -8,7 +8,9 @@ interface RecordFormResponseInput {
}
export class RoutingFormResponseRepository {
private static generateCreateFormResponseData(
constructor(private prismaClient: PrismaClient) {}
private generateCreateFormResponseData(
input: RecordFormResponseInput & { queuedFormResponseId?: string | null }
) {
return {
@@ -27,24 +29,24 @@ export class RoutingFormResponseRepository {
};
}
static async recordFormResponse(
async recordFormResponse(
input: RecordFormResponseInput & {
queuedFormResponseId?: string | null;
}
) {
return await prisma.app_RoutingForms_FormResponse.create({
return await this.prismaClient.app_RoutingForms_FormResponse.create({
data: this.generateCreateFormResponseData(input),
});
}
static async recordQueuedFormResponse(input: RecordFormResponseInput) {
return await prisma.app_RoutingForms_QueuedFormResponse.create({
async recordQueuedFormResponse(input: RecordFormResponseInput) {
return await this.prismaClient.app_RoutingForms_QueuedFormResponse.create({
data: this.generateCreateFormResponseData(input),
});
}
static async findFormResponseIncludeForm({ routingFormResponseId }: { routingFormResponseId: number }) {
return await prisma.app_RoutingForms_FormResponse.findUnique({
async findFormResponseIncludeForm({ routingFormResponseId }: { routingFormResponseId: number }) {
return await this.prismaClient.app_RoutingForms_FormResponse.findUnique({
where: {
id: routingFormResponseId,
},
@@ -61,8 +63,8 @@ export class RoutingFormResponseRepository {
});
}
static async findQueuedFormResponseIncludeForm({ queuedFormResponseId }: { queuedFormResponseId: string }) {
return await prisma.app_RoutingForms_QueuedFormResponse.findUnique({
async findQueuedFormResponseIncludeForm({ queuedFormResponseId }: { queuedFormResponseId: string }) {
return await this.prismaClient.app_RoutingForms_QueuedFormResponse.findUnique({
where: {
id: queuedFormResponseId,
},
@@ -79,8 +81,8 @@ export class RoutingFormResponseRepository {
});
}
static async getQueuedFormResponseFromId(id: string) {
return await prisma.app_RoutingForms_QueuedFormResponse.findUnique({
async getQueuedFormResponseFromId(id: string) {
return await this.prismaClient.app_RoutingForms_QueuedFormResponse.findUnique({
where: {
id,
},
@@ -142,7 +142,8 @@ export class AvailableSlotsService {
}
const dynamicEventType = getDefaultEvent(input.eventTypeSlug);
const usersForDynamicEventType = await new UserRepository(prisma).findManyUsersForDynamicEventType({
const userRepo = new UserRepository(prisma);
const usersForDynamicEventType = await userRepo.findManyUsersForDynamicEventType({
currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null,
usernameList: Array.isArray(input.usernameList)
? input.usernameList
@@ -249,7 +250,8 @@ export class AvailableSlotsService {
) {
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
log.info("getUserIdFromUsername", safeStringify({ organizationDetails, username }));
const [user] = await new UserRepository(prisma).findUsersByUsername({
const userRepo = new UserRepository(prisma);
const [user] = await userRepo.findUsersByUsername({
usernameList: [username],
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
});
@@ -938,11 +940,13 @@ export class AvailableSlotsService {
let routingFormResponse = null;
if (routingFormResponseId) {
routingFormResponse = await RoutingFormResponseRepository.findFormResponseIncludeForm({
const formResponseRepo = new RoutingFormResponseRepository(prisma);
routingFormResponse = await formResponseRepo.findFormResponseIncludeForm({
routingFormResponseId,
});
} else if (queuedFormResponseId) {
routingFormResponse = await RoutingFormResponseRepository.findQueuedFormResponseIncludeForm({
const formResponseRepo = new RoutingFormResponseRepository(prisma);
routingFormResponse = await formResponseRepo.findQueuedFormResponseIncludeForm({
queuedFormResponseId,
});
}