feat: add organization routing forms endpoints [v2] (#20580)

* feat: add organization routing forms endpoints

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

* refactor: reorganize team routing forms modules and add new endpoints

* fix: routing form responses and updated at

* fix: OrganizationsRoutingFormController e2e test

* refactor and fix stuff

* remove not needed files and fix comments

* fix OrganizationsTeamsRoutingFormsResponsesController

* fix tests

* fix tests

* fix: get routing form output

* remove log

* remove now

* fix getTeamRoutingForms types

* refactor: update routing forms API endpoints and repository methods

* added `teamIds?: number[]` filter

* Update get-routing-form-responses-params.input.ts

* Update get-routing-form-responses-params.input.ts

* fix e2e

* add Patch team routing form responses e2e

---------

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>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-04-11 06:58:00 -03:00
1 parent 9838488068
commit 3ee8032538
34 files changed
+3304 -100

No files matched your search

@@ -5,6 +5,7 @@ import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { OrganizationsBookingsModule } from "@/modules/organizations/bookings/organizations.bookings.module";
import { OrganizationsRoutingFormsModule } from "@/modules/organizations/routing-forms/organizations-routing-forms.module";
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module";
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
import { RouterModule } from "@/modules/router/router.module";
@@ -31,6 +32,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
OrganizationsTeamsBookingsModule,
OrganizationsUsersBookingsModule,
OrganizationsBookingsModule,
OrganizationsRoutingFormsModule,
RouterModule,
],
})
@@ -37,7 +37,7 @@ import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/s
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.controller";
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module";
import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module";
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/teams/schedules/organizations-teams-schedules.controller";
import { OrganizationsUsersController } from "@/modules/organizations/users/index/controllers/organizations-users.controller";
import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository";
@@ -0,0 +1,261 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { App_RoutingForms_Form, App_RoutingForms_FormResponse, Team, User } from "@prisma/client";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
describe("OrganizationsRoutingFormsResponsesController", () => {
let app: INestApplication;
let prismaWriteService: PrismaWriteService;
let org: Team;
let team: Team;
let apiKeyString: string;
let routingForm: App_RoutingForms_Form;
let routingFormResponse: App_RoutingForms_FormResponse;
let routingFormResponse2: App_RoutingForms_FormResponse;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let user: User;
const userEmail = `OrganizationsRoutingFormsResponsesController-key-bookings-2024-08-13-user-${randomString()}@api.com`;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
}).compile();
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
prismaWriteService = moduleRef.get<PrismaWriteService>(PrismaWriteService);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: `OrganizationsRoutingFormsResponsesController-teams-memberships-organization-${randomString()}`,
isOrganization: true,
});
team = await teamRepositoryFixture.create({
name: "OrganizationsRoutingFormsResponsesController orgs booking 1",
isOrganization: false,
parent: { connect: { id: org.id } },
});
user = await userRepositoryFixture.create({
email: userEmail,
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
await profileRepositoryFixture.create({
uid: `usr-${user.id}`,
username: userEmail,
organization: {
connect: {
id: org.id,
},
},
user: {
connect: {
id: user.id,
},
},
});
const now = new Date();
now.setDate(now.getDate() + 1);
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `${keyString}`;
routingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({
data: {
name: "Test Routing Form",
description: "Test Description",
disabled: false,
routes: JSON.stringify([]),
fields: JSON.stringify([]),
settings: JSON.stringify({}),
teamId: team.id,
userId: user.id,
},
});
routingFormResponse = await prismaWriteService.prisma.app_RoutingForms_FormResponse.create({
data: {
formId: routingForm.id,
response: JSON.stringify({ question1: "answer1", question2: "answer2" }),
},
});
routingFormResponse2 = await prismaWriteService.prisma.app_RoutingForms_FormResponse.create({
data: {
formId: routingForm.id,
response: { question1: "answer1", question2: "answer2" },
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
afterAll(async () => {
await prismaWriteService.prisma.app_RoutingForms_FormResponse.deleteMany({
where: {
formId: routingForm.id,
},
});
await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({
where: {
teamId: org.id,
},
});
await prismaWriteService.prisma.apiKey.deleteMany({
where: {
teamId: org.id,
},
});
await prismaWriteService.prisma.team.delete({
where: {
id: team.id,
},
});
await prismaWriteService.prisma.team.delete({
where: {
id: org.id,
},
});
await app.close();
});
describe(`GET /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => {
it("should not get routing form responses for non existing org", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/99999/routing-forms/${routingForm.id}/responses`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(403);
});
it("should not get routing form responses for non existing form", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/routing-forms/non-existent-id/responses`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(404);
});
it("should not get routing form responses without authentication", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses`)
.expect(401);
});
it("should get routing form responses", async () => {
return request(app.getHttpServer())
.get(
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?skip=0&take=2&sortUpdatedAt=asc&sortCreatedAt=desc`
)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const responses = responseBody.data as App_RoutingForms_FormResponse[];
expect(responses).toBeDefined();
expect(responses.length).toBeGreaterThan(0);
expect(responses.find((response) => response.id === routingFormResponse.id)).toBeDefined();
expect(responses.find((response) => response.id === routingFormResponse.id)?.formId).toEqual(
routingFormResponse.formId
);
expect(responses.find((response) => response.id === routingFormResponse2.id)).toBeDefined();
expect(responses.find((response) => response.id === routingFormResponse2.id)?.formId).toEqual(
routingFormResponse2.formId
);
});
});
});
describe(`PATCH /v2/organizations/:orgId/routing-forms/:routingFormId/responses/:responseId`, () => {
it("should not update routing form response for non existing org", async () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/99999/routing-forms/${routingForm.id}/responses/${routingFormResponse.id}`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.send({ response: JSON.stringify({ question1: "updated_answer1" }) })
.expect(403);
});
it("should not update routing form response for non existing form", async () => {
return request(app.getHttpServer())
.patch(
`/v2/organizations/${org.id}/routing-forms/non-existent-id/responses/${routingFormResponse.id}`
)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.send({ response: JSON.stringify({ question1: "updated_answer1" }) })
.expect(404);
});
it("should not update routing form response for non existing response", async () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses/99999`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.send({ response: JSON.stringify({ question1: "updated_answer1" }) })
.expect(404);
});
it("should not update routing form response without authentication", async () => {
return request(app.getHttpServer())
.patch(
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses/${routingFormResponse.id}`
)
.send({ response: JSON.stringify({ question1: "updated_answer1" }) })
.expect(401);
});
it("should update routing form response", async () => {
const updatedResponse = { question1: "updated_answer1", question2: "updated_answer2" };
return request(app.getHttpServer())
.patch(
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses/${routingFormResponse.id}`
)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.send({ response: updatedResponse })
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data).toBeDefined();
expect(data.id).toEqual(routingFormResponse.id);
expect(data.formId).toEqual(routingFormResponse.formId);
expect(data.response).toEqual(updatedResponse);
});
});
});
});
@@ -0,0 +1,81 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { GetRoutingFormResponsesOutput } from "@/modules/organizations/routing-forms/outputs/get-routing-form-responses.output";
import { OrganizationsRoutingFormsResponsesService } from "@/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service";
import { Body, Controller, Get, Param, Patch, Query, UseGuards, ParseIntPipe } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetRoutingFormResponsesParams } from "../inputs/get-routing-form-responses-params.input";
import { UpdateRoutingFormResponseInput } from "../inputs/update-routing-form-response.input";
import { UpdateRoutingFormResponseOutput } from "../outputs/update-routing-form-response.output";
@Controller({
path: "/v2/organizations/:orgId/routing-forms/:routingFormId/responses",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@ApiTags("Orgs / Routing forms")
@ApiHeader(API_KEY_HEADER)
export class OrganizationsRoutingFormsResponsesController {
constructor(
private readonly organizationsRoutingFormsResponsesService: OrganizationsRoutingFormsResponsesService
) {}
@Get("/")
@ApiOperation({ summary: "Get routing form responses" })
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
async getRoutingFormResponses(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("routingFormId") routingFormId: string,
@Query() queryParams: GetRoutingFormResponsesParams
): Promise<GetRoutingFormResponsesOutput> {
const { skip, take, ...filters } = queryParams;
const responses =
await this.organizationsRoutingFormsResponsesService.getOrganizationRoutingFormResponses(
orgId,
routingFormId,
skip ?? 0,
take ?? 250,
filters
);
return {
status: SUCCESS_STATUS,
data: responses,
};
}
@Patch("/:responseId")
@ApiOperation({ summary: "Update routing form response" })
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
async updateRoutingFormResponse(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("routingFormId") routingFormId: string,
@Param("responseId", ParseIntPipe) responseId: number,
@Body() updateRoutingFormResponseInput: UpdateRoutingFormResponseInput
): Promise<UpdateRoutingFormResponseOutput> {
const updatedResponse = await this.organizationsRoutingFormsResponsesService.updateRoutingFormResponse(
orgId,
routingFormId,
responseId,
updateRoutingFormResponseInput
);
return {
status: SUCCESS_STATUS,
data: updatedResponse,
};
}
}
@@ -0,0 +1,215 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { GetRoutingFormsOutput } from "@/modules/organizations/routing-forms/outputs/get-routing-forms.output";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { App_RoutingForms_Form, App_RoutingForms_FormResponse, Team, User } from "@prisma/client";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
describe("OrganizationsRoutingFormController", () => {
let app: INestApplication;
let prismaWriteService: PrismaWriteService;
let org: Team;
let team: Team;
let apiKeyString: string;
let routingForm: App_RoutingForms_Form;
let routingFormResponse: App_RoutingForms_FormResponse;
let routingFormResponse2: App_RoutingForms_FormResponse;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let user: User;
const userEmail = `OrganizationsRoutingFormsResponsesController-key-bookings-2024-08-13-user-${randomString()}@api.com`;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
}).compile();
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
prismaWriteService = moduleRef.get<PrismaWriteService>(PrismaWriteService);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: `OrganizationsRoutingFormsResponsesController-teams-memberships-organization-${randomString()}`,
isOrganization: true,
});
team = await teamRepositoryFixture.create({
name: "OrganizationsRoutingFormsResponsesController orgs booking 1",
isOrganization: false,
parent: { connect: { id: org.id } },
});
user = await userRepositoryFixture.create({
email: userEmail,
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
await profileRepositoryFixture.create({
uid: `usr-${user.id}`,
username: userEmail,
organization: {
connect: {
id: org.id,
},
},
user: {
connect: {
id: user.id,
},
},
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `${keyString}`;
routingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({
data: {
name: "Test Routing Form",
description: "Test Description",
disabled: false,
routes: [{ redirect: "http://google.com" }],
fields: [{ territory: "input" }],
settings: { test: "true" },
teamId: team.id,
userId: user.id,
},
});
routingFormResponse = await prismaWriteService.prisma.app_RoutingForms_FormResponse.create({
data: {
formId: routingForm.id,
response: JSON.stringify({ question1: "answer1", question2: "answer2" }),
},
});
routingFormResponse2 = await prismaWriteService.prisma.app_RoutingForms_FormResponse.create({
data: {
formId: routingForm.id,
response: JSON.stringify({ question1: "answer1", question2: "answer2" }),
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
afterAll(async () => {
await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({
where: {
teamId: org.id,
},
});
await prismaWriteService.prisma.apiKey.deleteMany({
where: {
teamId: org.id,
},
});
await prismaWriteService.prisma.team.delete({
where: {
id: team.id,
},
});
await prismaWriteService.prisma.team.delete({
where: {
id: org.id,
},
});
await app.close();
});
describe(`GET /v2/organizations/:orgId/routing-forms`, () => {
it("should not get routing forms for non existing org", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/99999/routing-forms`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(403);
});
it("should not get routing forms without authentication", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/routing-forms`).expect(401);
});
it("should get organization routing forms", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/routing-forms?skip=0&take=1`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
const responseBody: GetRoutingFormsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const routingForms = responseBody.data;
expect(routingForms).toBeDefined();
expect(routingForms.length).toBeGreaterThan(0);
expect(routingForms[0].id).toEqual(routingForm.id);
expect(routingForms[0].name).toEqual(routingForm.name);
expect(routingForms[0].description).toEqual(routingForm.description);
expect(routingForms[0].disabled).toEqual(routingForm.disabled);
});
});
it("should filter routing forms by name", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/routing-forms?name=Test`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
const responseBody: GetRoutingFormsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const routingForms = responseBody.data;
expect(routingForms).toBeDefined();
expect(routingForms.length).toBeGreaterThan(0);
expect(routingForms[0].name).toContain("Test");
});
});
it("should filter routing forms by disabled status", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/routing-forms?disabled=false`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
const responseBody: GetRoutingFormsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const routingForms = responseBody.data;
expect(routingForms).toBeDefined();
expect(routingForms.length).toBeGreaterThan(0);
expect(routingForms[0].disabled).toEqual(false);
expect(routingForms[0].fields?.[0]).toEqual({ territory: "input" });
expect(routingForms[0].routes?.[0]).toEqual({ redirect: "http://google.com" });
expect(routingForms[0].settings).toEqual({ test: "true" });
});
});
});
});
@@ -0,0 +1,54 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { GetRoutingFormsParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input";
import {
GetRoutingFormsOutput,
RoutingFormOutput,
} from "@/modules/organizations/routing-forms/outputs/get-routing-forms.output";
import { OrganizationsRoutingFormsService } from "@/modules/organizations/routing-forms/services/organizations-routing-forms.service";
import { Controller, Get, Param, Query, UseGuards, ParseIntPipe } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@Controller({
path: "/v2/organizations/:orgId/routing-forms",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@ApiTags("Orgs / Routing forms")
@ApiHeader(API_KEY_HEADER)
export class OrganizationsRoutingFormsController {
constructor(private readonly organizationsRoutingFormsService: OrganizationsRoutingFormsService) {}
@Get()
@ApiOperation({ summary: "Get organization routing forms" })
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
async getOrganizationRoutingForms(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() queryParams: GetRoutingFormsParams
): Promise<GetRoutingFormsOutput> {
const { skip, take, ...filters } = queryParams;
const routingForms = await this.organizationsRoutingFormsService.getOrganizationRoutingForms(
orgId,
skip ?? 0,
take ?? 250,
filters
);
return {
status: SUCCESS_STATUS,
data: routingForms.map((form) => plainToClass(RoutingFormOutput, form)),
};
}
}
@@ -0,0 +1,107 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import {
IsOptional,
IsString,
IsEnum,
IsISO8601,
IsDate,
IsNumber,
IsArray,
ArrayMinSize,
} from "class-validator";
enum SortOrder {
ASC = "asc",
DESC = "desc",
}
export class GetRoutingFormResponsesParams {
@ApiPropertyOptional({ type: Number, description: "Number of responses to skip" })
@Transform(({ value }) => value && parseInt(value))
@IsOptional()
skip?: number;
@ApiPropertyOptional({ type: Number, description: "Number of responses to take" })
@Transform(({ value }) => value && parseInt(value))
@IsOptional()
take?: number;
@ApiPropertyOptional({ enum: SortOrder, description: "Sort by creation time" })
@IsOptional()
@IsEnum(SortOrder)
sortCreatedAt?: "asc" | "desc";
@ApiPropertyOptional({ enum: SortOrder, description: "Sort by update time" })
@IsOptional()
@IsEnum(SortOrder)
sortUpdatedAt?: "asc" | "desc";
@ApiPropertyOptional({
type: String,
format: "date-time",
description: "Filter by responses created after this date",
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
afterCreatedAt?: Date;
@ApiPropertyOptional({
type: String,
format: "date-time",
description: "Filter by responses created before this date",
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
beforeCreatedAt?: Date;
@ApiPropertyOptional({
type: String,
format: "date-time",
description: "Filter by responses created after this date",
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
afterUpdatedAt?: Date;
@ApiPropertyOptional({
type: String,
format: "date-time",
description: "Filter by responses updated before this date",
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
beforeUpdatedAt?: Date;
@ApiPropertyOptional({ type: String, description: "Filter by responses routed to a specific booking" })
@IsOptional()
@IsString()
routedToBookingUid?: string;
}
export class GetRoutingFormsParams extends GetRoutingFormResponsesParams {
@IsOptional()
@Transform(({ value }) => {
if (typeof value === "string") {
return value.split(",").map((teamId: string) => parseInt(teamId));
}
return value;
})
@ApiPropertyOptional({
type: [Number],
description: "Filter by teamIds. Team ids must be separated by a comma.",
example: "?teamIds=100,200",
})
@IsArray()
@IsNumber({}, { each: true })
@ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" })
teamIds?: number[];
}
@@ -0,0 +1,89 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsOptional, IsBoolean, IsString, IsEnum, IsDate, IsISO8601 } from "class-validator";
enum SortOrder {
ASC = "asc",
DESC = "desc",
}
export class GetRoutingFormsParams {
@ApiPropertyOptional({ type: Number, description: "Number of routing forms to skip" })
@Transform(({ value }) => value && parseInt(value))
@IsOptional()
skip?: number;
@ApiPropertyOptional({ type: Number, description: "Number of routing forms to take" })
@Transform(({ value }) => value && parseInt(value))
@IsOptional()
take?: number;
@ApiPropertyOptional({ enum: SortOrder, description: "Sort by creation time" })
@IsOptional()
@IsEnum(SortOrder)
sortCreatedAt?: "asc" | "desc";
@ApiPropertyOptional({ enum: SortOrder, description: "Sort by update time" })
@IsOptional()
@IsEnum(SortOrder)
sortUpdatedAt?: "asc" | "desc";
@ApiPropertyOptional({ type: Boolean, description: "Filter by disabled status" })
@IsOptional()
@Transform(({ value }) => {
if (value === "true") return true;
if (value === "false") return false;
return value;
})
@IsBoolean()
disabled?: boolean;
@ApiPropertyOptional({ type: String, description: "Filter by name" })
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({
type: String,
format: "date-time",
description: "Filter by forms created after this date",
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
afterCreatedAt?: Date;
@ApiPropertyOptional({
type: String,
format: "date-time",
description: "Filter by forms created before this date",
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
beforeCreatedAt?: Date;
@ApiPropertyOptional({
type: String,
format: "date-time",
description: "Filter by forms updated after this date",
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
afterUpdatedAt?: Date;
@ApiPropertyOptional({
type: String,
format: "date-time",
description: "Filter by forms updated before this date",
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
beforeUpdatedAt?: Date;
}
@@ -0,0 +1,8 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsOptional } from "class-validator";
export class UpdateRoutingFormResponseInput {
@ApiPropertyOptional({ type: Object, description: "The updated response data" })
@IsOptional()
response?: Record<string, any>;
}
@@ -0,0 +1,28 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { Module } from "@nestjs/common";
import { OrganizationsRoutingFormsResponsesController } from "./controllers/organizations-routing-forms-responses.controller";
import { OrganizationsRoutingFormsController } from "./controllers/organizations-routing-forms.controller";
import { OrganizationsRoutingFormsRepository } from "./organizations-routing-forms.repository";
import { OrganizationsRoutingFormsResponsesService } from "./services/organizations-routing-forms-responses.service";
import { OrganizationsRoutingFormsService } from "./services/organizations-routing-forms.service";
@Module({
imports: [PrismaModule, StripeModule, RedisModule, RoutingFormsModule],
providers: [
MembershipsRepository,
OrganizationsRepository,
OrganizationsRoutingFormsRepository,
OrganizationsRoutingFormsService,
OrganizationsRoutingFormsResponsesService,
OrganizationsTeamsRoutingFormsResponsesOutputService,
],
controllers: [OrganizationsRoutingFormsController, OrganizationsRoutingFormsResponsesController],
})
export class OrganizationsRoutingFormsModule {}
@@ -0,0 +1,128 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationsRoutingFormsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getOrganizationRoutingForms(
orgId: number,
skip: number,
take: number,
options?: {
disabled?: boolean;
name?: string;
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
teamIds?: number[];
}
) {
const {
disabled,
name,
sortCreatedAt,
sortUpdatedAt,
afterCreatedAt,
beforeCreatedAt,
afterUpdatedAt,
beforeUpdatedAt,
teamIds,
} = options || {};
return this.dbRead.prisma.app_RoutingForms_Form.findMany({
where: {
team: { parentId: orgId, ...(teamIds?.length ? { id: { in: teamIds } } : {}) },
...(disabled !== undefined && { disabled }),
...(name && { name: { contains: name, mode: "insensitive" } }),
...(afterCreatedAt && { createdAt: { gte: afterCreatedAt } }),
...(beforeCreatedAt && { createdAt: { lte: beforeCreatedAt } }),
...(afterUpdatedAt && { updatedAt: { gte: afterUpdatedAt } }),
...(beforeUpdatedAt && { updatedAt: { lte: beforeUpdatedAt } }),
},
orderBy: [
...(sortCreatedAt ? [{ createdAt: sortCreatedAt }] : []),
...(sortUpdatedAt ? [{ updatedAt: sortUpdatedAt }] : []),
],
skip,
take,
});
}
async getOrganizationRoutingFormResponses(
orgId: number,
routingFormId: string,
skip: number,
take: number,
options?: {
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
routedToBookingUid?: string;
}
) {
const {
sortCreatedAt,
sortUpdatedAt,
afterCreatedAt,
beforeCreatedAt,
routedToBookingUid,
afterUpdatedAt,
beforeUpdatedAt,
} = options || {};
await this.dbRead.prisma.app_RoutingForms_Form.findFirstOrThrow({
where: {
team: { parentId: orgId },
id: routingFormId,
},
});
return this.dbRead.prisma.app_RoutingForms_FormResponse.findMany({
where: {
formId: routingFormId,
...(afterCreatedAt && { createdAt: { gte: afterCreatedAt } }),
...(beforeCreatedAt && { createdAt: { lte: beforeCreatedAt } }),
...(afterUpdatedAt && { updatedAt: { gte: afterUpdatedAt } }),
...(beforeUpdatedAt && { updatedAt: { lte: beforeUpdatedAt } }),
...(routedToBookingUid && { routedToBookingUid }),
},
orderBy: [
...(sortCreatedAt ? [{ createdAt: sortCreatedAt }] : []),
...(sortUpdatedAt ? [{ updatedAt: sortUpdatedAt }] : []),
],
skip,
take,
});
}
async updateRoutingFormResponse(
orgId: number,
routingFormId: string,
responseId: number,
data: {
response?: Record<string, any>;
}
) {
return this.dbWrite.prisma.app_RoutingForms_FormResponse.update({
where: {
id: responseId,
formId: routingFormId,
form: {
team: {
parentId: orgId,
},
},
},
data: {
...data,
},
});
}
}
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { RoutingFormResponseOutput } from "@calcom/platform-types";
export class GetRoutingFormResponsesOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({ type: RoutingFormResponseOutput })
@Expose()
@Type(() => RoutingFormResponseOutput)
data!: RoutingFormResponseOutput[];
}
@@ -0,0 +1,62 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class RoutingFormOutput {
@Expose()
id!: string;
@ApiProperty({ example: "My Form" })
@Expose()
name!: string;
@ApiProperty({ example: "This is the description." })
@Expose()
description!: string | null;
@ApiProperty({ example: 0 })
@Expose()
position!: number;
@Expose()
routes!: Record<string, unknown> | null;
@ApiProperty({ example: "2024-03-28T10:00:00.000Z" })
@Expose()
createdAt!: string;
@ApiProperty({ example: "2024-03-28T10:00:00.000Z" })
@Expose()
updatedAt!: string;
@Expose()
fields!: Record<string, unknown> | null;
@ApiProperty({ example: 2313 })
@Expose()
userId!: number;
@ApiProperty({ example: 4214321 })
@Expose()
teamId!: number | null;
@ApiProperty({ example: false })
@Expose()
disabled!: boolean;
@Expose()
settings!: Record<string, unknown> | null;
}
export class GetRoutingFormsOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({ type: [RoutingFormOutput] })
@Expose()
@Type(() => RoutingFormOutput)
data!: RoutingFormOutput[];
}
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { RoutingFormResponseOutput } from "@calcom/platform-types";
export class UpdateRoutingFormResponseOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({ type: RoutingFormResponseOutput })
@Expose()
@Type(() => RoutingFormResponseOutput)
data!: RoutingFormResponseOutput;
}
@@ -0,0 +1,55 @@
import { OrganizationsRoutingFormsRepository } from "@/modules/organizations/routing-forms/organizations-routing-forms.repository";
import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationsRoutingFormsResponsesService {
constructor(
private readonly organizationsRoutingFormsRepository: OrganizationsRoutingFormsRepository,
private readonly outputService: OrganizationsTeamsRoutingFormsResponsesOutputService
) {}
async getOrganizationRoutingFormResponses(
orgId: number,
routingFormId: string,
skip: number,
take: number,
options?: {
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
routedToBookingUid?: string;
}
) {
const responses = await this.organizationsRoutingFormsRepository.getOrganizationRoutingFormResponses(
orgId,
routingFormId,
skip,
take,
options
);
return this.outputService.getRoutingFormResponses(responses);
}
async updateRoutingFormResponse(
orgId: number,
routingFormId: string,
responseId: number,
data: {
response?: Record<string, any>;
}
) {
const updatedResponse = await this.organizationsRoutingFormsRepository.updateRoutingFormResponse(
orgId,
routingFormId,
responseId,
data
);
return this.outputService.getRoutingFormResponses([updatedResponse])[0];
}
}
@@ -0,0 +1,26 @@
import { OrganizationsRoutingFormsRepository } from "@/modules/organizations/routing-forms/organizations-routing-forms.repository";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationsRoutingFormsService {
constructor(private readonly organizationsRoutingFormsRepository: OrganizationsRoutingFormsRepository) {}
async getOrganizationRoutingForms(
orgId: number,
skip: number,
take: number,
options?: {
disabled?: boolean;
name?: string;
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
teamIds?: number[];
}
) {
return this.organizationsRoutingFormsRepository.getOrganizationRoutingForms(orgId, skip, take, options);
}
}
@@ -35,7 +35,7 @@ describe("Organizations Teams Routing Forms Responses", () => {
let org: Team;
let orgTeam: Team;
let routingFormResponseId: string;
const authEmail = `organizations-teams-routing-forms-responses-user-${randomString()}@api.com`;
let user: User;
let apiKeyString: string;
@@ -173,7 +173,9 @@ describe("Organizations Teams Routing Forms Responses", () => {
it("should get routing form responses", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses`)
.get(
`/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses?skip=0&take=1`
)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
@@ -186,9 +188,65 @@ describe("Organizations Teams Routing Forms Responses", () => {
expect(responseData[0].response).toEqual(routingFormResponses[0].response);
expect(responseData[0].formFillerId).toEqual(routingFormResponses[0].formFillerId);
expect(responseData[0].createdAt).toEqual(routingFormResponses[0].createdAt.toISOString());
routingFormResponseId = responseData[0].id;
});
});
describe(`PATCH /v2/organizations/:orgId/routing-forms/:routingFormId/responses/:responseId`, () => {
it("should not update routing form response for non existing org", async () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/99999/routing-forms/${routingFormId}/responses/${routingFormResponseId}`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.send({ response: JSON.stringify({ question1: "updated_answer1" }) })
.expect(403);
});
it("should not update routing form response for non existing form", async () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/routing-forms/non-existent-id/responses/${routingFormResponseId}`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.send({ response: JSON.stringify({ question1: "updated_answer1" }) })
.expect(404);
});
it("should not update routing form response for non existing response", async () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/routing-forms/${routingFormId}/responses/99999`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.send({ response: JSON.stringify({ question1: "updated_answer1" }) })
.expect(404);
});
it("should not update routing form response without authentication", async () => {
return request(app.getHttpServer())
.patch(
`/v2/organizations/${org.id}/routing-forms/${routingFormId}/responses/${routingFormResponseId}`
)
.send({ response: JSON.stringify({ question1: "updated_answer1" }) })
.expect(401);
});
it("should update routing form response", async () => {
const updatedResponse = { question1: "updated_answer1", question2: "updated_answer2" };
return request(app.getHttpServer())
.patch(
`/v2/organizations/${org.id}/routing-forms/${routingFormId}/responses/${routingFormResponseId}`
)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.send({ response: updatedResponse })
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data).toBeDefined();
expect(data.id).toEqual(routingFormResponseId);
expect(data.formId).toEqual(routingFormId);
expect(data.response).toEqual(updatedResponse);
});
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
@@ -8,14 +8,16 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { Controller, Get, Param, UseGuards } from "@nestjs/common";
import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input";
import { UpdateRoutingFormResponseInput } from "@/modules/organizations/routing-forms/inputs/update-routing-form-response.input";
import { UpdateRoutingFormResponseOutput } from "@/modules/organizations/routing-forms/outputs/update-routing-form-response.output";
import { GetRoutingFormResponsesOutput } from "@/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output";
import { OrganizationsTeamsRoutingFormsResponsesService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses.service";
import { Controller, Get, Patch, Param, ParseIntPipe, Query, UseGuards, Body } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { RoutingFormsResponsesService } from "../../../../routing-forms-responses/services/routing-forms-responses.service";
import { GetRoutingFormResponsesOutput } from "../outputs/get-routing-form-responses.output";
@Controller({
path: "/v2/organizations/:orgId/teams/:teamId/routing-forms/:routingFormId/responses",
version: API_VERSIONS_VALUES,
@@ -31,22 +33,58 @@ import { GetRoutingFormResponsesOutput } from "../outputs/get-routing-form-respo
)
@ApiHeader(API_KEY_HEADER)
export class OrganizationsTeamsRoutingFormsResponsesController {
constructor(private readonly routingFormsResponsesService: RoutingFormsResponsesService) {}
constructor(
private readonly organizationsTeamsRoutingFormsResponsesService: OrganizationsTeamsRoutingFormsResponsesService
) {}
@Get()
@ApiOperation({ summary: "Get routing form responses" })
@Roles("ORG_ADMIN")
@Get("/")
@ApiOperation({ summary: "Get organization team routing form responses" })
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
async getRoutingFormResponses(
@Param("routingFormId") routingFormId: string
@Param("routingFormId") routingFormId: string,
@Param("orgId", ParseIntPipe) orgId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetRoutingFormResponsesParams
): Promise<GetRoutingFormResponsesOutput> {
const routingFormResponses = await this.routingFormsResponsesService.getRoutingFormResponses(
routingFormId
);
const { skip, take, ...filters } = queryParams;
const routingFormResponses =
await this.organizationsTeamsRoutingFormsResponsesService.getTeamRoutingFormResponses(
teamId,
routingFormId,
skip ?? 0,
take ?? 250,
{ ...(filters ?? {}) }
);
return {
status: SUCCESS_STATUS,
data: routingFormResponses,
};
}
@Patch("/:responseId")
@ApiOperation({ summary: "Update routing form response" })
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
async updateRoutingFormResponse(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("routingFormId") routingFormId: string,
@Param("responseId", ParseIntPipe) responseId: number,
@Body() updateRoutingFormResponseInput: UpdateRoutingFormResponseInput
): Promise<UpdateRoutingFormResponseOutput> {
const updatedResponse =
await this.organizationsTeamsRoutingFormsResponsesService.updateTeamRoutingFormResponse(
teamId,
routingFormId,
responseId,
updateRoutingFormResponseInput
);
return {
status: SUCCESS_STATUS,
data: updatedResponse,
};
}
}
@@ -0,0 +1,206 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { GetRoutingFormsOutput } from "@/modules/organizations/routing-forms/outputs/get-routing-forms.output";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { App_RoutingForms_Form, Team, User } from "@prisma/client";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
describe("OrganizationsRoutingFormsResponsesController", () => {
let app: INestApplication;
let prismaWriteService: PrismaWriteService;
let org: Team;
let team: Team;
let apiKeyString: string;
let routingForm: App_RoutingForms_Form;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let user: User;
const userEmail = `OrganizationsRoutingFormsResponsesController-key-bookings-2024-08-13-user-${randomString()}@api.com`;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
}).compile();
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
prismaWriteService = moduleRef.get<PrismaWriteService>(PrismaWriteService);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: `OrganizationsRoutingFormsResponsesController-teams-memberships-organization-${randomString()}`,
isOrganization: true,
});
team = await teamRepositoryFixture.create({
name: "OrganizationsRoutingFormsResponsesController orgs booking 1",
isOrganization: false,
parent: { connect: { id: org.id } },
});
user = await userRepositoryFixture.create({
email: userEmail,
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
await profileRepositoryFixture.create({
uid: `usr-${user.id}`,
username: userEmail,
organization: {
connect: {
id: org.id,
},
},
user: {
connect: {
id: user.id,
},
},
});
const now = new Date();
now.setDate(now.getDate() + 1);
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `${keyString}`;
routingForm = await prismaWriteService.prisma.app_RoutingForms_Form.create({
data: {
name: "Test Routing Form",
description: "Test Description",
disabled: false,
routes: JSON.stringify([]),
fields: JSON.stringify([]),
settings: JSON.stringify({}),
teamId: team.id,
userId: user.id,
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
afterAll(async () => {
await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({
where: {
teamId: team.id,
},
});
await prismaWriteService.prisma.apiKey.deleteMany({
where: {
teamId: org.id,
},
});
await prismaWriteService.prisma.team.delete({
where: {
id: team.id,
},
});
await prismaWriteService.prisma.team.delete({
where: {
id: org.id,
},
});
await app.close();
});
describe(`GET /v2/organizations/:orgId/teams/:teamId/routing-forms`, () => {
it("should not get team routing forms for non existing org", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/99999/teams/${team.id}/routing-forms`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(403);
});
it("should not get team routing forms for non existing team", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/99999/routing-forms`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(404);
});
it("should not get team routing forms without authentication", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${team.id}/routing-forms`)
.expect(401);
});
it("should get team routing forms", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${team.id}/routing-forms`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
const responseBody: GetRoutingFormsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const routingForms = responseBody.data;
expect(routingForms).toBeDefined();
expect(routingForms.length).toBeGreaterThan(0);
expect(routingForms[0].id).toEqual(routingForm.id);
expect(routingForms[0].name).toEqual(routingForm.name);
expect(routingForms[0].description).toEqual(routingForm.description);
expect(routingForms[0].disabled).toEqual(routingForm.disabled);
});
});
it("should filter team routing forms by name", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${team.id}/routing-forms?name=Team`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
const responseBody: GetRoutingFormsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const routingForms = responseBody.data;
expect(routingForms).toBeDefined();
expect(routingForms.length).toBeGreaterThan(0);
expect(routingForms[0].name).toContain("Test Routing Form");
});
});
it("should filter team routing forms by disabled status", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${team.id}/routing-forms?disabled=false`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
const responseBody: GetRoutingFormsOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const routingForms = responseBody.data;
expect(routingForms).toBeDefined();
expect(routingForms.length).toBeGreaterThan(0);
expect(routingForms[0].disabled).toEqual(false);
});
});
});
});
@@ -0,0 +1,58 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input";
import {
GetRoutingFormsOutput,
RoutingFormOutput,
} from "@/modules/organizations/routing-forms/outputs/get-routing-forms.output";
import { OrganizationsTeamsRoutingFormsService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms.service";
import { Controller, Get, Param, Query, UseGuards, ParseIntPipe } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@Controller({
path: "/v2/organizations/:orgId/teams/:teamId/routing-forms",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@ApiTags("Orgs / Teams / Routing forms")
@ApiHeader(API_KEY_HEADER)
export class OrganizationsTeamsRoutingFormsController {
constructor(
private readonly organizationsTeamsRoutingFormsService: OrganizationsTeamsRoutingFormsService
) {}
@Get()
@ApiOperation({ summary: "Get team routing forms" })
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
async getTeamRoutingForms(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetRoutingFormResponsesParams
): Promise<GetRoutingFormsOutput> {
const { skip, take, ...filters } = queryParams;
const routingForms = await this.organizationsTeamsRoutingFormsService.getTeamRoutingForms(
teamId,
skip ?? 0,
take ?? 250,
{ ...(filters ?? {}) }
);
return {
status: SUCCESS_STATUS,
data: routingForms.map((form) => plainToClass(RoutingFormOutput, form)),
};
}
}
@@ -1,17 +0,0 @@
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { RoutingFormsResponsesModule } from "@/modules/routing-forms-responses/routing-forms-responses.module";
import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { Module } from "@nestjs/common";
import { OrganizationsTeamsRoutingFormsResponsesController } from "./controllers/organizations-teams-routing-forms-responses.controller";
@Module({
imports: [PrismaModule, StripeModule, RedisModule, RoutingFormsResponsesModule, RoutingFormsModule],
providers: [OrganizationsRepository, OrganizationsTeamsRepository],
controllers: [OrganizationsTeamsRoutingFormsResponsesController],
})
export class OrganizationsTeamsRoutingFormsModule {}
@@ -0,0 +1,37 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationsRoutingFormsRepository } from "@/modules/organizations/routing-forms/organizations-routing-forms.repository";
import { OrganizationsRoutingFormsResponsesService } from "@/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service";
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { RedisService } from "@/modules/redis/redis.service";
import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { Module } from "@nestjs/common";
import { OrganizationsTeamsRoutingFormsResponsesController } from "./controllers/organizations-teams-routing-forms-responses.controller";
import { OrganizationsTeamsRoutingFormsController } from "./controllers/organizations-teams-routing-forms.controller";
import { OrganizationsTeamsRoutingFormsResponsesRepository } from "./repositories/organizations-teams-routing-forms-responses.repository";
import { OrganizationsTeamsRoutingFormsRepository } from "./repositories/organizations-teams-routing-forms.repository";
import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "./services/organizations-teams-routing-forms-responses-output.service";
import { OrganizationsTeamsRoutingFormsResponsesService } from "./services/organizations-teams-routing-forms-responses.service";
import { OrganizationsTeamsRoutingFormsService } from "./services/organizations-teams-routing-forms.service";
@Module({
imports: [PrismaModule, StripeModule, RedisModule, RoutingFormsModule],
providers: [
OrganizationsTeamsRoutingFormsService,
OrganizationsTeamsRoutingFormsResponsesService,
OrganizationsTeamsRoutingFormsResponsesOutputService,
OrganizationsTeamsRoutingFormsResponsesRepository,
OrganizationsTeamsRoutingFormsRepository,
OrganizationsRepository,
OrganizationsTeamsRepository,
MembershipsRepository,
OrganizationsRoutingFormsResponsesService,
OrganizationsRoutingFormsRepository,
],
controllers: [OrganizationsTeamsRoutingFormsResponsesController, OrganizationsTeamsRoutingFormsController],
})
export class OrganizationsTeamsRoutingFormsModule {}
@@ -0,0 +1,60 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationsTeamsRoutingFormsResponsesRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getTeamRoutingFormResponses(
teamId: number,
routingFormId: string,
skip: number,
take: number,
options?: {
sortCreatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
routedToBookingUid?: string;
}
) {
const { sortCreatedAt, afterCreatedAt, beforeCreatedAt, routedToBookingUid } = options || {};
return this.dbRead.prisma.app_RoutingForms_FormResponse.findMany({
where: {
formId: routingFormId,
form: {
teamId,
},
...(afterCreatedAt && { createdAt: { gte: afterCreatedAt } }),
...(beforeCreatedAt && { createdAt: { lte: beforeCreatedAt } }),
...(routedToBookingUid && { routedToBookingUid }),
},
orderBy: [...(sortCreatedAt ? [{ createdAt: sortCreatedAt }] : [])],
skip,
take,
});
}
async updateTeamRoutingFormResponse(
teamId: number,
routingFormId: string,
responseId: number,
data: {
response?: Record<string, any>;
}
) {
return this.dbWrite.prisma.app_RoutingForms_FormResponse.update({
where: {
id: responseId,
formId: routingFormId,
form: {
teamId,
},
},
data: {
...data,
},
});
}
}
@@ -0,0 +1,53 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationsTeamsRoutingFormsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getTeamRoutingForms(
teamId: number,
skip: number,
take: number,
options?: {
disabled?: boolean;
name?: string;
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
}
) {
const {
disabled,
name,
sortCreatedAt,
sortUpdatedAt,
afterCreatedAt,
beforeCreatedAt,
afterUpdatedAt,
beforeUpdatedAt,
} = options || {};
return this.dbRead.prisma.app_RoutingForms_Form.findMany({
where: {
teamId,
...(disabled !== undefined && { disabled }),
...(name && { name: { contains: name, mode: "insensitive" } }),
...(afterCreatedAt && { createdAt: { gte: afterCreatedAt } }),
...(beforeCreatedAt && { createdAt: { lte: beforeCreatedAt } }),
...(afterUpdatedAt && { updatedAt: { gte: afterUpdatedAt } }),
...(beforeUpdatedAt && { updatedAt: { lte: beforeUpdatedAt } }),
},
orderBy: [
...(sortCreatedAt ? [{ createdAt: sortCreatedAt }] : []),
...(sortUpdatedAt ? [{ updatedAt: sortUpdatedAt }] : []),
],
skip,
take,
});
}
}
@@ -5,7 +5,7 @@ import { RoutingFormResponseOutput, RoutingFormResponseResponseOutput } from "@c
import { App_RoutingForms_FormResponse } from "@calcom/prisma/client";
@Injectable()
export class RoutingFormsResponsesOutputService {
export class OrganizationsTeamsRoutingFormsResponsesOutputService {
getRoutingFormResponses(
dbRoutingFormResponses: App_RoutingForms_FormResponse[]
): RoutingFormResponseOutput[] {
@@ -0,0 +1,56 @@
import { OrganizationsTeamsRoutingFormsResponsesOutputService } from "@/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service";
import { Injectable } from "@nestjs/common";
import { OrganizationsTeamsRoutingFormsResponsesRepository } from "../repositories/organizations-teams-routing-forms-responses.repository";
@Injectable()
export class OrganizationsTeamsRoutingFormsResponsesService {
constructor(
private readonly routingFormsRepository: OrganizationsTeamsRoutingFormsResponsesRepository,
private readonly routingFormsResponsesOutputService: OrganizationsTeamsRoutingFormsResponsesOutputService
) {}
async getTeamRoutingFormResponses(
teamId: number,
routingFormId: string,
skip: number,
take: number,
options?: {
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
routedToBookingUid?: string;
}
) {
const responses = await this.routingFormsRepository.getTeamRoutingFormResponses(
teamId,
routingFormId,
skip,
take,
options
);
return this.routingFormsResponsesOutputService.getRoutingFormResponses(responses);
}
async updateTeamRoutingFormResponse(
teamId: number,
routingFormId: string,
responseId: number,
data: {
response?: Record<string, any>;
}
) {
const updatedResponse = await this.routingFormsRepository.updateTeamRoutingFormResponse(
teamId,
routingFormId,
responseId,
data
);
return this.routingFormsResponsesOutputService.getRoutingFormResponses([updatedResponse])[0];
}
}
@@ -0,0 +1,26 @@
import { Injectable } from "@nestjs/common";
import { OrganizationsTeamsRoutingFormsRepository } from "../repositories/organizations-teams-routing-forms.repository";
@Injectable()
export class OrganizationsTeamsRoutingFormsService {
constructor(private readonly routingFormsRepository: OrganizationsTeamsRoutingFormsRepository) {}
async getTeamRoutingForms(
teamId: number,
skip: number,
take: number,
options?: {
disabled?: boolean;
name?: string;
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
}
) {
return this.routingFormsRepository.getTeamRoutingForms(teamId, skip, take, options);
}
}
@@ -1,20 +0,0 @@
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RoutingFormsResponsesRepository } from "@/modules/routing-forms-responses/routing-forms-responses.repository";
import { RoutingFormsResponsesOutputService } from "@/modules/routing-forms-responses/services/routing-forms-responses-output.service";
import { RoutingFormsResponsesService } from "@/modules/routing-forms-responses/services/routing-forms-responses.service";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [
RoutingFormsResponsesService,
RoutingFormsResponsesRepository,
RoutingFormsResponsesOutputService,
],
exports: [
RoutingFormsResponsesService,
RoutingFormsResponsesRepository,
RoutingFormsResponsesOutputService,
],
})
export class RoutingFormsResponsesModule {}
@@ -1,18 +0,0 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class RoutingFormsResponsesRepository {
constructor(private readonly dbRead: PrismaReadService) {}
async getRoutingFormResponses(routingFormId: string) {
return this.dbRead.prisma.app_RoutingForms_FormResponse.findMany({
where: {
formId: routingFormId,
},
orderBy: {
createdAt: "desc",
},
});
}
}
@@ -1,17 +0,0 @@
import { RoutingFormsResponsesOutputService } from "@/modules/routing-forms-responses/services/routing-forms-responses-output.service";
import { Injectable } from "@nestjs/common";
import { RoutingFormsResponsesRepository } from "../routing-forms-responses.repository";
@Injectable()
export class RoutingFormsResponsesService {
constructor(
private readonly routingFormsRepository: RoutingFormsResponsesRepository,
private readonly routingFormsResponsesOutputService: RoutingFormsResponsesOutputService
) {}
async getRoutingFormResponses(routingFormId: string) {
const responses = await this.routingFormsRepository.getRoutingFormResponses(routingFormId);
return this.routingFormsResponsesOutputService.getRoutingFormResponses(responses);
}
}
+787 -2
View File
@@ -2291,6 +2291,358 @@
]
}
},
"/v2/organizations/{orgId}/routing-forms": {
"get": {
"operationId": "OrganizationsRoutingFormsController_getOrganizationRoutingForms",
"summary": "Get organization routing forms",
"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": "skip",
"required": false,
"in": "query",
"description": "Number of responses to skip",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Number of responses to take",
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort by creation time",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "sortUpdatedAt",
"required": false,
"in": "query",
"description": "Sort by update time",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses updated before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "routedToBookingUid",
"required": false,
"in": "query",
"description": "Filter by responses routed to a specific booking",
"schema": {
"type": "string"
}
},
{
"name": "teamIds",
"required": false,
"in": "query",
"description": "Filter by teamIds",
"schema": {
"type": "array",
"items": {
"type": "number"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetRoutingFormsOutput"
}
}
}
}
},
"tags": [
"Orgs / Routing forms"
]
}
},
"/v2/organizations/{orgId}/routing-forms/{routingFormId}/responses": {
"get": {
"operationId": "OrganizationsRoutingFormsResponsesController_getRoutingFormResponses",
"summary": "Get routing form responses",
"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": "skip",
"required": false,
"in": "query",
"description": "Number of responses to skip",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Number of responses to take",
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort by creation time",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "sortUpdatedAt",
"required": false,
"in": "query",
"description": "Sort by update time",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses updated before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "routedToBookingUid",
"required": false,
"in": "query",
"description": "Filter by responses routed to a specific booking",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetRoutingFormResponsesOutput"
}
}
}
}
},
"tags": [
"Orgs / Routing forms"
]
}
},
"/v2/organizations/{orgId}/routing-forms/{routingFormId}/responses/{responseId}": {
"patch": {
"operationId": "OrganizationsRoutingFormsResponsesController_updateRoutingFormResponse",
"summary": "Update routing form response",
"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": "responseId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoutingFormResponseInput"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoutingFormResponseOutput"
}
}
}
}
},
"tags": [
"Orgs / Routing forms"
]
}
},
"/v2/organizations/{orgId}/schedules": {
"get": {
"operationId": "OrganizationsSchedulesController_getOrganizationSchedules",
@@ -3886,10 +4238,151 @@
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/routing-forms": {
"get": {
"operationId": "OrganizationsTeamsRoutingFormsController_getTeamRoutingForms",
"summary": "Get team routing forms",
"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": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of responses to skip",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Number of responses to take",
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort by creation time",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "sortUpdatedAt",
"required": false,
"in": "query",
"description": "Sort by update time",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses updated before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "routedToBookingUid",
"required": false,
"in": "query",
"description": "Filter by responses routed to a specific booking",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetRoutingFormsOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Routing forms"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses": {
"get": {
"operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses",
"summary": "Get routing form responses",
"summary": "Get organization team routing form responses",
"parameters": [
{
"name": "Authorization",
@@ -3907,6 +4400,115 @@
"schema": {
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of responses to skip",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Number of responses to take",
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort by creation time",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "sortUpdatedAt",
"required": false,
"in": "query",
"description": "Sort by update time",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses updated before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "routedToBookingUid",
"required": false,
"in": "query",
"description": "Filter by responses routed to a specific booking",
"schema": {
"type": "string"
}
}
],
"responses": {
@@ -3926,6 +4528,72 @@
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses/{responseId}": {
"patch": {
"operationId": "OrganizationsTeamsRoutingFormsResponsesController_updateRoutingFormResponse",
"summary": "Update routing form response",
"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": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "routingFormId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "responseId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoutingFormResponseInput"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoutingFormResponseOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Routing forms / Responses"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": {
"get": {
"operationId": "OrganizationsTeamsSchedulesController_getUserSchedules",
@@ -19149,6 +19817,123 @@
]
},
"GetRoutingFormResponsesOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/RoutingFormResponseOutput"
}
},
"required": [
"status",
"data"
]
},
"UpdateRoutingFormResponseInput": {
"type": "object",
"properties": {
"response": {
"type": "object",
"description": "The updated response data"
}
}
},
"UpdateRoutingFormResponseOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/RoutingFormResponseOutput"
}
},
"required": [
"status",
"data"
]
},
"RoutingFormOutput": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "My Form"
},
"description": {
"type": "string",
"nullable": true,
"example": "This is the description."
},
"position": {
"type": "number",
"example": 0
},
"createdAt": {
"type": "string",
"example": "2024-03-28T10:00:00.000Z"
},
"updatedAt": {
"type": "string",
"example": "2024-03-28T10:00:00.000Z"
},
"userId": {
"type": "number",
"example": 2313
},
"teamId": {
"type": "number",
"nullable": true,
"example": 4214321
},
"disabled": {
"type": "boolean",
"example": false
},
"id": {
"type": "string"
},
"routes": {
"type": "object",
"nullable": true
},
"fields": {
"type": "object",
"nullable": true
},
"settings": {
"type": "object",
"nullable": true
}
},
"required": [
"name",
"description",
"position",
"createdAt",
"updatedAt",
"userId",
"teamId",
"disabled",
"id",
"routes",
"fields",
"settings"
]
},
"GetRoutingFormsOutput": {
"type": "object",
"properties": {
"status": {
@@ -19162,7 +19947,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RoutingFormResponseOutput"
"$ref": "#/components/schemas/RoutingFormOutput"
}
}
},
+741 -2
View File
@@ -2197,6 +2197,340 @@
"tags": ["Orgs / Orgs"]
}
},
"/v2/organizations/{orgId}/routing-forms": {
"get": {
"operationId": "OrganizationsRoutingFormsController_getOrganizationRoutingForms",
"summary": "Get organization routing forms",
"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": "skip",
"required": false,
"in": "query",
"description": "Number of responses to skip",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Number of responses to take",
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort by creation time",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "sortUpdatedAt",
"required": false,
"in": "query",
"description": "Sort by update time",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses updated before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "routedToBookingUid",
"required": false,
"in": "query",
"description": "Filter by responses routed to a specific booking",
"schema": {
"type": "string"
}
},
{
"name": "teamIds",
"required": false,
"in": "query",
"description": "Filter by teamIds",
"schema": {
"type": "array",
"items": {
"type": "number"
}
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetRoutingFormsOutput"
}
}
}
}
},
"tags": ["Orgs / Routing forms"]
}
},
"/v2/organizations/{orgId}/routing-forms/{routingFormId}/responses": {
"get": {
"operationId": "OrganizationsRoutingFormsResponsesController_getRoutingFormResponses",
"summary": "Get routing form responses",
"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": "skip",
"required": false,
"in": "query",
"description": "Number of responses to skip",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Number of responses to take",
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort by creation time",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "sortUpdatedAt",
"required": false,
"in": "query",
"description": "Sort by update time",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses updated before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "routedToBookingUid",
"required": false,
"in": "query",
"description": "Filter by responses routed to a specific booking",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetRoutingFormResponsesOutput"
}
}
}
}
},
"tags": ["Orgs / Routing forms"]
}
},
"/v2/organizations/{orgId}/routing-forms/{routingFormId}/responses/{responseId}": {
"patch": {
"operationId": "OrganizationsRoutingFormsResponsesController_updateRoutingFormResponse",
"summary": "Update routing form response",
"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": "responseId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoutingFormResponseInput"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoutingFormResponseOutput"
}
}
}
}
},
"tags": ["Orgs / Routing forms"]
}
},
"/v2/organizations/{orgId}/schedules": {
"get": {
"operationId": "OrganizationsSchedulesController_getOrganizationSchedules",
@@ -3737,10 +4071,143 @@
"tags": ["Orgs / Teams / Memberships"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/routing-forms": {
"get": {
"operationId": "OrganizationsTeamsRoutingFormsController_getTeamRoutingForms",
"summary": "Get team routing forms",
"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": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of responses to skip",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Number of responses to take",
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort by creation time",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "sortUpdatedAt",
"required": false,
"in": "query",
"description": "Sort by update time",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses updated before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "routedToBookingUid",
"required": false,
"in": "query",
"description": "Filter by responses routed to a specific booking",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetRoutingFormsOutput"
}
}
}
}
},
"tags": ["Orgs / Teams / Routing forms"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses": {
"get": {
"operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses",
"summary": "Get routing form responses",
"summary": "Get organization team routing form responses",
"parameters": [
{
"name": "Authorization",
@@ -3758,6 +4225,109 @@
"schema": {
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of responses to skip",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Number of responses to take",
"schema": {
"type": "number"
}
},
{
"name": "sortCreatedAt",
"required": false,
"in": "query",
"description": "Sort by creation time",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "sortUpdatedAt",
"required": false,
"in": "query",
"description": "Sort by update time",
"schema": {
"enum": ["asc", "desc"],
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses created after this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "beforeUpdatedAt",
"required": false,
"in": "query",
"description": "Filter by responses updated before this date",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "routedToBookingUid",
"required": false,
"in": "query",
"description": "Filter by responses routed to a specific booking",
"schema": {
"type": "string"
}
}
],
"responses": {
@@ -3775,6 +4345,70 @@
"tags": ["Orgs / Teams / Routing forms / Responses"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses/{responseId}": {
"patch": {
"operationId": "OrganizationsTeamsRoutingFormsResponsesController_updateRoutingFormResponse",
"summary": "Update routing form response",
"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": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "routingFormId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "responseId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoutingFormResponseInput"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRoutingFormResponseOutput"
}
}
}
}
},
"tags": ["Orgs / Teams / Routing forms / Responses"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": {
"get": {
"operationId": "OrganizationsTeamsSchedulesController_getUserSchedules",
@@ -17462,6 +18096,111 @@
"required": ["id", "formId", "formFillerId", "routedToBookingUid", "response", "createdAt"]
},
"GetRoutingFormResponsesOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": ["success", "error"]
},
"data": {
"$ref": "#/components/schemas/RoutingFormResponseOutput"
}
},
"required": ["status", "data"]
},
"UpdateRoutingFormResponseInput": {
"type": "object",
"properties": {
"response": {
"type": "object",
"description": "The updated response data"
}
}
},
"UpdateRoutingFormResponseOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": ["success", "error"]
},
"data": {
"$ref": "#/components/schemas/RoutingFormResponseOutput"
}
},
"required": ["status", "data"]
},
"RoutingFormOutput": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "My Form"
},
"description": {
"type": "string",
"nullable": true,
"example": "This is the description."
},
"position": {
"type": "number",
"example": 0
},
"createdAt": {
"type": "string",
"example": "2024-03-28T10:00:00.000Z"
},
"updatedAt": {
"type": "string",
"example": "2024-03-28T10:00:00.000Z"
},
"userId": {
"type": "number",
"example": 2313
},
"teamId": {
"type": "number",
"nullable": true,
"example": 4214321
},
"disabled": {
"type": "boolean",
"example": false
},
"id": {
"type": "string"
},
"routes": {
"type": "object",
"nullable": true
},
"fields": {
"type": "object",
"nullable": true
},
"settings": {
"type": "object",
"nullable": true
}
},
"required": [
"name",
"description",
"position",
"createdAt",
"updatedAt",
"userId",
"teamId",
"disabled",
"id",
"routes",
"fields",
"settings"
]
},
"GetRoutingFormsOutput": {
"type": "object",
"properties": {
"status": {
@@ -17472,7 +18211,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RoutingFormResponseOutput"
"$ref": "#/components/schemas/RoutingFormOutput"
}
}
},
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `updatedAt` to the `App_RoutingForms_FormResponse` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "App_RoutingForms_FormResponse" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
+10 -8
View File
@@ -1061,15 +1061,17 @@ model App_RoutingForms_Form {
}
model App_RoutingForms_FormResponse {
id Int @id @default(autoincrement())
formFillerId String @default(cuid())
form App_RoutingForms_Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String
response Json
createdAt DateTime @default(now())
routedToBookingUid String? @unique
id Int @id @default(autoincrement())
formFillerId String @default(cuid())
form App_RoutingForms_Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String
response Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
routedToBookingUid String? @unique
// We should not cascade delete the booking, because we want to keep the form response even if the routedToBooking is deleted
routedToBooking Booking? @relation(fields: [routedToBookingUid], references: [uid])
routedToBooking Booking? @relation(fields: [routedToBookingUid], references: [uid])
chosenRouteId String?
@@unique([formFillerId, formId])