Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7de251e
Make identifier required
joeauyeung Jul 10, 2025
ec1b49d
Fallback to null if identifier isn't present
joeauyeung Jul 10, 2025
1f1ce53
Type fix
joeauyeung Jul 10, 2025
727108e
Type fixes
joeauyeung Jul 10, 2025
ae6d85d
Type fix
joeauyeung Jul 10, 2025
1c7e7ce
Merge branch 'main' into add-form-identifier-to-response
joeauyeung Jul 10, 2025
ba25c08
Create `RoutingFormResponseRepository`
joeauyeung Jul 11, 2025
80a995d
Create `RoutingFormResponseService`
joeauyeung Jul 11, 2025
44914b2
Use repsotiories to find form value
joeauyeung Jul 11, 2025
009236e
Merge branch 'main' into add-form-identifier-to-response
joeauyeung Jul 11, 2025
cbfafb6
Delete console.logs
joeauyeung Jul 11, 2025
b953a49
Undo change in `ZResponseInputSchema` schema
joeauyeung Jul 11, 2025
b9571aa
Type fix
joeauyeung Jul 11, 2025
ba9323c
Undo changes
joeauyeung Jul 11, 2025
255fadb
fix: correct import path in RoutingFormResponseService to resolve run…
devin-ai-integration[bot] Jul 11, 2025
6aa198c
Undo changes
joeauyeung Jul 14, 2025
6a7266d
Update type
joeauyeung Jul 14, 2025
1ce827b
Update typing
joeauyeung Jul 14, 2025
153e569
Address feedback
joeauyeung Jul 16, 2025
1c8a756
Address feedback
joeauyeung Jul 16, 2025
f5f048d
chore: Provide a suggestion for pr 22396, new structure (#22491)
emrysal Jul 16, 2025
26b2bdf
Merge branch 'main' into add-form-identifier-to-response
emrysal Jul 16, 2025
e8a2d13
Factory included wrong calls
emrysal Jul 16, 2025
4fff3d2
Fix test for findFieldValueByIdentifier
emrysal Jul 16, 2025
dfeffba
Add tests
joeauyeung Jul 17, 2025
d37869a
Fix test
joeauyeung Jul 17, 2025
a125b9f
Fix test
joeauyeung Jul 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 21 additions & 27 deletions packages/app-store/salesforce/lib/CrmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import jsforce from "@jsforce/jsforce-node";
import { RRule } from "rrule";
import { z } from "zod";

import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
import { getLocation } from "@calcom/lib/CalEventParser";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { RetryableError } from "@calcom/lib/crmManager/errors";
import { checkIfFreeEmailDomain } from "@calcom/lib/freeEmailDomainCheck/checkIfFreeEmailDomain";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { PrismaRoutingFormResponseRepository as RoutingFormResponseRepository } from "@calcom/lib/server/repository/PrismaRoutingFormResponseRepository";
import { AssignmentReasonRepository } from "@calcom/lib/server/repository/assignmentReason";
import { RoutingFormResponseDataFactory } from "@calcom/lib/server/service/routingForm/RoutingFormResponseDataFactory";
import { findFieldValueByIdentifier } from "@calcom/lib/server/service/routingForm/responseData/findFieldValueByIdentifier";
import { prisma } from "@calcom/prisma";
import type { CalendarEvent, CalEventResponses } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
Expand Down Expand Up @@ -433,7 +435,6 @@ export default class SalesforceCRMService implements CRM {
accessToken: this.accessToken,
instanceUrl: this.instanceUrl,
});

return await client.GetAccountRecordsForRRSkip(emailArray[0]);
} catch (error) {
log.error("Error getting account records for round robin skip", safeStringify({ error }));
Expand Down Expand Up @@ -1238,7 +1239,8 @@ export default class SalesforceCRMService implements CRM {
log.error(`BookingUid not passed. Cannot get form responses without it`);
return;
}
valueToWrite = await this.getTextValueFromRoutingFormResponse(fieldValue, bookingUid, recordId);
const formValue = await this.getTextValueFromRoutingFormResponse(fieldValue, bookingUid, recordId);
valueToWrite = formValue || "";
} else if (fieldValue.startsWith("{utm:")) {
if (!bookingUid) {
log.error(`BookingUid not passed. Cannot get tracking values without it`);
Expand Down Expand Up @@ -1283,20 +1285,8 @@ export default class SalesforceCRMService implements CRM {
prefix: [`[getTextValueFromRoutingFormResponse]: ${recordId} - bookingUid: ${bookingUid}`],
});

// Get the form response
const routingFormResponse = await prisma.app_RoutingForms_FormResponse.findFirst({
where: {
routedToBookingUid: bookingUid,
},
select: {
response: true,
},
});
if (!routingFormResponse) {
log.error("Routing form response not found");
return fieldValue;
}
const response = routingFormResponse.response as FormResponse;
let value;

const regex = /\{form:(.*?)\}/;
const regexMatch = fieldValue.match(regex);
if (!regexMatch) {
Expand All @@ -1310,19 +1300,23 @@ export default class SalesforceCRMService implements CRM {
return fieldValue;
}

// Search for fieldValue, only handle raw text return for now
for (const fieldId of Object.keys(response)) {
const field = response[fieldId];
if (field?.identifier === identifierField) {
return field.value.toString();
}
const routingFormResponseDataFactory = new RoutingFormResponseDataFactory({
logger: log,
routingFormResponseRepo: new RoutingFormResponseRepository(),
});
const findFieldResult = findFieldValueByIdentifier(
await routingFormResponseDataFactory.createWithBookingUid(bookingUid),
identifierField
);
if (findFieldResult.success) {
value = findFieldResult.data;
return String(value);
}
log.error(
`Could not find form response value for identifierField ${identifierField} in response keys ${Object.keys(
response
)}`
`Could not find field value for identifier ${identifierField} in bookingUid ${bookingUid}`,
`failed with error: ${findFieldResult.error}`
);

// If the field is not found, return the original field value
return fieldValue;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { PrismaClient } from "@calcom/prisma";
import prisma from "@calcom/prisma";

import type { RoutingFormResponseRepositoryInterface } from "./RoutingFormResponseRepository.interface";

export class PrismaRoutingFormResponseRepository implements RoutingFormResponseRepositoryInterface {
constructor(private readonly prismaClient: PrismaClient = prisma) {}

findByIdIncludeForm(id: number) {
return this.prismaClient.app_RoutingForms_FormResponse.findUnique({
where: {
id,
},
include: {
form: {
select: {
fields: true,
},
},
},
});
}

findByBookingUidIncludeForm(bookingUid: string) {
return this.prismaClient.app_RoutingForms_FormResponse.findUnique({
where: {
routedToBookingUid: bookingUid,
},
include: {
form: {
select: {
fields: true,
},
},
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { App_RoutingForms_Form, App_RoutingForms_FormResponse } from "@prisma/client";

export interface RoutingFormResponseRepositoryInterface {
findByIdIncludeForm(
id: number
): Promise<(App_RoutingForms_FormResponse & { form: { fields: App_RoutingForms_Form["fields"] } }) | null>;

findByBookingUidIncludeForm(
bookingUid: string
): Promise<(App_RoutingForms_FormResponse & { form: { fields: App_RoutingForms_Form["fields"] } }) | null>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, it, expect, beforeEach, vi } from "vitest";

import type { RoutingFormResponseRepositoryInterface } from "../../repository/RoutingFormResponseRepository.interface";
import { RoutingFormResponseDataFactory } from "./RoutingFormResponseDataFactory";
import { parseRoutingFormResponse } from "./responseData/parseRoutingFormResponse";

vi.mock("./responseData/parseRoutingFormResponse", () => ({
parseRoutingFormResponse: vi.fn(),
}));
Comment on lines +7 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Configure the mock to return a value for consistent test behavior.

The parseRoutingFormResponse mock should return a value to make the test assertions work correctly. The tests expect result to be "parsedData" but the mock doesn't return anything.

 vi.mock("./responseData/parseRoutingFormResponse", () => ({
-  parseRoutingFormResponse: vi.fn(),
+  parseRoutingFormResponse: vi.fn().mockReturnValue("parsedData"),
 }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vi.mock("./responseData/parseRoutingFormResponse", () => ({
parseRoutingFormResponse: vi.fn(),
}));
vi.mock("./responseData/parseRoutingFormResponse", () => ({
parseRoutingFormResponse: vi.fn().mockReturnValue("parsedData"),
}));
🤖 Prompt for AI Agents
In
packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.test.ts
around lines 7 to 9, the mock for parseRoutingFormResponse is defined but does
not return any value, causing test assertions expecting "parsedData" to fail.
Update the mock implementation to return "parsedData" so that the tests receive
the expected value and behave consistently.


const mockLogger = {
getSubLogger: () => ({
error: vi.fn(),
}),
};

const mockRoutingFormResponseRepo: RoutingFormResponseRepositoryInterface = {
findByBookingUidIncludeForm: vi.fn(),
findByIdIncludeForm: vi.fn(),
};

describe("RoutingFormResponseDataFactory", () => {
let factory: RoutingFormResponseDataFactory;

beforeEach(() => {
vi.clearAllMocks();
factory = new RoutingFormResponseDataFactory({
logger: mockLogger as any,
routingFormResponseRepo: mockRoutingFormResponseRepo,
});
});

describe("createWithBookingUid", () => {
it("should call parseRoutingFormResponse with correct data when form response is found", async () => {
const mockFormResponse = {
id: 1,
response: { name: "test" },
form: { fields: [{ label: "name", type: "text" }] },
};
const bookingUid = "test-uid";
vi.mocked(mockRoutingFormResponseRepo.findByBookingUidIncludeForm).mockResolvedValue(
mockFormResponse as any
);

const result = await factory.createWithBookingUid(bookingUid);

expect(mockRoutingFormResponseRepo.findByBookingUidIncludeForm).toHaveBeenCalledWith(bookingUid);
expect(parseRoutingFormResponse).toHaveBeenCalledWith(
mockFormResponse.response,
mockFormResponse.form.fields
);
});

it("should throw an error if form response is not found", async () => {
const bookingUid = "test-uid";
vi.mocked(mockRoutingFormResponseRepo.findByBookingUidIncludeForm).mockResolvedValue(null);

await expect(factory.createWithBookingUid(bookingUid)).rejects.toThrow("Form response not found");

expect(mockRoutingFormResponseRepo.findByBookingUidIncludeForm).toHaveBeenCalledWith(bookingUid);
expect(parseRoutingFormResponse).not.toHaveBeenCalled();
});
});

describe("createWithResponseId", () => {
it("should call parseRoutingFormResponse with correct data when form response is found", async () => {
const mockFormResponse = {
id: 1,
response: { email: "test@example.com" },
form: { fields: [{ label: "email", type: "email" }] },
};
const responseId = 1;
vi.mocked(mockRoutingFormResponseRepo.findByIdIncludeForm).mockResolvedValue(mockFormResponse as any);

const result = await factory.createWithResponseId(responseId);

expect(mockRoutingFormResponseRepo.findByIdIncludeForm).toHaveBeenCalledWith(responseId);
expect(parseRoutingFormResponse).toHaveBeenCalledWith(
mockFormResponse.response,
mockFormResponse.form.fields
);
});

it("should throw an error if form response is not found", async () => {
const responseId = 1;
vi.mocked(mockRoutingFormResponseRepo.findByIdIncludeForm).mockResolvedValue(null);

await expect(factory.createWithResponseId(responseId)).rejects.toThrow("Form response not found");

expect(mockRoutingFormResponseRepo.findByIdIncludeForm).toHaveBeenCalledWith(responseId);
expect(parseRoutingFormResponse).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type logger from "@calcom/lib/logger";

import type { RoutingFormResponseRepositoryInterface } from "../../repository/RoutingFormResponseRepository.interface";
import { parseRoutingFormResponse } from "./responseData/parseRoutingFormResponse";

interface Dependencies {
logger: typeof logger;
routingFormResponseRepo: RoutingFormResponseRepositoryInterface;
}

export class RoutingFormResponseDataFactory {
constructor(private readonly deps: Dependencies) {}

async createWithBookingUid(bookingUid: string) {
const log = this.deps.logger.getSubLogger({
prefix: ["[routingFormFieldService]", { bookingUid }],
});

const formResponse = await this.deps.routingFormResponseRepo.findByBookingUidIncludeForm(bookingUid);

if (!formResponse) {
log.error("Form response not found");
throw new Error("Form response not found");
}

return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
}

async createWithResponseId(responseId: number) {
const log = this.deps.logger.getSubLogger({
prefix: ["[routingFormFieldService]", { responseId }],
});

const formResponse = await this.deps.routingFormResponseRepo.findByIdIncludeForm(responseId);

if (!formResponse) {
log.error("Form response not found");
throw new Error("Form response not found");
}

return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, it, expect } from "vitest";

import { findFieldValueByIdentifier } from "./findFieldValueByIdentifier";
import type { RoutingFormResponseData } from "./types";

describe("findFieldValueByIdentifier", () => {
const responseData: RoutingFormResponseData = {
response: {
"field-123": { value: "test@example.com" },
"field-456": { value: "John Doe" },
},
fields: [
{ id: "field-123", label: "E-mail", identifier: "email", type: "text" },
{ id: "field-456", label: "Name", identifier: "name", type: "text" },
],
};

it("returns the correct value for an existing field identifier", async () => {
const result = findFieldValueByIdentifier(responseData, "email");
expect(result.success).toBe(true);
// @ts-expect-error we know data is defined here
expect(result.data).toBe("test@example.com");
});

it("throws an error and logs when identifier is not found", () => {
const invalidIdentifier = "unknown";

const result = findFieldValueByIdentifier(responseData, invalidIdentifier);
expect(result.success).toBe(false);
// @ts-expect-error we know error is defined here
expect(result.error).toBe(`Field with identifier ${invalidIdentifier} not found`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import getFieldIdentifier from "@calcom/app-store/routing-forms/lib/getFieldIdentifier";

import type { RoutingFormResponseData } from "./types";

type FindFieldValueByIdentifierResult =
| { success: true; data: string | string[] | number | null }
| { success: false; error: string };

export function findFieldValueByIdentifier(
data: RoutingFormResponseData,
identifier: string
): FindFieldValueByIdentifierResult {
const field = data.fields.find((field) => getFieldIdentifier(field) === identifier);
if (!field) {
return { success: false, error: `Field with identifier ${identifier} not found` };
}

const fieldValue = data.response[field.id]?.value;

return { success: true, data: fieldValue ?? null };
}
Loading
Loading