Skip to content

Commit

Permalink
feat: v2 routing forms responses endpoints (#19319)
Browse files Browse the repository at this point in the history
* wip: routing forms responses

* refactor structure

* Revert "refactor structure"

This reverts commit b641c06.

* routing forms

* remove unused file
  • Loading branch information
supalarry authored Feb 21, 2025
1 parent 0b3aa4f commit e6daed7
Show file tree
Hide file tree
Showing 19 changed files with 714 additions and 0 deletions.
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;
}
}
2 changes: 2 additions & 0 deletions apps/api/v2/src/modules/organizations/organizations.module.ts
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

0 comments on commit e6daed7

Please sign in to comment.