feat: Add routing-forms record response endpoint with available slots (#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>
This commit is contained in:
Hariom Balhara
2025-07-04 12:33:13 +00:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> hariom@cal.com <hariom@cal.com> cal.com Morgan
parent c90fded6b7
commit 056b821070
23 changed files with 1559 additions and 34 deletions
+279
View File
@@ -2542,6 +2542,126 @@
"tags": [
"Orgs / Routing forms"
]
},
"post": {
"operationId": "OrganizationsRoutingFormsResponsesController_createRoutingFormResponse",
"summary": "Create routing form response and get available slots",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "routingFormId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "start",
"required": true,
"in": "query",
"description": "\n Time starting from which available slots should be checked.\n \n Must be in UTC timezone as ISO 8601 datestring.\n \n You can pass date without hours which defaults to start of day or specify hours:\n 2024-08-13 (will have hours 00:00:00 aka at very beginning of the date) or you can specify hours manually like 2024-08-13T09:00:00Z\n ",
"example": "2050-09-05",
"schema": {
"type": "string"
}
},
{
"name": "end",
"required": true,
"in": "query",
"description": "\n Time until which available slots should be checked.\n \n Must be in UTC timezone as ISO 8601 datestring.\n \n You can pass date without hours which defaults to end of day or specify hours:\n 2024-08-20 (will have hours 23:59:59 aka at the very end of the date) or you can specify hours manually like 2024-08-20T18:00:00Z",
"example": "2050-09-06",
"schema": {
"type": "string"
}
},
{
"name": "timeZone",
"required": false,
"in": "query",
"description": "Time zone in which the available slots should be returned. Defaults to UTC.",
"example": "Europe/Rome",
"schema": {
"type": "string"
}
},
{
"name": "duration",
"required": false,
"in": "query",
"description": "If event type has multiple possible durations then you can specify the desired duration here. Also, if you are fetching slots for a dynamic event then you can specify the duration her which defaults to 30, meaning that returned slots will be each 30 minutes long.",
"example": "60",
"schema": {
"type": "number"
}
},
{
"name": "format",
"required": false,
"in": "query",
"description": "Format of slot times in response. Use 'range' to get start and end times.",
"example": "range",
"schema": {
"enum": [
"range",
"time"
],
"type": "string"
}
},
{
"name": "bookingUidToReschedule",
"required": false,
"in": "query",
"description": "The unique identifier of the booking being rescheduled. When provided will ensure that the original booking time appears within the returned available slots when rescheduling.",
"example": "abc123def456",
"schema": {
"type": "string"
}
},
{
"name": "queueResponse",
"required": false,
"in": "query",
"description": "Whether to queue the form response.",
"example": true,
"schema": {
"type": "boolean"
}
}
],
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateRoutingFormResponseOutput"
}
}
}
}
},
"tags": [
"Orgs / Routing forms"
]
}
},
"/v2/organizations/{orgId}/routing-forms/{routingFormId}/responses/{responseId}": {
@@ -23931,6 +24051,51 @@
"timeZone"
]
},
"Routing": {
"type": "object",
"properties": {
"responseId": {
"type": "number",
"description": "The ID of the routing form response that determined this booking assignment.",
"example": 123
},
"teamMemberIds": {
"description": "Array of team member IDs that were routed to handle this booking.",
"example": [
101,
102
],
"type": "array",
"items": {
"type": "number"
}
},
"teamMemberEmail": {
"type": "string",
"description": "The email of the team member assigned to handle this booking.",
"example": "john.doe@example.com"
},
"skipContactOwner": {
"type": "boolean",
"description": "Whether to skip contact owner assignment from CRM integration.",
"example": true
},
"crmAppSlug": {
"type": "string",
"description": "The CRM application slug for integration.",
"example": "salesforce"
},
"crmOwnerRecordType": {
"type": "string",
"description": "The CRM owner record type for contact assignment.",
"example": "Account"
}
},
"required": [
"responseId",
"teamMemberIds"
]
},
"CreateBookingInput_2024_08_13": {
"type": "object",
"properties": {
@@ -24036,6 +24201,21 @@
"type": "number",
"example": 30,
"description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n If not provided then event type default length will be used for the booking."
},
"routing": {
"description": "Routing information from routing forms that determined the booking assignment. Both responseId and teamMemberIds are required if provided.",
"example": {
"responseId": 123,
"teamMemberIds": [
101,
102
]
},
"allOf": [
{
"$ref": "#/components/schemas/Routing"
}
]
}
},
"required": [
@@ -24149,6 +24329,21 @@
"example": 30,
"description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n If not provided then event type default length will be used for the booking."
},
"routing": {
"description": "Routing information from routing forms that determined the booking assignment. Both responseId and teamMemberIds are required if provided.",
"example": {
"responseId": 123,
"teamMemberIds": [
101,
102
]
},
"allOf": [
{
"$ref": "#/components/schemas/Routing"
}
]
},
"instant": {
"type": "boolean",
"description": "Flag indicating if the booking is an instant booking. Only available for team events.",
@@ -24267,6 +24462,21 @@
"example": 30,
"description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n If not provided then event type default length will be used for the booking."
},
"routing": {
"description": "Routing information from routing forms that determined the booking assignment. Both responseId and teamMemberIds are required if provided.",
"example": {
"responseId": 123,
"teamMemberIds": [
101,
102
]
},
"allOf": [
{
"$ref": "#/components/schemas/Routing"
}
]
},
"recurrenceCount": {
"type": "number",
"description": "The number of recurrences. If not provided then event type recurrence count will be used. Can't be more than\n event type recurrence count",
@@ -26600,6 +26810,75 @@
"data"
]
},
"CreateRoutingFormResponseOutputData": {
"type": "object",
"properties": {
"eventTypeId": {
"type": "number",
"description": "The ID of the event type that was routed to.",
"example": 123
},
"routing": {
"description": "The routing information.",
"example": {
"eventTypeId": 123,
"routing": {
"teamMemberIds": [
101,
102
],
"teamMemberEmail": "john.doe@example.com",
"skipContactOwner": true
}
},
"allOf": [
{
"$ref": "#/components/schemas/Routing"
}
]
},
"routingCustomMessage": {
"type": "string",
"description": "A custom message to be displayed to the user in case of routing to a custom page.",
"example": "This is a custom message."
},
"routingExternalRedirectUrl": {
"type": "string",
"description": "The external redirect URL to be used in case of routing to a non cal.com event type URL.",
"example": "https://example.com/"
},
"slots": {
"oneOf": [
{
"$ref": "#/components/schemas/SlotsOutput_2024_09_04"
},
{
"$ref": "#/components/schemas/RangeSlotsOutput_2024_09_04"
}
]
}
}
},
"CreateRoutingFormResponseOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/CreateRoutingFormResponseOutputData"
}
},
"required": [
"status",
"data"
]
},
"RequestEmailVerificationInput": {
"type": "object",
"properties": {