diff --git a/apps/api/v2/src/modules/endpoints.module.ts b/apps/api/v2/src/modules/endpoints.module.ts index 7d7434a014b5a3..b7d0a6a67b3ff2 100644 --- a/apps/api/v2/src/modules/endpoints.module.ts +++ b/apps/api/v2/src/modules/endpoints.module.ts @@ -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, ], }) diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 18009785025726..6db22addbd04e9 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -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"; diff --git a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.e2e-spec.ts new file mode 100644 index 00000000000000..c0061033ced091 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.e2e-spec.ts @@ -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); + 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); + }); + }); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.ts b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.ts new file mode 100644 index 00000000000000..0909994c89778e --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms-responses.controller.ts @@ -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 { + 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 { + const updatedResponse = await this.organizationsRoutingFormsResponsesService.updateRoutingFormResponse( + orgId, + routingFormId, + responseId, + updateRoutingFormResponseInput + ); + + return { + status: SUCCESS_STATUS, + data: updatedResponse, + }; + } +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.e2e-spec.ts new file mode 100644 index 00000000000000..bbab48457792b8 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.e2e-spec.ts @@ -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); + 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" }); + }); + }); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.ts b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.ts new file mode 100644 index 00000000000000..dbbb61a4dba1b2 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/controllers/organizations-routing-forms.controller.ts @@ -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 { + 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)), + }; + } +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input.ts b/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input.ts new file mode 100644 index 00000000000000..48edda884df0db --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input.ts @@ -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[]; +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-forms-params.input.ts b/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-forms-params.input.ts new file mode 100644 index 00000000000000..9b3daa145a0668 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/inputs/get-routing-forms-params.input.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/inputs/update-routing-form-response.input.ts b/apps/api/v2/src/modules/organizations/routing-forms/inputs/update-routing-form-response.input.ts new file mode 100644 index 00000000000000..7e4de86edd853b --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/inputs/update-routing-form-response.input.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.module.ts b/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.module.ts new file mode 100644 index 00000000000000..cf458bbcbae6a9 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.module.ts @@ -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 {} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.repository.ts b/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.repository.ts new file mode 100644 index 00000000000000..251cc341a54d67 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/organizations-routing-forms.repository.ts @@ -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; + } + ) { + return this.dbWrite.prisma.app_RoutingForms_FormResponse.update({ + where: { + id: responseId, + formId: routingFormId, + form: { + team: { + parentId: orgId, + }, + }, + }, + data: { + ...data, + }, + }); + } +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-form-responses.output.ts b/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-form-responses.output.ts new file mode 100644 index 00000000000000..261275d54108b7 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-form-responses.output.ts @@ -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[]; +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-forms.output.ts b/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-forms.output.ts new file mode 100644 index 00000000000000..e09947e06c32ff --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/outputs/get-routing-forms.output.ts @@ -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 | 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 | null; + + @ApiProperty({ example: 2313 }) + @Expose() + userId!: number; + + @ApiProperty({ example: 4214321 }) + @Expose() + teamId!: number | null; + + @ApiProperty({ example: false }) + @Expose() + disabled!: boolean; + + @Expose() + settings!: Record | 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[]; +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/outputs/update-routing-form-response.output.ts b/apps/api/v2/src/modules/organizations/routing-forms/outputs/update-routing-form-response.output.ts new file mode 100644 index 00000000000000..13fe3b87068243 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/outputs/update-routing-form-response.output.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service.ts b/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service.ts new file mode 100644 index 00000000000000..a4a668fa17eb9e --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms-responses.service.ts @@ -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; + } + ) { + const updatedResponse = await this.organizationsRoutingFormsRepository.updateRoutingFormResponse( + orgId, + routingFormId, + responseId, + data + ); + + return this.outputService.getRoutingFormResponses([updatedResponse])[0]; + } +} diff --git a/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms.service.ts b/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms.service.ts new file mode 100644 index 00000000000000..eab3330261de61 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/routing-forms/services/organizations-routing-forms.service.ts @@ -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); + } +} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts index ddc80bf93480ab..e9720b5c27388f 100644 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts @@ -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); diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts index 073554c4d5c6cc..efd3817a8eae67 100644 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts @@ -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 { - 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 { + const updatedResponse = + await this.organizationsTeamsRoutingFormsResponsesService.updateTeamRoutingFormResponse( + teamId, + routingFormId, + responseId, + updateRoutingFormResponseInput + ); + + return { + status: SUCCESS_STATUS, + data: updatedResponse, + }; + } } diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.e2e-spec.ts new file mode 100644 index 00000000000000..ebed85f16b9ca0 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.e2e-spec.ts @@ -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); + 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); + }); + }); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.ts new file mode 100644 index 00000000000000..8d97528e870bc5 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms.controller.ts @@ -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 { + 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)), + }; + } +} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module.ts deleted file mode 100644 index d5f16df2990e59..00000000000000 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module.ts +++ /dev/null @@ -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 {} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module.ts new file mode 100644 index 00000000000000..e6b5ac672e7956 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module.ts @@ -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 {} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms-responses.repository.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms-responses.repository.ts new file mode 100644 index 00000000000000..8b466e7b736bb5 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms-responses.repository.ts @@ -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; + } + ) { + return this.dbWrite.prisma.app_RoutingForms_FormResponse.update({ + where: { + id: responseId, + formId: routingFormId, + form: { + teamId, + }, + }, + data: { + ...data, + }, + }); + } +} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms.repository.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms.repository.ts new file mode 100644 index 00000000000000..7645ff04eb739f --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/repositories/organizations-teams-routing-forms.repository.ts @@ -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, + }); + } +} diff --git a/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses-output.service.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service.ts similarity index 94% rename from apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses-output.service.ts rename to apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service.ts index 38c787ac36749a..87bfd32bfccfe8 100644 --- a/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses-output.service.ts +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses-output.service.ts @@ -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[] { diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses.service.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses.service.ts new file mode 100644 index 00000000000000..9bca5f1d0f4463 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms-responses.service.ts @@ -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; + } + ) { + const updatedResponse = await this.routingFormsRepository.updateTeamRoutingFormResponse( + teamId, + routingFormId, + responseId, + data + ); + + return this.routingFormsResponsesOutputService.getRoutingFormResponses([updatedResponse])[0]; + } +} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms.service.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms.service.ts new file mode 100644 index 00000000000000..5eae2716542870 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/services/organizations-teams-routing-forms.service.ts @@ -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); + } +} diff --git a/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.module.ts b/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.module.ts deleted file mode 100644 index 5f1a3ee792a808..00000000000000 --- a/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.module.ts +++ /dev/null @@ -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 {} diff --git a/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.repository.ts b/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.repository.ts deleted file mode 100644 index b079f43aa3fc77..00000000000000 --- a/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.repository.ts +++ /dev/null @@ -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", - }, - }); - } -} diff --git a/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses.service.ts b/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses.service.ts deleted file mode 100644 index c9c76f6603dd2e..00000000000000 --- a/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses.service.ts +++ /dev/null @@ -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); - } -} diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 483ae91ebe6e48..acf60144e3e8d3 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -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 ` where `` 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 ` where `` 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 ` where `` 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", @@ -3233,43 +3585,261 @@ "description": "For platform customers - OAuth client ID", "required": false, "schema": { - "type": "string" + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types" + ] + }, + "delete": { + "operationId": "OrganizationsEventTypesController_deleteTeamEventType", + "summary": "Delete a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": { + "post": { + "operationId": "OrganizationsEventTypesController_createPhoneCall", + "summary": "Create a phone call", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/event-types": { + "get": { + "operationId": "OrganizationsEventTypesController_getTeamsEventTypes", + "summary": "Get all team event types", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" } }, { - "name": "teamId", - "required": true, - "in": "path", + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, "schema": { "type": "number" } }, { - "name": "eventTypeId", - "required": true, - "in": "path", + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, "schema": { "type": "number" } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" - } - } - } - }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + "$ref": "#/components/schemas/GetTeamEventTypesOutput" } } } @@ -3278,10 +3848,12 @@ "tags": [ "Orgs / Teams / Event Types" ] - }, - "delete": { - "operationId": "OrganizationsEventTypesController_deleteTeamEventType", - "summary": "Delete a team event type", + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/memberships": { + "get": { + "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", + "summary": "Get all memberships", "parameters": [ { "name": "Authorization", @@ -3311,7 +3883,7 @@ } }, { - "name": "teamId", + "name": "orgId", "required": true, "in": "path", "schema": { @@ -3319,12 +3891,32 @@ } }, { - "name": "eventTypeId", + "name": "teamId", "required": true, "in": "path", "schema": { "type": "number" } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } } ], "responses": { @@ -3333,21 +3925,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + "$ref": "#/components/schemas/OrgTeamMembershipsOutputResponseDto" } } } } }, "tags": [ - "Orgs / Teams / Event Types" + "Orgs / Teams / Memberships" ] - } - }, - "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": { + }, "post": { - "operationId": "OrganizationsEventTypesController_createPhoneCall", - "summary": "Create a phone call", + "operationId": "OrganizationsTeamsMembershipsController_createOrgTeamMembership", + "summary": "Create a membership", "parameters": [ { "name": "Authorization", @@ -3377,7 +3967,7 @@ } }, { - "name": "eventTypeId", + "name": "orgId", "required": true, "in": "path", "schema": { @@ -3385,7 +3975,7 @@ } }, { - "name": "orgId", + "name": "teamId", "required": true, "in": "path", "schema": { @@ -3398,7 +3988,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreatePhoneCallInput" + "$ref": "#/components/schemas/CreateOrgTeamMembershipDto" } } } @@ -3409,21 +3999,21 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreatePhoneCallOutput" + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" } } } } }, "tags": [ - "Orgs / Teams / Event Types" + "Orgs / Teams / Memberships" ] } }, - "/v2/organizations/{orgId}/teams/event-types": { + "/v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId}": { "get": { - "operationId": "OrganizationsEventTypesController_getTeamsEventTypes", - "summary": "Get all team event types", + "operationId": "OrganizationsTeamsMembershipsController_getOrgTeamMembership", + "summary": "Get a membership", "parameters": [ { "name": "Authorization", @@ -3461,21 +4051,17 @@ } }, { - "name": "take", - "required": false, - "in": "query", - "description": "The number of items to return", - "example": 10, + "name": "teamId", + "required": true, + "in": "path", "schema": { "type": "number" } }, { - "name": "skip", - "required": false, - "in": "query", - "description": "The number of items to skip", - "example": 0, + "name": "membershipId", + "required": true, + "in": "path", "schema": { "type": "number" } @@ -3487,21 +4073,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetTeamEventTypesOutput" + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" } } } } }, "tags": [ - "Orgs / Teams / Event Types" + "Orgs / Teams / Memberships" ] - } - }, - "/v2/organizations/{orgId}/teams/{teamId}/memberships": { - "get": { - "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", - "summary": "Get all memberships", + }, + "delete": { + "operationId": "OrganizationsTeamsMembershipsController_deleteOrgTeamMembership", + "summary": "Delete a membership", "parameters": [ { "name": "Authorization", @@ -3547,21 +4131,9 @@ } }, { - "name": "take", - "required": false, - "in": "query", - "description": "The number of items to return", - "example": 10, - "schema": { - "type": "number" - } - }, - { - "name": "skip", - "required": false, - "in": "query", - "description": "The number of items to skip", - "example": 0, + "name": "membershipId", + "required": true, + "in": "path", "schema": { "type": "number" } @@ -3573,7 +4145,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgTeamMembershipsOutputResponseDto" + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" } } } @@ -3583,9 +4155,9 @@ "Orgs / Teams / Memberships" ] }, - "post": { - "operationId": "OrganizationsTeamsMembershipsController_createOrgTeamMembership", - "summary": "Create a membership", + "patch": { + "operationId": "OrganizationsTeamsMembershipsController_updateOrgTeamMembership", + "summary": "Update a membership", "parameters": [ { "name": "Authorization", @@ -3629,6 +4201,14 @@ "schema": { "type": "number" } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } } ], "requestBody": { @@ -3636,13 +4216,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateOrgTeamMembershipDto" + "$ref": "#/components/schemas/UpdateOrgTeamMembershipDto" } } } }, "responses": { - "201": { + "200": { "description": "", "content": { "application/json": { @@ -3658,60 +4238,127 @@ ] } }, - "/v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId}": { + "/v2/organizations/{orgId}/teams/{teamId}/routing-forms": { "get": { - "operationId": "OrganizationsTeamsMembershipsController_getOrgTeamMembership", - "summary": "Get a membership", + "operationId": "OrganizationsTeamsRoutingFormsController_getTeamRoutingForms", + "summary": "Get team routing forms", "parameters": [ { "name": "Authorization", "in": "header", - "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "description": "value must be `Bearer ` where `` 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": "x-cal-secret-key", - "in": "header", - "description": "For platform customers - OAuth client secret key", + "name": "sortUpdatedAt", "required": false, + "in": "query", + "description": "Sort by update time", "schema": { + "enum": [ + "asc", + "desc" + ], "type": "string" } }, { - "name": "x-cal-client-id", - "in": "header", - "description": "For platform customers - OAuth client ID", + "name": "afterCreatedAt", "required": false, + "in": "query", + "description": "Filter by responses created after this date", "schema": { + "format": "date-time", "type": "string" } }, { - "name": "orgId", - "required": true, - "in": "path", + "name": "beforeCreatedAt", + "required": false, + "in": "query", + "description": "Filter by responses created before this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } }, { - "name": "teamId", - "required": true, - "in": "path", + "name": "afterUpdatedAt", + "required": false, + "in": "query", + "description": "Filter by responses created after this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } }, { - "name": "membershipId", - "required": true, - "in": "path", + "name": "beforeUpdatedAt", + "required": false, + "in": "query", + "description": "Filter by responses updated before this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" + } + }, + { + "name": "routedToBookingUid", + "required": false, + "in": "query", + "description": "Filter by responses routed to a specific booking", + "schema": { + "type": "string" } } ], @@ -3721,43 +4368,35 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + "$ref": "#/components/schemas/GetRoutingFormsOutput" } } } } }, "tags": [ - "Orgs / Teams / Memberships" + "Orgs / Teams / Routing forms" ] - }, - "delete": { - "operationId": "OrganizationsTeamsMembershipsController_deleteOrgTeamMembership", - "summary": "Delete a membership", + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses": { + "get": { + "operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses", + "summary": "Get organization team routing form responses", "parameters": [ { "name": "Authorization", "in": "header", - "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "x-cal-secret-key", - "in": "header", - "description": "For platform customers - OAuth client secret key", - "required": false, + "description": "value must be `Bearer ` where `` is api key prefixed with cal_", + "required": true, "schema": { "type": "string" } }, { - "name": "x-cal-client-id", - "in": "header", - "description": "For platform customers - OAuth client ID", - "required": false, + "name": "routingFormId", + "required": true, + "in": "path", "schema": { "type": "string" } @@ -3779,117 +4418,120 @@ } }, { - "name": "membershipId", - "required": true, - "in": "path", + "name": "skip", + "required": false, + "in": "query", + "description": "Number of responses to skip", "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" - } - } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "Number of responses to take", + "schema": { + "type": "number" } - } - }, - "tags": [ - "Orgs / Teams / Memberships" - ] - }, - "patch": { - "operationId": "OrganizationsTeamsMembershipsController_updateOrgTeamMembership", - "summary": "Update a membership", - "parameters": [ + }, { - "name": "Authorization", - "in": "header", - "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "name": "sortCreatedAt", "required": false, + "in": "query", + "description": "Sort by creation time", "schema": { + "enum": [ + "asc", + "desc" + ], "type": "string" } }, { - "name": "x-cal-secret-key", - "in": "header", - "description": "For platform customers - OAuth client secret key", + "name": "sortUpdatedAt", "required": false, + "in": "query", + "description": "Sort by update time", "schema": { + "enum": [ + "asc", + "desc" + ], "type": "string" } }, { - "name": "x-cal-client-id", - "in": "header", - "description": "For platform customers - OAuth client ID", + "name": "afterCreatedAt", "required": false, + "in": "query", + "description": "Filter by responses created after this date", "schema": { + "format": "date-time", "type": "string" } }, { - "name": "orgId", - "required": true, - "in": "path", + "name": "beforeCreatedAt", + "required": false, + "in": "query", + "description": "Filter by responses created before this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } }, { - "name": "teamId", - "required": true, - "in": "path", + "name": "afterUpdatedAt", + "required": false, + "in": "query", + "description": "Filter by responses created after this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } }, { - "name": "membershipId", - "required": true, - "in": "path", + "name": "beforeUpdatedAt", + "required": false, + "in": "query", + "description": "Filter by responses updated before this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrgTeamMembershipDto" - } + }, + { + "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/OrgTeamMembershipOutputResponseDto" + "$ref": "#/components/schemas/GetRoutingFormResponsesOutput" } } } } }, "tags": [ - "Orgs / Teams / Memberships" + "Orgs / Teams / Routing forms / Responses" ] } }, - "/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses": { - "get": { - "operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses", - "summary": "Get routing form responses", + "/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses/{responseId}": { + "patch": { + "operationId": "OrganizationsTeamsRoutingFormsResponsesController_updateRoutingFormResponse", + "summary": "Update routing form response", "parameters": [ { "name": "Authorization", @@ -3900,6 +4542,14 @@ "type": "string" } }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, { "name": "routingFormId", "required": true, @@ -3907,15 +4557,33 @@ "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/GetRoutingFormResponsesOutput" + "$ref": "#/components/schemas/UpdateRoutingFormResponseOutput" } } } @@ -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" } } }, diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 9973d103c98872..492334a1a309df 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -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 ` where `` 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 ` where `` 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 ` where `` 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", @@ -3061,22 +3395,232 @@ } ], "responses": { - "200": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypeOutput" + } + } + } + } + }, + "tags": ["Orgs / Teams / Event Types"] + }, + "patch": { + "operationId": "OrganizationsEventTypesController_updateTeamEventType", + "summary": "Update a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + } + } + } + } + }, + "tags": ["Orgs / Teams / Event Types"] + }, + "delete": { + "operationId": "OrganizationsEventTypesController_deleteTeamEventType", + "summary": "Delete a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + } + } + } + } + }, + "tags": ["Orgs / Teams / Event Types"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": { + "post": { + "operationId": "OrganizationsEventTypesController_createPhoneCall", + "summary": "Create a phone call", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallInput" + } + } + } + }, + "responses": { + "201": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetTeamEventTypeOutput" + "$ref": "#/components/schemas/CreatePhoneCallOutput" } } } } }, "tags": ["Orgs / Teams / Event Types"] - }, - "patch": { - "operationId": "OrganizationsEventTypesController_updateTeamEventType", - "summary": "Update a team event type", + } + }, + "/v2/organizations/{orgId}/teams/event-types": { + "get": { + "operationId": "OrganizationsEventTypesController_getTeamsEventTypes", + "summary": "Get all team event types", "parameters": [ { "name": "Authorization", @@ -3106,7 +3650,7 @@ } }, { - "name": "teamId", + "name": "orgId", "required": true, "in": "path", "schema": { @@ -3114,41 +3658,45 @@ } }, { - "name": "eventTypeId", - "required": true, - "in": "path", + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, "schema": { "type": "number" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" - } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" } } - }, + ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + "$ref": "#/components/schemas/GetTeamEventTypesOutput" } } } } }, "tags": ["Orgs / Teams / Event Types"] - }, - "delete": { - "operationId": "OrganizationsEventTypesController_deleteTeamEventType", - "summary": "Delete a team event type", + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/memberships": { + "get": { + "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", + "summary": "Get all memberships", "parameters": [ { "name": "Authorization", @@ -3178,7 +3726,7 @@ } }, { - "name": "teamId", + "name": "orgId", "required": true, "in": "path", "schema": { @@ -3186,12 +3734,32 @@ } }, { - "name": "eventTypeId", + "name": "teamId", "required": true, "in": "path", "schema": { "type": "number" } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } } ], "responses": { @@ -3200,19 +3768,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + "$ref": "#/components/schemas/OrgTeamMembershipsOutputResponseDto" } } } } }, - "tags": ["Orgs / Teams / Event Types"] - } - }, - "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": { + "tags": ["Orgs / Teams / Memberships"] + }, "post": { - "operationId": "OrganizationsEventTypesController_createPhoneCall", - "summary": "Create a phone call", + "operationId": "OrganizationsTeamsMembershipsController_createOrgTeamMembership", + "summary": "Create a membership", "parameters": [ { "name": "Authorization", @@ -3242,7 +3808,7 @@ } }, { - "name": "eventTypeId", + "name": "orgId", "required": true, "in": "path", "schema": { @@ -3250,7 +3816,7 @@ } }, { - "name": "orgId", + "name": "teamId", "required": true, "in": "path", "schema": { @@ -3263,7 +3829,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreatePhoneCallInput" + "$ref": "#/components/schemas/CreateOrgTeamMembershipDto" } } } @@ -3274,19 +3840,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreatePhoneCallOutput" + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" } } } } }, - "tags": ["Orgs / Teams / Event Types"] + "tags": ["Orgs / Teams / Memberships"] } }, - "/v2/organizations/{orgId}/teams/event-types": { + "/v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId}": { "get": { - "operationId": "OrganizationsEventTypesController_getTeamsEventTypes", - "summary": "Get all team event types", + "operationId": "OrganizationsTeamsMembershipsController_getOrgTeamMembership", + "summary": "Get a membership", "parameters": [ { "name": "Authorization", @@ -3324,21 +3890,17 @@ } }, { - "name": "take", - "required": false, - "in": "query", - "description": "The number of items to return", - "example": 10, + "name": "teamId", + "required": true, + "in": "path", "schema": { "type": "number" } }, { - "name": "skip", - "required": false, - "in": "query", - "description": "The number of items to skip", - "example": 0, + "name": "membershipId", + "required": true, + "in": "path", "schema": { "type": "number" } @@ -3350,19 +3912,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetTeamEventTypesOutput" + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" } } } } }, - "tags": ["Orgs / Teams / Event Types"] - } - }, - "/v2/organizations/{orgId}/teams/{teamId}/memberships": { - "get": { - "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", - "summary": "Get all memberships", + "tags": ["Orgs / Teams / Memberships"] + }, + "delete": { + "operationId": "OrganizationsTeamsMembershipsController_deleteOrgTeamMembership", + "summary": "Delete a membership", "parameters": [ { "name": "Authorization", @@ -3408,21 +3968,9 @@ } }, { - "name": "take", - "required": false, - "in": "query", - "description": "The number of items to return", - "example": 10, - "schema": { - "type": "number" - } - }, - { - "name": "skip", - "required": false, - "in": "query", - "description": "The number of items to skip", - "example": 0, + "name": "membershipId", + "required": true, + "in": "path", "schema": { "type": "number" } @@ -3434,7 +3982,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgTeamMembershipsOutputResponseDto" + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" } } } @@ -3442,9 +3990,9 @@ }, "tags": ["Orgs / Teams / Memberships"] }, - "post": { - "operationId": "OrganizationsTeamsMembershipsController_createOrgTeamMembership", - "summary": "Create a membership", + "patch": { + "operationId": "OrganizationsTeamsMembershipsController_updateOrgTeamMembership", + "summary": "Update a membership", "parameters": [ { "name": "Authorization", @@ -3488,6 +4036,14 @@ "schema": { "type": "number" } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } } ], "requestBody": { @@ -3495,13 +4051,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateOrgTeamMembershipDto" + "$ref": "#/components/schemas/UpdateOrgTeamMembershipDto" } } } }, "responses": { - "201": { + "200": { "description": "", "content": { "application/json": { @@ -3515,60 +4071,121 @@ "tags": ["Orgs / Teams / Memberships"] } }, - "/v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId}": { + "/v2/organizations/{orgId}/teams/{teamId}/routing-forms": { "get": { - "operationId": "OrganizationsTeamsMembershipsController_getOrgTeamMembership", - "summary": "Get a membership", + "operationId": "OrganizationsTeamsRoutingFormsController_getTeamRoutingForms", + "summary": "Get team routing forms", "parameters": [ { "name": "Authorization", "in": "header", - "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "description": "value must be `Bearer ` where `` 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": "x-cal-secret-key", - "in": "header", - "description": "For platform customers - OAuth client secret key", + "name": "sortUpdatedAt", "required": false, + "in": "query", + "description": "Sort by update time", "schema": { + "enum": ["asc", "desc"], "type": "string" } }, { - "name": "x-cal-client-id", - "in": "header", - "description": "For platform customers - OAuth client ID", + "name": "afterCreatedAt", "required": false, + "in": "query", + "description": "Filter by responses created after this date", "schema": { + "format": "date-time", "type": "string" } }, { - "name": "orgId", - "required": true, - "in": "path", + "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": { - "type": "number" + "format": "date-time", + "type": "string" } }, { - "name": "teamId", - "required": true, - "in": "path", + "name": "beforeUpdatedAt", + "required": false, + "in": "query", + "description": "Filter by responses updated before this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } }, { - "name": "membershipId", - "required": true, - "in": "path", + "name": "routedToBookingUid", + "required": false, + "in": "query", + "description": "Filter by responses routed to a specific booking", "schema": { - "type": "number" + "type": "string" } } ], @@ -3578,41 +4195,33 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + "$ref": "#/components/schemas/GetRoutingFormsOutput" } } } } }, - "tags": ["Orgs / Teams / Memberships"] - }, - "delete": { - "operationId": "OrganizationsTeamsMembershipsController_deleteOrgTeamMembership", - "summary": "Delete a membership", + "tags": ["Orgs / Teams / Routing forms"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses": { + "get": { + "operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses", + "summary": "Get organization team routing form responses", "parameters": [ { "name": "Authorization", "in": "header", - "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "x-cal-secret-key", - "in": "header", - "description": "For platform customers - OAuth client secret key", - "required": false, + "description": "value must be `Bearer ` where `` is api key prefixed with cal_", + "required": true, "schema": { "type": "string" } }, { - "name": "x-cal-client-id", - "in": "header", - "description": "For platform customers - OAuth client ID", - "required": false, + "name": "routingFormId", + "required": true, + "in": "path", "schema": { "type": "string" } @@ -3634,113 +4243,112 @@ } }, { - "name": "membershipId", - "required": true, - "in": "path", + "name": "skip", + "required": false, + "in": "query", + "description": "Number of responses to skip", "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" - } - } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "Number of responses to take", + "schema": { + "type": "number" } - } - }, - "tags": ["Orgs / Teams / Memberships"] - }, - "patch": { - "operationId": "OrganizationsTeamsMembershipsController_updateOrgTeamMembership", - "summary": "Update a membership", - "parameters": [ + }, { - "name": "Authorization", - "in": "header", - "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "name": "sortCreatedAt", "required": false, + "in": "query", + "description": "Sort by creation time", "schema": { + "enum": ["asc", "desc"], "type": "string" } }, { - "name": "x-cal-secret-key", - "in": "header", - "description": "For platform customers - OAuth client secret key", + "name": "sortUpdatedAt", "required": false, + "in": "query", + "description": "Sort by update time", "schema": { + "enum": ["asc", "desc"], "type": "string" } }, { - "name": "x-cal-client-id", - "in": "header", - "description": "For platform customers - OAuth client ID", + "name": "afterCreatedAt", "required": false, + "in": "query", + "description": "Filter by responses created after this date", "schema": { + "format": "date-time", "type": "string" } }, { - "name": "orgId", - "required": true, - "in": "path", + "name": "beforeCreatedAt", + "required": false, + "in": "query", + "description": "Filter by responses created before this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } }, { - "name": "teamId", - "required": true, - "in": "path", + "name": "afterUpdatedAt", + "required": false, + "in": "query", + "description": "Filter by responses created after this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } }, { - "name": "membershipId", - "required": true, - "in": "path", + "name": "beforeUpdatedAt", + "required": false, + "in": "query", + "description": "Filter by responses updated before this date", "schema": { - "type": "number" + "format": "date-time", + "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrgTeamMembershipDto" - } + }, + { + "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/OrgTeamMembershipOutputResponseDto" + "$ref": "#/components/schemas/GetRoutingFormResponsesOutput" } } } } }, - "tags": ["Orgs / Teams / Memberships"] + "tags": ["Orgs / Teams / Routing forms / Responses"] } }, - "/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses": { - "get": { - "operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses", - "summary": "Get routing form responses", + "/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routingFormId}/responses/{responseId}": { + "patch": { + "operationId": "OrganizationsTeamsRoutingFormsResponsesController_updateRoutingFormResponse", + "summary": "Update routing form response", "parameters": [ { "name": "Authorization", @@ -3751,6 +4359,14 @@ "type": "string" } }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, { "name": "routingFormId", "required": true, @@ -3758,15 +4374,33 @@ "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/GetRoutingFormResponsesOutput" + "$ref": "#/components/schemas/UpdateRoutingFormResponseOutput" } } } @@ -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" } } }, diff --git a/packages/prisma/migrations/20250409135411_updated_at_routing_form_response/migration.sql b/packages/prisma/migrations/20250409135411_updated_at_routing_form_response/migration.sql new file mode 100644 index 00000000000000..03f6782083aeb7 --- /dev/null +++ b/packages/prisma/migrations/20250409135411_updated_at_routing_form_response/migration.sql @@ -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; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 26a3bdecf0a5ea..17c309acc1a271 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -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])