feat: v2 routing forms responses endpoints (#19319)

* wip: routing forms responses

* refactor structure

* Revert "refactor structure"

This reverts commit b641c06592cd9314008e81c4584039bd42393ac7.

* routing forms

* remove unused file
This commit is contained in:
Lauris Skraucis
2025-02-21 13:38:00 +05:30
committed by GitHub
parent 0b3aa4fd34
commit e6daed762b
19 changed files with 714 additions and 0 deletions
@@ -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;
}
}
@@ -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";
@@ -62,6 +63,7 @@ import { Module } from "@nestjs/common";
EventTypesModule_2024_06_14,
TeamsEventTypesModule,
TeamsModule,
OrganizationsTeamsRoutingFormsModule,
],
providers: [
OrganizationsRepository,
@@ -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();
});
});
@@ -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,
};
}
}
@@ -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 {}
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsEnum } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { RoutingFormResponseOutput } from "@calcom/platform-types";
export class GetRoutingFormResponsesOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({ type: [RoutingFormResponseOutput] })
@Expose()
@Type(() => RoutingFormResponseOutput)
data!: RoutingFormResponseOutput[];
}
@@ -0,0 +1,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 {}
@@ -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",
},
});
}
}
@@ -0,0 +1,31 @@
import { Injectable } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { RoutingFormResponseOutput, RoutingFormResponseResponseOutput } from "@calcom/platform-types";
import { App_RoutingForms_FormResponse } from "@calcom/prisma/client";
@Injectable()
export class RoutingFormsResponsesOutputService {
getRoutingFormResponses(
dbRoutingFormResponses: App_RoutingForms_FormResponse[]
): RoutingFormResponseOutput[] {
return dbRoutingFormResponses.map((response) => {
const parsed = plainToClass(RoutingFormResponseOutput, response, { strategy: "excludeAll" });
// note(Lauris): I don't know why plainToClass(RoutingFormResponseOutput)
// erases nested "response" object so parsing and attaching it manually
const parsedResponse: Record<string, RoutingFormResponseResponseOutput> = {};
const responseData = response.response || {};
for (const [key, value] of Object.entries(responseData)) {
parsedResponse[key] = plainToClass(RoutingFormResponseResponseOutput, value, {
strategy: "excludeAll",
});
}
return {
...parsed,
response: parsedResponse,
};
});
}
}
@@ -0,0 +1,17 @@
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);
}
}
@@ -0,0 +1,10 @@
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RoutingFormsRepository } from "@/modules/routing-forms/routing-forms.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [RoutingFormsRepository],
exports: [RoutingFormsRepository],
})
export class RoutingFormsModule {}
@@ -0,0 +1,16 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class RoutingFormsRepository {
constructor(private readonly dbRead: PrismaReadService) {}
async getTeamRoutingForm(teamId: number, routingFormId: string) {
return this.dbRead.prisma.app_RoutingForms_Form.findFirst({
where: {
id: routingFormId,
teamId,
},
});
}
}
+92
View File
@@ -2557,6 +2557,37 @@
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routing}-formId/responses": {
"get": {
"operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses",
"summary": "Get routing form responses",
"parameters": [
{
"name": "routing-formId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetRoutingFormResponsesOutput"
}
}
}
}
},
"tags": [
"Orgs / Teams / Routing forms / Responses"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": {
"get": {
"operationId": "OrganizationsTeamsSchedulesController_getUserSchedules",
@@ -14323,6 +14354,67 @@
"data"
]
},
"RoutingFormResponseOutput": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"formId": {
"type": "string"
},
"formFillerId": {
"type": "string"
},
"routedToBookingUid": {
"type": "string"
},
"response": {
"type": "object",
"example": {
"f00b26df-f54b-4985-8d98-17c5482c6a24": {
"label": "participant",
"value": "mamut"
}
}
},
"createdAt": {
"format": "date-time",
"type": "string"
}
},
"required": [
"id",
"formId",
"formFillerId",
"routedToBookingUid",
"response",
"createdAt"
]
},
"GetRoutingFormResponsesOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RoutingFormResponseOutput"
}
}
},
"required": [
"status",
"data"
]
},
"ProviderVerifyClientData": {
"type": "object",
"properties": {
@@ -0,0 +1,26 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TestingModule } from "@nestjs/testing";
import { Prisma, App_RoutingForms_Form } from "@prisma/client";
export class RoutingFormsRepositoryFixture {
private prismaReadClient: PrismaReadService["prisma"];
private prismaWriteClient: PrismaWriteService["prisma"];
constructor(private readonly module: TestingModule) {
this.prismaReadClient = module.get(PrismaReadService).prisma;
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
}
async get(routingFormId: App_RoutingForms_Form["id"]) {
return this.prismaReadClient.app_RoutingForms_Form.findUnique({ where: { id: routingFormId } });
}
async create(data: Prisma.App_RoutingForms_FormCreateInput) {
return this.prismaWriteClient.app_RoutingForms_Form.create({ data });
}
async delete(routingFormId: App_RoutingForms_Form["id"]) {
return this.prismaWriteClient.app_RoutingForms_Form.delete({ where: { id: routingFormId } });
}
}
+101
View File
@@ -2422,6 +2422,35 @@
"tags": ["Orgs / Teams / Memberships"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routing}-formId/responses": {
"get": {
"operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses",
"summary": "Get routing form responses",
"parameters": [
{
"name": "routing-formId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetRoutingFormResponsesOutput"
}
}
}
}
},
"tags": ["Orgs / Teams / Routing forms / Responses"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": {
"get": {
"operationId": "OrganizationsTeamsSchedulesController_getUserSchedules",
@@ -12917,6 +12946,54 @@
},
"required": ["status", "data"]
},
"RoutingFormResponseOutput": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"formId": {
"type": "string"
},
"formFillerId": {
"type": "string"
},
"routedToBookingUid": {
"type": "string"
},
"response": {
"type": "object",
"example": {
"f00b26df-f54b-4985-8d98-17c5482c6a24": {
"label": "participant",
"value": "mamut"
}
}
},
"createdAt": {
"format": "date-time",
"type": "string"
}
},
"required": ["id", "formId", "formFillerId", "routedToBookingUid", "response", "createdAt"]
},
"GetRoutingFormResponsesOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": ["success", "error"]
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RoutingFormResponseOutput"
}
}
},
"required": ["status", "data"]
},
"ProviderVerifyClientData": {
"type": "object",
"properties": {
@@ -13856,6 +13933,10 @@
"key": "value"
}
},
"rating": {
"type": "number",
"example": 4
},
"attendees": {
"type": "array",
"items": {
@@ -13996,6 +14077,10 @@
"key": "value"
}
},
"rating": {
"type": "number",
"example": 4
},
"attendees": {
"type": "array",
"items": {
@@ -14232,6 +14317,10 @@
"key": "value"
}
},
"rating": {
"type": "number",
"example": 4
},
"seatUid": {
"type": "string",
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
@@ -14362,6 +14451,10 @@
"key": "value"
}
},
"rating": {
"type": "number",
"example": 4
},
"seatUid": {
"type": "string",
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
@@ -14531,6 +14624,10 @@
"key": "value"
}
},
"rating": {
"type": "number",
"example": 4
},
"attendees": {
"type": "array",
"items": {
@@ -14656,6 +14753,10 @@
"key": "value"
}
},
"rating": {
"type": "number",
"example": 4
},
"attendees": {
"type": "array",
"items": {
+1
View File
@@ -10,3 +10,4 @@ export * from "./event-types";
export * from "./organizations";
export * from "./teams";
export * from "./embed";
export * from "./routing-forms";
@@ -0,0 +1 @@
export * from "./responses";
@@ -0,0 +1 @@
export * from "./routing-form-response.output";
@@ -0,0 +1,54 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { IsDate, IsInt, IsString, IsObject } from "class-validator";
export class RoutingFormResponseResponseOutput {
@ApiProperty()
@IsString()
@Expose()
label!: string;
@ApiPropertyOptional()
@IsString()
@Expose()
identifier?: string;
@ApiProperty()
@IsString()
@Expose()
value!: string | number | string[];
}
export class RoutingFormResponseOutput {
@ApiProperty()
@IsInt()
@Expose()
id!: string;
@ApiProperty()
@IsInt()
@Expose()
formId!: string;
@ApiProperty()
@IsString()
@Expose()
formFillerId!: string;
@ApiProperty()
@Expose()
@IsString()
routedToBookingUid!: string;
@ApiProperty({
example: { "f00b26df-f54b-4985-8d98-17c5482c6a24": { label: "participant", value: "mamut" } },
})
@IsObject()
@Expose()
response!: Record<string, RoutingFormResponseResponseOutput>;
@ApiProperty()
@Expose()
@IsDate()
createdAt!: Date;
}