Skip to content

Comments

fix: Create RoutingFormResponseService to get field value from identifier #22396

Merged
emrysal merged 27 commits intomainfrom
add-form-identifier-to-response
Jul 17, 2025
Merged

fix: Create RoutingFormResponseService to get field value from identifier #22396
emrysal merged 27 commits intomainfrom
add-form-identifier-to-response

Conversation

@joeauyeung
Copy link
Contributor

@joeauyeung joeauyeung commented Jul 10, 2025

Refactor routing form response handling and revert identifier changes

Summary

This PR refactors how routing form responses are handled in the Salesforce CRM integration by introducing a new service layer, while reverting problematic changes to the identifier field that were causing E2E test failures.

Key Changes:

  • Reverted identifier field changes: Removed the identifier field from FormResponse type and related routing form components to fix E2E test failures
  • New service architecture: Created RoutingFormResponseService and RoutingFormResponseRepository to encapsulate routing form response logic
  • CrmService refactor: Updated Salesforce CRM service to use the new service layer instead of direct database queries for form responses

The original approach of storing identifier in response data caused type inconsistencies across routing forms components. Instead, this PR implements a cleaner architecture where identifiers are retrieved from the form definition when needed.

Review & Testing Checklist for Human

  • Test Salesforce CRM integration end-to-end - Create a routing form, submit it, and verify Salesforce records are created correctly with form field values
  • Verify E2E tests pass completely - The original issue was failing E2E tests; confirm all routing forms E2E tests now pass
  • Check type safety - Ensure no TypeScript errors and that the identifier field removal doesn't cause runtime issues
  • Test new service layer edge cases - Verify the RoutingFormResponseService handles missing responses, invalid identifiers, and malformed data gracefully

Recommended test plan: Create a routing form with various field types, submit responses through different paths (direct form, router endpoint), then verify Salesforce integration can correctly extract field values using identifiers.


Diagram

%%{ init : { "theme" : "default" }}%%
graph TD
    CrmService["packages/app-store/salesforce/<br/>lib/CrmService.ts"]:::major-edit
    ResponseService["packages/lib/server/service/<br/>routingForm/routingFormResponse.service.ts"]:::major-edit
    ResponseRepo["packages/lib/server/repository/<br/>routingFormResponse.repository.ts"]:::major-edit
    FormInputFields["packages/app-store/routing-forms/<br/>components/FormInputFields.tsx"]:::minor-edit
    TypesDef["packages/app-store/routing-forms/<br/>types/types.d.ts"]:::minor-edit
    TestFiles["Test files<br/>(handleResponse.test.ts,<br/>onFormSubmission.test.ts)"]:::minor-edit
    
    CrmService -->|"uses"| ResponseService
    ResponseService -->|"uses"| ResponseRepo
    ResponseRepo -->|"queries"| Database[("Routing Form<br/>Responses DB")]:::context
    FormInputFields -->|"creates"| Database
    TypesDef -->|"defines types for"| FormInputFields
    TestFiles -->|"tests"| ResponseService
    
    subgraph Legend
        L1[Major Edit]:::major-edit
        L2[Minor Edit]:::minor-edit  
        L3[Context/No Edit]:::context
    end
    
    classDef major-edit fill:#90EE90
    classDef minor-edit fill:#87CEEB
    classDef context fill:#FFFFFF
Loading

Notes

@keithwillcode keithwillcode added core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO labels Jul 10, 2025
@graphite-app graphite-app bot requested a review from a team July 10, 2025 18:07

response[field.id] = {
label: field.label,
identifier: field?.identifier || null,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was generating the records to store. The field identifier wasn't being passed. All form fields should have an identifier

{
value: number | string | string[];
label: string;
identifier?: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ensure this property exists

response: z.record(
z.object({
label: z.string(),
identifier: z.string().optional(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ensure this property exists

Copy link
Member

Choose a reason for hiding this comment

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

I think we should still keep it optional in this PR and later make it nullable in a separate PR as it might cause failure for certain embeds that are already loaded.

@dosubot dosubot bot added routing-forms area: routing forms, routing, forms 🐛 bug Something isn't working labels Jul 10, 2025
@delve-auditor
Copy link

delve-auditor bot commented Jul 10, 2025

No security or compliance issues detected. Reviewed everything up to a125b9f.

Security Overview
  • 🔎 Scanned files: 10 changed file(s)
Detected Code Changes

The diff is too large to display a summary of code changes.

Reply to this PR with @delve-auditor followed by a description of what change you want and we'll auto-submit a change to this PR to implement it.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic reviewed 2 files and found no issues. Review PR in cubic.dev.

@graphite-app
Copy link

graphite-app bot commented Jul 10, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (07/10/25)

1 reviewer was added to this PR based on Keith Williams's automation.

@vercel vercel bot temporarily deployed to Preview – api July 10, 2025 18:21 Inactive
@vercel vercel bot temporarily deployed to Preview – cal July 10, 2025 18:21 Inactive
@vercel
Copy link

vercel bot commented Jul 10, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Skipped Deployments
Name Status Preview Comments Updated (UTC)
cal ⬜️ Ignored (Inspect) Visit Preview Jul 17, 2025 3:31pm
cal-eu ⬜️ Ignored (Inspect) Visit Preview Jul 17, 2025 3:31pm

@github-actions
Copy link
Contributor

github-actions bot commented Jul 10, 2025

E2E results are ready!

[field.id]: {
label: field.label,
identifier: field?.identifier,
identifier: field?.identifier || null,
Copy link
Member

@hariombalhara hariombalhara Jul 11, 2025

Choose a reason for hiding this comment

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

If we are relying on identifier to be always set then I think we should use https://github.com/calcom/cal.com/blob/add-teams-routing-forms-response-record-endpoint-back/packages/app-store/routing-forms/lib/getFieldIdentifier.ts#L3

Because, identifier property is directly set only when user modifies the identifier otherwise the default value which is the label is used

Copy link
Member

@hariombalhara hariombalhara left a comment

Choose a reason for hiding this comment

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

My concern is why it is required to store the identifier.

If at some place we need the identifier from the response, we could always query the fields by field.id as that is always there. This is how it has been thought of initially atleast.

Also, it won't fix existing responses and for those we would still need to derive identifier on the fly.

@github-actions github-actions bot marked this pull request as draft July 11, 2025 10:22
@github-actions github-actions bot marked this pull request as draft July 16, 2025 16:25
@joeauyeung joeauyeung marked this pull request as ready for review July 16, 2025 16:45
@joeauyeung joeauyeung requested a review from hariombalhara July 16, 2025 16:45
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/lib/server/repository/routingFormResponse.repository.ts (1)

3-33: Refactor static-only class to simple functions.

As identified in previous reviews, this class contains only static members and should be refactored to simple exported functions for better TypeScript/JavaScript idioms.

packages/lib/server/service/routingForm/routingFormResponse.service.ts (2)

46-49: Fix error message to reflect parsing failure.

The error message incorrectly states "Form response not found" when the actual issue is a parsing failure.


70-72: Add validation for field value existence.

The code assumes this.response[field.id] exists, but it might be undefined if the field wasn't answered in the form response.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ce827b and 1c8a756.

📒 Files selected for processing (3)
  • packages/app-store/salesforce/lib/CrmService.ts (4 hunks)
  • packages/lib/server/repository/routingFormResponse.repository.ts (1 hunks)
  • packages/lib/server/service/routingForm/routingFormResponse.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/app-store/salesforce/lib/CrmService.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/lib/server/service/insightsRouting.ts:367-368
Timestamp: 2025-07-15T13:02:17.359Z
Learning: In the InsightsRoutingService (packages/lib/server/service/insightsRouting.ts), multi-select filter data is already validated by zod before reaching the buildFormFieldSqlCondition method, so null/undefined values are not present in filterValue.data arrays and don't need to be filtered out.
packages/lib/server/repository/routingFormResponse.repository.ts (1)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
packages/lib/server/service/routingForm/routingFormResponse.service.ts (2)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/lib/server/service/insightsRouting.ts:367-368
Timestamp: 2025-07-15T13:02:17.359Z
Learning: In the InsightsRoutingService (packages/lib/server/service/insightsRouting.ts), multi-select filter data is already validated by zod before reaching the buildFormFieldSqlCondition method, so null/undefined values are not present in filterValue.data arrays and don't need to be filtered out.
🪛 Biome (1.9.4)
packages/lib/server/repository/routingFormResponse.repository.ts

[error] 3-33: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check
🔇 Additional comments (2)
packages/lib/server/service/routingForm/routingFormResponse.service.ts (2)

10-23: Well-structured class with proper typing.

The class constructor and structure follow good TypeScript patterns with proper dependency injection and Zod type inference.


61-68: Good improvements addressing previous feedback.

The method now properly uses getFieldIdentifier and returns null instead of throwing an error, which addresses previous review feedback.

emrysal and others added 2 commits July 16, 2025 22:40
* chore: Provide a suggestion for pr 22396, new structure

* Refactor to create two create methods with bookingUid and id

* Use `createWithBookingUid`

* Extract routing form response parser to seperate util

* Added more and improved test cases

* Fix

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.test.ts (1)

18-18: Remove unnecessary async keyword.

The test function is marked as async but doesn't use await and tests a synchronous function.

-  it("returns the correct value for an existing field identifier", async () => {
+  it("returns the correct value for an existing field identifier", () => {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c8a756 and 26b2bdf.

📒 Files selected for processing (9)
  • packages/app-store/salesforce/lib/CrmService.ts (4 hunks)
  • packages/lib/server/repository/PrismaRoutingFormResponseRepository.ts (1 hunks)
  • packages/lib/server/repository/RoutingFormResponseRepository.interface.ts (1 hunks)
  • packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.ts (1 hunks)
  • packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.test.ts (1 hunks)
  • packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.ts (1 hunks)
  • packages/lib/server/service/routingForm/responseData/parseRoutingFormResponse.test.ts (1 hunks)
  • packages/lib/server/service/routingForm/responseData/parseRoutingFormResponse.ts (1 hunks)
  • packages/lib/server/service/routingForm/responseData/types.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • packages/lib/server/service/routingForm/responseData/types.ts
  • packages/lib/server/repository/RoutingFormResponseRepository.interface.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/app-store/salesforce/lib/CrmService.ts
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.test.ts (1)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
packages/lib/server/service/routingForm/responseData/parseRoutingFormResponse.ts (1)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
packages/lib/server/service/routingForm/responseData/parseRoutingFormResponse.test.ts (1)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.ts (1)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.ts (1)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
packages/lib/server/repository/PrismaRoutingFormResponseRepository.ts (1)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
🧬 Code Graph Analysis (4)
packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.test.ts (2)
packages/lib/server/service/routingForm/responseData/types.ts (1)
  • RoutingFormResponseData (6-9)
packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.ts (1)
  • findFieldValueByIdentifier (9-21)
packages/lib/server/service/routingForm/responseData/parseRoutingFormResponse.ts (1)
packages/lib/server/service/routingForm/responseData/types.ts (1)
  • RoutingFormResponseData (6-9)
packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.ts (1)
packages/lib/server/service/routingForm/responseData/types.ts (1)
  • RoutingFormResponseData (6-9)
packages/lib/server/repository/PrismaRoutingFormResponseRepository.ts (1)
packages/lib/server/repository/RoutingFormResponseRepository.interface.ts (1)
  • RoutingFormResponseRepositoryInterface (3-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Security Check
🔇 Additional comments (19)
packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.test.ts (1)

1-16: Well-structured test setup with appropriate mock data.

The test imports and data structure correctly represent the expected RoutingFormResponseData type, providing good coverage for testing the function's behavior.

packages/lib/server/service/routingForm/responseData/parseRoutingFormResponse.ts (2)

1-6: Good use of unknown types for runtime validation.

The function signature correctly uses unknown types for parameters that will be validated at runtime using Zod schemas, which is the appropriate approach for parsing untrusted data.


7-10: Clean and straightforward parsing implementation.

The function correctly uses Zod schemas for validation and returns the expected structured data. The implicit throwing behavior on validation failure is appropriate for this type of parsing function.

packages/lib/server/service/routingForm/responseData/parseRoutingFormResponse.test.ts (8)

1-15: Well-structured test setup with comprehensive mock data.

The test imports and data setup correctly represent the expected input structure for the parsing function, providing a solid foundation for testing various scenarios.


16-20: Good basic functionality test.

The test correctly verifies that valid input produces the expected parsed output with proper structure and values.


22-31: Good coverage of optional properties.

The test ensures that optional properties like label in the response schema are correctly preserved during parsing.


33-53: Excellent coverage of array values for multi-select fields.

The test thoroughly covers the scenario where field values can be arrays, with appropriate field definition including options. This ensures the parsing function handles different value types correctly.


55-61: Good negative test for invalid response structure.

The test correctly verifies that the function throws when encountering invalid response data structure, ensuring proper validation.


63-72: Good validation of required field properties.

The test ensures that required properties like id in field definitions are properly validated and the function throws appropriately on missing required data.


74-93: Good coverage of optional field properties.

The test ensures that optional properties like selectText and deleted in field definitions are correctly preserved during parsing, demonstrating good schema flexibility.


95-98: Good basic type validation test.

The test ensures that the function properly validates input types and throws when receiving non-object/non-array inputs as expected.

packages/lib/server/repository/PrismaRoutingFormResponseRepository.ts (3)

1-7: Good dependency injection pattern with default prisma instance.

The class correctly implements the repository interface with proper dependency injection, allowing for easy testing with different Prisma client instances.


9-22: Efficient query implementation with selective field inclusion.

The method correctly implements the interface contract with efficient field selection, including only the necessary fields from the related form.


24-37: Consistent implementation with proper booking UID query.

The method correctly queries by routedToBookingUid and maintains consistency with the other method's structure and field selection.

packages/lib/server/service/routingForm/responseData/findFieldValueByIdentifier.ts (3)

1-7: Good type design with discriminated union for error handling.

The function uses appropriate imports and defines a well-structured discriminated union return type that provides type safety for both success and error cases.


9-16: Correct field lookup with informative error handling.

The function correctly searches for fields using the helper function and provides clear error messages when fields are not found.


18-21: Robust value retrieval with proper null handling.

The function correctly retrieves field values using optional chaining and nullish coalescing, providing appropriate defaults for missing values.

packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.ts (2)

1-9: LGTM! Good use of dependency injection pattern.

The imports are properly structured using type imports where appropriate, and the Dependencies interface provides clean separation of concerns for dependency injection.


11-12: LGTM! Clean dependency injection implementation.

The constructor properly uses readonly modifier and dependency injection pattern, which promotes testability and maintainability.

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

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

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

return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
}
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

Address code duplication and improve error handling.

This method has the same issues as createWithBookingUid and significant code duplication. Consider refactoring to reduce duplication.

  async createWithResponseId(responseId: number) {
+   if (!responseId || responseId <= 0) {
+     throw new Error("Valid response ID is required");
+   }
+   
    const log = this.deps.logger.getSubLogger({
-     prefix: ["[routingFormFieldService]", { responseId }],
+     prefix: ["[RoutingFormResponseDataFactory]", { responseId }],
    });

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

    if (!formResponse) {
-     log.error("Form response not found");
-     throw new Error("Form response not found");
+     log.error("Form response not found for response ID");
+     throw new Error(`Form response not found for response ID: ${responseId}`);
    }

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

Optional refactor to reduce code duplication:

+ private async createParsedResponse<T>(
+   identifier: T,
+   identifierName: string,
+   fetchFn: (id: T) => Promise<any>
+ ) {
+   const log = this.deps.logger.getSubLogger({
+     prefix: ["[RoutingFormResponseDataFactory]", { [identifierName]: identifier }],
+   });
+
+   const formResponse = await fetchFn(identifier);
+
+   if (!formResponse) {
+     log.error(`Form response not found for ${identifierName}`);
+     throw new Error(`Form response not found for ${identifierName}: ${identifier}`);
+   }
+
+   return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
+ }
+
  async createWithBookingUid(bookingUid: string) {
+   if (!bookingUid?.trim()) {
+     throw new Error("Booking UID is required");
+   }
+   return this.createParsedResponse(
+     bookingUid,
+     "bookingUid",
+     (uid) => this.deps.routingFormResponseRepo.findByBookingUid(uid)
+   );
  }

  async createWithResponseId(responseId: number) {
+   if (!responseId || responseId <= 0) {
+     throw new Error("Valid response ID is required");
+   }
+   return this.createParsedResponse(
+     responseId,
+     "responseId",
+     (id) => this.deps.routingFormResponseRepo.findById(id)
+   );
  }
📝 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
async createWithResponseId(responseId: number) {
const log = this.deps.logger.getSubLogger({
prefix: ["[routingFormFieldService]", { responseId }],
});
const formResponse = await this.deps.routingFormResponseRepo.findById(responseId);
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) {
+ if (!responseId || responseId <= 0) {
+ throw new Error("Valid response ID is required");
+ }
const log = this.deps.logger.getSubLogger({
- prefix: ["[routingFormFieldService]", { responseId }],
+ prefix: ["[RoutingFormResponseDataFactory]", { responseId }],
});
const formResponse = await this.deps.routingFormResponseRepo.findById(responseId);
if (!formResponse) {
- log.error("Form response not found");
- throw new Error("Form response not found");
+ log.error("Form response not found for response ID");
+ throw new Error(`Form response not found for response ID: ${responseId}`);
}
return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
}
🤖 Prompt for AI Agents
In packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.ts
around lines 29 to 42, the createWithResponseId method duplicates logic found in
createWithBookingUid, especially in error handling and logging. Refactor by
extracting the common logic for fetching and validating the form response into a
shared private method that both public methods can call. This will reduce
duplication and centralize error handling and logging for missing form
responses.

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

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

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

return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
}
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

Improve error handling and logging consistency.

Several improvements can be made:

  1. Incorrect logger prefix: The prefix shows "[routingFormFieldService]" but this is a factory class, not a field service.
  2. Generic error handling: Consider using more specific error types for better error categorization.
  3. Input validation: No validation on the bookingUid parameter.
  async createWithBookingUid(bookingUid: string) {
+   if (!bookingUid?.trim()) {
+     throw new Error("Booking UID is required");
+   }
+   
    const log = this.deps.logger.getSubLogger({
-     prefix: ["[routingFormFieldService]", { bookingUid }],
+     prefix: ["[RoutingFormResponseDataFactory]", { bookingUid }],
    });

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

    if (!formResponse) {
-     log.error("Form response not found");
-     throw new Error("Form response not found");
+     log.error("Form response not found for booking UID");
+     throw new Error(`Form response not found for booking UID: ${bookingUid}`);
    }

    return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
  }
📝 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
async createWithBookingUid(bookingUid: string) {
const log = this.deps.logger.getSubLogger({
prefix: ["[routingFormFieldService]", { bookingUid }],
});
const formResponse = await this.deps.routingFormResponseRepo.findByBookingUid(bookingUid);
if (!formResponse) {
log.error("Form response not found");
throw new Error("Form response not found");
}
return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
}
async createWithBookingUid(bookingUid: string) {
if (!bookingUid?.trim()) {
throw new Error("Booking UID is required");
}
const log = this.deps.logger.getSubLogger({
prefix: ["[RoutingFormResponseDataFactory]", { bookingUid }],
});
const formResponse = await this.deps.routingFormResponseRepo.findByBookingUid(bookingUid);
if (!formResponse) {
log.error("Form response not found for booking UID");
throw new Error(`Form response not found for booking UID: ${bookingUid}`);
}
return parseRoutingFormResponse(formResponse.response, formResponse.form.fields);
}
🤖 Prompt for AI Agents
In packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.ts
around lines 14 to 27, update the logger prefix to correctly reflect the factory
class instead of "[routingFormFieldService]". Add validation to check if the
bookingUid parameter is valid before proceeding. Replace the generic Error
thrown when the form response is not found with a more specific error type to
improve error categorization and handling. Ensure the logging and error throwing
are consistent with these changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fff3d2 and dfeffba.

📒 Files selected for processing (1)
  • packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.test.ts (1)
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Security Check
🔇 Additional comments (5)
packages/lib/server/service/routingForm/RoutingFormResponseDataFactory.test.ts (5)

11-21: LGTM! Mock objects are well-structured.

The mock objects correctly implement the expected interfaces with proper method signatures.


22-31: LGTM! Excellent test setup with proper isolation.

The test setup correctly uses beforeEach to clear mocks and create fresh instances, ensuring proper test isolation.


34-53: Test logic is sound, but mock return value assertion needs alignment.

The test correctly verifies the repository call and parsing function invocation. However, the assertion expect(result).toBe("parsedData") assumes the mock returns this value, which should be configured in the mock setup.


66-95: LGTM! Consistent test pattern with good coverage.

The tests follow the same well-structured pattern as the createWithBookingUid tests, covering both success and error scenarios with appropriate assertions.


1-97: Comprehensive test suite with excellent coverage.

This test suite provides thorough coverage of the RoutingFormResponseDataFactory class with:

  • Proper mocking of dependencies
  • Test isolation with beforeEach
  • Coverage of both success and error scenarios
  • Appropriate assertions for each test case
  • Consistent test patterns across methods

The tests effectively validate the factory's behavior and error handling.

Comment on lines +7 to +9
vi.mock("./responseData/parseRoutingFormResponse", () => ({
parseRoutingFormResponse: vi.fn(),
}));
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.

Copy link
Contributor

@emrysal emrysal left a comment

Choose a reason for hiding this comment

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

LGTM 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working core area: core, team members only crm-apps area: crm apps, salesforce, hubspot, close.com, sendgrid enterprise area: enterprise, audit log, organisation, SAML, SSO ready-for-e2e 💻 refactor routing-forms area: routing forms, routing, forms

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants