Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: v2 routing forms responses endpoints #19319

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { RoutingFormsRepository } from "@/modules/routing-forms/routing-forms.repository";
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
NotFoundException,
} from "@nestjs/common";
import { Request } from "express";

import { Team } from "@calcom/prisma/client";

@Injectable()
export class IsRoutingFormInTeam implements CanActivate {
constructor(private routingFormsRepository: RoutingFormsRepository) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
const teamId: string = request.params.teamId;
const routingFormId: string = request.params.routingFormId;

if (!routingFormId) {
throw new ForbiddenException("No routing form id found in request params.");
}

if (!teamId) {
throw new ForbiddenException("No team id found in request params.");
}

const routingForm = await this.routingFormsRepository.getTeamRoutingForm(Number(teamId), routingFormId);

if (!routingForm) {
throw new NotFoundException(
`Team with id=(${teamId}) routing form with id=(${routingFormId}) not found.`
);
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { OrganizationsTeamsService } from "@/modules/organizations/services/orga
import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service";
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service";
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
Expand All @@ -62,6 +63,7 @@ import { Module } from "@nestjs/common";
EventTypesModule_2024_06_14,
TeamsEventTypesModule,
TeamsModule,
OrganizationsTeamsRoutingFormsModule,
],
providers: [
OrganizationsRepository,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { GetRoutingFormResponsesOutput } from "@/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output";
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 { 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 { RoutingFormsRepositoryFixture } from "test/fixtures/repository/routing-forms.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";
import { Team } from "@calcom/prisma/client";

describe("Organizations Teams Routing Forms Responses", () => {
let app: INestApplication;

let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;

let teamsRepositoryFixture: TeamRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let routingFormsRepositoryFixture: RoutingFormsRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;

let org: Team;
let orgTeam: Team;

const authEmail = `organizations-teams-routing-forms-responses-user-${randomString()}@api.com`;
let user: User;
let apiKeyString: string;

let routingFormId: string;
const routingFormResponses = [
{
id: 1,
formFillerId: "cm78tvkvd0001kh8jq0tu5iq9",
response: {
"participant-field": {
label: "participant",
value: "mamut",
},
},
createdAt: new Date("2025-02-17T09:03:18.121Z"),
},
];

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
}).compile();

userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
routingFormsRepositoryFixture = new RoutingFormsRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);

org = await organizationsRepositoryFixture.create({
name: `organizations-teams-routing-forms-responses-organization-${randomString()}`,
isOrganization: true,
});

user = await userRepositoryFixture.create({
email: authEmail,
username: authEmail,
});

const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = keyString;

orgTeam = await teamsRepositoryFixture.create({
name: `organizations-teams-routing-forms-responses-team-${randomString()}`,
isOrganization: false,
parent: { connect: { id: org.id } },
});

await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});

await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: orgTeam.id } },
});

await profileRepositoryFixture.create({
uid: `usr-${user.id}`,
username: authEmail,
organization: {
connect: {
id: org.id,
},
},
user: {
connect: {
id: user.id,
},
},
});

const routingForm = await routingFormsRepositoryFixture.create({
name: "Test Routing Form",
description: null,
position: 0,
disabled: false,
fields: JSON.stringify([
{
type: "text",
label: "participant",
required: true,
},
]),
routes: JSON.stringify([
{
action: { type: "customPageMessage", value: "Thank you for your response" },
},
]),
user: {
connect: {
id: user.id,
},
},
team: {
connect: {
id: orgTeam.id,
},
},
responses: {
create: routingFormResponses,
},
});
routingFormId = routingForm.id;

app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);

await app.init();
});

it("should not get routing form responses for non existing org", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/99999/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses`)
.expect(401);
});

it("should not get routing form responses for non existing team", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/99999/routing-forms/${routingFormId}/responses`)
.expect(401);
});

it("should not get routing form responses for non existing routing form", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/99999/responses`)
.expect(401);
});

it("should get routing form responses", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses`)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
.then((response) => {
const responseBody: GetRoutingFormResponsesOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const responseData = responseBody.data;
expect(responseData).toBeDefined();
expect(responseData.length).toEqual(1);
expect(responseData[0].id).toEqual(routingFormResponses[0].id);
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());
});
});

afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
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 { 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 { ApiOperation, ApiTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";

import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { RoutingFormResponseOutput } from "@calcom/platform-types";

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,
})
@ApiTags("Orgs / Teams / Routing forms / Responses")
@UseGuards(
ApiAuthGuard,
IsOrgGuard,
IsTeamInOrg,
IsRoutingFormInTeam,
PlatformPlanGuard,
IsAdminAPIEnabledGuard
)
export class OrganizationsTeamsRoutingFormsResponsesController {
constructor(private readonly routingFormsResponsesService: RoutingFormsResponsesService) {}

@Get()
@ApiOperation({ summary: "Get routing form responses" })
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
async getRoutingFormResponses(
@Param("routingFormId") routingFormId: string
): Promise<GetRoutingFormResponsesOutput> {
const routingFormResponses = await this.routingFormsResponsesService.getRoutingFormResponses(
routingFormId
);

return {
status: SUCCESS_STATUS,
data: routingFormResponses,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/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 {}
Original file line number Diff line number Diff line change
@@ -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[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
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 {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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",
},
});
}
}
Loading
Loading