Skip to content

feat: Add routing-forms record response endpoint with available slots#22239

Merged
hariombalhara merged 14 commits intomainfrom
routing-forms-record-endpoint
Jul 4, 2025
Merged

feat: Add routing-forms record response endpoint with available slots#22239
hariombalhara merged 14 commits intomainfrom
routing-forms-record-endpoint

Conversation

@hariombalhara
Copy link
Copy Markdown
Member

@hariombalhara hariombalhara commented Jul 3, 2025

What does this PR do?

Added a new API endpoint to create a routing form response(POST v2/organizations/:orgId/routing-forms/:formId/responses) and returns available booking slots for the matched event type. The response from that endpoint could be utilised to do a booking for a particular timeslot and connect that with the routingForm response

How to connect a response with booking

Send POST request to https://api.cal.com/v2/organizations/:orgId/routing-forms/:formId/responses?start=2025-06-30T18:30:00.000Z&end=2025-07-23T09:00:00Z with JSON body

{
  "email": "john@example.com",  // email is a field in Routing Form with _identifier_=_email_
}

It will return a response like

{
    "status": "success",
    "data": {
        "routing": {
            "responseId": 476,
            "teamMemberIds": [
                19,
                21,
                22,
                25,
                26
            ],
            ... more props here
        },
        "eventTypeId": 56,
        "slots": {
            "2025-07-04": [
                {
                    "start": "2025-07-04T12:00:00.000Z"
                }
			]    
         }
    }
}

The routing object and eventTypeId and any slot available in slots can be used to create a booking now through
https://api.cal.com/v2/bookings (documented here) endpoint. You can pass the routing object as is. Take a look at this loom to see it all working

Followup

A new endpoint to use the queued response identified by queuedResponseId

Video Demo (if applicable):

https://www.loom.com/share/db67e598548b42789b7072a2e9996057

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

@vercel
Copy link
Copy Markdown

vercel bot commented Jul 3, 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 4, 2025 11:59am
cal-eu ⬜️ Ignored (Inspect) Visit Preview Jul 4, 2025 11:59am

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@keithwillcode keithwillcode added core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO labels Jul 3, 2025
@delve-auditor
Copy link
Copy Markdown

delve-auditor bot commented Jul 3, 2025

No security or compliance issues detected. Reviewed everything up to 889dd32.

Security Overview
  • 🔎 Scanned files: 23 changed file(s)
Detected Code Changes
Change Type Relevant files
Enhancement ► input.service.ts
    Add routing form data processing
► organizations-routing-forms-responses.controller.ts
    Add endpoint for creating routing form responses
► organizations-routing-forms-responses.service.ts
    Add routing form response handling
► handleResponse.ts
    Add support for identifier keyed responses and CRM routing
► getRoutedUrl.ts
    Add configurable CRM fetching
Configuration changes ► package.json
    Update platform libraries version
Refactor ► routerGetCrmContactOwnerEmail.ts
    Refactor email extraction logic
► swagger/documentation.json
    Update API documentation

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jul 3, 2025

E2E results are ready!

hariombalhara and others added 3 commits July 3, 2025 20:54
- Fix type mismatch where mockResponse was passed as identifierKeyedResponse
- identifierKeyedResponse expects Record<string, string | string[]> structure
- Updated test to pass correct data structure for type compatibility

Co-Authored-By: hariom@cal.com <hariom@cal.com>
…g in routing forms responses controller

- Change @query() to @Body() decorator for POST request data in controller
- Update service method to accept parsed body data directly
- Remove incorrect URLSearchParams parsing of request.body object
- Fix getRoutingUrl method to use form response data parameter

This resolves API v2 test failures by following proper NestJS patterns for POST request handling.

Co-Authored-By: hariom@cal.com <hariom@cal.com>
@hariombalhara hariombalhara force-pushed the routing-forms-record-endpoint branch from 00348e5 to 5f935ea Compare July 3, 2025 15:25

export default async function routerGetCrmContactOwnerEmail({
attributeRoutingConfig,
response,
Copy link
Copy Markdown
Member Author

@hariombalhara hariombalhara Jul 3, 2025

Choose a reason for hiding this comment

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

Bug-1: The response contains id of the field as they key and not the identifier. So we need to pass the one with identifier as they key here.

The one with id of the field, doesn't have the identifier anywhere and thus can't be compared with email identifier

Comment on lines +22 to +27
for (const identifier of Object.keys(identifierKeyedResponse)) {
const fieldResponse = identifierKeyedResponse[identifier];
if (identifier === "email") {
prospectEmail = fieldResponse instanceof Array ? fieldResponse[0] : fieldResponse;
break;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Logic changed as per the new response object which is identifier key based.

}

if (!contactOwner) {
if (!contactOwner || (!contactOwner.email && !contactOwner.recordType)) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Bug-2: contactOwner by default isn't null. See line 43. So, we must consider it as nullish if both email and recordType are nullish
image

form: serializableForm,
formFillerId: uuidv4(),
response: response,
identifierKeyedResponse: fieldsResponses,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we need to release platform libraries


return {
status: SUCCESS_STATUS,
data: result,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we need to control and hide fields that are not within the DTO use plainToClass combined with @Expose decorator in output DTO class

Comment on lines +40 to +45
if (routedUrlData.props.errorMessage) {
return {
status: "error",
error: { code: "ROUTING_ERROR", message: routedUrlData.props.errorMessage },
redirect: false,
};
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This fixes an existing case where in case of form field validation failure, we were still returning success with empty message.

@hariombalhara hariombalhara force-pushed the routing-forms-record-endpoint branch from de0bb41 to c63a7d8 Compare July 4, 2025 10:52
@vercel vercel bot temporarily deployed to Preview – api July 4, 2025 10:57 Inactive
@vercel vercel bot temporarily deployed to Preview – cal July 4, 2025 10:57 Inactive
action: chosenRoute.action,
});
const contactOwnerQuery =
identifierKeyedResponse && fetchCrm
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@joeauyeung By default having this variable as false, so that we intentionally not use the fixed logic for headless router or elsewhere except API v2's new endpoint.

This keeps the behaviour same for headless router as routerGetCrmContactOwnerEmail wasn't returning any contactOwner or other details

@socket-security
Copy link
Copy Markdown

socket-security bot commented Jul 4, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedmemory-cache@​0.2.010010010076100
Addedbcryptjs@​2.4.31001001008080

View full report

@hariombalhara hariombalhara marked this pull request as ready for review July 4, 2025 11:39
@hariombalhara hariombalhara requested a review from a team July 4, 2025 11:39
@hariombalhara hariombalhara requested a review from a team as a code owner July 4, 2025 11:39
@graphite-app graphite-app bot requested a review from a team July 4, 2025 11:39
@graphite-app
Copy link
Copy Markdown

graphite-app bot commented Jul 4, 2025

Graphite Automations

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

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

@dosubot dosubot bot added api area: API, enterprise API, access token, OAuth routing-forms area: routing forms, routing, forms ✨ feature New feature or request labels Jul 4, 2025
Copy link
Copy Markdown
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 found 6 issues across 23 files. Review them in cubic.dev

React with 👍 or 👎 to teach cubic. Tag @cubic-dev-ai to give specific feedback.

@hariombalhara hariombalhara enabled auto-merge (squash) July 4, 2025 12:01
@hariombalhara hariombalhara merged commit 056b821 into main Jul 4, 2025
64 of 66 checks passed
@hariombalhara hariombalhara deleted the routing-forms-record-endpoint branch July 4, 2025 12:33
ThyMinimalDev added a commit that referenced this pull request Jul 4, 2025
…#22239)

* feat: Add routing-forms record response endpoint with available slots

* fix: resolve TypeScript error in handleResponse.test.ts

- Fix type mismatch where mockResponse was passed as identifierKeyedResponse
- identifierKeyedResponse expects Record<string, string | string[]> structure
- Updated test to pass correct data structure for type compatibility

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

* fix: correct POST endpoint parameter handling and request body parsing in routing forms responses controller

- Change @query() to @Body() decorator for POST request data in controller
- Update service method to accept parsed body data directly
- Remove incorrect URLSearchParams parsing of request.body object
- Fix getRoutingUrl method to use form response data parameter

This resolves API v2 test failures by following proper NestJS patterns for POST request handling.

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

* Pass teamMemberEmail as well

* Devin fixes reverted

* Keep all routing related props together in both endpoints

* Remove newly added slots props from Slots documentation as they are used through internal fn call only

* fix test

* Pass skipContactOwner

* Pass crmAppSlug and crmOwnerRecordGType and add more tests

* handle external redirect case and form not found case

* hide props

* chore: bump platform libs

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hariom@cal.com <hariom@cal.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
ThyMinimalDev added a commit that referenced this pull request Jul 4, 2025
…#22239)

* feat: Add routing-forms record response endpoint with available slots

* fix: resolve TypeScript error in handleResponse.test.ts

- Fix type mismatch where mockResponse was passed as identifierKeyedResponse
- identifierKeyedResponse expects Record<string, string | string[]> structure
- Updated test to pass correct data structure for type compatibility

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

* fix: correct POST endpoint parameter handling and request body parsing in routing forms responses controller

- Change @query() to @Body() decorator for POST request data in controller
- Update service method to accept parsed body data directly
- Remove incorrect URLSearchParams parsing of request.body object
- Fix getRoutingUrl method to use form response data parameter

This resolves API v2 test failures by following proper NestJS patterns for POST request handling.

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

* Pass teamMemberEmail as well

* Devin fixes reverted

* Keep all routing related props together in both endpoints

* Remove newly added slots props from Slots documentation as they are used through internal fn call only

* fix test

* Pass skipContactOwner

* Pass crmAppSlug and crmOwnerRecordGType and add more tests

* handle external redirect case and form not found case

* hide props

* chore: bump platform libs

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hariom@cal.com <hariom@cal.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api area: API, enterprise API, access token, OAuth core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO ✨ feature New feature or request ready-for-e2e routing-forms area: routing forms, routing, forms

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants