From 78b90c3af0b80eab5a3b510b2eaf957626a20229 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Tue, 16 Dec 2025 00:16:49 -0300 Subject: [PATCH 01/21] refactor: implement authentication by patient and separate responsabilities --- infra/database/seed-dev.ts | 45 ++--- src/app/cryptography/crypography.service.ts | 6 +- .../appointments/appointments.controller.ts | 35 ++-- .../http/appointments/appointments.dtos.ts | 8 +- .../use-cases/cancel-appointment.use-case.ts | 7 +- .../use-cases/create-appointment.use-case.ts | 14 +- .../use-cases/get-appointments.use-case.ts | 40 +--- .../use-cases/update-appointment.use-case.ts | 8 +- src/app/http/auth/auth.controller.ts | 25 ++- src/app/http/auth/auth.dtos.ts | 3 + src/app/http/auth/auth.service.ts | 16 +- src/app/http/auth/tokens.repository.ts | 3 +- .../patient-requirements.controller.ts | 49 +++-- .../patient-requirements.dtos.ts | 19 +- .../patient-requirements.repository.ts | 44 +++-- .../patient-requirements.service.ts | 29 +-- .../patient-supports.controller.ts | 44 ++--- .../patient-supports/patient-supports.dtos.ts | 3 +- .../patient-supports.service.ts | 16 +- src/app/http/patients/patients.controller.ts | 47 ++--- src/app/http/patients/patients.dtos.ts | 11 +- src/app/http/patients/patients.repository.ts | 80 ++------ src/app/http/patients/patients.service.ts | 104 ++++------ .../http/referrals/referrals.controller.ts | 16 +- .../use-cases/get-referrals.use-case.ts | 38 +--- .../get-total-appointments.use-case.ts | 6 +- .../use-cases/get-total-patients.use-case.ts | 4 +- .../use-cases/get-total-referrals.use-case.ts | 6 +- .../get-total-referred-patients.use-case.ts | 2 +- src/app/http/users/users.controller.ts | 14 +- src/app/http/users/users.dtos.ts | 6 +- src/app/http/users/users.repository.ts | 9 +- src/common/decorators/auth-user.decorator.ts | 11 ++ .../{cookies.ts => cookies.decorator.ts} | 1 + .../decorators/current-user.decorator.ts | 10 - src/common/decorators/roles.decorator.ts | 4 +- src/common/guards/auth.guard.ts | 47 +++-- src/common/guards/roles.guard.ts | 4 +- src/domain/cookies.ts | 4 +- src/domain/entities/appointment.ts | 7 +- src/domain/entities/patient-requirement.ts | 26 +-- src/domain/entities/patient-support.ts | 6 +- src/domain/entities/patient.ts | 62 +++--- src/domain/entities/referral.ts | 9 +- src/domain/entities/token.ts | 13 +- src/domain/entities/user.ts | 30 +-- src/domain/enums/patient-requirements.ts | 25 +++ src/domain/enums/patients.ts | 18 ++ src/domain/enums/queries.ts | 10 + .../enums/{specialties.ts => shared.ts} | 0 src/domain/enums/statistics.ts | 8 + src/domain/enums/tokens.ts | 17 ++ src/domain/enums/users.ts | 5 + src/domain/modules/cryptography.ts | 6 +- src/domain/schemas/appointments/index.ts | 5 +- src/domain/schemas/appointments/requests.ts | 11 +- src/domain/schemas/appointments/responses.ts | 10 +- src/domain/schemas/auth.ts | 9 + src/domain/schemas/base.ts | 2 +- src/domain/schemas/patient-requirement.ts | 177 ----------------- .../schemas/patient-requirement/index.ts | 24 +++ .../schemas/patient-requirement/requests.ts | 76 ++++++++ .../schemas/patient-requirement/responses.ts | 56 ++++++ src/domain/schemas/patient-support.ts | 100 ---------- src/domain/schemas/patient-support/index.ts | 16 ++ .../schemas/patient-support/requests.ts | 32 ++++ .../schemas/patient-support/responses.ts | 21 ++ src/domain/schemas/patient.ts | 180 ------------------ src/domain/schemas/patient/index.ts | 39 ++++ src/domain/schemas/patient/requests.ts | 82 ++++++++ src/domain/schemas/patient/responses.ts | 40 ++++ src/domain/schemas/query.ts | 17 +- src/domain/schemas/referral/index.ts | 7 +- src/domain/schemas/referral/requests.ts | 11 +- src/domain/schemas/referral/responses.ts | 14 +- src/domain/schemas/shared.ts | 17 ++ src/domain/schemas/statistics/responses.ts | 6 +- src/domain/schemas/token.ts | 36 ++-- src/domain/schemas/user.ts | 72 ------- src/domain/schemas/user/index.ts | 25 +++ src/domain/schemas/user/requests.ts | 23 +++ src/domain/schemas/user/responses.ts | 9 + src/utils/utils.service.ts | 2 +- tests/config/api-client.ts | 20 +- tests/e2e/patients.spec.ts | 44 ++--- tests/e2e/users.spec.ts | 24 +-- 86 files changed, 1087 insertions(+), 1200 deletions(-) create mode 100644 src/common/decorators/auth-user.decorator.ts rename src/common/decorators/{cookies.ts => cookies.decorator.ts} (99%) delete mode 100644 src/common/decorators/current-user.decorator.ts create mode 100644 src/domain/enums/patient-requirements.ts create mode 100644 src/domain/enums/patients.ts create mode 100644 src/domain/enums/queries.ts rename src/domain/enums/{specialties.ts => shared.ts} (100%) create mode 100644 src/domain/enums/tokens.ts create mode 100644 src/domain/enums/users.ts delete mode 100644 src/domain/schemas/patient-requirement.ts create mode 100644 src/domain/schemas/patient-requirement/index.ts create mode 100644 src/domain/schemas/patient-requirement/requests.ts create mode 100644 src/domain/schemas/patient-requirement/responses.ts delete mode 100644 src/domain/schemas/patient-support.ts create mode 100644 src/domain/schemas/patient-support/index.ts create mode 100644 src/domain/schemas/patient-support/requests.ts create mode 100644 src/domain/schemas/patient-support/responses.ts delete mode 100644 src/domain/schemas/patient.ts create mode 100644 src/domain/schemas/patient/index.ts create mode 100644 src/domain/schemas/patient/requests.ts create mode 100644 src/domain/schemas/patient/responses.ts create mode 100644 src/domain/schemas/shared.ts delete mode 100644 src/domain/schemas/user.ts create mode 100644 src/domain/schemas/user/index.ts create mode 100644 src/domain/schemas/user/requests.ts create mode 100644 src/domain/schemas/user/responses.ts diff --git a/infra/database/seed-dev.ts b/infra/database/seed-dev.ts index 7324fb7..14e6b08 100644 --- a/infra/database/seed-dev.ts +++ b/infra/database/seed-dev.ts @@ -11,20 +11,20 @@ import { Referral } from '@/domain/entities/referral'; // import { Specialist } from '@/domain/entities/specialist'; import { User } from '@/domain/entities/user'; import { APPOINTMENT_STATUSES } from '@/domain/enums/appointments'; -import { REFERRAL_STATUSES } from '@/domain/enums/referrals'; -import { SPECIALTY_CATEGORIES } from '@/domain/enums/specialties'; import { - GENDERS, - PATIENT_CONDITIONS, - PATIENT_STATUS, -} from '@/domain/schemas/patient'; + PATIENT_REQUIREMENT_STATUSES, + PATIENT_REQUIREMENT_TYPES, +} from '@/domain/enums/patient-requirements'; import { - PATIENT_REQUIREMENT_STATUS, - PATIENT_REQUIREMENT_TYPE, -} from '@/domain/schemas/patient-requirement'; -// import { SPECIALIST_STATUS } from '@/domain/schemas/specialist'; -import { USER_ROLES } from '@/domain/schemas/user'; + PATIENT_CONDITIONS, + PATIENT_GENDERS, + PATIENT_STATUSES, +} from '@/domain/enums/patients'; +import { REFERRAL_STATUSES } from '@/domain/enums/referrals'; +import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; +import { USER_ROLES } from '@/domain/enums/users'; +// import { SPECIALIST_STATUS } from '@/domain/schemas/specialist'; import dataSource from './data.source'; const DATABASE_DEV_NAME = 'abnmo_dev'; @@ -144,30 +144,20 @@ async function main() { console.log(`👥 Creating ${i + 1} patients...`); } - const user = userRepository.create({ - name: faker.person.fullName(), - email: faker.internet.email().toLocaleLowerCase(), - password, - role: 'patient', - avatar_url: faker.image.avatar(), - }); - await userRepository.save(user); - const selectedState = faker.helpers.arrayElement(statesWithCities); // Set patient status: 10 pending, rest distributed among other statuses - let patientStatus: (typeof PATIENT_STATUS)[number]; + let patientStatus: (typeof PATIENT_STATUSES)[number]; if (i < 10) { patientStatus = 'pending'; } else { patientStatus = faker.helpers.arrayElement( - PATIENT_STATUS.filter((s) => s !== 'pending'), + PATIENT_STATUSES.filter((s) => s !== 'pending'), ); } const patient = patientRepository.create({ - user_id: user.id, - gender: faker.helpers.arrayElement(GENDERS), + gender: faker.helpers.arrayElement(PATIENT_GENDERS), date_of_birth: faker.date.birthdate({ min: 18, max: 80, mode: 'age' }), phone: faker.string.numeric(11), cpf: faker.string.numeric(11), @@ -242,10 +232,10 @@ async function main() { // Create between 0 and 2 requirements for each patient const requirementCount = faker.number.int({ min: 0, max: 2 }); for (let j = 0; j < requirementCount; j++) { - const status = faker.helpers.arrayElement(PATIENT_REQUIREMENT_STATUS); - const requirement = patientRequirementRepository.create({ + const status = faker.helpers.arrayElement(PATIENT_REQUIREMENT_STATUSES); + await patientRequirementRepository.save({ patient_id: patient.id, - type: faker.helpers.arrayElement(PATIENT_REQUIREMENT_TYPE), + type: faker.helpers.arrayElement(PATIENT_REQUIREMENT_TYPES), title: faker.lorem.words(3), description: faker.lorem.sentence(), status, @@ -263,7 +253,6 @@ async function main() { ? faker.date.between({ from: twoMonthsAgo, to: new Date() }) : new Date(), }); - await patientRequirementRepository.save(requirement); } } diff --git a/src/app/cryptography/crypography.service.ts b/src/app/cryptography/crypography.service.ts index d111c06..c02ee0c 100644 --- a/src/app/cryptography/crypography.service.ts +++ b/src/app/cryptography/crypography.service.ts @@ -3,7 +3,7 @@ import { JwtService, type JwtSignOptions } from '@nestjs/jwt'; import { compare, hash } from 'bcryptjs'; import type { Cryptography } from '@/domain/modules/cryptography'; -import type { AuthTokenPayloadByType } from '@/domain/schemas/token'; +import type { AuthTokenPayloads } from '@/domain/schemas/token'; @Injectable() export class CryptographyService implements Cryptography { @@ -19,9 +19,9 @@ export class CryptographyService implements Cryptography { return compare(plain, hash); } - async createToken( + async createToken( _type: T, - payload: AuthTokenPayloadByType[T], + payload: AuthTokenPayloads[T], options?: JwtSignOptions, ): Promise { return this.jwtService.signAsync(payload, options); diff --git a/src/app/http/appointments/appointments.controller.ts b/src/app/http/appointments/appointments.controller.ts index de606b6..7cd3fa8 100644 --- a/src/app/http/appointments/appointments.controller.ts +++ b/src/app/http/appointments/appointments.controller.ts @@ -10,12 +10,12 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { CurrentUser } from '@/common/decorators/current-user.decorator'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import type { GetAppointmentsResponseSchema } from '@/domain/schemas/appointments/responses'; -import { BaseResponseSchema } from '@/domain/schemas/base'; -import { UserSchema } from '@/domain/schemas/user'; +import type { GetAppointmentsResponse } from '@/domain/schemas/appointments/responses'; +import { BaseResponse } from '@/domain/schemas/base'; +import type { AuthUserDto } from '../auth/auth.dtos'; import { GetAppointmentsQuery } from './appointments.dtos'; import { CreateAppointmentDto, @@ -37,13 +37,13 @@ export class AppointmentsController { ) {} @Get() - @Roles(['manager', 'nurse', 'patient', 'specialist']) + @Roles(['manager', 'nurse', 'specialist', 'patient']) @ApiOperation({ summary: 'Lista todos os atendimentos' }) - async findAll( - @CurrentUser() user: UserSchema, + async getAppointments( @Query() query: GetAppointmentsQuery, - ): Promise { - const data = await this.getAppointmentsUseCase.execute({ user, query }); + @AuthUser() user: AuthUserDto, + ): Promise { + const data = await this.getAppointmentsUseCase.execute({ query, user }); return { success: true, @@ -56,13 +56,10 @@ export class AppointmentsController { @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Cadastra novo atendimento' }) async create( - @CurrentUser() user: UserSchema, + @AuthUser() user: AuthUserDto, @Body() createAppointmentDto: CreateAppointmentDto, - ): Promise { - await this.createAppointmentUseCase.execute({ - createAppointmentDto, - userId: user.id, - }); + ): Promise { + await this.createAppointmentUseCase.execute({ createAppointmentDto, user }); return { success: true, @@ -74,9 +71,9 @@ export class AppointmentsController { @Roles(['nurse', 'manager', 'specialist']) public async update( @Param('id') id: string, - @CurrentUser() user: UserSchema, + @AuthUser() user: AuthUserDto, @Body() updateAppointmentDto: UpdateAppointmentDto, - ): Promise { + ): Promise { await this.updateAppointmentUseCase.execute({ id, updateAppointmentDto, @@ -93,8 +90,8 @@ export class AppointmentsController { @Patch(':id/cancel') async cancel( @Param('id') id: string, - @CurrentUser() user: UserSchema, - ): Promise { + @AuthUser() user: AuthUserDto, + ): Promise { await this.cancelAppointmentUseCase.execute({ id, user }); return { diff --git a/src/app/http/appointments/appointments.dtos.ts b/src/app/http/appointments/appointments.dtos.ts index 7e7dfa0..97c0d40 100644 --- a/src/app/http/appointments/appointments.dtos.ts +++ b/src/app/http/appointments/appointments.dtos.ts @@ -6,6 +6,10 @@ import { updateAppointmentSchema, } from '@/domain/schemas/appointments/requests'; +export class GetAppointmentsQuery extends createZodDto( + getAppointmentsQuerySchema, +) {} + export class CreateAppointmentDto extends createZodDto( createAppointmentSchema, ) {} @@ -13,7 +17,3 @@ export class CreateAppointmentDto extends createZodDto( export class UpdateAppointmentDto extends createZodDto( updateAppointmentSchema, ) {} - -export class GetAppointmentsQuery extends createZodDto( - getAppointmentsQuerySchema, -) {} diff --git a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts index 61deb15..6904c6f 100644 --- a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts @@ -8,11 +8,12 @@ import { InjectRepository } from '@nestjs/typeorm'; import type { Repository } from 'typeorm'; import { Appointment } from '@/domain/entities/appointment'; -import { UserSchema } from '@/domain/schemas/user'; + +import type { AuthUserDto } from '../../auth/auth.dtos'; interface CancelAppointmentUseCaseRequest { id: string; - user: UserSchema; + user: AuthUserDto; } type CancelAppointmentUseCaseResponse = Promise; @@ -45,7 +46,7 @@ export class CancelAppointmentUseCase { await this.appointmentsRepository.save({ id, status: 'canceled' }); this.logger.log( - { appointmentId: id, userId: user.id }, + { appointmentId: id, userId: user.id, userEmail: user.email }, 'Appointment canceled successfully.', ); } diff --git a/src/app/http/appointments/use-cases/create-appointment.use-case.ts b/src/app/http/appointments/use-cases/create-appointment.use-case.ts index c140d8a..4fb920f 100644 --- a/src/app/http/appointments/use-cases/create-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/create-appointment.use-case.ts @@ -5,11 +5,12 @@ import type { Repository } from 'typeorm'; import { Appointment } from '@/domain/entities/appointment'; import { Patient } from '@/domain/entities/patient'; +import type { AuthUserDto } from '../../auth/auth.dtos'; import type { CreateAppointmentDto } from '../appointments.dtos'; interface CreateAppointmentUseCaseRequest { createAppointmentDto: CreateAppointmentDto; - userId: string; + user: AuthUserDto; } type CreateAppointmentUseCaseResponse = Promise; @@ -27,7 +28,7 @@ export class CreateAppointmentUseCase { async execute({ createAppointmentDto, - userId, + user, }: CreateAppointmentUseCaseRequest): CreateAppointmentUseCaseResponse { const { patient_id, date } = createAppointmentDto; @@ -56,11 +57,16 @@ export class CreateAppointmentUseCase { const appointment = await this.appointmentsRepository.save({ ...createAppointmentDto, - created_by: userId, + created_by: user.id, }); this.logger.log( - { patientId: patient_id, appointmentId: appointment.id }, + { + patientId: patient_id, + appointmentId: appointment.id, + userId: user.id, + userEmail: user.email, + }, 'Appointment created successfully', ); } diff --git a/src/app/http/appointments/use-cases/get-appointments.use-case.ts b/src/app/http/appointments/use-cases/get-appointments.use-case.ts index cb158a8..7a70afd 100644 --- a/src/app/http/appointments/use-cases/get-appointments.use-case.ts +++ b/src/app/http/appointments/use-cases/get-appointments.use-case.ts @@ -12,12 +12,12 @@ import { import { Appointment } from '@/domain/entities/appointment'; import type { AppointmentOrderBy } from '@/domain/enums/appointments'; import type { AppointmentResponse } from '@/domain/schemas/appointments/responses'; -import { UserSchema } from '@/domain/schemas/user'; +import type { AuthUserDto } from '../../auth/auth.dtos'; import type { GetAppointmentsQuery } from '../appointments.dtos'; interface GetAppointmentsUseCaseRequest { - user: UserSchema; + user: AuthUserDto; query: GetAppointmentsQuery; } @@ -53,7 +53,7 @@ export class GetAppointmentsUseCase { const endDate = query.endDate ? new Date(query.endDate) : null; if (user.role === 'patient') { - where.patient = { user: { id: user.id } }; + where.patient = { id: user.id }; } if (startDate && !endDate) { @@ -81,7 +81,7 @@ export class GetAppointmentsUseCase { } if (search) { - where.patient = { user: { name: ILike(`%${search}%`) } }; + where.patient = { name: ILike(`%${search}%`) }; } const total = await this.appointmentsRepository.count({ where }); @@ -89,42 +89,18 @@ export class GetAppointmentsUseCase { const orderBy = ORDER_BY_MAPPING[query.orderBy]; const order = orderBy === 'patient' - ? { patient: { user: { name: query.order } } } + ? { patient: { name: query.order } } : { [orderBy]: query.order }; - const appointmentsQuery = await this.appointmentsRepository.find({ - relations: { patient: { user: true } }, - select: { - patient: { - id: true, - user: { name: true, avatar_url: true }, - }, - }, + const appointments = await this.appointmentsRepository.find({ + select: { patient: { id: true, name: true, avatar_url: true } }, + relations: { patient: true }, skip: (page - 1) * perPage, take: perPage, order, where, }); - const appointments = appointmentsQuery.map((appointment) => ({ - id: appointment.id, - patient_id: appointment.patient_id, - date: appointment.date, - status: appointment.status, - category: appointment.category, - condition: appointment.condition, - annotation: appointment.annotation, - professional_name: appointment.professional_name, - created_by: appointment.created_by, - created_at: appointment.created_at, - updated_at: appointment.updated_at, - patient: { - name: appointment.patient.user.name, - email: appointment.patient.user.email, - avatar_url: appointment.patient.user.avatar_url, - }, - })); - return { appointments, total }; } } diff --git a/src/app/http/appointments/use-cases/update-appointment.use-case.ts b/src/app/http/appointments/use-cases/update-appointment.use-case.ts index d1e1a43..f491cd3 100644 --- a/src/app/http/appointments/use-cases/update-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/update-appointment.use-case.ts @@ -8,14 +8,14 @@ import { InjectRepository } from '@nestjs/typeorm'; import type { Repository } from 'typeorm'; import { Appointment } from '@/domain/entities/appointment'; -import { UserSchema } from '@/domain/schemas/user'; +import type { AuthUserDto } from '../../auth/auth.dtos'; import type { UpdateAppointmentDto } from '../appointments.dtos'; interface UpdateAppointmentUseCaseRequest { id: string; + user: AuthUserDto; updateAppointmentDto: UpdateAppointmentDto; - user: UserSchema; } type UpdateAppointmentUseCaseResponse = Promise; @@ -31,8 +31,8 @@ export class UpdateAppointmentUseCase { async execute({ id, - updateAppointmentDto, user, + updateAppointmentDto, }: UpdateAppointmentUseCaseRequest): UpdateAppointmentUseCaseResponse { const appointment = await this.appointmentsRepository.findOne({ where: { id }, @@ -53,7 +53,7 @@ export class UpdateAppointmentUseCase { await this.appointmentsRepository.save(appointment); this.logger.log( - { appointmentId: id, userId: user.id }, + { appointmentId: id, userId: user.id, userEmail: user.email }, 'Appointment updated successfully.', ); } diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index 85a4243..9a5c15c 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -9,12 +9,12 @@ import { import { ApiOperation } from '@nestjs/swagger'; import type { Request, Response } from 'express'; -import { Cookies } from '@/common/decorators/cookies'; -import { CurrentUser } from '@/common/decorators/current-user.decorator'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; +import { Cookies } from '@/common/decorators/cookies.decorator'; import { Public } from '@/common/decorators/public.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import { COOKIES_MAPPING } from '@/domain/cookies'; -import type { BaseResponseSchema } from '@/domain/schemas/base'; +import type { BaseResponse } from '@/domain/schemas/base'; import { UserSchema } from '@/domain/schemas/user'; import { UtilsService } from '@/utils/utils.service'; @@ -41,10 +41,11 @@ export class AuthController { @Req() request: Request, @Body() signInWithEmailDto: SignInWithEmailDto, @Res({ passthrough: true }) response: Response, - ): Promise { + ): Promise { const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; - const { accessToken } = await this.authService.signIn(signInWithEmailDto); + const { accessToken } = + await this.authService.signInUser(signInWithEmailDto); this.utilsService.setCookie(response, { name: COOKIES_MAPPING.access_token, @@ -63,10 +64,8 @@ export class AuthController { @Public() @Post('register') @ApiOperation({ summary: 'Registro de um novo usuário' }) - async register( - @Body() createUserDto: CreateUserDto, - ): Promise { - await this.authService.register(createUserDto); + async register(@Body() createUserDto: CreateUserDto): Promise { + await this.authService.registerUser(createUserDto); return { success: true, @@ -135,7 +134,7 @@ export class AuthController { @Req() request: Request, @Body() recoverPasswordDto: RecoverPasswordDto, @Res({ passthrough: true }) response: Response, - ): Promise { + ): Promise { const { passwordResetToken } = await this.authService.forgotPassword( recoverPasswordDto.email, ); @@ -156,11 +155,11 @@ export class AuthController { } @Post('change-password') - @Roles(['nurse', 'manager', 'patient', 'specialist', 'admin']) + @Roles(['nurse', 'manager', 'specialist', 'admin']) async changePassword( @Body() changePasswordDto: ChangePasswordDto, - @CurrentUser() user: UserSchema, - ): Promise { + @AuthUser() user: UserSchema, + ): Promise { await this.authService.changePassword(user, changePasswordDto); return { diff --git a/src/app/http/auth/auth.dtos.ts b/src/app/http/auth/auth.dtos.ts index b43cdef..2debe21 100644 --- a/src/app/http/auth/auth.dtos.ts +++ b/src/app/http/auth/auth.dtos.ts @@ -1,6 +1,7 @@ import { createZodDto } from 'nestjs-zod'; import { + authUserSchema, changePasswordSchema, recoverPasswordSchema, resetPasswordSchema, @@ -8,6 +9,8 @@ import { } from '@/domain/schemas/auth'; import { createAuthTokenSchema } from '@/domain/schemas/token'; +export class AuthUserDto extends createZodDto(authUserSchema) {} + export class SignInWithEmailDto extends createZodDto(signInWithEmailSchema) {} export class CreateAuthTokenDto extends createZodDto(createAuthTokenSchema) {} diff --git a/src/app/http/auth/auth.service.ts b/src/app/http/auth/auth.service.ts index 32e3ab2..fbbccd5 100644 --- a/src/app/http/auth/auth.service.ts +++ b/src/app/http/auth/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { CryptographyService } from '@/app/cryptography/crypography.service'; -import { AUTH_TOKENS_MAPPING } from '@/domain/schemas/token'; +import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; import { UserSchema } from '@/domain/schemas/user'; import { EnvService } from '@/env/env.service'; @@ -23,7 +23,7 @@ export class AuthService { private readonly envService: EnvService, ) {} - async register(createUserDto: CreateUserDto): Promise { + async registerUser(createUserDto: CreateUserDto): Promise { await this.usersService.create(createUserDto); // TODO: create e-mail template builder @@ -33,9 +33,11 @@ export class AuthService { // await this.mailService.sendEmail(createUserDto.email, subject, body); } - async signIn({ email, password, rememberMe }: SignInWithEmailDto): Promise<{ - accessToken: string; - }> { + async signInUser({ + email, + password, + rememberMe, + }: SignInWithEmailDto): Promise<{ accessToken: string }> { const user = await this.usersRepository.findByEmail(email); if (!user) { @@ -94,7 +96,7 @@ export class AuthService { return { passwordResetToken: 'dummy_token' }; } - const payload = { sub: user.id }; + const payload = { sub: user.id, role: user.role }; const passwordResetToken = await this.cryptographyService.createToken( AUTH_TOKENS_MAPPING.password_reset, @@ -162,7 +164,7 @@ export class AuthService { await this.tokensRepository.deleteToken(token); - const { accessToken } = await this.signIn({ + const { accessToken } = await this.signInUser({ email: user.email, password: newPassword, rememberMe: false, diff --git a/src/app/http/auth/tokens.repository.ts b/src/app/http/auth/tokens.repository.ts index c016b2d..1ec71b1 100644 --- a/src/app/http/auth/tokens.repository.ts +++ b/src/app/http/auth/tokens.repository.ts @@ -14,8 +14,7 @@ export class TokensRepository { ) {} async saveToken(data: CreateAuthTokenDto) { - const token = this.tokensRepository.create(data); - await this.tokensRepository.save(token); + await this.tokensRepository.save(data); } async findToken(token: string) { diff --git a/src/app/http/patient-requirements/patient-requirements.controller.ts b/src/app/http/patient-requirements/patient-requirements.controller.ts index 122e816..2720e43 100644 --- a/src/app/http/patient-requirements/patient-requirements.controller.ts +++ b/src/app/http/patient-requirements/patient-requirements.controller.ts @@ -9,19 +9,19 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { CurrentUser } from '@/common/decorators/current-user.decorator'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import { BaseResponseSchema } from '@/domain/schemas/base'; -import { - FindAllPatientsRequirementsByPatientIdResponseSchema, - FindAllPatientsRequirementsResponseSchema, -} from '@/domain/schemas/patient-requirement'; -import { UserSchema } from '@/domain/schemas/user'; +import { BaseResponse } from '@/domain/schemas/base'; +import type { + GetPatientRequirementsByPatientIdResponse, + GetPatientRequirementsResponse, +} from '@/domain/schemas/patient-requirement/responses'; +import type { AuthUserDto } from '../auth/auth.dtos'; import { CreatePatientRequirementDto, - FindAllPatientsRequirementsByPatientIdDto, - FindAllPatientsRequirementsQueryDto, + GetPatientRequirementsByPatientIdQuery, + GetPatientRequirementsQuery, } from './patient-requirements.dtos'; import { PatientRequirementsRepository } from './patient-requirements.repository'; import { PatientRequirementsService } from './patient-requirements.service'; @@ -39,11 +39,11 @@ export class PatientRequirementsController { @ApiOperation({ summary: 'Adiciona nova solicitação.' }) public async create( @Body() createPatientRequirementDto: CreatePatientRequirementDto, - @CurrentUser() currentUser: UserSchema, - ): Promise { + @AuthUser() authUser: AuthUserDto, + ): Promise { await this.patientRequirementsService.create( createPatientRequirementDto, - currentUser.id, + authUser, ); return { @@ -57,8 +57,8 @@ export class PatientRequirementsController { @ApiOperation({ summary: 'Aprova uma solicitação por ID.' }) async approve( @Param('id') id: string, - @CurrentUser() user: UserSchema, - ): Promise { + @AuthUser() user: AuthUserDto, + ): Promise { await this.patientRequirementsService.approve(id, user); return { @@ -72,9 +72,9 @@ export class PatientRequirementsController { @ApiOperation({ summary: 'Recusa uma solicitação por ID.' }) public async decline( @Param('id') id: string, - @CurrentUser() currentUser: UserSchema, - ): Promise { - await this.patientRequirementsService.decline(id, currentUser.id); + @AuthUser() authUser: AuthUserDto, + ): Promise { + await this.patientRequirementsService.decline(id, authUser); return { success: true, @@ -88,8 +88,8 @@ export class PatientRequirementsController { summary: 'Lista todas as solicitações de pacientes com paginação e filtros', }) async findAll( - @Query() filters: FindAllPatientsRequirementsQueryDto, - ): Promise { + @Query() filters: GetPatientRequirementsQuery, + ): Promise { const { requirements, total } = await this.patientRequirementsRepository.findAll(filters); @@ -107,8 +107,8 @@ export class PatientRequirementsController { }) async findAllByPatientId( @Param('id') id: string, - @Query() filters: FindAllPatientsRequirementsByPatientIdDto, - ): Promise { + @Query() filters: GetPatientRequirementsByPatientIdQuery, + ): Promise { const { requirements, total } = await this.patientRequirementsRepository.findAllByPatientId(id, filters); @@ -120,12 +120,11 @@ export class PatientRequirementsController { } @Get('/me') - @Roles(['patient']) @ApiOperation({ summary: 'Busca todas as solicitações do paciente logado.' }) async findAllByPatientLogged( - @CurrentUser() user: UserSchema, - @Query() filters: FindAllPatientsRequirementsByPatientIdDto, - ): Promise { + @AuthUser() user: AuthUserDto, + @Query() filters: GetPatientRequirementsByPatientIdQuery, + ): Promise { const { requirements, total } = await this.patientRequirementsRepository.findAllByPatientLogged( user.id, diff --git a/src/app/http/patient-requirements/patient-requirements.dtos.ts b/src/app/http/patient-requirements/patient-requirements.dtos.ts index b5ff52e..10d7067 100644 --- a/src/app/http/patient-requirements/patient-requirements.dtos.ts +++ b/src/app/http/patient-requirements/patient-requirements.dtos.ts @@ -2,21 +2,18 @@ import { createZodDto } from 'nestjs-zod'; import { createPatientRequirementSchema, - findAllPatientsRequirementsByPatientIdQuerySchema, - findAllPatientsRequirementsQuerySchema, - patientRequirementSchema, -} from '@/domain/schemas/patient-requirement'; + getPatientRequirementsByPatientIdQuerySchema, + getPatientRequirementsQuerySchema, +} from '@/domain/schemas/patient-requirement/requests'; -export class PatientRequirementDto extends createZodDto( - patientRequirementSchema, -) {} export class CreatePatientRequirementDto extends createZodDto( createPatientRequirementSchema, ) {} -export class FindAllPatientsRequirementsByPatientIdDto extends createZodDto( - findAllPatientsRequirementsByPatientIdQuerySchema, + +export class GetPatientRequirementsByPatientIdQuery extends createZodDto( + getPatientRequirementsByPatientIdQuerySchema, ) {} -export class FindAllPatientsRequirementsQueryDto extends createZodDto( - findAllPatientsRequirementsQuerySchema, +export class GetPatientRequirementsQuery extends createZodDto( + getPatientRequirementsQuerySchema, ) {} diff --git a/src/app/http/patient-requirements/patient-requirements.repository.ts b/src/app/http/patient-requirements/patient-requirements.repository.ts index 0ef48fa..8238082 100644 --- a/src/app/http/patient-requirements/patient-requirements.repository.ts +++ b/src/app/http/patient-requirements/patient-requirements.repository.ts @@ -2,16 +2,16 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { PatientRequirement } from '@/domain/entities/patient-requirement'; -import { - PatientRequirementByPatientIdResponseType, - PatientRequirementListItemSchema, - type PatientRequirementOrderBy, -} from '@/domain/schemas/patient-requirement'; +import type { PatientRequirementOrderBy } from '@/domain/enums/patient-requirements'; +import type { + PatientRequirementByPatientId, + PatientRequirementItem, +} from '@/domain/schemas/patient-requirement/responses'; import { CreatePatientRequirementDto, - type FindAllPatientsRequirementsByPatientIdDto, - FindAllPatientsRequirementsQueryDto, + type GetPatientRequirementsByPatientIdQuery, + GetPatientRequirementsQuery, } from './patient-requirements.dtos'; export class PatientRequirementsRepository { @@ -61,9 +61,9 @@ export class PatientRequirementsRepository { public async findAllByPatientId( id: string, - filters: FindAllPatientsRequirementsByPatientIdDto, + filters: GetPatientRequirementsByPatientIdQuery, ): Promise<{ - requirements: PatientRequirementByPatientIdResponseType[]; + requirements: PatientRequirementByPatientId[]; total: number; }> { const { status, startDate, endDate, page, perPage } = filters; @@ -97,8 +97,8 @@ export class PatientRequirementsRepository { const total = await query.getCount(); const rawRequirements = await query.getMany(); - const requirements: PatientRequirementByPatientIdResponseType[] = - rawRequirements.map((requirement) => ({ + const requirements: PatientRequirementByPatientId[] = rawRequirements.map( + (requirement) => ({ id: requirement.id, type: requirement.type, title: requirement.title, @@ -106,16 +106,17 @@ export class PatientRequirementsRepository { submitted_at: requirement.submitted_at, approved_at: requirement.approved_at, created_at: requirement.created_at, - })); + }), + ); return { requirements, total }; } async findAllByPatientLogged( patientId: string, - filters: FindAllPatientsRequirementsByPatientIdDto, + filters: GetPatientRequirementsByPatientIdQuery, ): Promise<{ - requirements: PatientRequirementByPatientIdResponseType[]; + requirements: PatientRequirementByPatientId[]; total: number; }> { const { status, startDate, endDate, page, perPage } = filters; @@ -146,8 +147,8 @@ export class PatientRequirementsRepository { const total = await query.getCount(); const rawRequirements = await query.getMany(); - const requirements: PatientRequirementByPatientIdResponseType[] = - rawRequirements.map((requirement) => ({ + const requirements: PatientRequirementByPatientId[] = rawRequirements.map( + (requirement) => ({ id: requirement.id, type: requirement.type, title: requirement.title, @@ -155,13 +156,14 @@ export class PatientRequirementsRepository { submitted_at: requirement.submitted_at, approved_at: requirement.approved_at, created_at: requirement.created_at, - })); + }), + ); return { requirements, total }; } - public async findAll(filters: FindAllPatientsRequirementsQueryDto): Promise<{ - requirements: PatientRequirementListItemSchema[]; + public async findAll(filters: GetPatientRequirementsQuery): Promise<{ + requirements: PatientRequirementItem[]; total: number; }> { const { @@ -241,8 +243,8 @@ export class PatientRequirementsRepository { created_at: requirement.created_at, patient: { id: requirement.patient.id, - name: requirement.patient.user.name, - avatar_url: requirement.patient.user.avatar_url, + name: requirement.patient.name, + avatar_url: requirement.patient.avatar_url, }, })); diff --git a/src/app/http/patient-requirements/patient-requirements.service.ts b/src/app/http/patient-requirements/patient-requirements.service.ts index 370c1fe..9ee4f18 100644 --- a/src/app/http/patient-requirements/patient-requirements.service.ts +++ b/src/app/http/patient-requirements/patient-requirements.service.ts @@ -5,8 +5,7 @@ import { NotFoundException, } from '@nestjs/common'; -import { UserSchema } from '@/domain/schemas/user'; - +import type { AuthUserDto } from '../auth/auth.dtos'; import { PatientsRepository } from '../patients/patients.repository'; import { CreatePatientRequirementDto } from './patient-requirements.dtos'; import { PatientRequirementsRepository } from './patient-requirements.repository'; @@ -22,28 +21,28 @@ export class PatientRequirementsService { async create( createPatientRequirementDto: CreatePatientRequirementDto, - userId: string, + authUser: AuthUserDto, ): Promise { const { patient_id } = createPatientRequirementDto; - const patientExists = await this.patientsRepository.findById(patient_id); + const patient = await this.patientsRepository.findById(patient_id); - if (!patientExists) { + if (!patient) { throw new NotFoundException('Paciente não encontrado.'); } await this.patientRequirementsRepository.create({ ...createPatientRequirementDto, - required_by: userId, + required_by: authUser.id, }); this.logger.log( - { patientId: patient_id, requiredBy: userId }, + { patientId: patient_id, createdBy: authUser.id }, 'Requirement created successfully', ); } - async approve(id: string, user: UserSchema): Promise { + async approve(id: string, authUser: AuthUserDto): Promise { const patientRequirement = await this.patientRequirementsRepository.findById(id); @@ -57,15 +56,19 @@ export class PatientRequirementsService { ); } - await this.patientRequirementsRepository.approve(id, user.id); + await this.patientRequirementsRepository.approve(id, authUser.id); this.logger.log( - { id: patientRequirement.id, userId: user.id, approvedAt: new Date() }, + { + id: patientRequirement.id, + userId: authUser.id, + approvedAt: new Date(), + }, 'Requirement approved successfully', ); } - async decline(id: string, declinedBy: string): Promise { + async decline(id: string, authUser: AuthUserDto): Promise { const requirement = await this.patientRequirementsRepository.findById(id); if (!requirement) { @@ -77,10 +80,10 @@ export class PatientRequirementsService { 'Solicitação precisa estar aguardando aprovação para ser recusada.', ); - await this.patientRequirementsRepository.decline(id, declinedBy); + await this.patientRequirementsRepository.decline(id, authUser.id); this.logger.log( - { id: requirement.id, userId: declinedBy, approvedAt: new Date() }, + { id: requirement.id, userId: authUser.id, approvedAt: new Date() }, 'Requirement declined successfully', ); } diff --git a/src/app/http/patient-supports/patient-supports.controller.ts b/src/app/http/patient-supports/patient-supports.controller.ts index c04efff..3a2fee1 100644 --- a/src/app/http/patient-supports/patient-supports.controller.ts +++ b/src/app/http/patient-supports/patient-supports.controller.ts @@ -11,16 +11,12 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { CurrentUser } from '@/common/decorators/current-user.decorator'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import type { User } from '@/domain/entities/user'; -import { - CreatePatientSupportResponseSchema, - DeletePatientSupportResponseSchema, - FindOnePatientsSupportResponseSchema, - UpdatePatientSupportResponseSchema, -} from '@/domain/schemas/patient-support'; +import type { BaseResponse } from '@/domain/schemas/base'; +import type { GetPatientSupportResponse } from '@/domain/schemas/patient-support/responses'; +import type { AuthUserDto } from '../auth/auth.dtos'; import { CreatePatientSupportDto } from '../patient-supports/patient-supports.dtos'; import { UpdatePatientSupportDto } from './patient-supports.dtos'; import { PatientSupportsRepository } from './patient-supports.repository'; @@ -36,9 +32,7 @@ export class PatientSupportsController { @Get(':id') @ApiOperation({ summary: 'Busca um contato de apoio pelo ID' }) - async findById( - @Param('id') id: string, - ): Promise { + async findById(@Param('id') id: string): Promise { const patientSupport = await this.patientSupportsRepository.findById(id); if (!patientSupport) { @@ -53,16 +47,16 @@ export class PatientSupportsController { } @Post(':patientId') - @Roles(['nurse', 'manager', 'patient']) + @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Registra um novo contato de apoio para um paciente', }) async createPatientSupport( @Param('patientId') patientId: string, + @AuthUser() authUser: AuthUserDto, @Body() createPatientSupportDto: CreatePatientSupportDto, - @CurrentUser() user: User, - ): Promise { - if (user.role === 'patient' && user.id !== patientId) { + ): Promise { + if (authUser.role === 'patient' && authUser.id !== patientId) { throw new ForbiddenException( 'Você não tem permissão para registrar contatos de apoio para este paciente.', ); @@ -80,14 +74,18 @@ export class PatientSupportsController { } @Put(':id') - @Roles(['nurse', 'manager', 'patient']) + @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Atualiza um contato de apoio pelo ID' }) async updatePatientSupport( @Param('id') id: string, + @AuthUser() authUser: AuthUserDto, @Body() updatePatientSupportDto: UpdatePatientSupportDto, - @CurrentUser() user: User, - ): Promise { - await this.patientSupportsService.update(id, updatePatientSupportDto, user); + ): Promise { + await this.patientSupportsService.update( + id, + updatePatientSupportDto, + authUser, + ); return { success: true, @@ -96,13 +94,13 @@ export class PatientSupportsController { } @Delete(':id') - @Roles(['nurse', 'manager', 'patient']) + @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Remove um contato de apoio pelo ID' }) async remove( @Param('id') id: string, - @CurrentUser() user: User, - ): Promise { - await this.patientSupportsService.remove(id, user); + @AuthUser() authUser: AuthUserDto, + ): Promise { + await this.patientSupportsService.remove(id, authUser); return { success: true, diff --git a/src/app/http/patient-supports/patient-supports.dtos.ts b/src/app/http/patient-supports/patient-supports.dtos.ts index d88910e..4de716f 100644 --- a/src/app/http/patient-supports/patient-supports.dtos.ts +++ b/src/app/http/patient-supports/patient-supports.dtos.ts @@ -3,11 +3,12 @@ import { createZodDto } from 'nestjs-zod'; import { createPatientSupportSchema, updatePatientSupportSchema, -} from '@/domain/schemas/patient-support'; +} from '@/domain/schemas/patient-support/requests'; export class CreatePatientSupportDto extends createZodDto( createPatientSupportSchema, ) {} + export class UpdatePatientSupportDto extends createZodDto( updatePatientSupportSchema, ) {} diff --git a/src/app/http/patient-supports/patient-supports.service.ts b/src/app/http/patient-supports/patient-supports.service.ts index 0e3b18b..b3c95c7 100644 --- a/src/app/http/patient-supports/patient-supports.service.ts +++ b/src/app/http/patient-supports/patient-supports.service.ts @@ -6,8 +6,8 @@ import { } from '@nestjs/common'; import { PatientSupport } from '@/domain/entities/patient-support'; -import type { User } from '@/domain/entities/user'; +import type { AuthUserDto } from '../auth/auth.dtos'; import { PatientsRepository } from '../patients/patients.repository'; import { CreatePatientSupportDto, @@ -60,7 +60,7 @@ export class PatientSupportsService { async update( id: string, updatePatientsSupportDto: UpdatePatientSupportDto, - user: User, + authUser: AuthUserDto, ): Promise { const patientSupport = await this.patientSupportsRepository.findById(id); @@ -68,7 +68,10 @@ export class PatientSupportsService { throw new NotFoundException('Contato de apoio não encontrado.'); } - if (user.role === 'patient' && user.id !== patientSupport.patient_id) { + if ( + authUser.role === 'patient' && + authUser.id !== patientSupport.patient_id + ) { throw new ForbiddenException( 'Você não tem permissão para atualizar este contato de apoio.', ); @@ -84,14 +87,17 @@ export class PatientSupportsService { ); } - async remove(id: string, user: User): Promise { + async remove(id: string, authUser: AuthUserDto): Promise { const patientSupport = await this.patientSupportsRepository.findById(id); if (!patientSupport) { throw new NotFoundException('Contato de apoio não encontrado.'); } - if (user.role === 'patient' && user.id !== patientSupport.patient_id) { + if ( + authUser.role === 'patient' && + authUser.id !== patientSupport.patient_id + ) { throw new ForbiddenException( 'Você não tem permissão para remover este contato de apoio.', ); diff --git a/src/app/http/patients/patients.controller.ts b/src/app/http/patients/patients.controller.ts index efad345..809ea47 100644 --- a/src/app/http/patients/patients.controller.ts +++ b/src/app/http/patients/patients.controller.ts @@ -11,20 +11,19 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { CurrentUser } from '@/common/decorators/current-user.decorator'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import { BaseResponseSchema } from '@/domain/schemas/base'; -import { - FindAllPatientsResponseSchema, - GetPatientResponseSchema, -} from '@/domain/schemas/patient'; -import { FindAllPatientsSupportResponseSchema } from '@/domain/schemas/patient-support'; -import type { UserSchema } from '@/domain/schemas/user'; - +import { BaseResponse } from '@/domain/schemas/base'; +import type { + GetPatientResponse, + GetPatientsResponse, +} from '@/domain/schemas/patient/responses'; +import type { GetPatientSupportsResponse } from '@/domain/schemas/patient-support/responses'; + +import type { AuthUserDto } from '../auth/auth.dtos'; import { PatientSupportsRepository } from '../patient-supports/patient-supports.repository'; import { - CreatePatientDto, - FindAllPatientQueryDto, + GetPatientsQuery, PatientScreeningDto, UpdatePatientDto, } from './patients.dtos'; @@ -44,10 +43,10 @@ export class PatientsController { @Roles(['patient']) @ApiOperation({ summary: 'Registra triagem do paciente' }) public async screening( - @CurrentUser() user: UserSchema, + @AuthUser() authUser: AuthUserDto, @Body() patientScreeningDto: PatientScreeningDto, - ): Promise { - await this.patientsService.screening(patientScreeningDto, user); + ): Promise { + await this.patientsService.screening(patientScreeningDto, authUser); return { success: true, @@ -59,8 +58,8 @@ export class PatientsController { @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Cadastra um novo paciente' }) public async create( - @Body() createPatientDto: CreatePatientDto, - ): Promise { + @Body() createPatientDto: PatientScreeningDto, + ): Promise { await this.patientsService.create(createPatientDto); return { @@ -73,8 +72,8 @@ export class PatientsController { @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Lista todos os pacientes' }) public async findAll( - @Query() filters: FindAllPatientQueryDto, - ): Promise { + @Query() filters: GetPatientsQuery, + ): Promise { const { patients, total } = await this.patientsRepository.findAll(filters); return { @@ -87,9 +86,7 @@ export class PatientsController { @Get(':id') @Roles(['manager', 'nurse', 'specialist']) @ApiOperation({ summary: 'Busca um paciente pelo ID' }) - public async findById( - @Param('id') id: string, - ): Promise { + public async findById(@Param('id') id: string): Promise { const patient = await this.patientsRepository.findById(id); if (!patient) { @@ -109,7 +106,7 @@ export class PatientsController { async update( @Param('id') id: string, @Body() updatePatientDto: UpdatePatientDto, - ): Promise { + ): Promise { await this.patientsService.update(id, updatePatientDto); return { @@ -121,9 +118,7 @@ export class PatientsController { @Patch(':id/inactivate') @Roles(['manager']) @ApiOperation({ summary: 'Inativa o Paciente pelo ID' }) - async inactivatePatient( - @Param('id') id: string, - ): Promise { + async inactivatePatient(@Param('id') id: string): Promise { await this.patientsService.deactivate(id); return { @@ -137,7 +132,7 @@ export class PatientsController { @ApiOperation({ summary: 'Lista todos os contatos de apoio de um paciente' }) async findAllPatientSupports( @Param('id') patientId: string, - ): Promise { + ): Promise { const patient = await this.patientsRepository.findById(patientId); if (!patient) { diff --git a/src/app/http/patients/patients.dtos.ts b/src/app/http/patients/patients.dtos.ts index 0f6bf8d..061d4a9 100644 --- a/src/app/http/patients/patients.dtos.ts +++ b/src/app/http/patients/patients.dtos.ts @@ -2,14 +2,15 @@ import { createZodDto } from 'nestjs-zod'; import { createPatientSchema, - findAllPatientsQuerySchema, + getPatientsQuerySchema, patientScreeningSchema, updatePatientSchema, -} from '@/domain/schemas/patient'; +} from '@/domain/schemas/patient/requests'; + +export class GetPatientsQuery extends createZodDto(getPatientsQuerySchema) {} export class PatientScreeningDto extends createZodDto(patientScreeningSchema) {} + export class CreatePatientDto extends createZodDto(createPatientSchema) {} -export class FindAllPatientQueryDto extends createZodDto( - findAllPatientsQuerySchema, -) {} + export class UpdatePatientDto extends createZodDto(updatePatientSchema) {} diff --git a/src/app/http/patients/patients.repository.ts b/src/app/http/patients/patients.repository.ts index 7d1bf0b..50b0b9a 100644 --- a/src/app/http/patients/patients.repository.ts +++ b/src/app/http/patients/patients.repository.ts @@ -3,9 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -import type { PatientOrderBy, PatientType } from '@/domain/schemas/patient'; +import type { PatientOrderBy } from '@/domain/enums/patients'; +import type { PatientResponse } from '@/domain/schemas/patient/responses'; -import { CreatePatientDto, FindAllPatientQueryDto } from './patients.dtos'; +import { CreatePatientDto, GetPatientsQuery } from './patients.dtos'; @Injectable() export class PatientsRepository { @@ -15,9 +16,9 @@ export class PatientsRepository { ) {} public async findAll( - filters: FindAllPatientQueryDto, + filters: GetPatientsQuery, includePending?: boolean, - ): Promise<{ patients: PatientType[]; total: number }> { + ): Promise<{ patients: PatientResponse[]; total: number }> { const { search, order, @@ -27,7 +28,6 @@ export class PatientsRepository { endDate, page, perPage, - all, } = filters; const ORDER_BY: Record = { @@ -70,77 +70,38 @@ export class PatientsRepository { query.orderBy(ORDER_BY[orderBy], order); - if (!all) { - query.skip((page - 1) * perPage).take(perPage); - } - + query.skip((page - 1) * perPage).take(perPage); const rawPatients = await query.getMany(); - const patients: PatientType[] = rawPatients.map( - ({ user, ...patientData }) => ({ - ...patientData, - name: user.name, - email: user.email, - avatar_url: user.avatar_url, + const patients: PatientResponse[] = rawPatients.map( + ({ ...patientData }) => ({ + id: patientData.id, + name: patientData.name, + email: patientData.email, + status: patientData.status, + avatar_url: patientData.avatar_url, + phone: patientData.phone, + created_at: patientData.created_at, }), ); return { patients, total }; } - public async findById(id: string): Promise { + public async findById(id: string): Promise { const patient = await this.patientsRepository.findOne({ - relations: { user: true, supports: true }, + relations: { supports: true }, where: { id }, select: { - user: { name: true, email: true, avatar_url: true }, - supports: { id: true, name: true, phone: true, kinship: true }, - }, - }); - - if (!patient) { - return null; - } - - const { user, ...patientData } = patient; - - return { - ...patientData, - name: user.name, - email: user.email, - avatar_url: user.avatar_url, - }; - } - - public async findByUserId(userId: string): Promise { - const patient = await this.patientsRepository.findOne({ - relations: { user: true, supports: true }, - where: { user_id: userId }, - select: { - user: { name: true, email: true, avatar_url: true }, supports: { id: true, name: true, phone: true, kinship: true }, }, }); - if (!patient) { - return null; - } - - const { user, ...patientData } = patient; - - return { - ...patientData, - name: user.name, - email: user.email, - avatar_url: user.avatar_url, - }; + return patient; } public async findByEmail(email: string): Promise { - return await this.patientsRepository.findOne({ - select: { user: true }, - where: { user: { email } }, - }); + return await this.patientsRepository.findOne({ where: { email } }); } public async findByCpf(cpf: string): Promise { @@ -148,8 +109,7 @@ export class PatientsRepository { } public async create(patient: CreatePatientDto): Promise { - const patientCreated = this.patientsRepository.create(patient); - return await this.patientsRepository.save(patientCreated); + return await this.patientsRepository.save(patient); } public async update(patient: Patient): Promise { diff --git a/src/app/http/patients/patients.service.ts b/src/app/http/patients/patients.service.ts index 00ea201..4b0885d 100644 --- a/src/app/http/patients/patients.service.ts +++ b/src/app/http/patients/patients.service.ts @@ -12,13 +12,9 @@ import { CryptographyService } from '@/app/cryptography/crypography.service'; import { Patient } from '@/domain/entities/patient'; import { PatientSupport } from '@/domain/entities/patient-support'; import { User } from '@/domain/entities/user'; -import type { UserSchema } from '@/domain/schemas/user'; -import { - CreatePatientDto, - type PatientScreeningDto, - UpdatePatientDto, -} from './patients.dtos'; +import type { AuthUserDto } from '../auth/auth.dtos'; +import { type PatientScreeningDto, UpdatePatientDto } from './patients.dtos'; import { PatientsRepository } from './patients.repository'; @Injectable() @@ -34,11 +30,11 @@ export class PatientsService { async screening( patientScreeningDto: PatientScreeningDto, - user: UserSchema, + authUser: AuthUserDto, ): Promise { - if (user.role !== 'patient') { + if (authUser.role !== 'patient') { this.logger.error( - { userId: user.id, email: user.email }, + { userId: authUser.id, email: authUser.email }, 'Screening failed: User is not a patient', ); throw new ForbiddenException( @@ -46,12 +42,12 @@ export class PatientsService { ); } - const patient = await this.patientsRepository.findByUserId(user.id); + const patient = await this.patientsRepository.findById(authUser.id); - if (patient) { + if (patient?.status !== 'pending') { this.logger.error( - { userId: user.id, email: user.email }, - 'Screening failed: Patient already registered', + { userId: authUser.id, email: authUser.email }, + 'Screening failed: Patient already finished the proccess', ); throw new ConflictException('Você já concluiu a triagem.'); } @@ -62,7 +58,11 @@ export class PatientsService { if (patientWithSameCpf) { this.logger.error( - { userId: user.id, email: user.email, cpf: patientScreeningDto.cpf }, + { + userId: authUser.id, + email: authUser.email, + cpf: patientScreeningDto.cpf, + }, 'Screening failed: CPF already registered', ); throw new ConflictException('Este CPF já está cadastrado.'); @@ -72,18 +72,9 @@ export class PatientsService { const patientsDataSource = manager.getRepository(Patient); const patientsSupportDataSource = manager.getRepository(PatientSupport); - const { name, supports, ...patientDto } = patientScreeningDto; + const { supports, ...patientDto } = patientScreeningDto; - if (name && name !== user.name) { - const usersDataSource = manager.getRepository(User); - await usersDataSource.update(user.id, { name }); - } - - const createdPatient = patientsDataSource.create({ - ...patientDto, - user_id: user.id, - }); - const savedPatient = await patientsDataSource.save(createdPatient); + await patientsDataSource.save(patientDto); if (supports && supports.length > 0) { const patientSupports = supports.map((support) => @@ -91,7 +82,7 @@ export class PatientsService { name: support.name, phone: support.phone, kinship: support.kinship, - patient_id: savedPatient.id, + patient_id: authUser.id, }), ); @@ -99,36 +90,32 @@ export class PatientsService { } this.logger.log( - { - id: savedPatient.id, - userId: savedPatient.user_id, - email: user.email, - }, - 'Screening: Patient created successfully', + { id: authUser.id, email: authUser.email }, + 'Screening: Patient finished successfully', ); }); } - async create(createPatientDto: CreatePatientDto): Promise { + async create(patientScreeningDto: PatientScreeningDto): Promise { const patient = await this.patientsRepository.findByEmail( - createPatientDto.email, + patientScreeningDto.email, ); if (patient) { this.logger.error( - { email: createPatientDto.email }, + { email: patientScreeningDto.email }, 'Create patient failed: E-mail already registered', ); throw new ConflictException('Este e-mail já está cadastrado.'); } const patientWithSameCpf = await this.patientsRepository.findByCpf( - createPatientDto.cpf, + patientScreeningDto.cpf, ); if (patientWithSameCpf) { this.logger.error( - { email: createPatientDto.email, cpf: createPatientDto.cpf }, + { email: patientScreeningDto.email, cpf: patientScreeningDto.cpf }, 'Create patient failed: CPF already registered', ); throw new ConflictException('Este CPF já está cadastrado.'); @@ -144,23 +131,24 @@ export class PatientsService { await this.cryptographyService.createHash(randomPassword); const newUser = usersDataSource.create({ - name: createPatientDto.name, - email: createPatientDto.email, + name: patientScreeningDto.name, + email: patientScreeningDto.email, password: hashedPassword, }); const user = await usersDataSource.save(newUser); + const { supports, ...patientDto } = patientScreeningDto; + const patient = patientsDataSource.create({ - ...createPatientDto, - user_id: user.id, + ...patientDto, status: 'active', }); const savedPatient = await patientsDataSource.save(patient); - if (createPatientDto.supports.length > 0) { - const patientSupports = createPatientDto.supports.map((support) => + if (supports && supports.length > 0) { + const patientSupports = supports.map((support) => patientsSupportDataSource.create({ name: support.name, phone: support.phone, @@ -173,11 +161,7 @@ export class PatientsService { } this.logger.log( - { - id: savedPatient.id, - userId: savedPatient.user_id, - email: user.email, - }, + { id: savedPatient.id, email: user.email }, 'Patient created successfully', ); }); @@ -198,7 +182,7 @@ export class PatientsService { updatePatientDto.cpf, ); - if (patientWithSameCpf && patientWithSameCpf.user_id !== patient.user_id) { + if (patientWithSameCpf && patientWithSameCpf.id !== id) { this.logger.error( { email: updatePatientDto.email, cpf: updatePatientDto.cpf }, 'Update patient failed: CPF already registered', @@ -207,31 +191,20 @@ export class PatientsService { } return await this.dataSource.transaction(async (manager) => { - const usersDataSource = manager.getRepository(User); const patientsDataSource = manager.getRepository(Patient); - if (updatePatientDto.name !== patient.name) { - await usersDataSource.update(patient.user_id, { - name: updatePatientDto.name, - }); - } - if (updatePatientDto.email !== patient.email) { - const existingUser = await usersDataSource.findOne({ + const emailAlreadyRegistered = await patientsDataSource.findOne({ where: { email: updatePatientDto.email }, }); - if (existingUser) { + if (emailAlreadyRegistered) { this.logger.error( { id: patient.id, email: updatePatientDto.email }, 'Update patient failed: E-mail already registered', ); throw new ConflictException('Este e-mail já está em uso.'); } - - await usersDataSource.update(patient.user_id, { - email: updatePatientDto.email, - }); } const updatedPatient = updatePatientDto; @@ -240,10 +213,7 @@ export class PatientsService { await patientsDataSource.save(patient); - this.logger.log( - { id: patient.id, userId: patient.user_id, email: patient.email }, - 'Patient updated successfully', - ); + this.logger.log({ id: patient.id }, 'Patient updated successfully'); }); } @@ -261,7 +231,7 @@ export class PatientsService { await this.patientsRepository.deactivate(id); this.logger.log( - { id: patient.id, userId: patient.user_id, email: patient.email }, + { id: patient.id, email: patient.email }, 'Patient deactivated successfully', ); } diff --git a/src/app/http/referrals/referrals.controller.ts b/src/app/http/referrals/referrals.controller.ts index 748a9b4..0a50c33 100644 --- a/src/app/http/referrals/referrals.controller.ts +++ b/src/app/http/referrals/referrals.controller.ts @@ -9,10 +9,10 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { CurrentUser } from '@/common/decorators/current-user.decorator'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import { BaseResponseSchema } from '@/domain/schemas/base'; -import type { GetReferralsResponseSchema } from '@/domain/schemas/referral/responses'; +import { BaseResponse } from '@/domain/schemas/base'; +import type { GetReferralsResponse } from '@/domain/schemas/referral/responses'; import { UserSchema } from '@/domain/schemas/user'; import { CreateReferralDto, GetReferralsQuery } from './referrals.dtos'; @@ -34,7 +34,7 @@ export class ReferralsController { @ApiOperation({ summary: 'Lista encaminhamentos cadastrados no sistema' }) async getReferrals( @Query() query: GetReferralsQuery, - ): Promise { + ): Promise { const data = await this.getReferralsUseCase.execute({ query }); return { @@ -48,9 +48,9 @@ export class ReferralsController { @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Cadastra um novo encaminhamento' }) async create( - @CurrentUser() currentUser: UserSchema, + @AuthUser() currentUser: UserSchema, @Body() createReferralDto: CreateReferralDto, - ): Promise { + ): Promise { await this.createReferralUseCase.execute({ createReferralDto, userId: currentUser.id, @@ -64,8 +64,8 @@ export class ReferralsController { @ApiOperation({ summary: 'Cancela um encaminhamento' }) async cancel( @Param('id') id: string, - @CurrentUser() user: UserSchema, - ): Promise { + @AuthUser() user: UserSchema, + ): Promise { await this.cancelReferralUseCase.execute({ id, userId: user.id }); return { diff --git a/src/app/http/referrals/use-cases/get-referrals.use-case.ts b/src/app/http/referrals/use-cases/get-referrals.use-case.ts index 94f7e25..554715b 100644 --- a/src/app/http/referrals/use-cases/get-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/get-referrals.use-case.ts @@ -11,7 +11,7 @@ import { import { Referral } from '@/domain/entities/referral'; import type { ReferralOrderBy } from '@/domain/enums/referrals'; -import type { GetReferralsResponseSchema } from '@/domain/schemas/referral/responses'; +import type { GetReferralsResponse } from '@/domain/schemas/referral/responses'; import { GetReferralsQuery } from '../referrals.dtos'; @@ -19,7 +19,7 @@ interface GetReferralsUseCaseRequest { query: GetReferralsQuery; } -type GetReferralsUseCaseResponse = Promise; +type GetReferralsUseCaseResponse = Promise; @Injectable() export class GetReferralsUseCase { @@ -71,7 +71,7 @@ export class GetReferralsUseCase { } if (search) { - where.patient = { user: { name: ILike(`%${search}%`) } }; + where.patient = { name: ILike(`%${search}%`) }; } const total = await this.referralsRepository.count({ where }); @@ -79,42 +79,18 @@ export class GetReferralsUseCase { const orderBy = ORDER_BY_MAPPING[query.orderBy]; const order = orderBy === 'patient' - ? { patient: { user: { name: query.order } } } + ? { patient: { name: query.order } } : { [orderBy]: query.order }; - const referralsQuery = await this.referralsRepository.find({ - relations: { patient: { user: true } }, - select: { - patient: { - id: true, - user: { name: true, avatar_url: true }, - }, - }, + const referrals = await this.referralsRepository.find({ + select: { patient: { id: true, name: true, avatar_url: true } }, + relations: { patient: true }, skip: (page - 1) * perPage, take: perPage, order, where, }); - const referrals = referralsQuery.map((referral) => ({ - id: referral.id, - patient_id: referral.patient_id, - date: referral.date, - status: referral.status, - category: referral.category, - condition: referral.condition, - annotation: referral.annotation, - professional_name: referral.professional_name, - created_by: referral.created_by, - created_at: referral.created_at, - updated_at: referral.updated_at, - patient: { - name: referral.patient.user.name, - email: referral.patient.user.email, - avatar_url: referral.patient.user.avatar_url, - }, - })); - return { referrals, total }; } } diff --git a/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts b/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts index b7a8ecd..bdc7eaa 100644 --- a/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts @@ -10,9 +10,9 @@ import { import { Appointment } from '@/domain/entities/appointment'; import type { AppointmentStatus } from '@/domain/enums/appointments'; -import type { SpecialtyCategory } from '@/domain/enums/specialties'; -import type { PatientCondition } from '@/domain/schemas/patient'; -import type { QueryPeriod } from '@/domain/schemas/query'; +import type { PatientCondition } from '@/domain/enums/patients'; +import type { QueryPeriod } from '@/domain/enums/queries'; +import type { SpecialtyCategory } from '@/domain/enums/shared'; import { UtilsService } from '@/utils/utils.service'; interface GetTotalAppointmentsUseCaseRequest { diff --git a/src/app/http/statistics/use-cases/get-total-patients.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients.use-case.ts index be41b60..6320cd3 100644 --- a/src/app/http/statistics/use-cases/get-total-patients.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients.use-case.ts @@ -10,8 +10,8 @@ import { } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -import type { PatientStatus } from '@/domain/schemas/patient'; -import type { QueryPeriod } from '@/domain/schemas/query'; +import type { PatientStatus } from '@/domain/enums/patients'; +import type { QueryPeriod } from '@/domain/enums/queries'; import { UtilsService } from '@/utils/utils.service'; interface GetTotalPatientsUseCaseRequest { diff --git a/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts index 6fe1129..d136161 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts @@ -9,10 +9,10 @@ import { } from 'typeorm'; import { Referral } from '@/domain/entities/referral'; +import type { PatientCondition } from '@/domain/enums/patients'; +import type { QueryPeriod } from '@/domain/enums/queries'; import type { ReferralStatus } from '@/domain/enums/referrals'; -import type { SpecialtyCategory } from '@/domain/enums/specialties'; -import type { PatientCondition } from '@/domain/schemas/patient'; -import type { QueryPeriod } from '@/domain/schemas/query'; +import type { SpecialtyCategory } from '@/domain/enums/shared'; import { UtilsService } from '@/utils/utils.service'; interface GetTotalReferralsUseCaseRequest { diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts index d64888f..da06d99 100644 --- a/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts @@ -11,7 +11,7 @@ import { } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -import type { QueryPeriod } from '@/domain/schemas/query'; +import type { QueryPeriod } from '@/domain/enums/queries'; import { UtilsService } from '@/utils/utils.service'; interface GetTotalReferredPatientsUseCaseRequest { diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index 41a0b01..8cbc551 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -1,13 +1,11 @@ import { Controller, Get } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { CurrentUser } from '@/common/decorators/current-user.decorator'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import type { - GetUserProfileResponseSchema, - UserSchema, -} from '@/domain/schemas/user'; +import type { GetUserResponse } from '@/domain/schemas/user/responses'; +import type { AuthUserDto } from '../auth/auth.dtos'; import { UsersService } from './users.service'; @ApiTags('Usuários') @@ -18,9 +16,9 @@ export class UsersController { @Get('profile') @Roles(['manager', 'nurse', 'specialist', 'patient']) async getProfile( - @CurrentUser() requestUser: UserSchema, - ): Promise { - const user = await this.usersService.getProfile(requestUser.id); + @AuthUser() authUser: AuthUserDto, + ): Promise { + const user = await this.usersService.getProfile(authUser.id); return { success: true, diff --git a/src/app/http/users/users.dtos.ts b/src/app/http/users/users.dtos.ts index b6d98c3..cab3a69 100644 --- a/src/app/http/users/users.dtos.ts +++ b/src/app/http/users/users.dtos.ts @@ -1,6 +1,10 @@ import { createZodDto } from 'nestjs-zod'; -import { createUserSchema, updateUserSchema } from '@/domain/schemas/user'; +import { + createUserSchema, + updateUserSchema, +} from '@/domain/schemas/user/requests'; export class CreateUserDto extends createZodDto(createUserSchema) {} + export class UpdateUserDto extends createZodDto(updateUserSchema) {} diff --git a/src/app/http/users/users.repository.ts b/src/app/http/users/users.repository.ts index f79a7d0..b9ee869 100644 --- a/src/app/http/users/users.repository.ts +++ b/src/app/http/users/users.repository.ts @@ -18,16 +18,11 @@ export class UsersRepository { } public async findById(id: string): Promise { - return await this.usersRepository.findOne({ - where: { id }, - relations: { patient: true }, - }); + return await this.usersRepository.findOne({ where: { id } }); } public async findByEmail(email: string): Promise { - return await this.usersRepository.findOne({ - where: { email }, - }); + return await this.usersRepository.findOne({ where: { email } }); } public async create(user: CreateUserDto): Promise { diff --git a/src/common/decorators/auth-user.decorator.ts b/src/common/decorators/auth-user.decorator.ts new file mode 100644 index 0000000..cfa509c --- /dev/null +++ b/src/common/decorators/auth-user.decorator.ts @@ -0,0 +1,11 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +import type { AuthUserDto } from '@/app/http/auth/auth.dtos'; + +export const AuthUser = createParamDecorator( + (_: unknown, context: ExecutionContext): AuthUserDto | undefined => { + const request = context.switchToHttp().getRequest<{ user?: AuthUserDto }>(); + + return request.user; + }, +); diff --git a/src/common/decorators/cookies.ts b/src/common/decorators/cookies.decorator.ts similarity index 99% rename from src/common/decorators/cookies.ts rename to src/common/decorators/cookies.decorator.ts index 8c44b7e..f927363 100644 --- a/src/common/decorators/cookies.ts +++ b/src/common/decorators/cookies.decorator.ts @@ -6,6 +6,7 @@ import type { Cookie } from '@/domain/cookies'; export const Cookies = createParamDecorator( (data: Cookie, ctx: ExecutionContext): string | Record => { const request = ctx.switchToHttp().getRequest(); + const cookies = request.signedCookies as Record; return data ? cookies[data] : cookies; diff --git a/src/common/decorators/current-user.decorator.ts b/src/common/decorators/current-user.decorator.ts deleted file mode 100644 index 852ccf4..0000000 --- a/src/common/decorators/current-user.decorator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; - -import type { User } from '@/domain/entities/user'; - -export const CurrentUser = createParamDecorator( - (data: unknown, context: ExecutionContext) => { - const request = context.switchToHttp().getRequest<{ user?: User }>(); - return request.user; - }, -); diff --git a/src/common/decorators/roles.decorator.ts b/src/common/decorators/roles.decorator.ts index accde44..3b3f6e7 100644 --- a/src/common/decorators/roles.decorator.ts +++ b/src/common/decorators/roles.decorator.ts @@ -1,5 +1,5 @@ import { Reflector } from '@nestjs/core'; -import type { UserRoleType } from '@/domain/schemas/user'; +import type { AuthTokenRole } from '@/domain/enums/tokens'; -export const Roles = Reflector.createDecorator(); +export const Roles = Reflector.createDecorator(); diff --git a/src/common/guards/auth.guard.ts b/src/common/guards/auth.guard.ts index a13605f..d583976 100644 --- a/src/common/guards/auth.guard.ts +++ b/src/common/guards/auth.guard.ts @@ -5,12 +5,15 @@ import { UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; import { CryptographyService } from '@/app/cryptography/crypography.service'; -import { UsersRepository } from '@/app/http/users/users.repository'; +import type { AuthUserDto } from '@/app/http/auth/auth.dtos'; import type { Cookie } from '@/domain/cookies'; -import type { AccessTokenPayloadType } from '@/domain/schemas/token'; -import type { UserSchema } from '@/domain/schemas/user'; +import { Patient } from '@/domain/entities/patient'; +import { User } from '@/domain/entities/user'; +import type { AccessTokenPayload } from '@/domain/schemas/token'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; @@ -19,7 +22,10 @@ export class AuthGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly cryptographyService: CryptographyService, - private readonly usersRepository: UsersRepository, + @InjectRepository(User) + private readonly usersRepository: Repository, + @InjectRepository(Patient) + private readonly patientsRepository: Repository, ) {} async canActivate(context: ExecutionContext): Promise { @@ -34,7 +40,7 @@ export class AuthGuard implements CanActivate { const request = context.switchToHttp().getRequest<{ signedCookies?: Record; - user?: UserSchema; + user?: AuthUserDto; }>(); const token = request.signedCookies?.access_token; @@ -45,26 +51,43 @@ export class AuthGuard implements CanActivate { try { const tokenPayload = - await this.cryptographyService.verifyToken( - token, - ); + await this.cryptographyService.verifyToken(token); + const userId = tokenPayload.sub; + const role = tokenPayload.role; if (!userId) { throw new UnauthorizedException('Token inválido.'); } - const user = await this.usersRepository.findById(userId); + if (role === 'patient') { + const user = await this.patientsRepository.findOne({ + select: { id: true, email: true }, + where: { id: userId }, + }); + + if (!user) { + throw new UnauthorizedException('Usuário não encontrado.'); + } + + request.user = { id: user.id, email: user.email, role }; + + return true; + } + + const user = await this.usersRepository.findOne({ + select: { id: true, email: true, role: true }, + where: { id: userId }, + }); if (!user) { throw new UnauthorizedException('Usuário não encontrado.'); } - request.user = user; + request.user = { id: user.id, email: user.email, role }; return true; - } catch (error) { - console.error('Auth error:', error); + } catch { throw new UnauthorizedException('Token inválido ou expirado.'); } } diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts index 23a8749..d9d1877 100644 --- a/src/common/guards/roles.guard.ts +++ b/src/common/guards/roles.guard.ts @@ -6,7 +6,7 @@ import { } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import type { UserSchema } from '@/domain/schemas/user'; +import type { AuthUserDto } from '@/app/http/auth/auth.dtos'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; import { Roles } from '../decorators/roles.decorator'; @@ -30,7 +30,7 @@ export class RolesGuard implements CanActivate { return true; } - const request = context.switchToHttp().getRequest<{ user?: UserSchema }>(); + const request = context.switchToHttp().getRequest<{ user?: AuthUserDto }>(); const user = request.user; diff --git a/src/domain/cookies.ts b/src/domain/cookies.ts index b7a9ba8..25096d4 100644 --- a/src/domain/cookies.ts +++ b/src/domain/cookies.ts @@ -1,6 +1,6 @@ -import type { AuthTokenType } from './schemas/token'; +import type { AuthTokenKey } from './enums/tokens'; -export type Cookie = AuthTokenType; +export type Cookie = AuthTokenKey; export type Cookies = Record; export const COOKIES_MAPPING: Cookies = { diff --git a/src/domain/entities/appointment.ts b/src/domain/entities/appointment.ts index 31f1efe..8184f21 100644 --- a/src/domain/entities/appointment.ts +++ b/src/domain/entities/appointment.ts @@ -12,12 +12,9 @@ import { APPOINTMENT_STATUSES, type AppointmentStatus, } from '../enums/appointments'; -import { - SPECIALTY_CATEGORIES, - type SpecialtyCategory, -} from '../enums/specialties'; +import { PATIENT_CONDITIONS, type PatientCondition } from '../enums/patients'; +import { SPECIALTY_CATEGORIES, type SpecialtyCategory } from '../enums/shared'; import type { AppointmentSchema } from '../schemas/appointments'; -import { PATIENT_CONDITIONS, type PatientCondition } from '../schemas/patient'; import { Patient } from './patient'; @Entity('appointments') diff --git a/src/domain/entities/patient-requirement.ts b/src/domain/entities/patient-requirement.ts index 5d75be1..6356862 100644 --- a/src/domain/entities/patient-requirement.ts +++ b/src/domain/entities/patient-requirement.ts @@ -9,12 +9,12 @@ import { } from 'typeorm'; import { - PATIENT_REQUIREMENT_STATUS, - PATIENT_REQUIREMENT_TYPE, - PatientRequirementSchema, - PatientRequirementStatusType, - PatientRequirementType, -} from '../schemas/patient-requirement'; + PATIENT_REQUIREMENT_STATUSES, + PATIENT_REQUIREMENT_TYPES, + type PatientRequirementStatus, + type PatientRequirementType, +} from '../enums/patient-requirements'; +import type { PatientRequirementSchema } from '../schemas/patient-requirement'; import { Patient } from './patient'; @Entity('patient_requirements') @@ -25,7 +25,7 @@ export class PatientRequirement implements PatientRequirementSchema { @Column('uuid') patient_id: string; - @Column({ type: 'enum', enum: PATIENT_REQUIREMENT_TYPE }) + @Column({ type: 'enum', enum: PATIENT_REQUIREMENT_TYPES }) type: PatientRequirementType; @Column({ type: 'varchar', length: 255 }) @@ -36,13 +36,13 @@ export class PatientRequirement implements PatientRequirementSchema { @Column({ type: 'enum', - enum: PATIENT_REQUIREMENT_STATUS, + enum: PATIENT_REQUIREMENT_STATUSES, default: 'pending', }) - status: PatientRequirementStatusType; + status: PatientRequirementStatus; - @Column({ type: 'uuid' }) - required_by: string; + @Column({ type: 'timestamp', nullable: true }) + submitted_at: Date | null; @Column({ type: 'uuid', nullable: true }) approved_by: string | null; @@ -50,8 +50,8 @@ export class PatientRequirement implements PatientRequirementSchema { @Column({ type: 'timestamp', nullable: true }) approved_at: Date | null; - @Column({ type: 'timestamp', nullable: true }) - submitted_at: Date | null; + @Column({ type: 'uuid' }) + created_by: string; @CreateDateColumn({ type: 'timestamp' }) created_at: Date; diff --git a/src/domain/entities/patient-support.ts b/src/domain/entities/patient-support.ts index f1c914d..e6fe41b 100644 --- a/src/domain/entities/patient-support.ts +++ b/src/domain/entities/patient-support.ts @@ -16,13 +16,13 @@ export class PatientSupport implements PatientSupportSchema { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ type: 'varchar', length: 255 }) + @Column('uuid') patient_id: string; - @Column({ type: 'varchar', length: 255 }) + @Column({ type: 'varchar', length: 64 }) name: string; - @Column({ type: 'char', length: 11 }) + @Column({ type: 'varchar', length: 11 }) phone: string; @Column({ type: 'varchar', length: 50 }) diff --git a/src/domain/entities/patient.ts b/src/domain/entities/patient.ts index 590e7cd..171b0fd 100644 --- a/src/domain/entities/patient.ts +++ b/src/domain/entities/patient.ts @@ -2,9 +2,7 @@ import { Column, CreateDateColumn, Entity, - JoinColumn, OneToMany, - OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; @@ -13,15 +11,14 @@ import { BRAZILIAN_STATES, type BrazilianState, } from '@/constants/brazilian-states'; -import { User } from '@/domain/entities/user'; import { - Gender, - GENDERS, - PATIENT_STATUS, - PatientSchema, - PatientStatus, -} from '../schemas/patient'; + PATIENT_GENDERS, + PATIENT_STATUSES, + type PatientGender, + type PatientStatus, +} from '../enums/patients'; +import type { PatientSchema } from '../schemas/patient'; import { Appointment } from './appointment'; import { PatientRequirement } from './patient-requirement'; import { PatientSupport } from './patient-support'; @@ -32,26 +29,38 @@ export class Patient implements PatientSchema { @PrimaryGeneratedColumn('uuid') id: string; - @Column('uuid') - user_id: string; + @Column({ type: 'varchar', length: 64 }) + name: string; - @Column({ type: 'enum', enum: GENDERS }) - gender: Gender; + @Column({ type: 'varchar', length: 64 }) + email: string; - @Column({ type: 'date' }) - date_of_birth: Date; + @Column({ type: 'varchar', nullable: true }) + password: string | null; - @Column({ type: 'char', length: 11 }) - phone: string; + @Column({ type: 'varchar', nullable: true }) + avatar_url: string | null; - @Column({ type: 'char', length: 11, unique: true }) - cpf: string; + @Column({ type: 'enum', enum: PATIENT_STATUSES, default: 'pending' }) + status: PatientStatus; + + @Column({ type: 'enum', enum: PATIENT_GENDERS, default: 'prefer_not_to_say' }) + gender: PatientGender; + + @Column({ type: 'timestamp', nullable: true }) + date_of_birth: Date | null; + + @Column({ type: 'varchar', length: 11, nullable: true }) + phone: string | null; - @Column({ type: 'enum', enum: BRAZILIAN_STATES }) - state: BrazilianState; + @Column({ type: 'varchar', length: 11, unique: true, nullable: true }) + cpf: string | null; - @Column({ type: 'varchar', length: 50 }) - city: string; + @Column({ type: 'enum', enum: BRAZILIAN_STATES, nullable: true }) + state: BrazilianState | null; + + @Column({ type: 'varchar', nullable: true }) + city: string | null; @Column({ type: 'tinyint', width: 1, default: 0 }) has_disability: boolean; @@ -71,19 +80,12 @@ export class Patient implements PatientSchema { @Column({ type: 'tinyint', width: 1, default: 0 }) has_nmo_diagnosis: boolean; - @Column({ type: 'enum', enum: PATIENT_STATUS, default: 'pending' }) - status: PatientStatus; - @CreateDateColumn({ type: 'timestamp' }) created_at: Date; @UpdateDateColumn({ type: 'timestamp' }) updated_at: Date; - @OneToOne(() => User) - @JoinColumn({ name: 'user_id' }) - user: User; - @OneToMany(() => PatientSupport, (support) => support.patient) supports: PatientSupport[]; diff --git a/src/domain/entities/referral.ts b/src/domain/entities/referral.ts index 4f9e2c3..634c663 100644 --- a/src/domain/entities/referral.ts +++ b/src/domain/entities/referral.ts @@ -8,12 +8,9 @@ import { UpdateDateColumn, } from 'typeorm'; +import { PATIENT_CONDITIONS, type PatientCondition } from '../enums/patients'; import { REFERRAL_STATUSES, type ReferralStatus } from '../enums/referrals'; -import { - SPECIALTY_CATEGORIES, - type SpecialtyCategory, -} from '../enums/specialties'; -import { PATIENT_CONDITIONS, PatientCondition } from '../schemas/patient'; +import { SPECIALTY_CATEGORIES, type SpecialtyCategory } from '../enums/shared'; import { ReferralSchema } from '../schemas/referral'; import { Patient } from './patient'; @@ -40,7 +37,7 @@ export class Referral implements ReferralSchema { @Column({ type: 'varchar', length: 2000, nullable: true }) annotation: string | null; - @Column({ type: 'varchar', length: 255, nullable: true }) + @Column({ type: 'varchar', length: 64, nullable: true }) professional_name: string | null; @Column('uuid') diff --git a/src/domain/entities/token.ts b/src/domain/entities/token.ts index 194ef49..7f1ef56 100644 --- a/src/domain/entities/token.ts +++ b/src/domain/entities/token.ts @@ -5,14 +5,11 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; -import { - AUTH_TOKENS, - type AuthTokenSchema, - type AuthTokenType, -} from '../schemas/token'; +import { AUTH_TOKENS, type AuthTokenKey } from '../enums/tokens'; +import type { AuthToken } from '../schemas/token'; @Entity('tokens') -export class Token implements AuthTokenSchema { +export class Token implements AuthToken { @PrimaryGeneratedColumn({ type: 'integer' }) id: number; @@ -22,11 +19,11 @@ export class Token implements AuthTokenSchema { @Column({ type: 'varchar', nullable: true }) email: string | null; - @Column({ type: 'varchar', length: 255 }) + @Column({ type: 'varchar' }) token: string; @Column({ type: 'enum', enum: AUTH_TOKENS }) - type: AuthTokenType; + type: AuthTokenKey; @CreateDateColumn({ type: 'timestamp', nullable: true }) expires_at: Date | null; diff --git a/src/domain/entities/user.ts b/src/domain/entities/user.ts index 1e91b53..e7b76ab 100644 --- a/src/domain/entities/user.ts +++ b/src/domain/entities/user.ts @@ -2,44 +2,44 @@ import { Column, CreateDateColumn, Entity, - OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; import { USER_ROLES, - type UserRoleType, - type UserSchema, -} from '../schemas/user'; -import { Patient } from './patient'; + USER_STATUSES, + type UserRole, + type UserStatus, +} from '../enums/users'; +import type { UserSchema } from '../schemas/user'; @Entity('users') export class User implements UserSchema { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ type: 'varchar', length: 255 }) + @Column({ type: 'varchar', length: 64 }) name: string; - @Column({ type: 'varchar', length: 255, unique: true }) + @Column({ type: 'varchar', length: 64 }) email: string; - @Column({ type: 'varchar', length: 255 }) + @Column({ type: 'varchar' }) password: string; - @Column({ type: 'enum', enum: USER_ROLES, default: 'patient' }) - role: UserRoleType; - - @Column({ type: 'varchar', length: 255, default: null }) + @Column({ type: 'varchar', nullable: true }) avatar_url: string | null; + @Column({ type: 'enum', enum: USER_ROLES }) + role: UserRole; + + @Column({ type: 'enum', enum: USER_STATUSES, default: 'active' }) + status: UserStatus; + @CreateDateColumn({ type: 'timestamp' }) created_at: Date; @UpdateDateColumn({ type: 'timestamp' }) updated_at: Date; - - @OneToOne(() => Patient, (patient) => patient.user) - patient: Patient; } diff --git a/src/domain/enums/patient-requirements.ts b/src/domain/enums/patient-requirements.ts new file mode 100644 index 0000000..1182a85 --- /dev/null +++ b/src/domain/enums/patient-requirements.ts @@ -0,0 +1,25 @@ +export const PATIENT_REQUIREMENT_TYPES = [ + 'screening', + 'medical_report', +] as const; +export type PatientRequirementType = (typeof PATIENT_REQUIREMENT_TYPES)[number]; + +export const PATIENT_REQUIREMENT_STATUSES = [ + 'pending', + 'under_review', + 'approved', + 'declined', +] as const; +export type PatientRequirementStatus = + (typeof PATIENT_REQUIREMENT_STATUSES)[number]; + +export const PATIENT_REQUIREMENTS_ORDER_BY = [ + 'name', + 'status', + 'type', + 'date', + 'approved_at', + 'submitted_at', +] as const; +export type PatientRequirementOrderBy = + (typeof PATIENT_REQUIREMENTS_ORDER_BY)[number]; diff --git a/src/domain/enums/patients.ts b/src/domain/enums/patients.ts new file mode 100644 index 0000000..df4134c --- /dev/null +++ b/src/domain/enums/patients.ts @@ -0,0 +1,18 @@ +export const PATIENT_GENDERS = [ + 'male_cis', + 'female_cis', + 'male_trans', + 'female_trans', + 'non_binary', + 'prefer_not_to_say', +] as const; +export type PatientGender = (typeof PATIENT_GENDERS)[number]; + +export const PATIENT_STATUSES = ['active', 'inactive', 'pending'] as const; +export type PatientStatus = (typeof PATIENT_STATUSES)[number]; + +export const PATIENT_CONDITIONS = ['in_crisis', 'stable'] as const; +export type PatientCondition = (typeof PATIENT_CONDITIONS)[number]; + +export const PATIENT_ORDER_BY = ['name', 'email', 'status', 'date'] as const; +export type PatientOrderBy = (typeof PATIENT_ORDER_BY)[number]; diff --git a/src/domain/enums/queries.ts b/src/domain/enums/queries.ts new file mode 100644 index 0000000..4f555eb --- /dev/null +++ b/src/domain/enums/queries.ts @@ -0,0 +1,10 @@ +export const QUERY_ORDERS = ['ASC', 'DESC'] as const; +export type QueryOrder = (typeof QUERY_ORDERS)[number]; + +export const QUERY_PERIODS = [ + 'today', + 'last-year', + 'last-month', + 'last-week', +] as const; +export type QueryPeriod = (typeof QUERY_PERIODS)[number]; diff --git a/src/domain/enums/specialties.ts b/src/domain/enums/shared.ts similarity index 100% rename from src/domain/enums/specialties.ts rename to src/domain/enums/shared.ts diff --git a/src/domain/enums/statistics.ts b/src/domain/enums/statistics.ts index d2705d9..bc0e83a 100644 --- a/src/domain/enums/statistics.ts +++ b/src/domain/enums/statistics.ts @@ -1,2 +1,10 @@ +import type { PatientGender } from './patients'; + +export const PATIENT_STATISTICS = ['gender', 'total'] as const; +export type PatientStatisticsResult = { + gender: PatientGender; + total: number; +}; + export const PATIENTS_STATISTIC_FIELDS = ['gender', 'city', 'state'] as const; export type PatientsStatisticField = (typeof PATIENTS_STATISTIC_FIELDS)[number]; diff --git a/src/domain/enums/tokens.ts b/src/domain/enums/tokens.ts new file mode 100644 index 0000000..92743ae --- /dev/null +++ b/src/domain/enums/tokens.ts @@ -0,0 +1,17 @@ +import { USER_ROLES } from './users'; + +export const AUTH_TOKENS_MAPPING = { + access_token: 'access_token', + password_reset: 'password_reset', + invite_token: 'invite_token', +} as const; +export type AuthTokenKey = keyof typeof AUTH_TOKENS_MAPPING; + +export const AUTH_TOKENS = [ + AUTH_TOKENS_MAPPING.access_token, + AUTH_TOKENS_MAPPING.password_reset, + AUTH_TOKENS_MAPPING.invite_token, +] as const; + +export const AUTH_TOKEN_ROLES = [...USER_ROLES, 'patient'] as const; +export type AuthTokenRole = (typeof AUTH_TOKEN_ROLES)[number]; diff --git a/src/domain/enums/users.ts b/src/domain/enums/users.ts new file mode 100644 index 0000000..0c48cb8 --- /dev/null +++ b/src/domain/enums/users.ts @@ -0,0 +1,5 @@ +export const USER_ROLES = ['admin', 'nurse', 'specialist', 'manager'] as const; +export type UserRole = (typeof USER_ROLES)[number]; + +export const USER_STATUSES = ['active', 'inactive'] as const; +export type UserStatus = (typeof USER_STATUSES)[number]; diff --git a/src/domain/modules/cryptography.ts b/src/domain/modules/cryptography.ts index 63b2726..fb5edfb 100644 --- a/src/domain/modules/cryptography.ts +++ b/src/domain/modules/cryptography.ts @@ -1,15 +1,15 @@ import type { JwtSignOptions } from '@nestjs/jwt'; -import type { AuthTokenPayloadByType } from '../schemas/token'; +import type { AuthTokenPayloads } from '../schemas/token'; export abstract class Cryptography { abstract createHash(plain: string): Promise; abstract compareHash(plain: string, hash: string): Promise; - abstract createToken( + abstract createToken( _type: T, - payload: AuthTokenPayloadByType[T], + payload: AuthTokenPayloads[T], options?: JwtSignOptions, ): Promise; diff --git a/src/domain/schemas/appointments/index.ts b/src/domain/schemas/appointments/index.ts index a9080ac..d7fff1e 100644 --- a/src/domain/schemas/appointments/index.ts +++ b/src/domain/schemas/appointments/index.ts @@ -1,9 +1,8 @@ import { z } from 'zod'; import { APPOINTMENT_STATUSES } from '@/domain/enums/appointments'; -import { SPECIALTY_CATEGORIES } from '@/domain/enums/specialties'; - -import { PATIENT_CONDITIONS } from '../patient'; +import { PATIENT_CONDITIONS } from '@/domain/enums/patients'; +import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; export const appointmentSchema = z .object({ diff --git a/src/domain/schemas/appointments/requests.ts b/src/domain/schemas/appointments/requests.ts index a94ed97..07730bf 100644 --- a/src/domain/schemas/appointments/requests.ts +++ b/src/domain/schemas/appointments/requests.ts @@ -4,10 +4,11 @@ import { APPOINTMENT_ORDER_BY, APPOINTMENT_STATUSES, } from '@/domain/enums/appointments'; -import { SPECIALTY_CATEGORIES } from '@/domain/enums/specialties'; +import { PATIENT_CONDITIONS } from '@/domain/enums/patients'; +import { QUERY_ORDERS } from '@/domain/enums/queries'; +import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; -import { PATIENT_CONDITIONS } from '../patient'; -import { baseQuerySchema, QUERY_ORDER } from '../query'; +import { baseQuerySchema } from '../query'; import { appointmentSchema } from '.'; export const createAppointmentSchema = appointmentSchema.pick({ @@ -18,7 +19,6 @@ export const createAppointmentSchema = appointmentSchema.pick({ annotation: true, professional_name: true, }); -export type CreateAppointmentDto = z.infer; export const updateAppointmentSchema = appointmentSchema.pick({ date: true, @@ -26,7 +26,6 @@ export const updateAppointmentSchema = appointmentSchema.pick({ condition: true, annotation: true, }); -export type UpdateAppointmentSchema = z.infer; export const getAppointmentsQuerySchema = baseQuerySchema .pick({ @@ -42,7 +41,7 @@ export const getAppointmentsQuerySchema = baseQuerySchema category: z.enum(SPECIALTY_CATEGORIES).optional(), condition: z.enum(PATIENT_CONDITIONS).optional(), orderBy: z.enum(APPOINTMENT_ORDER_BY).optional().default('date'), - order: z.enum(QUERY_ORDER).optional().default('DESC'), + order: z.enum(QUERY_ORDERS).optional().default('DESC'), }) .refine( (data) => { diff --git a/src/domain/schemas/appointments/responses.ts b/src/domain/schemas/appointments/responses.ts index 6c8687a..df95d43 100644 --- a/src/domain/schemas/appointments/responses.ts +++ b/src/domain/schemas/appointments/responses.ts @@ -1,15 +1,11 @@ import { z } from 'zod'; import { baseResponseSchema } from '../base'; -import { patientResponseSchema } from '../patient'; +import { patientSchema } from '../patient'; import { appointmentSchema } from '.'; export const appointmentResponseSchema = appointmentSchema.extend({ - patient: patientResponseSchema.pick({ - name: true, - email: true, - avatar_url: true, - }), + patient: patientSchema.pick({ name: true, email: true, avatar_url: true }), }); export type AppointmentResponse = z.infer; @@ -19,6 +15,6 @@ export const getAppointmentsResponseSchema = baseResponseSchema.extend({ total: z.number(), }), }); -export type GetAppointmentsResponseSchema = z.infer< +export type GetAppointmentsResponse = z.infer< typeof getAppointmentsResponseSchema >; diff --git a/src/domain/schemas/auth.ts b/src/domain/schemas/auth.ts index 1327977..8b413db 100644 --- a/src/domain/schemas/auth.ts +++ b/src/domain/schemas/auth.ts @@ -1,5 +1,14 @@ import { z } from 'zod'; +import { AUTH_TOKEN_ROLES } from '../enums/tokens'; + +export const authUserSchema = z.object({ + id: z.string().uuid(), + email: z.string().email(), + role: z.enum(AUTH_TOKEN_ROLES), +}); +export type AuthUserSchema = z.infer; + export const signInWithEmailSchema = z.object({ email: z.string().email(), password: z.string().min(8), diff --git a/src/domain/schemas/base.ts b/src/domain/schemas/base.ts index 3cda5dc..d759bba 100644 --- a/src/domain/schemas/base.ts +++ b/src/domain/schemas/base.ts @@ -4,4 +4,4 @@ export const baseResponseSchema = z.object({ success: z.boolean().describe('Confirma se a operação foi bem-sucedida.'), message: z.string().describe('Mensagem de resposta pertinente à requisição.'), }); -export type BaseResponseSchema = z.infer; +export type BaseResponse = z.infer; diff --git a/src/domain/schemas/patient-requirement.ts b/src/domain/schemas/patient-requirement.ts deleted file mode 100644 index 817af2a..0000000 --- a/src/domain/schemas/patient-requirement.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { z } from 'zod'; - -import { baseResponseSchema } from './base'; -import { patientSchema } from './patient'; -import { baseQuerySchema } from './query'; -import { userSchema } from './user'; - -export const PATIENT_REQUIREMENT_TYPE = [ - 'screening', - 'medical_report', -] as const; -export type PatientRequirementType = (typeof PATIENT_REQUIREMENT_TYPE)[number]; - -export const PATIENT_REQUIREMENT_STATUS = [ - 'pending', - 'under_review', - 'approved', - 'declined', -] as const; -export type PatientRequirementStatusType = - (typeof PATIENT_REQUIREMENT_STATUS)[number]; - -export const PATIENT_REQUIREMENTS_ORDER_BY = [ - 'name', - 'status', - 'type', - 'date', - 'approved_at', - 'submitted_at', -] as const; -export type PatientRequirementOrderBy = - (typeof PATIENT_REQUIREMENTS_ORDER_BY)[number]; - -export const patientRequirementSchema = z - .object({ - id: z.string().uuid(), - patient_id: z.string().uuid(), - type: z.enum(PATIENT_REQUIREMENT_TYPE), - title: z.string().max(255), - description: z.string().max(500).nullable(), - status: z.enum(PATIENT_REQUIREMENT_STATUS).default('pending'), - required_by: z.string().uuid(), - approved_by: z.string().uuid().nullable(), - approved_at: z.coerce.date().nullable(), - submitted_at: z.coerce.date().nullable(), - created_at: z.coerce.date(), - updated_at: z.coerce.date(), - }) - .strict(); -export type PatientRequirementSchema = z.infer; - -export const createPatientRequirementSchema = patientRequirementSchema.pick({ - patient_id: true, - type: true, - title: true, - description: true, -}); -export type CreatePatientRequirementSchema = z.infer< - typeof createPatientRequirementSchema ->; - -export const findAllPatientsRequirementsQuerySchema = baseQuerySchema - .pick({ - search: true, - order: true, - startDate: true, - endDate: true, - perPage: true, - page: true, - }) - .extend({ - status: z.enum(PATIENT_REQUIREMENT_STATUS).optional(), - orderBy: z - .enum(PATIENT_REQUIREMENTS_ORDER_BY) - .optional() - .default('approved_at'), - }) - .refine( - (data) => { - if (data.startDate && data.endDate) { - return data.startDate < data.endDate; - } - return true; - }, - { - message: 'It should be greater than `startDate`', - path: ['endDate'], - }, - ); - -export type findAllPatientsRequirementsQuerySchema = z.infer< - typeof findAllPatientsRequirementsQuerySchema ->; - -export const patientRequirementListItemSchema = patientRequirementSchema - .pick({ - id: true, - type: true, - title: true, - status: true, - description: true, - submitted_at: true, - approved_at: true, - created_at: true, - }) - .extend({ - patient: patientSchema - .pick({ id: true }) - .merge(userSchema.pick({ name: true })), - }); - -export type PatientRequirementListItemSchema = z.infer< - typeof patientRequirementListItemSchema ->; - -export const findAllPatientsRequirementsResponseSchema = - baseResponseSchema.extend({ - data: z.object({ - requirements: z.array(patientRequirementListItemSchema), - total: z.number(), - }), - }); -export type FindAllPatientsRequirementsResponseSchema = z.infer< - typeof findAllPatientsRequirementsResponseSchema ->; - -export const findAllPatientsRequirementsByPatientIdQuerySchema = baseQuerySchema - .pick({ - startDate: true, - endDate: true, - perPage: true, - page: true, - limit: true, - }) - .extend({ - status: z.enum(PATIENT_REQUIREMENT_STATUS).optional(), - }) - .refine( - (data) => { - if (data.startDate && data.endDate) { - return data.startDate < data.endDate; - } - return true; - }, - { - message: 'It should be greater than `startDate`', - path: ['endDate'], - }, - ); -export type FindAllPatientsRequirementsByPatientIdQuerySchema = z.infer< - typeof findAllPatientsRequirementsByPatientIdQuerySchema ->; - -export const patientRequirementByPatientIdResponseSchema = - patientRequirementSchema.pick({ - id: true, - type: true, - title: true, - status: true, - submitted_at: true, - approved_at: true, - created_at: true, - }); -export type PatientRequirementByPatientIdResponseType = z.infer< - typeof patientRequirementByPatientIdResponseSchema ->; - -export const findAllPatientsRequirementsByPatientIdResponseSchema = - baseResponseSchema.extend({ - data: z.object({ - requirements: z.array(patientRequirementByPatientIdResponseSchema), - total: z.number(), - }), - }); -export type FindAllPatientsRequirementsByPatientIdResponseSchema = z.infer< - typeof findAllPatientsRequirementsByPatientIdResponseSchema ->; diff --git a/src/domain/schemas/patient-requirement/index.ts b/src/domain/schemas/patient-requirement/index.ts new file mode 100644 index 0000000..9484605 --- /dev/null +++ b/src/domain/schemas/patient-requirement/index.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; + +import { + PATIENT_REQUIREMENT_STATUSES, + PATIENT_REQUIREMENT_TYPES, +} from '@/domain/enums/patient-requirements'; + +export const patientRequirementSchema = z + .object({ + id: z.string().uuid(), + patient_id: z.string().uuid(), + type: z.enum(PATIENT_REQUIREMENT_TYPES), + title: z.string().max(255), + description: z.string().max(500).nullable(), + status: z.enum(PATIENT_REQUIREMENT_STATUSES).default('pending'), + submitted_at: z.coerce.date().nullable(), + approved_by: z.string().uuid().nullable(), + approved_at: z.coerce.date().nullable(), + created_by: z.string().uuid(), + created_at: z.coerce.date(), + updated_at: z.coerce.date(), + }) + .strict(); +export type PatientRequirementSchema = z.infer; diff --git a/src/domain/schemas/patient-requirement/requests.ts b/src/domain/schemas/patient-requirement/requests.ts new file mode 100644 index 0000000..0397e61 --- /dev/null +++ b/src/domain/schemas/patient-requirement/requests.ts @@ -0,0 +1,76 @@ +import { z } from 'zod'; + +import { + PATIENT_REQUIREMENT_STATUSES, + PATIENT_REQUIREMENTS_ORDER_BY, +} from '@/domain/enums/patient-requirements'; + +import { baseQuerySchema } from '../query'; +import { patientRequirementSchema } from '.'; + +export const createPatientRequirementSchema = patientRequirementSchema.pick({ + patient_id: true, + type: true, + title: true, + description: true, +}); +export type CreatePatientRequirement = z.infer< + typeof createPatientRequirementSchema +>; + +export const getPatientRequirementsQuerySchema = baseQuerySchema + .pick({ + search: true, + order: true, + startDate: true, + endDate: true, + perPage: true, + page: true, + }) + .extend({ + status: z.enum(PATIENT_REQUIREMENT_STATUSES).optional(), + orderBy: z + .enum(PATIENT_REQUIREMENTS_ORDER_BY) + .optional() + .default('approved_at'), + }) + .refine( + (data) => { + if (data.startDate && data.endDate) { + return data.startDate < data.endDate; + } + return true; + }, + { + message: 'It should be greater than `startDate`', + path: ['endDate'], + }, + ); +export type GetPatientRequirementsQuery = z.infer< + typeof getPatientRequirementsQuerySchema +>; + +export const getPatientRequirementsByPatientIdQuerySchema = baseQuerySchema + .pick({ + startDate: true, + endDate: true, + perPage: true, + page: true, + limit: true, + }) + .extend({ status: z.enum(PATIENT_REQUIREMENT_STATUSES).optional() }) + .refine( + (data) => { + if (data.startDate && data.endDate) { + return data.startDate < data.endDate; + } + return true; + }, + { + message: 'It should be greater than `startDate`', + path: ['endDate'], + }, + ); +export type GetPatientRequirementsByPatientIdQuery = z.infer< + typeof getPatientRequirementsByPatientIdQuerySchema +>; diff --git a/src/domain/schemas/patient-requirement/responses.ts b/src/domain/schemas/patient-requirement/responses.ts new file mode 100644 index 0000000..ba220ec --- /dev/null +++ b/src/domain/schemas/patient-requirement/responses.ts @@ -0,0 +1,56 @@ +import { z } from 'zod'; + +import { baseResponseSchema } from '../base'; +import { patientSchema } from '../patient'; +import { patientRequirementSchema } from '.'; + +export const patientRequirementItemSchema = patientRequirementSchema + .pick({ + id: true, + type: true, + title: true, + status: true, + description: true, + submitted_at: true, + approved_at: true, + created_at: true, + }) + .extend({ patient: patientSchema.pick({ id: true, name: true }) }); +export type PatientRequirementItem = z.infer< + typeof patientRequirementItemSchema +>; + +export const getPatientRequirementsResponseSchema = baseResponseSchema.extend({ + data: z.object({ + requirements: z.array(patientRequirementItemSchema), + total: z.number(), + }), +}); +export type GetPatientRequirementsResponse = z.infer< + typeof getPatientRequirementsResponseSchema +>; + +export const patientRequirementByPatientIdSchema = + patientRequirementSchema.pick({ + id: true, + type: true, + title: true, + status: true, + submitted_at: true, + approved_at: true, + created_at: true, + }); +export type PatientRequirementByPatientId = z.infer< + typeof patientRequirementByPatientIdSchema +>; + +export const getPatientRequirementsByPatientIdResponseSchema = + baseResponseSchema.extend({ + data: z.object({ + requirements: z.array(patientRequirementByPatientIdSchema), + total: z.number(), + }), + }); +export type GetPatientRequirementsByPatientIdResponse = z.infer< + typeof getPatientRequirementsByPatientIdResponseSchema +>; diff --git a/src/domain/schemas/patient-support.ts b/src/domain/schemas/patient-support.ts deleted file mode 100644 index 0c469a0..0000000 --- a/src/domain/schemas/patient-support.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { z } from 'zod'; - -import { baseResponseSchema } from './base'; - -// Entity - -export const patientSupportSchema = z - .object({ - id: z.string().uuid(), - patient_id: z.string().uuid(), - name: z.string().min(3).max(100), - phone: z - .string() - .regex(/^\d+$/) - .refine((num) => num.length === 11), - kinship: z.string(), - created_at: z.coerce.date(), - updated_at: z.coerce.date(), - }) - .strict(); -export type PatientSupportSchema = z.infer; - -//Create - -export const createPatientSupportSchema = patientSupportSchema.pick({ - patient_id: true, - name: true, - phone: true, - kinship: true, -}); -export type CreatePatientSupportSchema = z.infer< - typeof createPatientSupportSchema ->; - -export const createPatientSupportResponseSchema = baseResponseSchema.extend({}); -export type CreatePatientSupportResponseSchema = z.infer< - typeof createPatientSupportResponseSchema ->; - -export const findAllPatientsSupportResponseSchema = baseResponseSchema.extend({ - data: z.object({ - patient_supports: z.array(patientSupportSchema), - total: z.number(), - }), -}); -export type FindAllPatientsSupportResponseSchema = z.infer< - typeof findAllPatientsSupportResponseSchema ->; - -export const findOnePatientsSupportResponseSchema = baseResponseSchema.extend({ - data: patientSupportSchema, -}); -export type FindOnePatientsSupportResponseSchema = z.infer< - typeof findOnePatientsSupportResponseSchema ->; - -//Update - -export const updatePatientSupportParamsSchema = z.object({ - id: z.string().uuid(), -}); -export type UpdatePatientSupportParamsSchema = z.infer< - typeof updatePatientSupportParamsSchema ->; - -export const updatePatientSupportSchema = patientSupportSchema.omit({ - id: true, - patient_id: true, - created_at: true, - updated_at: true, -}); -export type UpdatePatientSupportSchema = z.infer< - typeof updatePatientSupportSchema ->; - -export const updatePatientSupportResponseSchema = baseResponseSchema.extend({}); -export type UpdatePatientSupportResponseSchema = z.infer< - typeof updatePatientSupportResponseSchema ->; - -//Delete - -export const deletePatientSupportParamsSchema = z.object({ - id: z.string().uuid(), -}); -export type DeletePatientSupportParamsSchema = z.infer< - typeof deletePatientSupportParamsSchema ->; - -export const disablePatientSupportResponseSchema = baseResponseSchema.extend( - {}, -); -export type DisablePatientSupportResponseSchema = z.infer< - typeof disablePatientSupportResponseSchema ->; - -export const deletePatientSupportResponseSchema = baseResponseSchema.extend({}); -export type DeletePatientSupportResponseSchema = z.infer< - typeof deletePatientSupportResponseSchema ->; diff --git a/src/domain/schemas/patient-support/index.ts b/src/domain/schemas/patient-support/index.ts new file mode 100644 index 0000000..4891c4a --- /dev/null +++ b/src/domain/schemas/patient-support/index.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +import { nameSchema, phoneSchema } from '../shared'; + +export const patientSupportSchema = z + .object({ + id: z.string().uuid(), + patient_id: z.string().uuid(), + name: nameSchema, + phone: phoneSchema, + kinship: z.string().max(50), + created_at: z.coerce.date(), + updated_at: z.coerce.date(), + }) + .strict(); +export type PatientSupportSchema = z.infer; diff --git a/src/domain/schemas/patient-support/requests.ts b/src/domain/schemas/patient-support/requests.ts new file mode 100644 index 0000000..f9edc80 --- /dev/null +++ b/src/domain/schemas/patient-support/requests.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +import { patientSupportSchema } from '.'; + +export const createPatientSupportSchema = patientSupportSchema.pick({ + patient_id: true, + name: true, + phone: true, + kinship: true, +}); +export type CreatePatientSupport = z.infer; + +export const updatePatientSupportParamsSchema = z.object({ + id: z.string().uuid(), +}); +export type UpdatePatientSupportParams = z.infer< + typeof updatePatientSupportParamsSchema +>; + +export const updatePatientSupportSchema = patientSupportSchema.pick({ + name: true, + phone: true, + kinship: true, +}); +export type UpdatePatientSupport = z.infer; + +export const deletePatientSupportParamsSchema = z.object({ + id: z.string().uuid(), +}); +export type DeletePatientSupportParams = z.infer< + typeof deletePatientSupportParamsSchema +>; diff --git a/src/domain/schemas/patient-support/responses.ts b/src/domain/schemas/patient-support/responses.ts new file mode 100644 index 0000000..30cfa5d --- /dev/null +++ b/src/domain/schemas/patient-support/responses.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +import { baseResponseSchema } from '../base'; +import { patientSupportSchema } from '.'; + +export const getPatientSupportsResponseSchema = baseResponseSchema.extend({ + data: z.object({ + patient_supports: z.array(patientSupportSchema), + total: z.number(), + }), +}); +export type GetPatientSupportsResponse = z.infer< + typeof getPatientSupportsResponseSchema +>; + +export const getPatientSupportResponseSchema = baseResponseSchema.extend({ + data: patientSupportSchema, +}); +export type GetPatientSupportResponse = z.infer< + typeof getPatientSupportResponseSchema +>; diff --git a/src/domain/schemas/patient.ts b/src/domain/schemas/patient.ts deleted file mode 100644 index 3c8c53a..0000000 --- a/src/domain/schemas/patient.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { z } from 'zod'; - -import { BRAZILIAN_STATES } from '@/constants/brazilian-states'; -import { ONLY_NUMBERS_REGEX } from '@/constants/regex'; - -import { baseResponseSchema } from './base'; -import { createPatientSupportSchema } from './patient-support'; -import { patientSupportSchema } from './patient-support'; -import { baseQuerySchema } from './query'; -import { userSchema } from './user'; - -export const GENDERS = [ - 'male_cis', - 'female_cis', - 'male_trans', - 'female_trans', - 'non_binary', - 'prefer_not_to_say', -] as const; -export type Gender = (typeof GENDERS)[number]; - -export const PATIENT_STATUS = ['active', 'inactive', 'pending'] as const; -export type PatientStatus = (typeof PATIENT_STATUS)[number]; - -export const PATIENT_ORDER_BY = ['name', 'email', 'status', 'date'] as const; -export type PatientOrderBy = (typeof PATIENT_ORDER_BY)[number]; - -export const PATIENT_STATISTICS = ['gender', 'total'] as const; -export type PatientStatisticsResult = { - gender: Gender; - total: number; -}; - -export const PATIENT_CONDITIONS = ['in_crisis', 'stable'] as const; -export type PatientCondition = (typeof PATIENT_CONDITIONS)[number]; - -export const patientSchema = z - .object({ - id: z.string().uuid(), - user_id: z.string().uuid(), - gender: z.enum(GENDERS).default('prefer_not_to_say'), - date_of_birth: z.coerce.date(), - phone: z - .string() - .min(10) - .max(11) - .regex(ONLY_NUMBERS_REGEX, 'Only numbers are accepted'), - cpf: z.string().max(11), - state: z.enum(BRAZILIAN_STATES), - city: z.string(), - // medical report - has_disability: z.boolean().default(false), - disability_desc: z.string().nullable(), - need_legal_assistance: z.boolean().default(false), - take_medication: z.boolean().default(false), - medication_desc: z.string().nullable(), - has_nmo_diagnosis: z.boolean().default(false), - status: z.enum(PATIENT_STATUS).default('pending'), - created_at: z.coerce.date(), - updated_at: z.coerce.date(), - }) - .strict(); -export type PatientSchema = z.infer; - -export const patientResponseSchema = patientSchema - .merge( - userSchema.pick({ - name: true, - email: true, - avatar_url: true, - }), - ) - .extend({ - supports: z.array( - patientSupportSchema.pick({ - id: true, - name: true, - phone: true, - kinship: true, - }), - ), - }); -export type PatientType = z.infer; - -export const patientScreeningSchema = patientSchema - .omit({ - id: true, - user_id: true, - status: true, - created_at: true, - updated_at: true, - }) - .merge(userSchema.pick({ name: true })) - .extend({ - supports: z - .array( - createPatientSupportSchema.pick({ - name: true, - phone: true, - kinship: true, - }), - ) - .nullable() - .default([]), - }); -export type PatientScreeningSchema = z.infer; - -export const createPatientSchema = patientSchema - .omit({ id: true, created_at: true, updated_at: true }) - .merge(userSchema.pick({ name: true, email: true })) - .extend({ - supports: z - .array( - createPatientSupportSchema.pick({ - name: true, - phone: true, - kinship: true, - }), - ) - .optional() - .default([]), - }); -export type CreatePatientSchema = z.infer; - -export const updatePatientSchema = patientSchema - .omit({ - id: true, - user_id: true, - created_at: true, - updated_at: true, - status: true, - }) - .merge(userSchema.pick({ name: true, email: true })); - -export type UpdatePatientSchema = z.infer; - -export const findAllPatientsQuerySchema = baseQuerySchema - .pick({ - search: true, - order: true, - page: true, - perPage: true, - startDate: true, - endDate: true, - }) - .extend({ - all: z.coerce.boolean().optional(), - status: z.enum(PATIENT_STATUS).optional(), - orderBy: z.enum(PATIENT_ORDER_BY).optional().default('name'), - }) - .refine( - (data) => { - if (data.startDate && data.endDate) { - return data.startDate < data.endDate; - } - return true; - }, - { - message: 'It should be greater than `startDate`', - path: ['endDate'], - }, - ); -export type FindAllPatientsQuerySchema = z.infer< - typeof findAllPatientsQuerySchema ->; - -export const findAllPatientsResponseSchema = baseResponseSchema.extend({ - data: z.object({ - patients: z.array(patientResponseSchema), - total: z.number(), - }), -}); -export type FindAllPatientsResponseSchema = z.infer< - typeof findAllPatientsResponseSchema ->; - -export const getPatientResponseSchema = baseResponseSchema.extend({ - data: patientResponseSchema, -}); -export type GetPatientResponseSchema = z.infer; diff --git a/src/domain/schemas/patient/index.ts b/src/domain/schemas/patient/index.ts new file mode 100644 index 0000000..af10772 --- /dev/null +++ b/src/domain/schemas/patient/index.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +import { BRAZILIAN_STATES } from '@/constants/brazilian-states'; +import { PATIENT_GENDERS, PATIENT_STATUSES } from '@/domain/enums/patients'; + +import { + avatarSchema, + emailSchema, + nameSchema, + passwordSchema, + phoneSchema, +} from '../shared'; + +export const patientSchema = z + .object({ + id: z.string().uuid(), + name: nameSchema, + email: emailSchema, + password: passwordSchema.nullable(), + avatar_url: avatarSchema.nullable(), + status: z.enum(PATIENT_STATUSES).default('pending'), + gender: z.enum(PATIENT_GENDERS).default('prefer_not_to_say'), + date_of_birth: z.coerce.date().nullable(), + phone: phoneSchema.nullable(), + cpf: z.string().max(11).nullable(), + state: z.enum(BRAZILIAN_STATES).nullable(), + city: z.string().nullable(), + // medical report + has_disability: z.boolean().default(false), + disability_desc: z.string().max(500).nullable(), + need_legal_assistance: z.boolean().default(false), + take_medication: z.boolean().default(false), + medication_desc: z.string().max(500).nullable(), + has_nmo_diagnosis: z.boolean().default(false), + created_at: z.coerce.date(), + updated_at: z.coerce.date(), + }) + .strict(); +export type PatientSchema = z.infer; diff --git a/src/domain/schemas/patient/requests.ts b/src/domain/schemas/patient/requests.ts new file mode 100644 index 0000000..6582cc4 --- /dev/null +++ b/src/domain/schemas/patient/requests.ts @@ -0,0 +1,82 @@ +import { z } from 'zod'; + +import { BRAZILIAN_STATES } from '@/constants/brazilian-states'; +import { + PATIENT_GENDERS, + PATIENT_ORDER_BY, + PATIENT_STATUSES, +} from '@/domain/enums/patients'; + +import { createPatientSupportSchema } from '../patient-support/requests'; +import { baseQuerySchema } from '../query'; +import { emailSchema, nameSchema, phoneSchema } from '../shared'; +import { patientSchema } from '.'; + +export const createPatientSchema = z + .object({ + name: nameSchema, + email: emailSchema, + gender: z.enum(PATIENT_GENDERS).default('prefer_not_to_say'), + date_of_birth: z.coerce.date(), + phone: phoneSchema, + cpf: z.string().max(11), + state: z.enum(BRAZILIAN_STATES), + city: z.string(), + }) + .merge( + patientSchema.pick({ + has_disability: true, + disability_desc: true, + need_legal_assistance: true, + take_medication: true, + medication_desc: true, + has_nmo_diagnosis: true, + }), + ); +export type CreatePatient = z.infer; + +export const patientScreeningSchema = createPatientSchema.extend({ + supports: z + .array( + createPatientSupportSchema.pick({ + name: true, + phone: true, + kinship: true, + }), + ) + .nullable() + .default([]), +}); +export type PatientScreening = z.infer; + +export const updatePatientSchema = patientScreeningSchema + .omit({ supports: true }) + .merge(patientSchema.pick({ status: true })); +export type UpdatePatient = z.infer; + +export const getPatientsQuerySchema = baseQuerySchema + .pick({ + search: true, + order: true, + page: true, + perPage: true, + startDate: true, + endDate: true, + }) + .extend({ + status: z.enum(PATIENT_STATUSES).optional(), + orderBy: z.enum(PATIENT_ORDER_BY).optional().default('name'), + }) + .refine( + (data) => { + if (data.startDate && data.endDate) { + return data.startDate < data.endDate; + } + return true; + }, + { + message: 'It should be greater than `startDate`', + path: ['endDate'], + }, + ); +export type GetPatientsQuery = z.infer; diff --git a/src/domain/schemas/patient/responses.ts b/src/domain/schemas/patient/responses.ts new file mode 100644 index 0000000..3c1f25a --- /dev/null +++ b/src/domain/schemas/patient/responses.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; + +import { baseResponseSchema } from '../base'; +import { patientSupportSchema } from '../patient-support'; +import { patientSchema } from '.'; + +export const patientResponseSchema = patientSchema.pick({ + id: true, + name: true, + email: true, + status: true, + avatar_url: true, + phone: true, + created_at: true, +}); +export type PatientResponse = z.infer; + +export const getPatientsResponseSchema = baseResponseSchema.extend({ + data: z.object({ + patients: z.array(patientResponseSchema), + total: z.number(), + }), +}); +export type GetPatientsResponse = z.infer; + +export const getPatientResponseSchema = baseResponseSchema.extend({ + data: patientSchema + .omit({ password: true }) + .extend({ supports: z.array(patientSupportSchema) }), +}); +export type GetPatientResponse = z.infer; + +export const getAllPatientsListResponseSchema = baseResponseSchema.extend({ + data: z.object({ + patients: z.array(patientSchema.pick({ id: true, name: true, cpf: true })), + }), +}); +export type GetAllPatientsListResponse = z.infer< + typeof getAllPatientsListResponseSchema +>; diff --git a/src/domain/schemas/query.ts b/src/domain/schemas/query.ts index 92d7ad8..6e265d1 100644 --- a/src/domain/schemas/query.ts +++ b/src/domain/schemas/query.ts @@ -1,20 +1,11 @@ import { z } from 'zod'; -export const QUERY_ORDER = ['ASC', 'DESC'] as const; -export type QueryOrder = (typeof QUERY_ORDER)[number]; - -export const QUERY_PERIOD = [ - 'today', - 'last-year', - 'last-month', - 'last-week', -] as const; -export type QueryPeriod = (typeof QUERY_PERIOD)[number]; +import { QUERY_ORDERS, QUERY_PERIODS } from '../enums/queries'; export const baseQuerySchema = z.object({ search: z.string().optional(), - order: z.enum(QUERY_ORDER).optional(), - period: z.enum(QUERY_PERIOD).optional().default('today'), + order: z.enum(QUERY_ORDERS).optional(), + period: z.enum(QUERY_PERIODS).optional().default('today'), page: z.coerce.number().min(1).optional().default(1), perPage: z.coerce.number().min(1).max(50).optional().default(10), limit: z.coerce.number().min(1).optional().default(10), @@ -22,4 +13,4 @@ export const baseQuerySchema = z.object({ endDate: z.string().datetime().optional(), withPercentage: z.coerce.boolean().optional().default(false), }); -export type BaseQuerySchema = z.infer; +export type BaseQuery = z.infer; diff --git a/src/domain/schemas/referral/index.ts b/src/domain/schemas/referral/index.ts index 6589aaf..4995c8b 100644 --- a/src/domain/schemas/referral/index.ts +++ b/src/domain/schemas/referral/index.ts @@ -1,9 +1,8 @@ import { z } from 'zod'; +import { PATIENT_CONDITIONS } from '@/domain/enums/patients'; import { REFERRAL_STATUSES } from '@/domain/enums/referrals'; -import { SPECIALTY_CATEGORIES } from '@/domain/enums/specialties'; - -import { PATIENT_CONDITIONS } from '../patient'; +import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; export const referralSchema = z .object({ @@ -14,7 +13,7 @@ export const referralSchema = z category: z.enum(SPECIALTY_CATEGORIES), condition: z.enum(PATIENT_CONDITIONS), annotation: z.string().max(2000).nullable(), - professional_name: z.string().max(255).nullable(), + professional_name: z.string().max(64).nullable(), created_by: z.string().uuid(), created_at: z.coerce.date(), updated_at: z.coerce.date(), diff --git a/src/domain/schemas/referral/requests.ts b/src/domain/schemas/referral/requests.ts index 5d6bda6..e425ff6 100644 --- a/src/domain/schemas/referral/requests.ts +++ b/src/domain/schemas/referral/requests.ts @@ -1,10 +1,11 @@ import { z } from 'zod'; +import { PATIENT_CONDITIONS } from '@/domain/enums/patients'; +import { QUERY_ORDERS } from '@/domain/enums/queries'; import { REFERRAL_ORDER_BY, REFERRAL_STATUSES } from '@/domain/enums/referrals'; -import { SPECIALTY_CATEGORIES } from '@/domain/enums/specialties'; +import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; -import { PATIENT_CONDITIONS } from '../patient'; -import { baseQuerySchema, QUERY_ORDER } from '../query'; +import { baseQuerySchema } from '../query'; import { referralSchema } from '.'; export const createReferralSchema = referralSchema.pick({ @@ -15,7 +16,7 @@ export const createReferralSchema = referralSchema.pick({ annotation: true, professional_name: true, }); -export type CreateReferralSchema = z.infer; +export type CreateReferral = z.infer; export const getReferralsQuerySchema = baseQuerySchema .pick({ @@ -31,7 +32,7 @@ export const getReferralsQuerySchema = baseQuerySchema category: z.enum(SPECIALTY_CATEGORIES).optional(), condition: z.enum(PATIENT_CONDITIONS).optional(), orderBy: z.enum(REFERRAL_ORDER_BY).optional().default('date'), - order: z.enum(QUERY_ORDER).optional().default('DESC'), + order: z.enum(QUERY_ORDERS).optional().default('DESC'), }) .refine( (data) => { diff --git a/src/domain/schemas/referral/responses.ts b/src/domain/schemas/referral/responses.ts index 3a218ba..093e5b5 100644 --- a/src/domain/schemas/referral/responses.ts +++ b/src/domain/schemas/referral/responses.ts @@ -1,17 +1,13 @@ import { z } from 'zod'; import { baseResponseSchema } from '../base'; -import { patientResponseSchema } from '../patient'; +import { patientSchema } from '../patient'; import { referralSchema } from '.'; export const referralResponseSchema = referralSchema.extend({ - patient: patientResponseSchema.pick({ - name: true, - email: true, - avatar_url: true, - }), + patient: patientSchema.pick({ name: true, email: true, avatar_url: true }), }); -export type ReferralResponseSchema = z.infer; +export type ReferralResponse = z.infer; export const getReferralsResponseSchema = baseResponseSchema.extend({ data: z.object({ @@ -19,6 +15,4 @@ export const getReferralsResponseSchema = baseResponseSchema.extend({ total: z.number(), }), }); -export type GetReferralsResponseSchema = z.infer< - typeof getReferralsResponseSchema ->; +export type GetReferralsResponse = z.infer; diff --git a/src/domain/schemas/shared.ts b/src/domain/schemas/shared.ts new file mode 100644 index 0000000..302db91 --- /dev/null +++ b/src/domain/schemas/shared.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +import { ONLY_NUMBERS_REGEX } from '@/constants/regex'; + +export const nameSchema = z.string().min(3).max(64); + +export const emailSchema = z.string().min(3).max(64); + +export const passwordSchema = z.string().min(8).max(64); + +export const avatarSchema = z.string().url(); + +export const phoneSchema = z + .string() + .min(10) + .max(11) + .regex(ONLY_NUMBERS_REGEX, 'Only numbers are accepted'); diff --git a/src/domain/schemas/statistics/responses.ts b/src/domain/schemas/statistics/responses.ts index 9d1610e..6e12b16 100644 --- a/src/domain/schemas/statistics/responses.ts +++ b/src/domain/schemas/statistics/responses.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; import { BRAZILIAN_STATES } from '@/constants/brazilian-states'; -import { SPECIALTY_CATEGORIES } from '@/domain/enums/specialties'; +import { PATIENT_GENDERS } from '@/domain/enums/patients'; +import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; import { baseResponseSchema } from '../base'; -import { GENDERS } from '../patient'; // Patients @@ -22,7 +22,7 @@ export type GetTotalPatientsByStatusResponse = z.infer< >; export const totalPatientsByGenderSchema = z.object({ - gender: z.enum(GENDERS), + gender: z.enum(PATIENT_GENDERS), total: z.number(), }); export type TotalPatientsByGender = z.infer; diff --git a/src/domain/schemas/token.ts b/src/domain/schemas/token.ts index 96da411..c96873e 100644 --- a/src/domain/schemas/token.ts +++ b/src/domain/schemas/token.ts @@ -1,19 +1,10 @@ import { z } from 'zod'; -import type { UserRoleType } from './user'; - -export const AUTH_TOKENS_MAPPING = { - access_token: 'access_token', - password_reset: 'password_reset', - invite_token: 'invite_token', -} as const; -export type AuthTokenType = keyof typeof AUTH_TOKENS_MAPPING; - -export const AUTH_TOKENS = [ - AUTH_TOKENS_MAPPING.access_token, - AUTH_TOKENS_MAPPING.password_reset, - AUTH_TOKENS_MAPPING.invite_token, -] as const; +import { + AUTH_TOKENS, + type AUTH_TOKENS_MAPPING, + type AuthTokenRole, +} from '../enums/tokens'; export const authTokenSchema = z .object({ @@ -26,7 +17,7 @@ export const authTokenSchema = z created_at: z.coerce.date(), }) .strict(); -export type AuthTokenSchema = z.infer; +export type AuthToken = z.infer; export const createAuthTokenSchema = authTokenSchema.pick({ user_id: true, @@ -35,14 +26,13 @@ export const createAuthTokenSchema = authTokenSchema.pick({ type: true, expires_at: true, }); -export type CreateAuthTokenSchema = z.infer; -export type AccessTokenPayloadType = { sub: string; role: UserRoleType }; -export type PasswordResetPayloadType = { sub: string }; -export type InviteTokenPayloadType = { sub: string; role: UserRoleType }; +export type AccessTokenPayload = { sub: string; role: AuthTokenRole }; +export type PasswordResetPayload = { sub: string; role: AuthTokenRole }; +export type InviteTokenPayload = { sub: string; role: AuthTokenRole }; -export type AuthTokenPayloadByType = { - [AUTH_TOKENS_MAPPING.access_token]: AccessTokenPayloadType; - [AUTH_TOKENS_MAPPING.password_reset]: PasswordResetPayloadType; - [AUTH_TOKENS_MAPPING.invite_token]: InviteTokenPayloadType; +export type AuthTokenPayloads = { + [AUTH_TOKENS_MAPPING.access_token]: AccessTokenPayload; + [AUTH_TOKENS_MAPPING.password_reset]: PasswordResetPayload; + [AUTH_TOKENS_MAPPING.invite_token]: InviteTokenPayload; }; diff --git a/src/domain/schemas/user.ts b/src/domain/schemas/user.ts deleted file mode 100644 index c976076..0000000 --- a/src/domain/schemas/user.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { z } from 'zod'; - -import { baseResponseSchema } from './base'; - -// Entity - -export const USER_ROLES = [ - 'admin', - 'nurse', - 'specialist', - 'manager', - 'patient', -] as const; -export type UserRoleType = (typeof USER_ROLES)[number]; - -export const userSchema = z - .object({ - id: z.string().uuid(), - name: z.string().min(3), - email: z.string().email().max(255), - password: z.string().min(8).max(255), - role: z.enum(USER_ROLES), - avatar_url: z.string().url().nullable(), - created_at: z.coerce.date(), - updated_at: z.coerce.date(), - }) - .strict(); -export type UserSchema = z.infer; - -// Create - -export const createUserSchema = userSchema.pick({ - name: true, - email: true, - password: true, -}); -export type CreateUserSchema = z.infer; - -// Update - -export const updateUserParamsSchema = z.object({ - id: z.string().uuid(), -}); -export type UpdateUserParamsSchema = z.infer; - -export const updateUserSchema = userSchema.omit({ - id: true, - password: true, - created_at: true, - updated_at: true, -}); -export type UpdateUserSchema = z.infer; - -export const updateUserResponseSchema = baseResponseSchema.extend({}); -export type UpdateUserResponseSchema = z.infer; - -export const disableUserResponseSchema = baseResponseSchema.extend({}); -export type DisableUserResponseSchema = z.infer< - typeof disableUserResponseSchema ->; - -export const deleteUserResponseSchema = baseResponseSchema.extend({}); -export type DeleteUserResponseSchema = z.infer; - -export const getUserProfileResponseSchema = baseResponseSchema - .extend({ - data: userSchema.omit({ password: true }), - }) - .strict(); -export type GetUserProfileResponseSchema = z.infer< - typeof getUserProfileResponseSchema ->; diff --git a/src/domain/schemas/user/index.ts b/src/domain/schemas/user/index.ts new file mode 100644 index 0000000..7aa3b47 --- /dev/null +++ b/src/domain/schemas/user/index.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import { USER_ROLES, USER_STATUSES } from '@/domain/enums/users'; + +import { + avatarSchema, + emailSchema, + nameSchema, + passwordSchema, +} from '../shared'; + +export const userSchema = z + .object({ + id: z.string().uuid(), + name: nameSchema, + email: emailSchema, + password: passwordSchema, + avatar_url: avatarSchema.nullable(), + role: z.enum(USER_ROLES), + status: z.enum(USER_STATUSES).default('active'), + created_at: z.coerce.date(), + updated_at: z.coerce.date(), + }) + .strict(); +export type UserSchema = z.infer; diff --git a/src/domain/schemas/user/requests.ts b/src/domain/schemas/user/requests.ts new file mode 100644 index 0000000..68cc580 --- /dev/null +++ b/src/domain/schemas/user/requests.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +import { userSchema } from '.'; + +export const createUserSchema = userSchema.pick({ + name: true, + email: true, + password: true, +}); +export type CreateUser = z.infer; + +export const updateUserParamsSchema = z.object({ + id: z.string().uuid(), +}); +export type UpdateUserParams = z.infer; + +export const updateUserSchema = userSchema.omit({ + id: true, + password: true, + created_at: true, + updated_at: true, +}); +export type UpdateUser = z.infer; diff --git a/src/domain/schemas/user/responses.ts b/src/domain/schemas/user/responses.ts new file mode 100644 index 0000000..cde22e6 --- /dev/null +++ b/src/domain/schemas/user/responses.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +import { baseResponseSchema } from '../base'; +import { userSchema } from '.'; + +export const getUserResponseSchema = baseResponseSchema.extend({ + data: userSchema.omit({ password: true }), +}); +export type GetUserResponse = z.infer; diff --git a/src/utils/utils.service.ts b/src/utils/utils.service.ts index a3ce020..6e54912 100644 --- a/src/utils/utils.service.ts +++ b/src/utils/utils.service.ts @@ -8,7 +8,7 @@ import { } from 'date-fns'; import { type CookieOptions, Response } from 'express'; -import type { QueryPeriod } from '@/domain/schemas/query'; +import type { QueryPeriod } from '@/domain/enums/queries'; import { EnvService } from '@/env/env.service'; type SetCookieOptions = CookieOptions & { diff --git a/tests/config/api-client.ts b/tests/config/api-client.ts index e6e835d..0e993cf 100644 --- a/tests/config/api-client.ts +++ b/tests/config/api-client.ts @@ -3,7 +3,7 @@ import { hash } from 'bcryptjs'; import request, { Response } from 'supertest'; import { User } from '@/domain/entities/user'; -import type { UserRoleType } from '@/domain/schemas/user'; +import type { UserRole } from '@/domain/enums/users'; import { getTestApp, getTestDataSource } from './setup'; @@ -21,16 +21,16 @@ interface RequestOptions { interface CachedUser { email: string; password: string; - role: UserRoleType; + role: UserRole; id: string; createdAt: number; } class UserCache { - private static cache = new Map(); + private static cache = new Map(); private static cacheTimeout = 30000; // 30 seconds cache - static async getOrCreateUser(role: UserRoleType): Promise { + static async getOrCreateUser(role: UserRole): Promise { const now = Date.now(); const cached = this.cache.get(role); @@ -288,7 +288,7 @@ class ApiClient { * @returns Authenticated API client for the created user */ async createUserWithRoleAndLogin( - role: UserRoleType = 'patient', + role: UserRole, userData?: Partial<{ name: string; email: string; @@ -377,11 +377,11 @@ class ApiClient { /** * Convenience method to create and login as patient user (default role) */ - async createPatientAndLogin( - userData?: Partial<{ name: string; email: string; password: string }>, - ): Promise { - return this.createUserWithRoleAndLogin('patient', userData); - } + // async createPatientAndLogin( + // userData?: Partial<{ name: string; email: string; password: string }>, + // ): Promise { + // return this.createUserWithRoleAndLogin('patient', userData); + // } } class CookieAuthenticatedApiClient extends ApiClient { diff --git a/tests/e2e/patients.spec.ts b/tests/e2e/patients.spec.ts index 333322f..48fb615 100644 --- a/tests/e2e/patients.spec.ts +++ b/tests/e2e/patients.spec.ts @@ -19,17 +19,17 @@ describe('Patients E2E Tests', () => { ); }); - it('should return 401 for patient role (insufficient permissions)', async () => { - const client = await api(app).createPatientAndLogin(); - const response = await client.get('/patients').send(); - - expect(response.status).toBe(401); - expect(response.body).toHaveProperty('success', false); - expect(response.body).toHaveProperty( - 'message', - 'Você não tem permissão para executar esta ação.', - ); - }); + // it('should return 401 for patient role (insufficient permissions)', async () => { + // const client = await api(app).createPatientAndLogin(); + // const response = await client.get('/patients').send(); + + // expect(response.status).toBe(401); + // expect(response.body).toHaveProperty('success', false); + // expect(response.body).toHaveProperty( + // 'message', + // 'Você não tem permissão para executar esta ação.', + // ); + // }); it('should return patients list for admin role', async () => { const client = await api(app).createAdminAndLogin(); @@ -86,17 +86,17 @@ describe('Patients E2E Tests', () => { ); }); - it('should return 401 for patient role (insufficient permissions)', async () => { - const client = await api(app).createPatientAndLogin(); - const response = await client.post('/patients').send({}); - - expect(response.status).toBe(401); - expect(response.body).toHaveProperty('success', false); - expect(response.body).toHaveProperty( - 'message', - 'Você não tem permissão para executar esta ação.', - ); - }); + // it('should return 401 for patient role (insufficient permissions)', async () => { + // const client = await api(app).createPatientAndLogin(); + // const response = await client.post('/patients').send({}); + + // expect(response.status).toBe(401); + // expect(response.body).toHaveProperty('success', false); + // expect(response.body).toHaveProperty( + // 'message', + // 'Você não tem permissão para executar esta ação.', + // ); + // }); it('should create patient for admin role', async () => { const client = await api(app).createAdminAndLogin(); diff --git a/tests/e2e/users.spec.ts b/tests/e2e/users.spec.ts index 774217b..c8f2c6a 100644 --- a/tests/e2e/users.spec.ts +++ b/tests/e2e/users.spec.ts @@ -32,18 +32,18 @@ describe('Users E2E Tests', () => { expect(response.body).toHaveProperty('data'); }); - it('should return user profile for authenticated patient', async () => { - const client = await api(app).createPatientAndLogin(); - const response = await client.get('/users/profile').send(); - - expect(response.status).toBe(200); - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty( - 'message', - 'Dados do usuário retornado com sucesso.', - ); - expect(response.body).toHaveProperty('data'); - }); + // it('should return user profile for authenticated patient', async () => { + // const client = await api(app).createPatientAndLogin(); + // const response = await client.get('/users/profile').send(); + + // expect(response.status).toBe(200); + // expect(response.body).toHaveProperty('success', true); + // expect(response.body).toHaveProperty( + // 'message', + // 'Dados do usuário retornado com sucesso.', + // ); + // expect(response.body).toHaveProperty('data'); + // }); it('should return user profile for authenticated manager', async () => { const client = await api(app).createManagerAndLogin(); From de217508131c1cfa68eb4690fbf4ccd3a84590b5 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Fri, 19 Dec 2025 15:48:25 -0300 Subject: [PATCH 02/21] refactor: create new initial migrate with correct datetime type in db --- .github/copilot-instructions.md | 212 +++++++++++++++++- .../migrations/1765286865155-Initial.ts | 39 ---- .../migrations/1766169704537-Initial.ts | 35 +++ infra/database/seed-dev.ts | 48 +++- src/app/cryptography/crypography.service.ts | 2 +- .../use-cases/get-appointments.use-case.ts | 6 +- src/app/http/auth/auth.controller.ts | 2 +- src/app/http/auth/auth.dtos.ts | 2 +- src/app/http/auth/auth.module.ts | 4 +- src/app/http/auth/auth.service.ts | 2 +- src/app/http/patients/patients.controller.ts | 4 +- src/app/http/patients/patients.dtos.ts | 2 +- src/app/http/patients/patients.repository.ts | 2 +- .../http/referrals/referrals.controller.ts | 4 +- src/app/http/referrals/referrals.dtos.ts | 2 +- .../use-cases/get-referrals.use-case.ts | 8 +- ...et-total-referrals-by-category.use-case.ts | 2 +- ...tal-referred-patients-by-state.use-case.ts | 5 +- src/app/http/users/users.controller.ts | 2 +- src/app/http/users/users.dtos.ts | 2 +- src/common/guards/auth.guard.ts | 2 +- src/domain/entities/appointment.ts | 8 +- src/domain/entities/patient-requirement.ts | 8 +- src/domain/entities/patient-support.ts | 4 +- src/domain/entities/patient.ts | 14 +- src/domain/entities/referral.ts | 8 +- src/domain/entities/specialist.ts | 4 +- src/domain/entities/token.ts | 6 +- src/domain/entities/user.ts | 6 +- src/domain/enums/patients.ts | 8 + src/domain/enums/users.ts | 2 +- src/domain/modules/cryptography.ts | 2 +- src/domain/schemas/appointments/index.ts | 4 +- src/domain/schemas/appointments/responses.ts | 2 +- .../schemas/patient-requirement/responses.ts | 2 +- .../schemas/{patient => patients}/index.ts | 8 +- .../schemas/{patient => patients}/requests.ts | 2 +- .../{patient => patients}/responses.ts | 0 .../schemas/{referral => referrals}/index.ts | 4 +- .../{referral => referrals}/requests.ts | 0 .../{referral => referrals}/responses.ts | 2 +- src/domain/schemas/{token.ts => tokens.ts} | 0 src/domain/schemas/{user => users}/index.ts | 0 .../schemas/{user => users}/requests.ts | 0 .../schemas/{user => users}/responses.ts | 0 45 files changed, 358 insertions(+), 123 deletions(-) delete mode 100644 infra/database/migrations/1765286865155-Initial.ts create mode 100644 infra/database/migrations/1766169704537-Initial.ts rename src/domain/schemas/{patient => patients}/index.ts (86%) rename src/domain/schemas/{patient => patients}/requests.ts (98%) rename src/domain/schemas/{patient => patients}/responses.ts (100%) rename src/domain/schemas/{referral => referrals}/index.ts (89%) rename src/domain/schemas/{referral => referrals}/requests.ts (100%) rename src/domain/schemas/{referral => referrals}/responses.ts (92%) rename src/domain/schemas/{token.ts => tokens.ts} (100%) rename src/domain/schemas/{user => users}/index.ts (100%) rename src/domain/schemas/{user => users}/requests.ts (100%) rename src/domain/schemas/{user => users}/responses.ts (100%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 493db45..d57b06b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,21 +1,215 @@ # Copilot Instructions for ABNMO Platform -## Architecture Overview +NestJS SaaS API managing patients, referrals, appointments, and health tracking. Uses TypeORM with MySQL and Zod schemas for validation. -This is an NestJS application for a SaaS platform managing patients and appointments. +## Architecture: MVC Pattern -- **Stack**: NestJS API with TypeORM and MySQL +**Stack**: NestJS + TypeORM + MySQL + Zod -## Code Patterns & Conventions +Module structure (`/src/app/http/{featureName}`): +``` +{feature}/ +├── {feature}.module.ts # Module definition with imports/providers +├── {feature}.controller.ts # Routes and request handling +├── {feature}.dtos.ts # DTOs created from Zod schemas +└── use-cases/ + ├── get-{feature}.use-case.ts # Read operations + ├── create-{feature}.use-case.ts # Create operations + ├── update-{feature}.use-case.ts # Update operations + └── cancel-{feature}.use-case.ts # Soft delete/cancel operations +``` -Always adhere to the conventions in `/docs` files and follow these patterns: +## Module Organization -- **File Structure**: Follow a consistent file structure for services, routes, and components. -- **Naming Conventions**: Use camelCase for variables and functions, PascalCase for classes and components. +### 1. Module File (`*.module.ts`) -## Common Gotchas +Register entities, inject TypeORM repositories, and declare use-case providers: -1. **Database Relations**: Always destructure relations from the main table object, example: `relations: { user: true }` +```typescript +@Module({ + imports: [TypeOrmModule.forFeature([Entity1, Entity2])], + controllers: [FeatureController], + providers: [ + GetFeatureUseCase, + CreateFeatureUseCase, + UpdateFeatureUseCase, + CancelFeatureUseCase, + ], +}) +export class FeatureModule {} +``` + +### 2. DTOs File (`*.dtos.ts`) + +Create DTOs exclusively from Zod schemas in `/domain/schemas`. Use `createZodDto()` and name DTOs explicitly: + +```typescript +import { createZodDto } from 'nestjs-zod'; +import { createAppointmentSchema, updateAppointmentSchema, getAppointmentsQuerySchema } from '@/domain/schemas/appointments/requests'; + +export class CreateAppointmentDto extends createZodDto(createAppointmentSchema) {} +export class UpdateAppointmentDto extends createZodDto(updateAppointmentSchema) {} +export class GetAppointmentsQuery extends createZodDto(getAppointmentsQuerySchema) {} +``` + +**Naming**: `{Action}{Entity}Dto` (e.g., `CreateAppointmentDto`, `GetAppointmentsQuery`) + +### 3. Controller File (`*.controller.ts`) + +Inject all use-cases and call them based on HTTP methods: + +```typescript +@Controller('appointments') +export class AppointmentsController { + constructor( + private readonly getAppointmentsUseCase: GetAppointmentsUseCase, + private readonly createAppointmentUseCase: CreateAppointmentUseCase, + private readonly updateAppointmentUseCase: UpdateAppointmentUseCase, + private readonly cancelAppointmentUseCase: CancelAppointmentUseCase, + ) {} + + @Get() + async get(@Query() query: GetAppointmentsQuery): Promise { + const data = await this.getAppointmentsUseCase.execute(query); + return { success: true, data }; + } + + @Post() + async create(@Body() dto: CreateAppointmentDto): Promise { + await this.createAppointmentUseCase.execute(dto); + return { success: true }; + } +} +``` + +### 4. Use-Case Files (`use-cases/*.use-case.ts`) + +One use-case per file, one responsibility. Define input/output types explicitly: + +```typescript +interface GetAppointmentsUseCaseRequest { + query: GetAppointmentsQuery; + user: AuthUserDto; +} + +type GetAppointmentsUseCaseResponse = Promise; + +@Injectable() +export class GetAppointmentsUseCase { + constructor( + @InjectRepository(Appointment) + private readonly appointmentsRepository: Repository, + ) {} + + async execute(request: GetAppointmentsUseCaseRequest): GetAppointmentsUseCaseResponse { + // Implementation + } +} +``` + +**Naming**: `{Action}{Entity}UseCase` (e.g., `CreateAppointmentUseCase`, `GetAppointmentsUseCase`) + +## Zod Schemas & Enums + +Centralize validation and types in `/domain/schemas` and `/domain/enums`: + +- **Schemas** (`/domain/schemas/{entity}/{type}.ts`): Define request/response validation +- **Enums** (`/domain/enums/{entity}.ts`): Define constants and types + +Example enum pattern: +```typescript +export const APPOINTMENT_STATUSES = ['scheduled', 'canceled', 'completed'] as const; +export type AppointmentStatus = (typeof APPOINTMENT_STATUSES)[number]; +``` + +DTOs inherit validation directly from schemas—no manual definition needed. + +## Naming Conventions + +Clear, explicit, human-readable names. Reduce cognitive load: + +- **Variables/Functions**: `camelCase` (e.g., `getUserAppointments`, `createdAt`) +- **Classes/Types**: `PascalCase` (e.g., `CreateAppointmentDto`, `AppointmentStatus`) +- **Enums/Constants**: `SCREAMING_SNAKE_CASE` (e.g., `APPOINTMENT_STATUSES`, `MAX_RESULTS_LIMIT`) +- **Files**: `kebab-case` (e.g., `create-appointment.use-case.ts`, `appointments.dtos.ts`) + +Files should match their exports: `get-total-patients.use-case.ts` exports `GetTotalPatientsUseCase`. + +## Database Patterns + +### Queries + +- **Always select fields**: `select: { id: true, name: true }`—avoid over-fetching +- **Count operations**: Select only `id` for performance +- **Relations**: Destructure explicitly: `relations: { user: true }` + +```typescript +const appointments = await this.appointmentsRepository.find({ + where: { patientId: id }, + select: { id: true, date: true, status: true }, + relations: { patient: true }, +}); +``` + +### Repository Access + +Inject TypeORM repositories directly into use-cases. No separate repository files: + +```typescript +@Injectable() +export class CreateAppointmentUseCase { + constructor( + @InjectRepository(Appointment) + private readonly appointmentsRepository: Repository, + ) {} +} +``` + +## Common Patterns + +### Error Handling + +Use NestJS exceptions with descriptive messages: + +```typescript +if (!patient) { + throw new NotFoundException('Patient not found.'); +} + +if (date > maxDate) { + throw new BadRequestException('Appointment date exceeds 3-month limit.'); +} +``` + +### Logging + +Log significant events in use-cases: + +```typescript +private readonly logger = new Logger(CreateAppointmentUseCase.name); + +this.logger.log({ + patientId, + appointmentId, + userId, +}, 'Appointment created successfully'); +``` + +### Query Builders + +Use query builders for complex filtering: + +```typescript +const where: FindOptionsWhere = { + status: status ?? Not('pending'), +}; + +if (period) { + where.created_at = Between(dateRange.startDate, dateRange.endDate); +} + +const result = await this.patientsRepository.find({ where }); +``` ## Writing Guidelines diff --git a/infra/database/migrations/1765286865155-Initial.ts b/infra/database/migrations/1765286865155-Initial.ts deleted file mode 100644 index f1884cb..0000000 --- a/infra/database/migrations/1765286865155-Initial.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Initial1765286865155 implements MigrationInterface { - name = 'Initial1765286865155' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE \`users\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(255) NOT NULL, \`email\` varchar(255) NOT NULL, \`password\` varchar(255) NOT NULL, \`role\` enum ('admin', 'nurse', 'specialist', 'manager', 'patient') NOT NULL DEFAULT 'patient', \`avatar_url\` varchar(255) NULL, \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_97672ac88f789774dd47f7c8be\` (\`email\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`patient_requirements\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`type\` enum ('screening', 'medical_report') NOT NULL, \`title\` varchar(255) NOT NULL, \`description\` varchar(500) NULL, \`status\` enum ('pending', 'under_review', 'approved', 'declined') NOT NULL DEFAULT 'pending', \`required_by\` varchar(255) NOT NULL, \`approved_by\` varchar(255) NULL, \`approved_at\` timestamp NULL, \`submitted_at\` timestamp NULL, \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`patient_supports\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`name\` varchar(255) NOT NULL, \`phone\` char(11) NOT NULL, \`kinship\` varchar(50) NOT NULL, \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`referrals\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` timestamp NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(2000) NULL, \`professional_name\` varchar(255) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`patients\` (\`id\` varchar(36) NOT NULL, \`user_id\` varchar(255) NOT NULL, \`gender\` enum ('male_cis', 'female_cis', 'male_trans', 'female_trans', 'non_binary', 'prefer_not_to_say') NOT NULL, \`date_of_birth\` date NOT NULL, \`phone\` char(11) NOT NULL, \`cpf\` char(11) NOT NULL, \`state\` enum ('AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO') NOT NULL, \`city\` varchar(50) NOT NULL, \`has_disability\` tinyint(1) NOT NULL DEFAULT '0', \`disability_desc\` varchar(500) NULL, \`need_legal_assistance\` tinyint(1) NOT NULL DEFAULT '0', \`take_medication\` tinyint(1) NOT NULL DEFAULT '0', \`medication_desc\` varchar(500) NULL, \`has_nmo_diagnosis\` tinyint(1) NOT NULL DEFAULT '0', \`status\` enum ('active', 'inactive', 'pending') NOT NULL DEFAULT 'pending', \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_5947301223f5a908fd5e372b0f\` (\`cpf\`), UNIQUE INDEX \`REL_7fe1518dc780fd777669b5cb7a\` (\`user_id\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`appointments\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` timestamp NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(500) NULL, \`professional_name\` varchar(255) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`user_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'password_reset', 'invite_token') NOT NULL, \`expires_at\` timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`ALTER TABLE \`patient_requirements\` ADD CONSTRAINT \`FK_77b87c61cff4793ae6a4ac50070\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE \`patient_supports\` ADD CONSTRAINT \`FK_62c23ddd34837a0c09faf875425\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE \`referrals\` ADD CONSTRAINT \`FK_bb61873c1c10fe8662f540f0625\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE \`patients\` ADD CONSTRAINT \`FK_7fe1518dc780fd777669b5cb7a0\` FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE \`appointments\` ADD CONSTRAINT \`FK_3330f054416745deaa2cc130700\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`appointments\` DROP FOREIGN KEY \`FK_3330f054416745deaa2cc130700\``); - await queryRunner.query(`ALTER TABLE \`patients\` DROP FOREIGN KEY \`FK_7fe1518dc780fd777669b5cb7a0\``); - await queryRunner.query(`ALTER TABLE \`referrals\` DROP FOREIGN KEY \`FK_bb61873c1c10fe8662f540f0625\``); - await queryRunner.query(`ALTER TABLE \`patient_supports\` DROP FOREIGN KEY \`FK_62c23ddd34837a0c09faf875425\``); - await queryRunner.query(`ALTER TABLE \`patient_requirements\` DROP FOREIGN KEY \`FK_77b87c61cff4793ae6a4ac50070\``); - await queryRunner.query(`DROP TABLE \`tokens\``); - await queryRunner.query(`DROP TABLE \`appointments\``); - await queryRunner.query(`DROP INDEX \`REL_7fe1518dc780fd777669b5cb7a\` ON \`patients\``); - await queryRunner.query(`DROP INDEX \`IDX_5947301223f5a908fd5e372b0f\` ON \`patients\``); - await queryRunner.query(`DROP TABLE \`patients\``); - await queryRunner.query(`DROP TABLE \`referrals\``); - await queryRunner.query(`DROP TABLE \`patient_supports\``); - await queryRunner.query(`DROP TABLE \`patient_requirements\``); - await queryRunner.query(`DROP INDEX \`IDX_97672ac88f789774dd47f7c8be\` ON \`users\``); - await queryRunner.query(`DROP TABLE \`users\``); - } - -} diff --git a/infra/database/migrations/1766169704537-Initial.ts b/infra/database/migrations/1766169704537-Initial.ts new file mode 100644 index 0000000..949dadf --- /dev/null +++ b/infra/database/migrations/1766169704537-Initial.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Initial1766169704537 implements MigrationInterface { + name = 'Initial1766169704537' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE \`patient_requirements\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`type\` enum ('screening', 'medical_report') NOT NULL, \`title\` varchar(255) NOT NULL, \`description\` varchar(500) NULL, \`status\` enum ('pending', 'under_review', 'approved', 'declined') NOT NULL DEFAULT 'pending', \`submitted_at\` datetime NULL, \`approved_by\` varchar(255) NULL, \`approved_at\` datetime NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`patient_supports\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`name\` varchar(64) NOT NULL, \`phone\` varchar(11) NOT NULL, \`kinship\` varchar(50) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`referrals\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(2000) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`patients\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NULL, \`avatar_url\` varchar(255) NULL, \`status\` enum ('active', 'inactive', 'pending') NOT NULL DEFAULT 'pending', \`gender\` enum ('male_cis', 'female_cis', 'male_trans', 'female_trans', 'non_binary', 'prefer_not_to_say') NOT NULL DEFAULT 'prefer_not_to_say', \`date_of_birth\` datetime NULL, \`phone\` varchar(11) NULL, \`cpf\` varchar(11) NULL, \`state\` enum ('AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO') NULL, \`city\` varchar(255) NULL, \`has_disability\` tinyint(1) NOT NULL DEFAULT '0', \`disability_desc\` varchar(500) NULL, \`need_legal_assistance\` tinyint(1) NOT NULL DEFAULT '0', \`take_medication\` tinyint(1) NOT NULL DEFAULT '0', \`medication_desc\` varchar(500) NULL, \`nmo_diagnosis\` enum ('anti_aqp4_positive', 'anti_mog_positive', 'both_negative', 'no_diagnosis') NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_5947301223f5a908fd5e372b0f\` (\`cpf\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`appointments\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(500) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`user_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'password_reset', 'invite_token') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`users\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NOT NULL, \`avatar_url\` varchar(255) NULL, \`role\` enum ('admin', 'manager', 'nurse', 'specialist') NOT NULL, \`status\` enum ('active', 'inactive') NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`ALTER TABLE \`patient_requirements\` ADD CONSTRAINT \`FK_77b87c61cff4793ae6a4ac50070\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE \`patient_supports\` ADD CONSTRAINT \`FK_62c23ddd34837a0c09faf875425\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE \`referrals\` ADD CONSTRAINT \`FK_bb61873c1c10fe8662f540f0625\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE \`appointments\` ADD CONSTRAINT \`FK_3330f054416745deaa2cc130700\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`appointments\` DROP FOREIGN KEY \`FK_3330f054416745deaa2cc130700\``); + await queryRunner.query(`ALTER TABLE \`referrals\` DROP FOREIGN KEY \`FK_bb61873c1c10fe8662f540f0625\``); + await queryRunner.query(`ALTER TABLE \`patient_supports\` DROP FOREIGN KEY \`FK_62c23ddd34837a0c09faf875425\``); + await queryRunner.query(`ALTER TABLE \`patient_requirements\` DROP FOREIGN KEY \`FK_77b87c61cff4793ae6a4ac50070\``); + await queryRunner.query(`DROP TABLE \`users\``); + await queryRunner.query(`DROP TABLE \`tokens\``); + await queryRunner.query(`DROP TABLE \`appointments\``); + await queryRunner.query(`DROP INDEX \`IDX_5947301223f5a908fd5e372b0f\` ON \`patients\``); + await queryRunner.query(`DROP TABLE \`patients\``); + await queryRunner.query(`DROP TABLE \`referrals\``); + await queryRunner.query(`DROP TABLE \`patient_supports\``); + await queryRunner.query(`DROP TABLE \`patient_requirements\``); + } + +} diff --git a/infra/database/seed-dev.ts b/infra/database/seed-dev.ts index 14e6b08..f80a1cf 100644 --- a/infra/database/seed-dev.ts +++ b/infra/database/seed-dev.ts @@ -18,7 +18,9 @@ import { import { PATIENT_CONDITIONS, PATIENT_GENDERS, + PATIENT_NMO_DIAGNOSTICS, PATIENT_STATUSES, + type PatientStatus, } from '@/domain/enums/patients'; import { REFERRAL_STATUSES } from '@/domain/enums/referrals'; import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; @@ -137,8 +139,28 @@ async function main() { const twoMonthsAhead = new Date(); twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() + 2); - const totalOfPatients = 100; + await patientRepository.save({ + name: faker.person.fullName(), + email: 'patient@ipecode.com.br', + password, + avatar_url: faker.image.avatar(), + status: 'active', + gender: faker.helpers.arrayElement(PATIENT_GENDERS), + date_of_birth: faker.date.birthdate({ min: 18, max: 80, mode: 'age' }), + phone: faker.string.numeric(11), + cpf: faker.string.numeric(11), + state: 'BA', + city: getRandomCity('BA'), + has_disability: faker.datatype.boolean(), + disability_desc: faker.lorem.sentence(), + need_legal_assistance: faker.datatype.boolean(), + take_medication: faker.datatype.boolean(), + medication_desc: faker.lorem.sentence(), + nmo_diagnosis: faker.helpers.arrayElement(PATIENT_NMO_DIAGNOSTICS), + created_at: faker.date.between({ from: fourMonthsAgo, to: new Date() }), + }); + const totalOfPatients = 100; for (let i = 0; i < totalOfPatients; i++) { if ((i + 1) % 20 === 0) { console.log(`👥 Creating ${i + 1} patients...`); @@ -147,7 +169,7 @@ async function main() { const selectedState = faker.helpers.arrayElement(statesWithCities); // Set patient status: 10 pending, rest distributed among other statuses - let patientStatus: (typeof PATIENT_STATUSES)[number]; + let patientStatus: PatientStatus; if (i < 10) { patientStatus = 'pending'; } else { @@ -157,6 +179,11 @@ async function main() { } const patient = patientRepository.create({ + name: faker.person.fullName(), + email: faker.internet.email().toLowerCase(), + password, + avatar_url: faker.image.avatar(), + status: patientStatus, gender: faker.helpers.arrayElement(PATIENT_GENDERS), date_of_birth: faker.date.birthdate({ min: 18, max: 80, mode: 'age' }), phone: faker.string.numeric(11), @@ -168,8 +195,7 @@ async function main() { need_legal_assistance: faker.datatype.boolean(), take_medication: faker.datatype.boolean(), medication_desc: faker.lorem.sentence(), - has_nmo_diagnosis: faker.datatype.boolean(), - status: patientStatus, + nmo_diagnosis: faker.helpers.arrayElement(PATIENT_NMO_DIAGNOSTICS), created_at: faker.date.between({ from: fourMonthsAgo, to: new Date() }), }); await patientRepository.save(patient); @@ -239,19 +265,19 @@ async function main() { title: faker.lorem.words(3), description: faker.lorem.sentence(), status, - required_by: faker.string.uuid(), submitted_at: status === 'under_review' ? faker.date.between({ from: oneMonthAgo, to: new Date() }) - : new Date(), + : null, approved_at: status === 'approved' - ? faker.date.between({ from: oneMonthAgo, to: new Date() }) - : null, - created_at: - status === 'pending' ? faker.date.between({ from: twoMonthsAgo, to: new Date() }) - : new Date(), + : null, + created_by: faker.string.uuid(), + created_at: faker.date.between({ + from: fourMonthsAgo, + to: new Date(), + }), }); } } diff --git a/src/app/cryptography/crypography.service.ts b/src/app/cryptography/crypography.service.ts index c02ee0c..121c315 100644 --- a/src/app/cryptography/crypography.service.ts +++ b/src/app/cryptography/crypography.service.ts @@ -3,7 +3,7 @@ import { JwtService, type JwtSignOptions } from '@nestjs/jwt'; import { compare, hash } from 'bcryptjs'; import type { Cryptography } from '@/domain/modules/cryptography'; -import type { AuthTokenPayloads } from '@/domain/schemas/token'; +import type { AuthTokenPayloads } from '@/domain/schemas/tokens'; @Injectable() export class CryptographyService implements Cryptography { diff --git a/src/app/http/appointments/use-cases/get-appointments.use-case.ts b/src/app/http/appointments/use-cases/get-appointments.use-case.ts index 7a70afd..6cec5ff 100644 --- a/src/app/http/appointments/use-cases/get-appointments.use-case.ts +++ b/src/app/http/appointments/use-cases/get-appointments.use-case.ts @@ -57,15 +57,15 @@ export class GetAppointmentsUseCase { } if (startDate && !endDate) { - where.date = MoreThanOrEqual(startDate); + where.created_at = MoreThanOrEqual(startDate); } if (endDate && !startDate) { - where.date = LessThanOrEqual(endDate); + where.created_at = LessThanOrEqual(endDate); } if (startDate && endDate) { - where.date = Between(startDate, endDate); + where.created_at = Between(startDate, endDate); } if (status) { diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index 9a5c15c..3a08e0b 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -15,7 +15,7 @@ import { Public } from '@/common/decorators/public.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import { COOKIES_MAPPING } from '@/domain/cookies'; import type { BaseResponse } from '@/domain/schemas/base'; -import { UserSchema } from '@/domain/schemas/user'; +import { UserSchema } from '@/domain/schemas/users'; import { UtilsService } from '@/utils/utils.service'; import { CreateUserDto } from '../users/users.dtos'; diff --git a/src/app/http/auth/auth.dtos.ts b/src/app/http/auth/auth.dtos.ts index 2debe21..ea8d235 100644 --- a/src/app/http/auth/auth.dtos.ts +++ b/src/app/http/auth/auth.dtos.ts @@ -7,7 +7,7 @@ import { resetPasswordSchema, signInWithEmailSchema, } from '@/domain/schemas/auth'; -import { createAuthTokenSchema } from '@/domain/schemas/token'; +import { createAuthTokenSchema } from '@/domain/schemas/tokens'; export class AuthUserDto extends createZodDto(authUserSchema) {} diff --git a/src/app/http/auth/auth.module.ts b/src/app/http/auth/auth.module.ts index 605aa9e..d42f58e 100644 --- a/src/app/http/auth/auth.module.ts +++ b/src/app/http/auth/auth.module.ts @@ -6,7 +6,9 @@ import { CryptographyModule } from '@/app/cryptography/cryptography.module'; import { MailModule } from '@/app/mail/mail.module'; import { AuthGuard } from '@/common/guards/auth.guard'; import { RolesGuard } from '@/common/guards/roles.guard'; +import { Patient } from '@/domain/entities/patient'; import { Token } from '@/domain/entities/token'; +import { User } from '@/domain/entities/user'; import { EnvModule } from '@/env/env.module'; import { UtilsModule } from '@/utils/utils.module'; @@ -17,7 +19,7 @@ import { TokensRepository } from './tokens.repository'; @Module({ imports: [ - TypeOrmModule.forFeature([Token]), + TypeOrmModule.forFeature([Patient, Token, User]), CryptographyModule, UsersModule, UtilsModule, diff --git a/src/app/http/auth/auth.service.ts b/src/app/http/auth/auth.service.ts index fbbccd5..eefb49d 100644 --- a/src/app/http/auth/auth.service.ts +++ b/src/app/http/auth/auth.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { CryptographyService } from '@/app/cryptography/crypography.service'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; -import { UserSchema } from '@/domain/schemas/user'; +import { UserSchema } from '@/domain/schemas/users'; import { EnvService } from '@/env/env.service'; import type { CreateUserDto } from '../users/users.dtos'; diff --git a/src/app/http/patients/patients.controller.ts b/src/app/http/patients/patients.controller.ts index 809ea47..9c91613 100644 --- a/src/app/http/patients/patients.controller.ts +++ b/src/app/http/patients/patients.controller.ts @@ -14,11 +14,11 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import { BaseResponse } from '@/domain/schemas/base'; +import type { GetPatientSupportsResponse } from '@/domain/schemas/patient-support/responses'; import type { GetPatientResponse, GetPatientsResponse, -} from '@/domain/schemas/patient/responses'; -import type { GetPatientSupportsResponse } from '@/domain/schemas/patient-support/responses'; +} from '@/domain/schemas/patients/responses'; import type { AuthUserDto } from '../auth/auth.dtos'; import { PatientSupportsRepository } from '../patient-supports/patient-supports.repository'; diff --git a/src/app/http/patients/patients.dtos.ts b/src/app/http/patients/patients.dtos.ts index 061d4a9..863edb6 100644 --- a/src/app/http/patients/patients.dtos.ts +++ b/src/app/http/patients/patients.dtos.ts @@ -5,7 +5,7 @@ import { getPatientsQuerySchema, patientScreeningSchema, updatePatientSchema, -} from '@/domain/schemas/patient/requests'; +} from '@/domain/schemas/patients/requests'; export class GetPatientsQuery extends createZodDto(getPatientsQuerySchema) {} diff --git a/src/app/http/patients/patients.repository.ts b/src/app/http/patients/patients.repository.ts index 50b0b9a..4e2c893 100644 --- a/src/app/http/patients/patients.repository.ts +++ b/src/app/http/patients/patients.repository.ts @@ -4,7 +4,7 @@ import { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; import type { PatientOrderBy } from '@/domain/enums/patients'; -import type { PatientResponse } from '@/domain/schemas/patient/responses'; +import type { PatientResponse } from '@/domain/schemas/patients/responses'; import { CreatePatientDto, GetPatientsQuery } from './patients.dtos'; diff --git a/src/app/http/referrals/referrals.controller.ts b/src/app/http/referrals/referrals.controller.ts index 0a50c33..e034164 100644 --- a/src/app/http/referrals/referrals.controller.ts +++ b/src/app/http/referrals/referrals.controller.ts @@ -12,8 +12,8 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import { BaseResponse } from '@/domain/schemas/base'; -import type { GetReferralsResponse } from '@/domain/schemas/referral/responses'; -import { UserSchema } from '@/domain/schemas/user'; +import type { GetReferralsResponse } from '@/domain/schemas/referrals/responses'; +import { UserSchema } from '@/domain/schemas/users'; import { CreateReferralDto, GetReferralsQuery } from './referrals.dtos'; import { CancelReferralUseCase } from './use-cases/cancel-referral.use-case'; diff --git a/src/app/http/referrals/referrals.dtos.ts b/src/app/http/referrals/referrals.dtos.ts index dd1d7f7..9d06ab3 100644 --- a/src/app/http/referrals/referrals.dtos.ts +++ b/src/app/http/referrals/referrals.dtos.ts @@ -3,7 +3,7 @@ import { createZodDto } from 'nestjs-zod'; import { createReferralSchema, getReferralsQuerySchema, -} from '@/domain/schemas/referral/requests'; +} from '@/domain/schemas/referrals/requests'; export class CreateReferralDto extends createZodDto(createReferralSchema) {} diff --git a/src/app/http/referrals/use-cases/get-referrals.use-case.ts b/src/app/http/referrals/use-cases/get-referrals.use-case.ts index 554715b..f954154 100644 --- a/src/app/http/referrals/use-cases/get-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/get-referrals.use-case.ts @@ -11,7 +11,7 @@ import { import { Referral } from '@/domain/entities/referral'; import type { ReferralOrderBy } from '@/domain/enums/referrals'; -import type { GetReferralsResponse } from '@/domain/schemas/referral/responses'; +import type { GetReferralsResponse } from '@/domain/schemas/referrals/responses'; import { GetReferralsQuery } from '../referrals.dtos'; @@ -59,15 +59,15 @@ export class GetReferralsUseCase { } if (startDate && !endDate) { - where.date = MoreThanOrEqual(startDate); + where.created_at = MoreThanOrEqual(startDate); } if (endDate && !startDate) { - where.date = LessThanOrEqual(endDate); + where.created_at = LessThanOrEqual(endDate); } if (startDate && endDate) { - where.date = Between(startDate, endDate); + where.created_at = Between(startDate, endDate); } if (search) { diff --git a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts index 6db9d54..ba9194d 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts @@ -40,7 +40,7 @@ export class GetTotalReferralsByCategoryUseCase { queryBuilder: SelectQueryBuilder, ) { if (startDate && endDate) { - queryBuilder.andWhere('referral.date BETWEEN :start AND :end', { + queryBuilder.andWhere('referral.created_at BETWEEN :start AND :end', { start: startDate, end: endDate, }); diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts index f1cc30b..82fdb83 100644 --- a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts @@ -36,15 +36,14 @@ export class GetTotalReferredPatientsByStateUseCase { return this.patientsRepository .createQueryBuilder('patient') .innerJoin('patient.referrals', 'referral') - .where('referral.referred_to IS NOT NULL') - .andWhere('referral.referred_to != :empty', { empty: '' }); + .where('referral.id IS NOT NULL'); }; function getQueryBuilderWithFilters( queryBuilder: SelectQueryBuilder, ) { if (startDate && endDate) { - queryBuilder.andWhere('referral.date BETWEEN :start AND :end', { + queryBuilder.andWhere('referral.created_at BETWEEN :start AND :end', { start: startDate, end: endDate, }); diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index 8cbc551..d320c66 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -3,7 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import type { GetUserResponse } from '@/domain/schemas/user/responses'; +import type { GetUserResponse } from '@/domain/schemas/users/responses'; import type { AuthUserDto } from '../auth/auth.dtos'; import { UsersService } from './users.service'; diff --git a/src/app/http/users/users.dtos.ts b/src/app/http/users/users.dtos.ts index cab3a69..3f9d9ff 100644 --- a/src/app/http/users/users.dtos.ts +++ b/src/app/http/users/users.dtos.ts @@ -3,7 +3,7 @@ import { createZodDto } from 'nestjs-zod'; import { createUserSchema, updateUserSchema, -} from '@/domain/schemas/user/requests'; +} from '@/domain/schemas/users/requests'; export class CreateUserDto extends createZodDto(createUserSchema) {} diff --git a/src/common/guards/auth.guard.ts b/src/common/guards/auth.guard.ts index d583976..d7749a3 100644 --- a/src/common/guards/auth.guard.ts +++ b/src/common/guards/auth.guard.ts @@ -13,7 +13,7 @@ import type { AuthUserDto } from '@/app/http/auth/auth.dtos'; import type { Cookie } from '@/domain/cookies'; import { Patient } from '@/domain/entities/patient'; import { User } from '@/domain/entities/user'; -import type { AccessTokenPayload } from '@/domain/schemas/token'; +import type { AccessTokenPayload } from '@/domain/schemas/tokens'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; diff --git a/src/domain/entities/appointment.ts b/src/domain/entities/appointment.ts index 8184f21..2b71340 100644 --- a/src/domain/entities/appointment.ts +++ b/src/domain/entities/appointment.ts @@ -25,7 +25,7 @@ export class Appointment implements AppointmentSchema { @Column('uuid') patient_id: string; - @Column({ type: 'timestamp' }) + @Column({ type: 'datetime' }) date: Date; @Column({ type: 'enum', enum: APPOINTMENT_STATUSES, default: 'scheduled' }) @@ -40,16 +40,16 @@ export class Appointment implements AppointmentSchema { @Column({ type: 'varchar', length: 500, nullable: true }) annotation: string | null; - @Column({ type: 'varchar', length: 255, nullable: true }) + @Column({ type: 'varchar', length: 64, nullable: true }) professional_name: string | null; @Column('uuid') created_by: string; - @CreateDateColumn({ type: 'timestamp' }) + @CreateDateColumn({ type: 'datetime' }) created_at: Date; - @UpdateDateColumn({ type: 'timestamp' }) + @UpdateDateColumn({ type: 'datetime' }) updated_at: Date; @ManyToOne(() => Patient, (patient) => patient.appointments) diff --git a/src/domain/entities/patient-requirement.ts b/src/domain/entities/patient-requirement.ts index 6356862..02dbc0c 100644 --- a/src/domain/entities/patient-requirement.ts +++ b/src/domain/entities/patient-requirement.ts @@ -41,22 +41,22 @@ export class PatientRequirement implements PatientRequirementSchema { }) status: PatientRequirementStatus; - @Column({ type: 'timestamp', nullable: true }) + @Column({ type: 'datetime', nullable: true }) submitted_at: Date | null; @Column({ type: 'uuid', nullable: true }) approved_by: string | null; - @Column({ type: 'timestamp', nullable: true }) + @Column({ type: 'datetime', nullable: true }) approved_at: Date | null; @Column({ type: 'uuid' }) created_by: string; - @CreateDateColumn({ type: 'timestamp' }) + @CreateDateColumn({ type: 'datetime' }) created_at: Date; - @UpdateDateColumn({ type: 'timestamp' }) + @UpdateDateColumn({ type: 'datetime' }) updated_at: Date; @ManyToOne(() => Patient, (patient) => patient.requirements) diff --git a/src/domain/entities/patient-support.ts b/src/domain/entities/patient-support.ts index e6fe41b..5ac0002 100644 --- a/src/domain/entities/patient-support.ts +++ b/src/domain/entities/patient-support.ts @@ -28,10 +28,10 @@ export class PatientSupport implements PatientSupportSchema { @Column({ type: 'varchar', length: 50 }) kinship: string; - @CreateDateColumn({ type: 'timestamp' }) + @CreateDateColumn({ type: 'datetime' }) created_at: Date; - @UpdateDateColumn({ type: 'timestamp' }) + @UpdateDateColumn({ type: 'datetime' }) updated_at: Date; @ManyToOne(() => Patient, (patient) => patient.supports) diff --git a/src/domain/entities/patient.ts b/src/domain/entities/patient.ts index 171b0fd..f43c577 100644 --- a/src/domain/entities/patient.ts +++ b/src/domain/entities/patient.ts @@ -14,11 +14,13 @@ import { import { PATIENT_GENDERS, + PATIENT_NMO_DIAGNOSTICS, PATIENT_STATUSES, type PatientGender, + type PatientNmoDiagnosis, type PatientStatus, } from '../enums/patients'; -import type { PatientSchema } from '../schemas/patient'; +import type { PatientSchema } from '../schemas/patients'; import { Appointment } from './appointment'; import { PatientRequirement } from './patient-requirement'; import { PatientSupport } from './patient-support'; @@ -47,7 +49,7 @@ export class Patient implements PatientSchema { @Column({ type: 'enum', enum: PATIENT_GENDERS, default: 'prefer_not_to_say' }) gender: PatientGender; - @Column({ type: 'timestamp', nullable: true }) + @Column({ type: 'datetime', nullable: true }) date_of_birth: Date | null; @Column({ type: 'varchar', length: 11, nullable: true }) @@ -77,13 +79,13 @@ export class Patient implements PatientSchema { @Column({ type: 'varchar', length: 500, nullable: true }) medication_desc: string | null; - @Column({ type: 'tinyint', width: 1, default: 0 }) - has_nmo_diagnosis: boolean; + @Column({ type: 'enum', enum: PATIENT_NMO_DIAGNOSTICS, nullable: true }) + nmo_diagnosis: PatientNmoDiagnosis | null; - @CreateDateColumn({ type: 'timestamp' }) + @CreateDateColumn({ type: 'datetime' }) created_at: Date; - @UpdateDateColumn({ type: 'timestamp' }) + @UpdateDateColumn({ type: 'datetime' }) updated_at: Date; @OneToMany(() => PatientSupport, (support) => support.patient) diff --git a/src/domain/entities/referral.ts b/src/domain/entities/referral.ts index 634c663..4cd9d9c 100644 --- a/src/domain/entities/referral.ts +++ b/src/domain/entities/referral.ts @@ -11,7 +11,7 @@ import { import { PATIENT_CONDITIONS, type PatientCondition } from '../enums/patients'; import { REFERRAL_STATUSES, type ReferralStatus } from '../enums/referrals'; import { SPECIALTY_CATEGORIES, type SpecialtyCategory } from '../enums/shared'; -import { ReferralSchema } from '../schemas/referral'; +import { ReferralSchema } from '../schemas/referrals'; import { Patient } from './patient'; @Entity('referrals') @@ -22,7 +22,7 @@ export class Referral implements ReferralSchema { @Column('uuid') patient_id: string; - @Column({ type: 'timestamp' }) + @Column({ type: 'datetime' }) date: Date; @Column({ type: 'enum', enum: REFERRAL_STATUSES, default: 'scheduled' }) @@ -43,10 +43,10 @@ export class Referral implements ReferralSchema { @Column('uuid') created_by: string; - @CreateDateColumn({ type: 'timestamp' }) + @CreateDateColumn({ type: 'datetime' }) created_at: Date; - @UpdateDateColumn({ type: 'timestamp' }) + @UpdateDateColumn({ type: 'datetime' }) updated_at: Date; @ManyToOne(() => Patient, (patient) => patient.appointments) diff --git a/src/domain/entities/specialist.ts b/src/domain/entities/specialist.ts index fc68f88..68ed82d 100644 --- a/src/domain/entities/specialist.ts +++ b/src/domain/entities/specialist.ts @@ -32,10 +32,10 @@ // @Column({ type: 'enum', enum: SPECIALIST_STATUS, default: 'active' }) // status: SpecialistStatusType; -// @CreateDateColumn({ type: 'timestamp' }) +// @CreateDateColumn({ type: 'datetime' }) // created_at: Date; -// @UpdateDateColumn({ type: 'timestamp' }) +// @UpdateDateColumn({ type: 'datetime' }) // updated_at: Date; // @OneToOne(() => User) diff --git a/src/domain/entities/token.ts b/src/domain/entities/token.ts index 7f1ef56..38a12ea 100644 --- a/src/domain/entities/token.ts +++ b/src/domain/entities/token.ts @@ -6,7 +6,7 @@ import { } from 'typeorm'; import { AUTH_TOKENS, type AuthTokenKey } from '../enums/tokens'; -import type { AuthToken } from '../schemas/token'; +import type { AuthToken } from '../schemas/tokens'; @Entity('tokens') export class Token implements AuthToken { @@ -25,9 +25,9 @@ export class Token implements AuthToken { @Column({ type: 'enum', enum: AUTH_TOKENS }) type: AuthTokenKey; - @CreateDateColumn({ type: 'timestamp', nullable: true }) + @CreateDateColumn({ type: 'datetime', nullable: true }) expires_at: Date | null; - @CreateDateColumn({ type: 'timestamp' }) + @CreateDateColumn({ type: 'datetime' }) created_at: Date; } diff --git a/src/domain/entities/user.ts b/src/domain/entities/user.ts index e7b76ab..2299af1 100644 --- a/src/domain/entities/user.ts +++ b/src/domain/entities/user.ts @@ -12,7 +12,7 @@ import { type UserRole, type UserStatus, } from '../enums/users'; -import type { UserSchema } from '../schemas/user'; +import type { UserSchema } from '../schemas/users'; @Entity('users') export class User implements UserSchema { @@ -37,9 +37,9 @@ export class User implements UserSchema { @Column({ type: 'enum', enum: USER_STATUSES, default: 'active' }) status: UserStatus; - @CreateDateColumn({ type: 'timestamp' }) + @CreateDateColumn({ type: 'datetime' }) created_at: Date; - @UpdateDateColumn({ type: 'timestamp' }) + @UpdateDateColumn({ type: 'datetime' }) updated_at: Date; } diff --git a/src/domain/enums/patients.ts b/src/domain/enums/patients.ts index df4134c..2be2a80 100644 --- a/src/domain/enums/patients.ts +++ b/src/domain/enums/patients.ts @@ -14,5 +14,13 @@ export type PatientStatus = (typeof PATIENT_STATUSES)[number]; export const PATIENT_CONDITIONS = ['in_crisis', 'stable'] as const; export type PatientCondition = (typeof PATIENT_CONDITIONS)[number]; +export const PATIENT_NMO_DIAGNOSTICS = [ + 'anti_aqp4_positive', + 'anti_mog_positive', + 'both_negative', + 'no_diagnosis', +] as const; +export type PatientNmoDiagnosis = (typeof PATIENT_NMO_DIAGNOSTICS)[number]; + export const PATIENT_ORDER_BY = ['name', 'email', 'status', 'date'] as const; export type PatientOrderBy = (typeof PATIENT_ORDER_BY)[number]; diff --git a/src/domain/enums/users.ts b/src/domain/enums/users.ts index 0c48cb8..f6f04b8 100644 --- a/src/domain/enums/users.ts +++ b/src/domain/enums/users.ts @@ -1,4 +1,4 @@ -export const USER_ROLES = ['admin', 'nurse', 'specialist', 'manager'] as const; +export const USER_ROLES = ['admin', 'manager', 'nurse', 'specialist'] as const; export type UserRole = (typeof USER_ROLES)[number]; export const USER_STATUSES = ['active', 'inactive'] as const; diff --git a/src/domain/modules/cryptography.ts b/src/domain/modules/cryptography.ts index fb5edfb..6d7be30 100644 --- a/src/domain/modules/cryptography.ts +++ b/src/domain/modules/cryptography.ts @@ -1,6 +1,6 @@ import type { JwtSignOptions } from '@nestjs/jwt'; -import type { AuthTokenPayloads } from '../schemas/token'; +import type { AuthTokenPayloads } from '../schemas/tokens'; export abstract class Cryptography { abstract createHash(plain: string): Promise; diff --git a/src/domain/schemas/appointments/index.ts b/src/domain/schemas/appointments/index.ts index d7fff1e..61c4675 100644 --- a/src/domain/schemas/appointments/index.ts +++ b/src/domain/schemas/appointments/index.ts @@ -4,6 +4,8 @@ import { APPOINTMENT_STATUSES } from '@/domain/enums/appointments'; import { PATIENT_CONDITIONS } from '@/domain/enums/patients'; import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; +import { nameSchema } from '../shared'; + export const appointmentSchema = z .object({ id: z.string().uuid(), @@ -13,7 +15,7 @@ export const appointmentSchema = z category: z.enum(SPECIALTY_CATEGORIES), condition: z.enum(PATIENT_CONDITIONS), annotation: z.string().max(500).nullable(), - professional_name: z.string().max(255).nullable(), + professional_name: nameSchema.nullable(), created_by: z.string().uuid(), created_at: z.coerce.date(), updated_at: z.coerce.date(), diff --git a/src/domain/schemas/appointments/responses.ts b/src/domain/schemas/appointments/responses.ts index df95d43..80f5927 100644 --- a/src/domain/schemas/appointments/responses.ts +++ b/src/domain/schemas/appointments/responses.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { baseResponseSchema } from '../base'; -import { patientSchema } from '../patient'; +import { patientSchema } from '../patients'; import { appointmentSchema } from '.'; export const appointmentResponseSchema = appointmentSchema.extend({ diff --git a/src/domain/schemas/patient-requirement/responses.ts b/src/domain/schemas/patient-requirement/responses.ts index ba220ec..dad7d38 100644 --- a/src/domain/schemas/patient-requirement/responses.ts +++ b/src/domain/schemas/patient-requirement/responses.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { baseResponseSchema } from '../base'; -import { patientSchema } from '../patient'; +import { patientSchema } from '../patients'; import { patientRequirementSchema } from '.'; export const patientRequirementItemSchema = patientRequirementSchema diff --git a/src/domain/schemas/patient/index.ts b/src/domain/schemas/patients/index.ts similarity index 86% rename from src/domain/schemas/patient/index.ts rename to src/domain/schemas/patients/index.ts index af10772..78ed604 100644 --- a/src/domain/schemas/patient/index.ts +++ b/src/domain/schemas/patients/index.ts @@ -1,7 +1,11 @@ import { z } from 'zod'; import { BRAZILIAN_STATES } from '@/constants/brazilian-states'; -import { PATIENT_GENDERS, PATIENT_STATUSES } from '@/domain/enums/patients'; +import { + PATIENT_GENDERS, + PATIENT_NMO_DIAGNOSTICS, + PATIENT_STATUSES, +} from '@/domain/enums/patients'; import { avatarSchema, @@ -31,7 +35,7 @@ export const patientSchema = z need_legal_assistance: z.boolean().default(false), take_medication: z.boolean().default(false), medication_desc: z.string().max(500).nullable(), - has_nmo_diagnosis: z.boolean().default(false), + nmo_diagnosis: z.enum(PATIENT_NMO_DIAGNOSTICS).nullable(), created_at: z.coerce.date(), updated_at: z.coerce.date(), }) diff --git a/src/domain/schemas/patient/requests.ts b/src/domain/schemas/patients/requests.ts similarity index 98% rename from src/domain/schemas/patient/requests.ts rename to src/domain/schemas/patients/requests.ts index 6582cc4..72230df 100644 --- a/src/domain/schemas/patient/requests.ts +++ b/src/domain/schemas/patients/requests.ts @@ -30,7 +30,7 @@ export const createPatientSchema = z need_legal_assistance: true, take_medication: true, medication_desc: true, - has_nmo_diagnosis: true, + nmo_diagnosis: true, }), ); export type CreatePatient = z.infer; diff --git a/src/domain/schemas/patient/responses.ts b/src/domain/schemas/patients/responses.ts similarity index 100% rename from src/domain/schemas/patient/responses.ts rename to src/domain/schemas/patients/responses.ts diff --git a/src/domain/schemas/referral/index.ts b/src/domain/schemas/referrals/index.ts similarity index 89% rename from src/domain/schemas/referral/index.ts rename to src/domain/schemas/referrals/index.ts index 4995c8b..4842987 100644 --- a/src/domain/schemas/referral/index.ts +++ b/src/domain/schemas/referrals/index.ts @@ -4,6 +4,8 @@ import { PATIENT_CONDITIONS } from '@/domain/enums/patients'; import { REFERRAL_STATUSES } from '@/domain/enums/referrals'; import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; +import { nameSchema } from '../shared'; + export const referralSchema = z .object({ id: z.string().uuid(), @@ -13,7 +15,7 @@ export const referralSchema = z category: z.enum(SPECIALTY_CATEGORIES), condition: z.enum(PATIENT_CONDITIONS), annotation: z.string().max(2000).nullable(), - professional_name: z.string().max(64).nullable(), + professional_name: nameSchema.nullable(), created_by: z.string().uuid(), created_at: z.coerce.date(), updated_at: z.coerce.date(), diff --git a/src/domain/schemas/referral/requests.ts b/src/domain/schemas/referrals/requests.ts similarity index 100% rename from src/domain/schemas/referral/requests.ts rename to src/domain/schemas/referrals/requests.ts diff --git a/src/domain/schemas/referral/responses.ts b/src/domain/schemas/referrals/responses.ts similarity index 92% rename from src/domain/schemas/referral/responses.ts rename to src/domain/schemas/referrals/responses.ts index 093e5b5..656342d 100644 --- a/src/domain/schemas/referral/responses.ts +++ b/src/domain/schemas/referrals/responses.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { baseResponseSchema } from '../base'; -import { patientSchema } from '../patient'; +import { patientSchema } from '../patients'; import { referralSchema } from '.'; export const referralResponseSchema = referralSchema.extend({ diff --git a/src/domain/schemas/token.ts b/src/domain/schemas/tokens.ts similarity index 100% rename from src/domain/schemas/token.ts rename to src/domain/schemas/tokens.ts diff --git a/src/domain/schemas/user/index.ts b/src/domain/schemas/users/index.ts similarity index 100% rename from src/domain/schemas/user/index.ts rename to src/domain/schemas/users/index.ts diff --git a/src/domain/schemas/user/requests.ts b/src/domain/schemas/users/requests.ts similarity index 100% rename from src/domain/schemas/user/requests.ts rename to src/domain/schemas/users/requests.ts diff --git a/src/domain/schemas/user/responses.ts b/src/domain/schemas/users/responses.ts similarity index 100% rename from src/domain/schemas/user/responses.ts rename to src/domain/schemas/users/responses.ts From b1764f3faf9fd997e1e0bcd19381d7e67d99a4cc Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Fri, 19 Dec 2025 19:27:15 -0300 Subject: [PATCH 03/21] refactor(patients): update module to match new use-case pattern --- .../patient-requirements.service.ts | 23 +- .../patient-supports.service.ts | 29 ++- src/app/http/patients/patients.controller.ts | 118 +++------ src/app/http/patients/patients.dtos.ts | 3 - src/app/http/patients/patients.module.ts | 29 +-- src/app/http/patients/patients.repository.ts | 122 --------- src/app/http/patients/patients.service.ts | 238 ------------------ .../use-cases/create-patient.use-case.ts | 65 +++++ .../use-cases/deactivate-patient.use-case.ts | 55 ++++ .../use-cases/get-patient.use-case.ts | 52 ++++ .../use-cases/get-patients.use-case.ts | 96 +++++++ .../use-cases/update-patient.use-case.ts | 81 ++++++ src/domain/schemas/patients/requests.ts | 31 ++- 13 files changed, 449 insertions(+), 493 deletions(-) delete mode 100644 src/app/http/patients/patients.repository.ts delete mode 100644 src/app/http/patients/patients.service.ts create mode 100644 src/app/http/patients/use-cases/create-patient.use-case.ts create mode 100644 src/app/http/patients/use-cases/deactivate-patient.use-case.ts create mode 100644 src/app/http/patients/use-cases/get-patient.use-case.ts create mode 100644 src/app/http/patients/use-cases/get-patients.use-case.ts create mode 100644 src/app/http/patients/use-cases/update-patient.use-case.ts diff --git a/src/app/http/patient-requirements/patient-requirements.service.ts b/src/app/http/patient-requirements/patient-requirements.service.ts index 9ee4f18..997f2d6 100644 --- a/src/app/http/patient-requirements/patient-requirements.service.ts +++ b/src/app/http/patient-requirements/patient-requirements.service.ts @@ -4,9 +4,12 @@ import { Logger, NotFoundException, } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; import type { AuthUserDto } from '../auth/auth.dtos'; -import { PatientsRepository } from '../patients/patients.repository'; import { CreatePatientRequirementDto } from './patient-requirements.dtos'; import { PatientRequirementsRepository } from './patient-requirements.repository'; @@ -15,8 +18,9 @@ export class PatientRequirementsService { private readonly logger = new Logger(PatientRequirementsService.name); constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, private readonly patientRequirementsRepository: PatientRequirementsRepository, - private readonly patientsRepository: PatientsRepository, ) {} async create( @@ -25,10 +29,13 @@ export class PatientRequirementsService { ): Promise { const { patient_id } = createPatientRequirementDto; - const patient = await this.patientsRepository.findById(patient_id); + const patient = await this.patientsRepository.findOne({ + where: { id: patient_id }, + select: { id: true }, + }); if (!patient) { - throw new NotFoundException('Paciente não encontrado.'); + throw new NotFoundException('Patient not found.'); } await this.patientRequirementsRepository.create({ @@ -47,12 +54,12 @@ export class PatientRequirementsService { await this.patientRequirementsRepository.findById(id); if (!patientRequirement) { - throw new NotFoundException('Solicitação não encontrada'); + throw new NotFoundException('Request not found.'); } if (patientRequirement.status !== 'under_review') { throw new ConflictException( - 'Solicitação precisa estar aguardando aprovação para ser aprovada.', + 'Request must be awaiting approval to be approved.', ); } @@ -72,12 +79,12 @@ export class PatientRequirementsService { const requirement = await this.patientRequirementsRepository.findById(id); if (!requirement) { - throw new NotFoundException('Solicitação não encontrada.'); + throw new NotFoundException('Request not found.'); } if (requirement.status !== 'under_review') throw new ConflictException( - 'Solicitação precisa estar aguardando aprovação para ser recusada.', + 'Request must be awaiting approval to be declined.', ); await this.patientRequirementsRepository.decline(id, authUser.id); diff --git a/src/app/http/patient-supports/patient-supports.service.ts b/src/app/http/patient-supports/patient-supports.service.ts index b3c95c7..7ce49e2 100644 --- a/src/app/http/patient-supports/patient-supports.service.ts +++ b/src/app/http/patient-supports/patient-supports.service.ts @@ -4,11 +4,13 @@ import { Logger, NotFoundException, } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; +import { Patient } from '@/domain/entities/patient'; import { PatientSupport } from '@/domain/entities/patient-support'; import type { AuthUserDto } from '../auth/auth.dtos'; -import { PatientsRepository } from '../patients/patients.repository'; import { CreatePatientSupportDto, UpdatePatientSupportDto, @@ -20,7 +22,8 @@ export class PatientSupportsService { private readonly logger = new Logger(PatientSupportsService.name); constructor( - private readonly patientsRepository: PatientsRepository, + @InjectRepository(Patient) + private readonly patientsRepository: Repository, private readonly patientSupportsRepository: PatientSupportsRepository, ) {} @@ -28,10 +31,13 @@ export class PatientSupportsService { createPatientSupportDto: CreatePatientSupportDto, patientId: string, ): Promise { - const patientExists = await this.patientsRepository.findById(patientId); + const patientExists = await this.patientsRepository.findOne({ + where: { id: patientId }, + select: { id: true }, + }); if (!patientExists) { - throw new NotFoundException('Paciente não encontrado.'); + throw new NotFoundException('Patient not found.'); } const patientSupport = await this.patientSupportsRepository.create({ @@ -48,10 +54,13 @@ export class PatientSupportsService { } async findAllByPatientId(patientId: string): Promise { - const patientExists = await this.patientsRepository.findById(patientId); + const patientExists = await this.patientsRepository.findOne({ + where: { id: patientId }, + select: { id: true }, + }); if (!patientExists) { - throw new NotFoundException('Paciente não encontrado.'); + throw new NotFoundException('Patient not found.'); } return await this.patientSupportsRepository.findAllByPatientId(patientId); @@ -65,7 +74,7 @@ export class PatientSupportsService { const patientSupport = await this.patientSupportsRepository.findById(id); if (!patientSupport) { - throw new NotFoundException('Contato de apoio não encontrado.'); + throw new NotFoundException('Support network not found.'); } if ( @@ -73,7 +82,7 @@ export class PatientSupportsService { authUser.id !== patientSupport.patient_id ) { throw new ForbiddenException( - 'Você não tem permissão para atualizar este contato de apoio.', + 'You do not have permission to update this support network.', ); } @@ -91,7 +100,7 @@ export class PatientSupportsService { const patientSupport = await this.patientSupportsRepository.findById(id); if (!patientSupport) { - throw new NotFoundException('Contato de apoio não encontrado.'); + throw new NotFoundException('Support network not found.'); } if ( @@ -99,7 +108,7 @@ export class PatientSupportsService { authUser.id !== patientSupport.patient_id ) { throw new ForbiddenException( - 'Você não tem permissão para remover este contato de apoio.', + 'You do not have permission to remove this support network.', ); } diff --git a/src/app/http/patients/patients.controller.ts b/src/app/http/patients/patients.controller.ts index 9c91613..64a6576 100644 --- a/src/app/http/patients/patients.controller.ts +++ b/src/app/http/patients/patients.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Get, - NotFoundException, Param, Patch, Post, @@ -11,92 +10,74 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import { BaseResponse } from '@/domain/schemas/base'; -import type { GetPatientSupportsResponse } from '@/domain/schemas/patient-support/responses'; import type { GetPatientResponse, GetPatientsResponse, } from '@/domain/schemas/patients/responses'; -import type { AuthUserDto } from '../auth/auth.dtos'; -import { PatientSupportsRepository } from '../patient-supports/patient-supports.repository'; import { + CreatePatientDto, GetPatientsQuery, - PatientScreeningDto, UpdatePatientDto, } from './patients.dtos'; -import { PatientsRepository } from './patients.repository'; -import { PatientsService } from './patients.service'; +import { CreatePatientUseCase } from './use-cases/create-patient.use-case'; +import { DeactivatePatientUseCase } from './use-cases/deactivate-patient.use-case'; +import { GetPatientUseCase } from './use-cases/get-patient.use-case'; +import { GetPatientsUseCase } from './use-cases/get-patients.use-case'; +import { UpdatePatientUseCase } from './use-cases/update-patient.use-case'; @ApiTags('Pacientes') @Controller('patients') export class PatientsController { constructor( - private readonly patientsService: PatientsService, - private readonly patientsRepository: PatientsRepository, - private readonly patientsSupportsRepository: PatientSupportsRepository, + private readonly getPatientsUseCase: GetPatientsUseCase, + private readonly getPatientUseCase: GetPatientUseCase, + private readonly createPatientUseCase: CreatePatientUseCase, + private readonly updatePatientUseCase: UpdatePatientUseCase, + private readonly deativatePatientUseCase: DeactivatePatientUseCase, ) {} - @Post('/screening') - @Roles(['patient']) - @ApiOperation({ summary: 'Registra triagem do paciente' }) - public async screening( - @AuthUser() authUser: AuthUserDto, - @Body() patientScreeningDto: PatientScreeningDto, - ): Promise { - await this.patientsService.screening(patientScreeningDto, authUser); - - return { - success: true, - message: 'Triagem realizada com sucesso.', - }; - } - - @Post() - @Roles(['manager', 'nurse']) - @ApiOperation({ summary: 'Cadastra um novo paciente' }) - public async create( - @Body() createPatientDto: PatientScreeningDto, - ): Promise { - await this.patientsService.create(createPatientDto); - - return { - success: true, - message: 'Cadastro realizado com sucesso.', - }; - } - @Get() @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Lista todos os pacientes' }) - public async findAll( - @Query() filters: GetPatientsQuery, + async getPatients( + @Query() query: GetPatientsQuery, ): Promise { - const { patients, total } = await this.patientsRepository.findAll(filters); + const data = await this.getPatientsUseCase.execute({ query }); return { success: true, message: 'Lista de pacientes retornada com sucesso.', - data: { patients, total }, + data, }; } @Get(':id') @Roles(['manager', 'nurse', 'specialist']) @ApiOperation({ summary: 'Busca um paciente pelo ID' }) - public async findById(@Param('id') id: string): Promise { - const patient = await this.patientsRepository.findById(id); - - if (!patient) { - throw new NotFoundException('Paciente não encontrado.'); - } + async getPatientById(@Param('id') id: string): Promise { + const data = await this.getPatientUseCase.execute({ id }); return { success: true, message: 'Paciente retornado com sucesso.', - data: patient, + data, + }; + } + + @Post() + @Roles(['manager', 'nurse']) + @ApiOperation({ summary: 'Cadastra um novo paciente' }) + async create( + @Body() createPatientDto: CreatePatientDto, + ): Promise { + await this.createPatientUseCase.execute({ createPatientDto }); + + return { + success: true, + message: 'Paciente registrado com sucesso.', }; } @@ -107,7 +88,7 @@ export class PatientsController { @Param('id') id: string, @Body() updatePatientDto: UpdatePatientDto, ): Promise { - await this.patientsService.update(id, updatePatientDto); + await this.updatePatientUseCase.execute({ id, updatePatientDto }); return { success: true, @@ -115,40 +96,15 @@ export class PatientsController { }; } - @Patch(':id/inactivate') + @Patch(':id/deactivate') @Roles(['manager']) - @ApiOperation({ summary: 'Inativa o Paciente pelo ID' }) - async inactivatePatient(@Param('id') id: string): Promise { - await this.patientsService.deactivate(id); + @ApiOperation({ summary: 'Inativa um paciente pelo ID' }) + async deactivatePatient(@Param('id') id: string): Promise { + await this.deativatePatientUseCase.execute({ id }); return { success: true, message: 'Paciente inativado com sucesso.', }; } - - @Get(':id/patient-supports') - @Roles(['manager', 'nurse', 'specialist', 'patient']) - @ApiOperation({ summary: 'Lista todos os contatos de apoio de um paciente' }) - async findAllPatientSupports( - @Param('id') patientId: string, - ): Promise { - const patient = await this.patientsRepository.findById(patientId); - - if (!patient) { - throw new NotFoundException('Paciente não encontrado.'); - } - - const patientSupports = - await this.patientsSupportsRepository.findAllByPatientId(patientId); - - return { - success: true, - message: 'Lista de contatos de apoio retornada com sucesso.', - data: { - patient_supports: patientSupports, - total: patientSupports.length, - }, - }; - } } diff --git a/src/app/http/patients/patients.dtos.ts b/src/app/http/patients/patients.dtos.ts index 863edb6..c8afa6c 100644 --- a/src/app/http/patients/patients.dtos.ts +++ b/src/app/http/patients/patients.dtos.ts @@ -3,14 +3,11 @@ import { createZodDto } from 'nestjs-zod'; import { createPatientSchema, getPatientsQuerySchema, - patientScreeningSchema, updatePatientSchema, } from '@/domain/schemas/patients/requests'; export class GetPatientsQuery extends createZodDto(getPatientsQuerySchema) {} -export class PatientScreeningDto extends createZodDto(patientScreeningSchema) {} - export class CreatePatientDto extends createZodDto(createPatientSchema) {} export class UpdatePatientDto extends createZodDto(updatePatientSchema) {} diff --git a/src/app/http/patients/patients.module.ts b/src/app/http/patients/patients.module.ts index 929d363..5d4f22c 100644 --- a/src/app/http/patients/patients.module.ts +++ b/src/app/http/patients/patients.module.ts @@ -1,24 +1,25 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CryptographyModule } from '@/app/cryptography/cryptography.module'; -import { UsersModule } from '@/app/http/users/users.module'; import { Patient } from '@/domain/entities/patient'; -import { PatientSupportsModule } from '../patient-supports/patient-supports.module'; import { PatientsController } from './patients.controller'; -import { PatientsRepository } from './patients.repository'; -import { PatientsService } from './patients.service'; +import { CreatePatientUseCase } from './use-cases/create-patient.use-case'; +import { DeactivatePatientUseCase } from './use-cases/deactivate-patient.use-case'; +import { GetPatientUseCase } from './use-cases/get-patient.use-case'; +import { GetPatientsUseCase } from './use-cases/get-patients.use-case'; +import { UpdatePatientUseCase } from './use-cases/update-patient.use-case'; @Module({ - imports: [ - CryptographyModule, - UsersModule, - TypeOrmModule.forFeature([Patient]), - forwardRef(() => PatientSupportsModule), - ], + imports: [TypeOrmModule.forFeature([Patient])], controllers: [PatientsController], - providers: [PatientsService, PatientsRepository], - exports: [PatientsRepository, TypeOrmModule.forFeature([Patient])], + providers: [ + GetPatientUseCase, + GetPatientsUseCase, + CreatePatientUseCase, + UpdatePatientUseCase, + DeactivatePatientUseCase, + ], + exports: [TypeOrmModule.forFeature([Patient])], }) export class PatientsModule {} diff --git a/src/app/http/patients/patients.repository.ts b/src/app/http/patients/patients.repository.ts deleted file mode 100644 index 4e2c893..0000000 --- a/src/app/http/patients/patients.repository.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { Patient } from '@/domain/entities/patient'; -import type { PatientOrderBy } from '@/domain/enums/patients'; -import type { PatientResponse } from '@/domain/schemas/patients/responses'; - -import { CreatePatientDto, GetPatientsQuery } from './patients.dtos'; - -@Injectable() -export class PatientsRepository { - constructor( - @InjectRepository(Patient) - private readonly patientsRepository: Repository, - ) {} - - public async findAll( - filters: GetPatientsQuery, - includePending?: boolean, - ): Promise<{ patients: PatientResponse[]; total: number }> { - const { - search, - order, - orderBy, - status, - startDate, - endDate, - page, - perPage, - } = filters; - - const ORDER_BY: Record = { - name: 'user.name', - email: 'user.email', - status: 'patient.status', - date: 'patient.created_at', - }; - - const query = this.patientsRepository - .createQueryBuilder('patient') - .leftJoinAndSelect('patient.user', 'user') - .select(['patient', 'user.name', 'user.email', 'user.avatar_url']); - - if (search) { - query.andWhere(`user.name LIKE :search`, { search: `%${search}%` }); - query.orWhere(`user.email LIKE :search`, { search: `%${search}%` }); - } - - if (status) { - query.andWhere('patient.status = :status', { status }); - } - - if (!status && !includePending) { - query.andWhere("patient.status != 'pending'"); - } - - if (startDate && endDate) { - query.andWhere('patient.created_at BETWEEN :startDate AND :endDate', { - startDate, - endDate, - }); - } - - if (startDate && !endDate) { - query.andWhere('patient.created_at >= :startDate', { startDate }); - } - - const total = await query.getCount(); - - query.orderBy(ORDER_BY[orderBy], order); - - query.skip((page - 1) * perPage).take(perPage); - const rawPatients = await query.getMany(); - - const patients: PatientResponse[] = rawPatients.map( - ({ ...patientData }) => ({ - id: patientData.id, - name: patientData.name, - email: patientData.email, - status: patientData.status, - avatar_url: patientData.avatar_url, - phone: patientData.phone, - created_at: patientData.created_at, - }), - ); - - return { patients, total }; - } - - public async findById(id: string): Promise { - const patient = await this.patientsRepository.findOne({ - relations: { supports: true }, - where: { id }, - select: { - supports: { id: true, name: true, phone: true, kinship: true }, - }, - }); - - return patient; - } - - public async findByEmail(email: string): Promise { - return await this.patientsRepository.findOne({ where: { email } }); - } - - public async findByCpf(cpf: string): Promise { - return await this.patientsRepository.findOne({ where: { cpf } }); - } - - public async create(patient: CreatePatientDto): Promise { - return await this.patientsRepository.save(patient); - } - - public async update(patient: Patient): Promise { - return await this.patientsRepository.save(patient); - } - - public async deactivate(id: string): Promise { - return this.patientsRepository.save({ id, status: 'inactive' }); - } -} diff --git a/src/app/http/patients/patients.service.ts b/src/app/http/patients/patients.service.ts deleted file mode 100644 index 4b0885d..0000000 --- a/src/app/http/patients/patients.service.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { - ConflictException, - ForbiddenException, - Injectable, - Logger, - NotFoundException, -} from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; - -import { CryptographyService } from '@/app/cryptography/crypography.service'; -import { Patient } from '@/domain/entities/patient'; -import { PatientSupport } from '@/domain/entities/patient-support'; -import { User } from '@/domain/entities/user'; - -import type { AuthUserDto } from '../auth/auth.dtos'; -import { type PatientScreeningDto, UpdatePatientDto } from './patients.dtos'; -import { PatientsRepository } from './patients.repository'; - -@Injectable() -export class PatientsService { - private readonly logger = new Logger(PatientsService.name); - - constructor( - @InjectDataSource() - private readonly dataSource: DataSource, - private readonly patientsRepository: PatientsRepository, - private readonly cryptographyService: CryptographyService, - ) {} - - async screening( - patientScreeningDto: PatientScreeningDto, - authUser: AuthUserDto, - ): Promise { - if (authUser.role !== 'patient') { - this.logger.error( - { userId: authUser.id, email: authUser.email }, - 'Screening failed: User is not a patient', - ); - throw new ForbiddenException( - 'Você não tem permissão para executar esta ação.', - ); - } - - const patient = await this.patientsRepository.findById(authUser.id); - - if (patient?.status !== 'pending') { - this.logger.error( - { userId: authUser.id, email: authUser.email }, - 'Screening failed: Patient already finished the proccess', - ); - throw new ConflictException('Você já concluiu a triagem.'); - } - - const patientWithSameCpf = await this.patientsRepository.findByCpf( - patientScreeningDto.cpf, - ); - - if (patientWithSameCpf) { - this.logger.error( - { - userId: authUser.id, - email: authUser.email, - cpf: patientScreeningDto.cpf, - }, - 'Screening failed: CPF already registered', - ); - throw new ConflictException('Este CPF já está cadastrado.'); - } - - return await this.dataSource.transaction(async (manager) => { - const patientsDataSource = manager.getRepository(Patient); - const patientsSupportDataSource = manager.getRepository(PatientSupport); - - const { supports, ...patientDto } = patientScreeningDto; - - await patientsDataSource.save(patientDto); - - if (supports && supports.length > 0) { - const patientSupports = supports.map((support) => - patientsSupportDataSource.create({ - name: support.name, - phone: support.phone, - kinship: support.kinship, - patient_id: authUser.id, - }), - ); - - await patientsSupportDataSource.save(patientSupports); - } - - this.logger.log( - { id: authUser.id, email: authUser.email }, - 'Screening: Patient finished successfully', - ); - }); - } - - async create(patientScreeningDto: PatientScreeningDto): Promise { - const patient = await this.patientsRepository.findByEmail( - patientScreeningDto.email, - ); - - if (patient) { - this.logger.error( - { email: patientScreeningDto.email }, - 'Create patient failed: E-mail already registered', - ); - throw new ConflictException('Este e-mail já está cadastrado.'); - } - - const patientWithSameCpf = await this.patientsRepository.findByCpf( - patientScreeningDto.cpf, - ); - - if (patientWithSameCpf) { - this.logger.error( - { email: patientScreeningDto.email, cpf: patientScreeningDto.cpf }, - 'Create patient failed: CPF already registered', - ); - throw new ConflictException('Este CPF já está cadastrado.'); - } - - return await this.dataSource.transaction(async (manager) => { - const usersDataSource = manager.getRepository(User); - const patientsDataSource = manager.getRepository(Patient); - const patientsSupportDataSource = manager.getRepository(PatientSupport); - - const randomPassword = Math.random().toString(36).slice(-8); - const hashedPassword = - await this.cryptographyService.createHash(randomPassword); - - const newUser = usersDataSource.create({ - name: patientScreeningDto.name, - email: patientScreeningDto.email, - password: hashedPassword, - }); - - const user = await usersDataSource.save(newUser); - - const { supports, ...patientDto } = patientScreeningDto; - - const patient = patientsDataSource.create({ - ...patientDto, - status: 'active', - }); - - const savedPatient = await patientsDataSource.save(patient); - - if (supports && supports.length > 0) { - const patientSupports = supports.map((support) => - patientsSupportDataSource.create({ - name: support.name, - phone: support.phone, - kinship: support.kinship, - patient_id: savedPatient.id, - }), - ); - - await patientsSupportDataSource.save(patientSupports); - } - - this.logger.log( - { id: savedPatient.id, email: user.email }, - 'Patient created successfully', - ); - }); - } - - async update(id: string, updatePatientDto: UpdatePatientDto): Promise { - const patient = await this.patientsRepository.findById(id); - - if (!patient) { - this.logger.error( - { email: updatePatientDto.email }, - 'Update patient failed: Patient not found', - ); - throw new NotFoundException('Paciente não encontrado.'); - } - - const patientWithSameCpf = await this.patientsRepository.findByCpf( - updatePatientDto.cpf, - ); - - if (patientWithSameCpf && patientWithSameCpf.id !== id) { - this.logger.error( - { email: updatePatientDto.email, cpf: updatePatientDto.cpf }, - 'Update patient failed: CPF already registered', - ); - throw new ConflictException('Este CPF já está cadastrado.'); - } - - return await this.dataSource.transaction(async (manager) => { - const patientsDataSource = manager.getRepository(Patient); - - if (updatePatientDto.email !== patient.email) { - const emailAlreadyRegistered = await patientsDataSource.findOne({ - where: { email: updatePatientDto.email }, - }); - - if (emailAlreadyRegistered) { - this.logger.error( - { id: patient.id, email: updatePatientDto.email }, - 'Update patient failed: E-mail already registered', - ); - throw new ConflictException('Este e-mail já está em uso.'); - } - } - - const updatedPatient = updatePatientDto; - - Object.assign(patient, updatedPatient); - - await patientsDataSource.save(patient); - - this.logger.log({ id: patient.id }, 'Patient updated successfully'); - }); - } - - async deactivate(id: string): Promise { - const patient = await this.patientsRepository.findById(id); - - if (!patient) { - throw new NotFoundException('Paciente não encontrado.'); - } - - if (patient.status == 'inactive') { - throw new ConflictException('Paciente já está inativo.'); - } - - await this.patientsRepository.deactivate(id); - - this.logger.log( - { id: patient.id, email: patient.email }, - 'Patient deactivated successfully', - ); - } -} diff --git a/src/app/http/patients/use-cases/create-patient.use-case.ts b/src/app/http/patients/use-cases/create-patient.use-case.ts new file mode 100644 index 0000000..7528d3d --- /dev/null +++ b/src/app/http/patients/use-cases/create-patient.use-case.ts @@ -0,0 +1,65 @@ +import { ConflictException, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; + +import type { CreatePatientDto } from '../patients.dtos'; + +interface CreatePatientUseCaseRequest { + createPatientDto: CreatePatientDto; +} + +type CreatePatientUseCaseResponse = Promise; + +@Injectable() +export class CreatePatientUseCase { + private readonly logger = new Logger(CreatePatientUseCase.name); + + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + ) {} + + async execute({ + createPatientDto, + }: CreatePatientUseCaseRequest): CreatePatientUseCaseResponse { + const { email, cpf } = createPatientDto; + + const patientWithEmail = await this.patientsRepository.findOne({ + where: { email }, + select: { id: true }, + }); + + if (patientWithEmail) { + this.logger.error( + { email }, + 'Create patient failed: Email already registered', + ); + throw new ConflictException('O e-mail informado já está registrado.'); + } + + const patientWithCpf = await this.patientsRepository.findOne({ + where: { cpf }, + select: { id: true }, + }); + + if (patientWithCpf) { + this.logger.error( + { cpf }, + 'Create patient failed: CPF already registered', + ); + throw new ConflictException('O CPF informado já está registrado.'); + } + + const patient = await this.patientsRepository.save({ + ...createPatientDto, + status: 'active', + }); + + this.logger.log( + { patientId: patient.id, email }, + 'Patient created successfully', + ); + } +} diff --git a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts new file mode 100644 index 0000000..9f90c05 --- /dev/null +++ b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts @@ -0,0 +1,55 @@ +import { + ConflictException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; + +interface DeactivatePatientUseCaseRequest { + id: string; +} + +type DeactivatePatientUseCaseResponse = Promise; + +@Injectable() +export class DeactivatePatientUseCase { + private readonly logger = new Logger(DeactivatePatientUseCase.name); + + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + ) {} + + async execute({ + id, + }: DeactivatePatientUseCaseRequest): DeactivatePatientUseCaseResponse { + const patient = await this.patientsRepository.findOne({ + select: { id: true, status: true }, + where: { id }, + }); + + if (!patient) { + this.logger.error( + { patientId: id }, + 'Cancel patient failed: Patient not found', + ); + throw new NotFoundException('Paciente não encontrado.'); + } + + if (patient.status === 'inactive') { + this.logger.error( + { patientId: id }, + 'Cancel patient failed: Patient already inactive', + ); + throw new ConflictException('Este paciente já está inativo.'); + } + + await this.patientsRepository.save({ id, status: 'inactive' }); + + this.logger.log({ patientId: id }, 'Patient deactivated successfully'); + } +} diff --git a/src/app/http/patients/use-cases/get-patient.use-case.ts b/src/app/http/patients/use-cases/get-patient.use-case.ts new file mode 100644 index 0000000..0d5ed33 --- /dev/null +++ b/src/app/http/patients/use-cases/get-patient.use-case.ts @@ -0,0 +1,52 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; + +interface GetPatientUseCaseRequest { + id: string; +} + +type GetPatientUseCaseResponse = Promise; + +@Injectable() +export class GetPatientUseCase { + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + ) {} + + async execute({ id }: GetPatientUseCaseRequest): GetPatientUseCaseResponse { + const patient = await this.patientsRepository.findOne({ + relations: { supports: true }, + where: { id }, + select: { + id: true, + name: true, + email: true, + status: true, + avatar_url: true, + phone: true, + cpf: true, + gender: true, + date_of_birth: true, + state: true, + city: true, + has_disability: true, + disability_desc: true, + need_legal_assistance: true, + take_medication: true, + medication_desc: true, + nmo_diagnosis: true, + created_at: true, + }, + }); + + if (!patient) { + throw new NotFoundException('Paciente não encontrado.'); + } + + return patient; + } +} diff --git a/src/app/http/patients/use-cases/get-patients.use-case.ts b/src/app/http/patients/use-cases/get-patients.use-case.ts new file mode 100644 index 0000000..0863dc1 --- /dev/null +++ b/src/app/http/patients/use-cases/get-patients.use-case.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Between, + type FindOptionsWhere, + ILike, + LessThanOrEqual, + MoreThanOrEqual, + Not, + type Repository, +} from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import type { PatientOrderBy } from '@/domain/enums/patients'; +import type { PatientResponse } from '@/domain/schemas/patients/responses'; + +import type { GetPatientsQuery } from '../patients.dtos'; + +interface GetPatientsUseCaseRequest { + query: GetPatientsQuery; +} + +type GetPatientsUseCaseResponse = Promise<{ + patients: PatientResponse[]; + total: number; +}>; + +@Injectable() +export class GetPatientsUseCase { + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + ) {} + + async execute({ + query, + }: GetPatientsUseCaseRequest): GetPatientsUseCaseResponse { + const { + search, + order, + orderBy, + status, + startDate, + endDate, + page, + perPage, + } = query; + + const ORDER_BY_MAPPING: Record = { + name: 'name', + email: 'email', + status: 'status', + date: 'created_at', + }; + + const where: FindOptionsWhere = { + status: status ?? Not('pending'), + }; + + if (search) { + where.name = ILike(`%${search}%`); + } + + if (startDate && endDate) { + where.created_at = Between(new Date(startDate), new Date(endDate)); + } + + if (startDate && !endDate) { + where.created_at = MoreThanOrEqual(new Date(startDate)); + } + + if (endDate && !startDate) { + where.created_at = LessThanOrEqual(new Date(endDate)); + } + + const total = await this.patientsRepository.count({ where }); + + const patients = await this.patientsRepository.find({ + where, + select: { + id: true, + name: true, + email: true, + status: true, + avatar_url: true, + phone: true, + created_at: true, + }, + order: { [ORDER_BY_MAPPING[orderBy]]: order }, + skip: (page - 1) * perPage, + take: perPage, + }); + + return { patients, total }; + } +} diff --git a/src/app/http/patients/use-cases/update-patient.use-case.ts b/src/app/http/patients/use-cases/update-patient.use-case.ts new file mode 100644 index 0000000..8584c98 --- /dev/null +++ b/src/app/http/patients/use-cases/update-patient.use-case.ts @@ -0,0 +1,81 @@ +import { + ConflictException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; + +import type { UpdatePatientDto } from '../patients.dtos'; + +interface UpdatePatientUseCaseRequest { + updatePatientDto: UpdatePatientDto; + id: string; +} + +type UpdatePatientUseCaseResponse = Promise; + +@Injectable() +export class UpdatePatientUseCase { + private readonly logger = new Logger(UpdatePatientUseCase.name); + + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + ) {} + + async execute({ + id, + updatePatientDto, + }: UpdatePatientUseCaseRequest): UpdatePatientUseCaseResponse { + const patient = await this.patientsRepository.findOne({ + select: { id: true, email: true, cpf: true }, + where: { id }, + }); + + if (!patient) { + this.logger.error( + { patientId: id }, + 'Update patient failed: Patient not found', + ); + throw new NotFoundException('Paciente não encontrado.'); + } + + if (updatePatientDto.cpf && updatePatientDto.cpf !== patient.cpf) { + const patientWithCpf = await this.patientsRepository.findOne({ + where: { cpf: updatePatientDto.cpf }, + select: { id: true }, + }); + + if (patientWithCpf && patientWithCpf.id !== id) { + this.logger.error( + { patientId: id, cpf: updatePatientDto.cpf }, + 'Update patient failed: CPF already registered', + ); + throw new ConflictException('O CPF informado já está registrado.'); + } + } + + if (updatePatientDto.email && updatePatientDto.email !== patient.email) { + const patientWithEmail = await this.patientsRepository.findOne({ + where: { email: updatePatientDto.email }, + select: { id: true }, + }); + + if (patientWithEmail && patientWithEmail.id !== id) { + this.logger.error( + { patientId: id, email: updatePatientDto.email }, + 'Update patient failed: Email already registered', + ); + throw new ConflictException('O e-mail informado já está registrado.'); + } + } + + await this.patientsRepository.save({ id, ...updatePatientDto }); + + this.logger.log({ patientId: id }, 'Patient updated successfully'); + } +} diff --git a/src/domain/schemas/patients/requests.ts b/src/domain/schemas/patients/requests.ts index 72230df..0899496 100644 --- a/src/domain/schemas/patients/requests.ts +++ b/src/domain/schemas/patients/requests.ts @@ -3,9 +3,11 @@ import { z } from 'zod'; import { BRAZILIAN_STATES } from '@/constants/brazilian-states'; import { PATIENT_GENDERS, + PATIENT_NMO_DIAGNOSTICS, PATIENT_ORDER_BY, PATIENT_STATUSES, } from '@/domain/enums/patients'; +import { QUERY_ORDERS } from '@/domain/enums/queries'; import { createPatientSupportSchema } from '../patient-support/requests'; import { baseQuerySchema } from '../query'; @@ -22,6 +24,16 @@ export const createPatientSchema = z cpf: z.string().max(11), state: z.enum(BRAZILIAN_STATES), city: z.string(), + nmo_diagnosis: z.enum(PATIENT_NMO_DIAGNOSTICS), + supports: z + .array( + createPatientSupportSchema.pick({ + name: true, + phone: true, + kinship: true, + }), + ) + .min(1), }) .merge( patientSchema.pick({ @@ -30,26 +42,11 @@ export const createPatientSchema = z need_legal_assistance: true, take_medication: true, medication_desc: true, - nmo_diagnosis: true, }), ); export type CreatePatient = z.infer; -export const patientScreeningSchema = createPatientSchema.extend({ - supports: z - .array( - createPatientSupportSchema.pick({ - name: true, - phone: true, - kinship: true, - }), - ) - .nullable() - .default([]), -}); -export type PatientScreening = z.infer; - -export const updatePatientSchema = patientScreeningSchema +export const updatePatientSchema = createPatientSchema .omit({ supports: true }) .merge(patientSchema.pick({ status: true })); export type UpdatePatient = z.infer; @@ -57,7 +54,6 @@ export type UpdatePatient = z.infer; export const getPatientsQuerySchema = baseQuerySchema .pick({ search: true, - order: true, page: true, perPage: true, startDate: true, @@ -65,6 +61,7 @@ export const getPatientsQuerySchema = baseQuerySchema }) .extend({ status: z.enum(PATIENT_STATUSES).optional(), + order: z.enum(QUERY_ORDERS).optional().default('ASC'), orderBy: z.enum(PATIENT_ORDER_BY).optional().default('name'), }) .refine( From 39321bc41125591f00b6e22a2d1e72d9c320a0b1 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Fri, 19 Dec 2025 20:02:54 -0300 Subject: [PATCH 04/21] refactor(patient-supports): update module to match new use-case pattern --- .github/copilot-instructions.md | 9 +- .../patient-supports.controller.ts | 65 ++++------ .../patient-supports.module.ts | 21 +-- .../patient-supports.repository.ts | 47 ------- .../patient-supports.service.ts | 122 ------------------ .../create-patient-support.use-case.ts | 48 +++++++ .../delete-patient-support.use-case.ts | 56 ++++++++ .../update-patient-support.use-case.ts | 59 +++++++++ src/app/http/patients/patients.controller.ts | 10 ++ src/app/http/patients/patients.module.ts | 1 - .../http/referrals/referrals.controller.ts | 8 +- .../use-cases/create-referrals.use-case.ts | 2 +- .../schemas/patient-support/requests.ts | 14 -- 13 files changed, 220 insertions(+), 242 deletions(-) delete mode 100644 src/app/http/patient-supports/patient-supports.repository.ts delete mode 100644 src/app/http/patient-supports/patient-supports.service.ts create mode 100644 src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts create mode 100644 src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts create mode 100644 src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d57b06b..b4284b3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -171,19 +171,22 @@ export class CreateAppointmentUseCase { Use NestJS exceptions with descriptive messages: +- **User-facing messages**: Use Portuguese (pt-BR) for exception messages. These will be displayed in the UI. +- **Internal messages**: Use English for logging. These are for development and debugging. + ```typescript if (!patient) { - throw new NotFoundException('Patient not found.'); + throw new NotFoundException('Paciente não encontrado.'); } if (date > maxDate) { - throw new BadRequestException('Appointment date exceeds 3-month limit.'); + throw new BadRequestException('A data de atendimento deve estar dentro dos próximos 3 meses.'); } ``` ### Logging -Log significant events in use-cases: +Log significant events in use-cases with English messages: ```typescript private readonly logger = new Logger(CreateAppointmentUseCase.name); diff --git a/src/app/http/patient-supports/patient-supports.controller.ts b/src/app/http/patient-supports/patient-supports.controller.ts index 3a2fee1..7a65436 100644 --- a/src/app/http/patient-supports/patient-supports.controller.ts +++ b/src/app/http/patient-supports/patient-supports.controller.ts @@ -3,8 +3,6 @@ import { Controller, Delete, ForbiddenException, - Get, - NotFoundException, Param, Post, Put, @@ -14,58 +12,45 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import type { BaseResponse } from '@/domain/schemas/base'; -import type { GetPatientSupportResponse } from '@/domain/schemas/patient-support/responses'; import type { AuthUserDto } from '../auth/auth.dtos'; -import { CreatePatientSupportDto } from '../patient-supports/patient-supports.dtos'; -import { UpdatePatientSupportDto } from './patient-supports.dtos'; -import { PatientSupportsRepository } from './patient-supports.repository'; -import { PatientSupportsService } from './patient-supports.service'; +import { + CreatePatientSupportDto, + UpdatePatientSupportDto, +} from './patient-supports.dtos'; +import { CreatePatientSupportUseCase } from './use-cases/create-patient-support.use-case'; +import { DeletePatientSupportUseCase } from './use-cases/delete-patient-support.use-case'; +import { UpdatePatientSupportUseCase } from './use-cases/update-patient-support.use-case'; @ApiTags('Rede de apoio') @Controller('patient-supports') export class PatientSupportsController { constructor( - private readonly patientSupportsService: PatientSupportsService, - private readonly patientSupportsRepository: PatientSupportsRepository, + private readonly createPatientSupportUseCase: CreatePatientSupportUseCase, + private readonly updatePatientSupportUseCase: UpdatePatientSupportUseCase, + private readonly cancelPatientSupportUseCase: DeletePatientSupportUseCase, ) {} - @Get(':id') - @ApiOperation({ summary: 'Busca um contato de apoio pelo ID' }) - async findById(@Param('id') id: string): Promise { - const patientSupport = await this.patientSupportsRepository.findById(id); - - if (!patientSupport) { - throw new NotFoundException('Contato de apoio não encontrado.'); - } - - return { - success: true, - message: 'Contato de apoio retornado com sucesso.', - data: patientSupport, - }; - } - @Post(':patientId') - @Roles(['nurse', 'manager']) + @Roles(['nurse', 'manager', 'patient']) @ApiOperation({ summary: 'Registra um novo contato de apoio para um paciente', }) async createPatientSupport( @Param('patientId') patientId: string, - @AuthUser() authUser: AuthUserDto, + @AuthUser() user: AuthUserDto, @Body() createPatientSupportDto: CreatePatientSupportDto, ): Promise { - if (authUser.role === 'patient' && authUser.id !== patientId) { + if (user.role === 'patient' && user.id !== patientId) { throw new ForbiddenException( 'Você não tem permissão para registrar contatos de apoio para este paciente.', ); } - await this.patientSupportsService.create( - createPatientSupportDto, + await this.createPatientSupportUseCase.execute({ patientId, - ); + createPatientSupportDto, + }); return { success: true, @@ -74,18 +59,18 @@ export class PatientSupportsController { } @Put(':id') - @Roles(['nurse', 'manager']) + @Roles(['nurse', 'manager', 'patient']) @ApiOperation({ summary: 'Atualiza um contato de apoio pelo ID' }) async updatePatientSupport( @Param('id') id: string, - @AuthUser() authUser: AuthUserDto, + @AuthUser() user: AuthUserDto, @Body() updatePatientSupportDto: UpdatePatientSupportDto, ): Promise { - await this.patientSupportsService.update( + await this.updatePatientSupportUseCase.execute({ id, + user, updatePatientSupportDto, - authUser, - ); + }); return { success: true, @@ -94,13 +79,13 @@ export class PatientSupportsController { } @Delete(':id') - @Roles(['nurse', 'manager']) + @Roles(['nurse', 'manager', 'patient']) @ApiOperation({ summary: 'Remove um contato de apoio pelo ID' }) - async remove( + async removePatientSupport( @Param('id') id: string, - @AuthUser() authUser: AuthUserDto, + @AuthUser() user: AuthUserDto, ): Promise { - await this.patientSupportsService.remove(id, authUser); + await this.cancelPatientSupportUseCase.execute({ id, user }); return { success: true, diff --git a/src/app/http/patient-supports/patient-supports.module.ts b/src/app/http/patient-supports/patient-supports.module.ts index 6023383..794a54a 100644 --- a/src/app/http/patient-supports/patient-supports.module.ts +++ b/src/app/http/patient-supports/patient-supports.module.ts @@ -1,20 +1,21 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { PatientsModule } from '@/app/http/patients/patients.module'; +import { Patient } from '@/domain/entities/patient'; import { PatientSupport } from '@/domain/entities/patient-support'; import { PatientSupportsController } from './patient-supports.controller'; -import { PatientSupportsRepository } from './patient-supports.repository'; -import { PatientSupportsService } from './patient-supports.service'; +import { CreatePatientSupportUseCase } from './use-cases/create-patient-support.use-case'; +import { DeletePatientSupportUseCase } from './use-cases/delete-patient-support.use-case'; +import { UpdatePatientSupportUseCase } from './use-cases/update-patient-support.use-case'; @Module({ - imports: [ - forwardRef(() => PatientsModule), - TypeOrmModule.forFeature([PatientSupport]), - ], + imports: [TypeOrmModule.forFeature([Patient, PatientSupport])], controllers: [PatientSupportsController], - providers: [PatientSupportsService, PatientSupportsRepository], - exports: [PatientSupportsService, PatientSupportsRepository], + providers: [ + CreatePatientSupportUseCase, + UpdatePatientSupportUseCase, + DeletePatientSupportUseCase, + ], }) export class PatientSupportsModule {} diff --git a/src/app/http/patient-supports/patient-supports.repository.ts b/src/app/http/patient-supports/patient-supports.repository.ts deleted file mode 100644 index 9ea948c..0000000 --- a/src/app/http/patient-supports/patient-supports.repository.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { PatientSupport } from '@/domain/entities/patient-support'; - -import { CreatePatientSupportDto } from './patient-supports.dtos'; - -@Injectable() -export class PatientSupportsRepository { - constructor( - @InjectRepository(PatientSupport) - private readonly patientSupportsRepository: Repository, - ) {} - - public async findById(id: string): Promise { - return await this.patientSupportsRepository.findOne({ - where: { id: id }, - }); - } - - public async findAllByPatientId( - patientId: string, - ): Promise { - return await this.patientSupportsRepository.find({ - where: { patient_id: patientId }, - }); - } - - public async create( - createPatientSupportDto: CreatePatientSupportDto, - ): Promise { - const patientSupportCreated = this.patientSupportsRepository.create( - createPatientSupportDto, - ); - - return await this.patientSupportsRepository.save(patientSupportCreated); - } - - public async update(patientSupport: PatientSupport): Promise { - return await this.patientSupportsRepository.save(patientSupport); - } - - public async remove(patientSupport: PatientSupport): Promise { - return await this.patientSupportsRepository.remove(patientSupport); - } -} diff --git a/src/app/http/patient-supports/patient-supports.service.ts b/src/app/http/patient-supports/patient-supports.service.ts deleted file mode 100644 index 7ce49e2..0000000 --- a/src/app/http/patient-supports/patient-supports.service.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - ForbiddenException, - Injectable, - Logger, - NotFoundException, -} from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import type { Repository } from 'typeorm'; - -import { Patient } from '@/domain/entities/patient'; -import { PatientSupport } from '@/domain/entities/patient-support'; - -import type { AuthUserDto } from '../auth/auth.dtos'; -import { - CreatePatientSupportDto, - UpdatePatientSupportDto, -} from './patient-supports.dtos'; -import { PatientSupportsRepository } from './patient-supports.repository'; - -@Injectable() -export class PatientSupportsService { - private readonly logger = new Logger(PatientSupportsService.name); - - constructor( - @InjectRepository(Patient) - private readonly patientsRepository: Repository, - private readonly patientSupportsRepository: PatientSupportsRepository, - ) {} - - async create( - createPatientSupportDto: CreatePatientSupportDto, - patientId: string, - ): Promise { - const patientExists = await this.patientsRepository.findOne({ - where: { id: patientId }, - select: { id: true }, - }); - - if (!patientExists) { - throw new NotFoundException('Patient not found.'); - } - - const patientSupport = await this.patientSupportsRepository.create({ - ...createPatientSupportDto, - patient_id: patientId, - }); - - this.logger.log( - { id: patientSupport.id, patientId: patientSupport.patient_id }, - 'Support network created successfully', - ); - - return patientSupport; - } - - async findAllByPatientId(patientId: string): Promise { - const patientExists = await this.patientsRepository.findOne({ - where: { id: patientId }, - select: { id: true }, - }); - - if (!patientExists) { - throw new NotFoundException('Patient not found.'); - } - - return await this.patientSupportsRepository.findAllByPatientId(patientId); - } - - async update( - id: string, - updatePatientsSupportDto: UpdatePatientSupportDto, - authUser: AuthUserDto, - ): Promise { - const patientSupport = await this.patientSupportsRepository.findById(id); - - if (!patientSupport) { - throw new NotFoundException('Support network not found.'); - } - - if ( - authUser.role === 'patient' && - authUser.id !== patientSupport.patient_id - ) { - throw new ForbiddenException( - 'You do not have permission to update this support network.', - ); - } - - Object.assign(patientSupport, updatePatientsSupportDto); - - await this.patientSupportsRepository.update(patientSupport); - - this.logger.log( - { id: patientSupport.id, patientId: patientSupport.patient_id }, - 'Support network updated successfully', - ); - } - - async remove(id: string, authUser: AuthUserDto): Promise { - const patientSupport = await this.patientSupportsRepository.findById(id); - - if (!patientSupport) { - throw new NotFoundException('Support network not found.'); - } - - if ( - authUser.role === 'patient' && - authUser.id !== patientSupport.patient_id - ) { - throw new ForbiddenException( - 'You do not have permission to remove this support network.', - ); - } - - await this.patientSupportsRepository.remove(patientSupport); - - this.logger.log( - { id: patientSupport.id, patientId: patientSupport.patient_id }, - 'Support network removed successfully', - ); - } -} diff --git a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts new file mode 100644 index 0000000..550ef9d --- /dev/null +++ b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts @@ -0,0 +1,48 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import { PatientSupport } from '@/domain/entities/patient-support'; + +import type { CreatePatientSupportDto } from '../patient-supports.dtos'; + +interface CreatePatientSupportUseCaseRequest { + patientId: string; + createPatientSupportDto: CreatePatientSupportDto; +} + +type CreatePatientSupportUseCaseResponse = Promise; + +@Injectable() +export class CreatePatientSupportUseCase { + private readonly logger = new Logger(CreatePatientSupportUseCase.name); + + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + @InjectRepository(PatientSupport) + private readonly patientSupportsRepository: Repository, + ) {} + + async execute({ + patientId, + createPatientSupportDto, + }: CreatePatientSupportUseCaseRequest): CreatePatientSupportUseCaseResponse { + const patient = await this.patientsRepository.findOne({ + where: { id: patientId }, + select: { id: true }, + }); + + if (!patient) { + throw new NotFoundException('Paciente não encontrado.'); + } + + await this.patientSupportsRepository.save({ + ...createPatientSupportDto, + patient_id: patientId, + }); + + this.logger.log({ patientId }, 'Support network created successfully'); + } +} diff --git a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts new file mode 100644 index 0000000..96696a3 --- /dev/null +++ b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts @@ -0,0 +1,56 @@ +import { + ForbiddenException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { PatientSupport } from '@/domain/entities/patient-support'; + +import type { AuthUserDto } from '../../auth/auth.dtos'; + +interface DeletePatientSupportUseCaseRequest { + id: string; + user: AuthUserDto; +} + +type DeletePatientSupportUseCaseResponse = Promise; + +@Injectable() +export class DeletePatientSupportUseCase { + private readonly logger = new Logger(DeletePatientSupportUseCase.name); + + constructor( + @InjectRepository(PatientSupport) + private readonly patientSupportsRepository: Repository, + ) {} + + async execute({ + id, + user, + }: DeletePatientSupportUseCaseRequest): DeletePatientSupportUseCaseResponse { + const patientSupport = await this.patientSupportsRepository.findOne({ + select: { id: true, patient_id: true }, + where: { id }, + }); + + if (!patientSupport) { + throw new NotFoundException('Contato de apoio não encontrado.'); + } + + if (user.role === 'patient' && user.id !== patientSupport.patient_id) { + throw new ForbiddenException( + 'Você não tem permissão para remover este contato de apoio.', + ); + } + + await this.patientSupportsRepository.remove(patientSupport); + + this.logger.log( + { id, patientId: patientSupport.patient_id }, + 'Support network removed successfully', + ); + } +} diff --git a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts new file mode 100644 index 0000000..bbaa775 --- /dev/null +++ b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts @@ -0,0 +1,59 @@ +import { + ForbiddenException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { PatientSupport } from '@/domain/entities/patient-support'; + +import type { AuthUserDto } from '../../auth/auth.dtos'; +import type { UpdatePatientSupportDto } from '../patient-supports.dtos'; + +interface UpdatePatientSupportUseCaseRequest { + id: string; + updatePatientSupportDto: UpdatePatientSupportDto; + user: AuthUserDto; +} + +type UpdatePatientSupportUseCaseResponse = Promise; + +@Injectable() +export class UpdatePatientSupportUseCase { + private readonly logger = new Logger(UpdatePatientSupportUseCase.name); + + constructor( + @InjectRepository(PatientSupport) + private readonly patientSupportsRepository: Repository, + ) {} + + async execute({ + id, + updatePatientSupportDto, + user, + }: UpdatePatientSupportUseCaseRequest): UpdatePatientSupportUseCaseResponse { + const patientSupport = await this.patientSupportsRepository.findOne({ + select: { id: true, patient_id: true }, + where: { id }, + }); + + if (!patientSupport) { + throw new NotFoundException('Contato de apoio não encontrado.'); + } + + if (user.role === 'patient' && user.id !== patientSupport.patient_id) { + throw new ForbiddenException( + 'Você não tem permissão para atualizar este contato de apoio.', + ); + } + + await this.patientSupportsRepository.save({ + id, + ...updatePatientSupportDto, + }); + + this.logger.log({ id }, 'Support network updated successfully'); + } +} diff --git a/src/app/http/patients/patients.controller.ts b/src/app/http/patients/patients.controller.ts index 64a6576..c65390b 100644 --- a/src/app/http/patients/patients.controller.ts +++ b/src/app/http/patients/patients.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + ForbiddenException, Get, Param, Patch, @@ -10,6 +11,7 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import { BaseResponse } from '@/domain/schemas/base'; import type { @@ -17,6 +19,7 @@ import type { GetPatientsResponse, } from '@/domain/schemas/patients/responses'; +import type { AuthUserDto } from '../auth/auth.dtos'; import { CreatePatientDto, GetPatientsQuery, @@ -86,8 +89,15 @@ export class PatientsController { @ApiOperation({ summary: 'Atualiza um paciente pelo ID' }) async update( @Param('id') id: string, + @AuthUser() user: AuthUserDto, @Body() updatePatientDto: UpdatePatientDto, ): Promise { + if (user.role === 'patient' && user.id !== id) { + throw new ForbiddenException( + 'Você não tem permissão para atualizar este paciente.', + ); + } + await this.updatePatientUseCase.execute({ id, updatePatientDto }); return { diff --git a/src/app/http/patients/patients.module.ts b/src/app/http/patients/patients.module.ts index 5d4f22c..964a085 100644 --- a/src/app/http/patients/patients.module.ts +++ b/src/app/http/patients/patients.module.ts @@ -20,6 +20,5 @@ import { UpdatePatientUseCase } from './use-cases/update-patient.use-case'; UpdatePatientUseCase, DeactivatePatientUseCase, ], - exports: [TypeOrmModule.forFeature([Patient])], }) export class PatientsModule {} diff --git a/src/app/http/referrals/referrals.controller.ts b/src/app/http/referrals/referrals.controller.ts index e034164..cb4d081 100644 --- a/src/app/http/referrals/referrals.controller.ts +++ b/src/app/http/referrals/referrals.controller.ts @@ -13,8 +13,8 @@ import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import { BaseResponse } from '@/domain/schemas/base'; import type { GetReferralsResponse } from '@/domain/schemas/referrals/responses'; -import { UserSchema } from '@/domain/schemas/users'; +import type { AuthUserDto } from '../auth/auth.dtos'; import { CreateReferralDto, GetReferralsQuery } from './referrals.dtos'; import { CancelReferralUseCase } from './use-cases/cancel-referral.use-case'; import { CreateReferralUseCase } from './use-cases/create-referrals.use-case'; @@ -48,12 +48,12 @@ export class ReferralsController { @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Cadastra um novo encaminhamento' }) async create( - @AuthUser() currentUser: UserSchema, + @AuthUser() user: AuthUserDto, @Body() createReferralDto: CreateReferralDto, ): Promise { await this.createReferralUseCase.execute({ + userId: user.id, createReferralDto, - userId: currentUser.id, }); return { success: true, message: 'Encaminhamento cadastrado com sucesso.' }; @@ -64,7 +64,7 @@ export class ReferralsController { @ApiOperation({ summary: 'Cancela um encaminhamento' }) async cancel( @Param('id') id: string, - @AuthUser() user: UserSchema, + @AuthUser() user: AuthUserDto, ): Promise { await this.cancelReferralUseCase.execute({ id, userId: user.id }); diff --git a/src/app/http/referrals/use-cases/create-referrals.use-case.ts b/src/app/http/referrals/use-cases/create-referrals.use-case.ts index f7c79ee..c8785f5 100644 --- a/src/app/http/referrals/use-cases/create-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/create-referrals.use-case.ts @@ -8,8 +8,8 @@ import { Referral } from '@/domain/entities/referral'; import { CreateReferralDto } from '../referrals.dtos'; interface CreateReferralUseCaseRequest { - createReferralDto: CreateReferralDto; userId: string; + createReferralDto: CreateReferralDto; } type CreateReferralUseCaseResponse = Promise; diff --git a/src/domain/schemas/patient-support/requests.ts b/src/domain/schemas/patient-support/requests.ts index f9edc80..93b0855 100644 --- a/src/domain/schemas/patient-support/requests.ts +++ b/src/domain/schemas/patient-support/requests.ts @@ -10,23 +10,9 @@ export const createPatientSupportSchema = patientSupportSchema.pick({ }); export type CreatePatientSupport = z.infer; -export const updatePatientSupportParamsSchema = z.object({ - id: z.string().uuid(), -}); -export type UpdatePatientSupportParams = z.infer< - typeof updatePatientSupportParamsSchema ->; - export const updatePatientSupportSchema = patientSupportSchema.pick({ name: true, phone: true, kinship: true, }); export type UpdatePatientSupport = z.infer; - -export const deletePatientSupportParamsSchema = z.object({ - id: z.string().uuid(), -}); -export type DeletePatientSupportParams = z.infer< - typeof deletePatientSupportParamsSchema ->; From 7c4c3bff98c56a036191b916408c90e92dc32ca6 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Fri, 19 Dec 2025 20:07:54 -0300 Subject: [PATCH 05/21] fix(patients): add transaction to ensure consistency if patient supports failed --- .../use-cases/create-patient.use-case.ts | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/app/http/patients/use-cases/create-patient.use-case.ts b/src/app/http/patients/use-cases/create-patient.use-case.ts index 7528d3d..c4e1748 100644 --- a/src/app/http/patients/use-cases/create-patient.use-case.ts +++ b/src/app/http/patients/use-cases/create-patient.use-case.ts @@ -1,8 +1,10 @@ import { ConflictException, Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import type { Repository } from 'typeorm'; +import { DataSource } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; +import { PatientSupport } from '@/domain/entities/patient-support'; import type { CreatePatientDto } from '../patients.dtos'; @@ -19,16 +21,18 @@ export class CreatePatientUseCase { constructor( @InjectRepository(Patient) private readonly patientsRepository: Repository, + @InjectDataSource() + private readonly dataSource: DataSource, ) {} async execute({ createPatientDto, }: CreatePatientUseCaseRequest): CreatePatientUseCaseResponse { - const { email, cpf } = createPatientDto; + const { email, cpf, supports, ...patientData } = createPatientDto; const patientWithEmail = await this.patientsRepository.findOne({ - where: { email }, select: { id: true }, + where: { email }, }); if (patientWithEmail) { @@ -40,8 +44,8 @@ export class CreatePatientUseCase { } const patientWithCpf = await this.patientsRepository.findOne({ - where: { cpf }, select: { id: true }, + where: { cpf }, }); if (patientWithCpf) { @@ -52,14 +56,34 @@ export class CreatePatientUseCase { throw new ConflictException('O CPF informado já está registrado.'); } - const patient = await this.patientsRepository.save({ - ...createPatientDto, - status: 'active', - }); + await this.dataSource.transaction(async (manager) => { + const patientsDataSource = manager.getRepository(Patient); + const patientSupportsDataSource = manager.getRepository(PatientSupport); - this.logger.log( - { patientId: patient.id, email }, - 'Patient created successfully', - ); + const patient = await patientsDataSource.save({ + ...patientData, + email, + cpf, + status: 'active', + }); + + if (supports && supports.length > 0) { + const patientSupports = supports.map((support) => + patientSupportsDataSource.create({ + name: support.name, + phone: support.phone, + kinship: support.kinship, + patient_id: patient.id, + }), + ); + + await patientSupportsDataSource.save(patientSupports); + } + + this.logger.log( + { patientId: patient.id, email }, + 'Patient created successfully', + ); + }); } } From 96652dab13ddffcd00f449b5456f9addf3ed063f Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Fri, 19 Dec 2025 21:18:59 -0300 Subject: [PATCH 06/21] refactor(patient-requirements): update module to match new use-case pattern --- .../use-cases/get-appointments.use-case.ts | 4 +- .../patient-requirements.controller.ts | 110 ++++---- .../patient-requirements.dtos.ts | 8 +- .../patient-requirements.module.ts | 27 +- .../patient-requirements.repository.ts | 253 ------------------ .../patient-requirements.service.ts | 97 ------- .../approve-patient-requirement.use-case.ts | 66 +++++ .../create-patient-requirement.use-case.ts | 59 ++++ .../decline-patient-requirement.use-case.ts | 66 +++++ ...ent-requirements-by-patient-id.use-case.ts | 82 ++++++ .../get-patient-requirements.use-case.ts | 92 +++++++ src/domain/enums/appointments.ts | 4 +- src/domain/enums/patient-requirements.ts | 4 +- src/domain/schemas/appointments/requests.ts | 4 +- .../schemas/patient-requirement/requests.ts | 5 +- .../schemas/patient-requirement/responses.ts | 1 + 16 files changed, 441 insertions(+), 441 deletions(-) delete mode 100644 src/app/http/patient-requirements/patient-requirements.repository.ts delete mode 100644 src/app/http/patient-requirements/patient-requirements.service.ts create mode 100644 src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts create mode 100644 src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts create mode 100644 src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts create mode 100644 src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts create mode 100644 src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts diff --git a/src/app/http/appointments/use-cases/get-appointments.use-case.ts b/src/app/http/appointments/use-cases/get-appointments.use-case.ts index 6cec5ff..a3dcf61 100644 --- a/src/app/http/appointments/use-cases/get-appointments.use-case.ts +++ b/src/app/http/appointments/use-cases/get-appointments.use-case.ts @@ -10,7 +10,7 @@ import { } from 'typeorm'; import { Appointment } from '@/domain/entities/appointment'; -import type { AppointmentOrderBy } from '@/domain/enums/appointments'; +import type { AppointmentsOrderBy } from '@/domain/enums/appointments'; import type { AppointmentResponse } from '@/domain/schemas/appointments/responses'; import type { AuthUserDto } from '../../auth/auth.dtos'; @@ -39,7 +39,7 @@ export class GetAppointmentsUseCase { }: GetAppointmentsUseCaseRequest): GetAppointmentsUseCaseResponse { const { search, status, category, condition, page, perPage } = query; - const ORDER_BY_MAPPING: Record = { + const ORDER_BY_MAPPING: Record = { date: 'created_at', patient: 'patient', status: 'status', diff --git a/src/app/http/patient-requirements/patient-requirements.controller.ts b/src/app/http/patient-requirements/patient-requirements.controller.ts index 2720e43..0a36d8b 100644 --- a/src/app/http/patient-requirements/patient-requirements.controller.ts +++ b/src/app/http/patient-requirements/patient-requirements.controller.ts @@ -23,28 +23,34 @@ import { GetPatientRequirementsByPatientIdQuery, GetPatientRequirementsQuery, } from './patient-requirements.dtos'; -import { PatientRequirementsRepository } from './patient-requirements.repository'; -import { PatientRequirementsService } from './patient-requirements.service'; +import { ApprovePatientRequirementUseCase } from './use-cases/approve-patient-requirement.use-case'; +import { CreatePatientRequirementUseCase } from './use-cases/create-patient-requirement.use-case'; +import { DeclinePatientRequirementUseCase } from './use-cases/decline-patient-requirement.use-case'; +import { GetPatientRequirementsUseCase } from './use-cases/get-patient-requirements.use-case'; +import { GetPatientRequirementsByPatientIdUseCase } from './use-cases/get-patient-requirements-by-patient-id.use-case'; @ApiTags('Pendências do paciente') @Controller('patient-requirements') export class PatientRequirementsController { constructor( - private readonly patientRequirementsService: PatientRequirementsService, - private readonly patientRequirementsRepository: PatientRequirementsRepository, + private readonly createPatientRequirementUseCase: CreatePatientRequirementUseCase, + private readonly approvePatientRequirementUseCase: ApprovePatientRequirementUseCase, + private readonly declinePatientRequirementUseCase: DeclinePatientRequirementUseCase, + private readonly getPatientRequirementsUseCase: GetPatientRequirementsUseCase, + private readonly getPatientRequirementsByPatientIdUseCase: GetPatientRequirementsByPatientIdUseCase, ) {} @Post() @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Adiciona nova solicitação.' }) - public async create( + @ApiOperation({ summary: 'Adiciona uma nova solicitação.' }) + async create( + @AuthUser() user: AuthUserDto, @Body() createPatientRequirementDto: CreatePatientRequirementDto, - @AuthUser() authUser: AuthUserDto, ): Promise { - await this.patientRequirementsService.create( + await this.createPatientRequirementUseCase.execute({ createPatientRequirementDto, - authUser, - ); + user, + }); return { success: true, @@ -52,14 +58,31 @@ export class PatientRequirementsController { }; } + @Get() + @Roles(['nurse', 'manager']) + @ApiOperation({ + summary: 'Lista as solicitações com paginação e filtros', + }) + async getPatientRequirements( + @Query() query: GetPatientRequirementsQuery, + ): Promise { + const data = await this.getPatientRequirementsUseCase.execute({ query }); + + return { + success: true, + message: 'Lista de solicitações retornada com sucesso', + data, + }; + } + @Patch(':id/approve') @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Aprova uma solicitação por ID.' }) + @ApiOperation({ summary: 'Aprova uma solicitação pelo ID.' }) async approve( @Param('id') id: string, @AuthUser() user: AuthUserDto, ): Promise { - await this.patientRequirementsService.approve(id, user); + await this.approvePatientRequirementUseCase.execute({ id, user }); return { success: true, @@ -69,12 +92,12 @@ export class PatientRequirementsController { @Patch(':id/decline') @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Recusa uma solicitação por ID.' }) - public async decline( + @ApiOperation({ summary: 'Recusa uma solicitação pelo ID.' }) + async decline( @Param('id') id: string, - @AuthUser() authUser: AuthUserDto, + @AuthUser() user: AuthUserDto, ): Promise { - await this.patientRequirementsService.decline(id, authUser); + await this.declinePatientRequirementUseCase.execute({ id, user }); return { success: true, @@ -82,59 +105,24 @@ export class PatientRequirementsController { }; } - @Get() - @Roles(['nurse', 'manager']) + @Get('me') @ApiOperation({ - summary: 'Lista todas as solicitações de pacientes com paginação e filtros', + summary: + 'Lista as solicitações do paciente logado com paginação e filtros.', }) - async findAll( - @Query() filters: GetPatientRequirementsQuery, - ): Promise { - const { requirements, total } = - await this.patientRequirementsRepository.findAll(filters); - - return { - success: true, - message: 'Lista de solicitações retornada com sucesso', - data: { requirements, total }, - }; - } - - @Get(':id') - @Roles(['nurse', 'manager']) - @ApiOperation({ - summary: 'Lista todas as solicitações do paciente pelo ID.', - }) - async findAllByPatientId( - @Param('id') id: string, - @Query() filters: GetPatientRequirementsByPatientIdQuery, - ): Promise { - const { requirements, total } = - await this.patientRequirementsRepository.findAllByPatientId(id, filters); - - return { - success: true, - message: 'Lista de solicitações do paciente retornada com sucesso.', - data: { requirements, total }, - }; - } - - @Get('/me') - @ApiOperation({ summary: 'Busca todas as solicitações do paciente logado.' }) - async findAllByPatientLogged( + async getPatientRequirementsLogged( @AuthUser() user: AuthUserDto, - @Query() filters: GetPatientRequirementsByPatientIdQuery, + @Query() query: GetPatientRequirementsByPatientIdQuery, ): Promise { - const { requirements, total } = - await this.patientRequirementsRepository.findAllByPatientLogged( - user.id, - filters, - ); + const data = await this.getPatientRequirementsByPatientIdUseCase.execute({ + patientId: user.id, + query, + }); return { success: true, message: 'Lista de solicitações retornada com sucesso.', - data: { requirements, total }, + data, }; } } diff --git a/src/app/http/patient-requirements/patient-requirements.dtos.ts b/src/app/http/patient-requirements/patient-requirements.dtos.ts index 10d7067..5cfe49f 100644 --- a/src/app/http/patient-requirements/patient-requirements.dtos.ts +++ b/src/app/http/patient-requirements/patient-requirements.dtos.ts @@ -10,10 +10,10 @@ export class CreatePatientRequirementDto extends createZodDto( createPatientRequirementSchema, ) {} -export class GetPatientRequirementsByPatientIdQuery extends createZodDto( - getPatientRequirementsByPatientIdQuerySchema, -) {} - export class GetPatientRequirementsQuery extends createZodDto( getPatientRequirementsQuerySchema, ) {} + +export class GetPatientRequirementsByPatientIdQuery extends createZodDto( + getPatientRequirementsByPatientIdQuerySchema, +) {} diff --git a/src/app/http/patient-requirements/patient-requirements.module.ts b/src/app/http/patient-requirements/patient-requirements.module.ts index a8a174e..26985ca 100644 --- a/src/app/http/patient-requirements/patient-requirements.module.ts +++ b/src/app/http/patient-requirements/patient-requirements.module.ts @@ -1,26 +1,25 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CryptographyModule } from '@/app/cryptography/cryptography.module'; import { Patient } from '@/domain/entities/patient'; import { PatientRequirement } from '@/domain/entities/patient-requirement'; -import { User } from '@/domain/entities/user'; -import { PatientsModule } from '../patients/patients.module'; -import { UsersModule } from '../users/users.module'; import { PatientRequirementsController } from './patient-requirements.controller'; -import { PatientRequirementsRepository } from './patient-requirements.repository'; -import { PatientRequirementsService } from './patient-requirements.service'; +import { ApprovePatientRequirementUseCase } from './use-cases/approve-patient-requirement.use-case'; +import { CreatePatientRequirementUseCase } from './use-cases/create-patient-requirement.use-case'; +import { DeclinePatientRequirementUseCase } from './use-cases/decline-patient-requirement.use-case'; +import { GetPatientRequirementsUseCase } from './use-cases/get-patient-requirements.use-case'; +import { GetPatientRequirementsByPatientIdUseCase } from './use-cases/get-patient-requirements-by-patient-id.use-case'; @Module({ - imports: [ - CryptographyModule, - UsersModule, - PatientsModule, - TypeOrmModule.forFeature([PatientRequirement, Patient, User]), - ], + imports: [TypeOrmModule.forFeature([Patient, PatientRequirement])], controllers: [PatientRequirementsController], - providers: [PatientRequirementsService, PatientRequirementsRepository], - exports: [PatientRequirementsRepository], + providers: [ + CreatePatientRequirementUseCase, + ApprovePatientRequirementUseCase, + DeclinePatientRequirementUseCase, + GetPatientRequirementsUseCase, + GetPatientRequirementsByPatientIdUseCase, + ], }) export class PatientRequirementsModule {} diff --git a/src/app/http/patient-requirements/patient-requirements.repository.ts b/src/app/http/patient-requirements/patient-requirements.repository.ts deleted file mode 100644 index 8238082..0000000 --- a/src/app/http/patient-requirements/patient-requirements.repository.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { PatientRequirement } from '@/domain/entities/patient-requirement'; -import type { PatientRequirementOrderBy } from '@/domain/enums/patient-requirements'; -import type { - PatientRequirementByPatientId, - PatientRequirementItem, -} from '@/domain/schemas/patient-requirement/responses'; - -import { - CreatePatientRequirementDto, - type GetPatientRequirementsByPatientIdQuery, - GetPatientRequirementsQuery, -} from './patient-requirements.dtos'; - -export class PatientRequirementsRepository { - constructor( - @InjectRepository(PatientRequirement) - private readonly patientRequirementsRepository: Repository, - ) {} - - public async findById(id: string): Promise { - return await this.patientRequirementsRepository.findOne({ where: { id } }); - } - - public async create( - createPatientRequirementDto: CreatePatientRequirementDto & { - required_by: string; - }, - ): Promise { - const requirementCreated = this.patientRequirementsRepository.create( - createPatientRequirementDto, - ); - return await this.patientRequirementsRepository.save(requirementCreated); - } - - public async approve( - id: string, - approvedBy: string, - ): Promise { - return this.patientRequirementsRepository.save({ - id, - status: 'approved', - approved_by: approvedBy, - approved_at: new Date(), - }); - } - - public async decline( - id: string, - declinedBy: string, - ): Promise { - return this.patientRequirementsRepository.save({ - id, - status: 'declined', - approved_by: declinedBy, - approved_at: new Date(), - }); - } - - public async findAllByPatientId( - id: string, - filters: GetPatientRequirementsByPatientIdQuery, - ): Promise<{ - requirements: PatientRequirementByPatientId[]; - total: number; - }> { - const { status, startDate, endDate, page, perPage } = filters; - - const query = this.patientRequirementsRepository - .createQueryBuilder('patientRequirements') - .where('patientRequirements.patient_id = :id', { id }); - - if (status) { - query.andWhere('patientRequirements.status = :status', { status }); - } - - if (startDate && endDate) { - query.andWhere( - 'patientRequirements.created_at BETWEEN :startDate AND :endDate', - { - startDate, - endDate, - }, - ); - } - - if (startDate && !endDate) { - query.andWhere('patientRequirements.created_at >= :startDate', { - startDate, - }); - } - - query.skip((page - 1) * perPage).take(perPage); - - const total = await query.getCount(); - const rawRequirements = await query.getMany(); - - const requirements: PatientRequirementByPatientId[] = rawRequirements.map( - (requirement) => ({ - id: requirement.id, - type: requirement.type, - title: requirement.title, - status: requirement.status, - submitted_at: requirement.submitted_at, - approved_at: requirement.approved_at, - created_at: requirement.created_at, - }), - ); - - return { requirements, total }; - } - - async findAllByPatientLogged( - patientId: string, - filters: GetPatientRequirementsByPatientIdQuery, - ): Promise<{ - requirements: PatientRequirementByPatientId[]; - total: number; - }> { - const { status, startDate, endDate, page, perPage } = filters; - - const query = this.patientRequirementsRepository - .createQueryBuilder('patientRequirements') - .where('patientRequirements.patient_id = :id', { id: patientId }); - - if (status) { - query.andWhere('patientRequirements.status = :status', { status }); - } - - if (startDate && endDate) { - query.andWhere( - 'patientRequirements.created_at BETWEEN :startDate AND :endDate', - { startDate, endDate }, - ); - } - - if (startDate && !endDate) { - query.andWhere('patientRequirements.created_at >= :startDate', { - startDate, - }); - } - - query.skip((page - 1) * perPage).take(perPage); - - const total = await query.getCount(); - const rawRequirements = await query.getMany(); - - const requirements: PatientRequirementByPatientId[] = rawRequirements.map( - (requirement) => ({ - id: requirement.id, - type: requirement.type, - title: requirement.title, - status: requirement.status, - submitted_at: requirement.submitted_at, - approved_at: requirement.approved_at, - created_at: requirement.created_at, - }), - ); - - return { requirements, total }; - } - - public async findAll(filters: GetPatientRequirementsQuery): Promise<{ - requirements: PatientRequirementItem[]; - total: number; - }> { - const { - search, - status, - order, - orderBy, - startDate, - endDate, - page, - perPage, - } = filters; - - const ORDER_BY: Record = { - name: 'user.name', - type: 'requirement.type', - status: 'requirement.status', - approved_at: 'requirement.approved_at', - submitted_at: 'requirement.submitted_at', - date: 'requirement.created_at', - }; - - const query = this.patientRequirementsRepository - .createQueryBuilder('requirement') - .leftJoinAndSelect('requirement.patient', 'patient') - .leftJoinAndSelect('patient.user', 'user') - .select([ - 'requirement.id', - 'requirement.type', - 'requirement.title', - 'requirement.description', - 'requirement.status', - 'requirement.submitted_at', - 'requirement.approved_at', - 'requirement.created_at', - 'patient.id', - 'user.name', - 'user.avatar_url', - ]); - - if (search) { - query.andWhere(`user.name LIKE :search`, { search: `%${search}%` }); - } - - if (status) { - query.andWhere('requirement.status = :status', { status }); - } - - if (startDate && endDate) { - query.andWhere('requirement.created_at BETWEEN :startDate AND :endDate', { - startDate, - endDate, - }); - } - - if (startDate && !endDate) { - query.andWhere('requirement.created_at >= :startDate', { - startDate, - }); - } - - const total = await query.getCount(); - - query.orderBy(ORDER_BY[orderBy], order); - query.skip((page - 1) * perPage).take(perPage); - - const rawRequirements = await query.getMany(); - - const requirements = rawRequirements.map((requirement) => ({ - id: requirement.id, - type: requirement.type, - title: requirement.title, - description: requirement.description, - status: requirement.status, - submitted_at: requirement.submitted_at, - approved_at: requirement.approved_at, - created_at: requirement.created_at, - patient: { - id: requirement.patient.id, - name: requirement.patient.name, - avatar_url: requirement.patient.avatar_url, - }, - })); - - return { requirements, total }; - } -} diff --git a/src/app/http/patient-requirements/patient-requirements.service.ts b/src/app/http/patient-requirements/patient-requirements.service.ts deleted file mode 100644 index 997f2d6..0000000 --- a/src/app/http/patient-requirements/patient-requirements.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - ConflictException, - Injectable, - Logger, - NotFoundException, -} from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import type { Repository } from 'typeorm'; - -import { Patient } from '@/domain/entities/patient'; - -import type { AuthUserDto } from '../auth/auth.dtos'; -import { CreatePatientRequirementDto } from './patient-requirements.dtos'; -import { PatientRequirementsRepository } from './patient-requirements.repository'; - -@Injectable() -export class PatientRequirementsService { - private readonly logger = new Logger(PatientRequirementsService.name); - - constructor( - @InjectRepository(Patient) - private readonly patientsRepository: Repository, - private readonly patientRequirementsRepository: PatientRequirementsRepository, - ) {} - - async create( - createPatientRequirementDto: CreatePatientRequirementDto, - authUser: AuthUserDto, - ): Promise { - const { patient_id } = createPatientRequirementDto; - - const patient = await this.patientsRepository.findOne({ - where: { id: patient_id }, - select: { id: true }, - }); - - if (!patient) { - throw new NotFoundException('Patient not found.'); - } - - await this.patientRequirementsRepository.create({ - ...createPatientRequirementDto, - required_by: authUser.id, - }); - - this.logger.log( - { patientId: patient_id, createdBy: authUser.id }, - 'Requirement created successfully', - ); - } - - async approve(id: string, authUser: AuthUserDto): Promise { - const patientRequirement = - await this.patientRequirementsRepository.findById(id); - - if (!patientRequirement) { - throw new NotFoundException('Request not found.'); - } - - if (patientRequirement.status !== 'under_review') { - throw new ConflictException( - 'Request must be awaiting approval to be approved.', - ); - } - - await this.patientRequirementsRepository.approve(id, authUser.id); - - this.logger.log( - { - id: patientRequirement.id, - userId: authUser.id, - approvedAt: new Date(), - }, - 'Requirement approved successfully', - ); - } - - async decline(id: string, authUser: AuthUserDto): Promise { - const requirement = await this.patientRequirementsRepository.findById(id); - - if (!requirement) { - throw new NotFoundException('Request not found.'); - } - - if (requirement.status !== 'under_review') - throw new ConflictException( - 'Request must be awaiting approval to be declined.', - ); - - await this.patientRequirementsRepository.decline(id, authUser.id); - - this.logger.log( - { id: requirement.id, userId: authUser.id, approvedAt: new Date() }, - 'Requirement declined successfully', - ); - } -} diff --git a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts new file mode 100644 index 0000000..b2cf28b --- /dev/null +++ b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts @@ -0,0 +1,66 @@ +import { + ConflictException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { PatientRequirement } from '@/domain/entities/patient-requirement'; + +import type { AuthUserDto } from '../../auth/auth.dtos'; + +interface ApprovePatientRequirementUseCaseRequest { + id: string; + user: AuthUserDto; +} + +type ApprovePatientRequirementUseCaseResponse = Promise; + +@Injectable() +export class ApprovePatientRequirementUseCase { + private readonly logger = new Logger(ApprovePatientRequirementUseCase.name); + + constructor( + @InjectRepository(PatientRequirement) + private readonly patientRequirementsRepository: Repository, + ) {} + + async execute({ + id, + user, + }: ApprovePatientRequirementUseCaseRequest): ApprovePatientRequirementUseCaseResponse { + const requirement = await this.patientRequirementsRepository.findOne({ + select: { id: true, status: true }, + where: { id }, + }); + + if (!requirement) { + this.logger.error( + { id }, + 'Approve patient requirement failed: Requirement not found', + ); + throw new NotFoundException('Solicitação não encontrada.'); + } + + if (requirement.status !== 'under_review') { + this.logger.error( + { id, status: requirement.status }, + 'Approve patient requirement failed: Invalid status', + ); + throw new ConflictException( + 'A solicitação deve estar aguardando aprovação.', + ); + } + + await this.patientRequirementsRepository.save({ + id, + status: 'approved', + approved_by: user.id, + approved_at: new Date(), + }); + + this.logger.log({ id }, 'Patient requirement approved successfully'); + } +} diff --git a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts new file mode 100644 index 0000000..e04b000 --- /dev/null +++ b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts @@ -0,0 +1,59 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import { PatientRequirement } from '@/domain/entities/patient-requirement'; + +import type { AuthUserDto } from '../../auth/auth.dtos'; +import type { CreatePatientRequirementDto } from '../patient-requirements.dtos'; + +interface CreatePatientRequirementUseCaseRequest { + createPatientRequirementDto: CreatePatientRequirementDto; + user: AuthUserDto; +} + +type CreatePatientRequirementUseCaseResponse = Promise; + +@Injectable() +export class CreatePatientRequirementUseCase { + private readonly logger = new Logger(CreatePatientRequirementUseCase.name); + + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + @InjectRepository(PatientRequirement) + private readonly patientRequirementsRepository: Repository, + ) {} + + async execute({ + createPatientRequirementDto, + user, + }: CreatePatientRequirementUseCaseRequest): CreatePatientRequirementUseCaseResponse { + const { patient_id } = createPatientRequirementDto; + + const patient = await this.patientsRepository.findOne({ + where: { id: patient_id }, + select: { id: true }, + }); + + if (!patient) { + this.logger.error( + { patientId: patient_id }, + 'Create requirement failed: Patient not found', + ); + throw new NotFoundException('Paciente não encontrado.'); + } + + await this.patientRequirementsRepository.save({ + ...createPatientRequirementDto, + required_by: user.id, + status: 'under_review', + }); + + this.logger.log( + { patientId: patient_id, userId: user.id }, + 'Requirement created successfully', + ); + } +} diff --git a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts new file mode 100644 index 0000000..1792736 --- /dev/null +++ b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts @@ -0,0 +1,66 @@ +import { + ConflictException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { PatientRequirement } from '@/domain/entities/patient-requirement'; + +import type { AuthUserDto } from '../../auth/auth.dtos'; + +interface DeclinePatientRequirementUseCaseRequest { + id: string; + user: AuthUserDto; +} + +type DeclinePatientRequirementUseCaseResponse = Promise; + +@Injectable() +export class DeclinePatientRequirementUseCase { + private readonly logger = new Logger(DeclinePatientRequirementUseCase.name); + + constructor( + @InjectRepository(PatientRequirement) + private readonly patientRequirementsRepository: Repository, + ) {} + + async execute({ + id, + user, + }: DeclinePatientRequirementUseCaseRequest): DeclinePatientRequirementUseCaseResponse { + const requirement = await this.patientRequirementsRepository.findOne({ + select: { id: true, status: true }, + where: { id }, + }); + + if (!requirement) { + this.logger.error( + { id }, + 'Decline patient requirement failed: Requirement not found', + ); + throw new NotFoundException('Solicitação não encontrada.'); + } + + if (requirement.status !== 'under_review') { + this.logger.error( + { id, status: requirement.status }, + 'Decline patient requirement failed: Invalid status', + ); + throw new ConflictException( + 'A solicitação deve estar aguardando aprovação.', + ); + } + + await this.patientRequirementsRepository.save({ + id, + status: 'declined', + approved_by: user.id, + approved_at: new Date(), + }); + + this.logger.log({ id }, 'Patient requirement declined successfully'); + } +} diff --git a/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts b/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts new file mode 100644 index 0000000..f27834c --- /dev/null +++ b/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts @@ -0,0 +1,82 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Between, + type FindOptionsWhere, + LessThanOrEqual, + MoreThanOrEqual, + type Repository, +} from 'typeorm'; + +import { PatientRequirement } from '@/domain/entities/patient-requirement'; +import type { PatientRequirementByPatientId } from '@/domain/schemas/patient-requirement/responses'; + +import type { GetPatientRequirementsByPatientIdQuery } from '../patient-requirements.dtos'; + +interface GetPatientRequirementsByPatientIdUseCaseRequest { + patientId: string; + query: GetPatientRequirementsByPatientIdQuery; +} + +type GetPatientRequirementsByPatientIdUseCaseResponse = Promise<{ + requirements: PatientRequirementByPatientId[]; + total: number; +}>; + +@Injectable() +export class GetPatientRequirementsByPatientIdUseCase { + private readonly logger = new Logger( + GetPatientRequirementsByPatientIdUseCase.name, + ); + + constructor( + @InjectRepository(PatientRequirement) + private readonly patientRequirementsRepository: Repository, + ) {} + + async execute({ + patientId, + query, + }: GetPatientRequirementsByPatientIdUseCaseRequest): GetPatientRequirementsByPatientIdUseCaseResponse { + const { status, startDate, endDate, page, perPage } = query; + + const where: FindOptionsWhere = { + patient_id: patientId, + }; + + if (status) { + where.status = status; + } + + if (startDate && endDate) { + where.created_at = Between(new Date(startDate), new Date(endDate)); + } + + if (startDate && !endDate) { + where.created_at = MoreThanOrEqual(new Date(startDate)); + } + + if (endDate && !startDate) { + where.created_at = LessThanOrEqual(new Date(endDate)); + } + + const total = await this.patientRequirementsRepository.count({ where }); + + const requirements = await this.patientRequirementsRepository.find({ + where, + select: { + id: true, + type: true, + title: true, + status: true, + submitted_at: true, + approved_at: true, + created_at: true, + }, + skip: (page - 1) * perPage, + take: perPage, + }); + + return { requirements, total }; + } +} diff --git a/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts b/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts new file mode 100644 index 0000000..885fd71 --- /dev/null +++ b/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Between, + type FindOptionsWhere, + ILike, + LessThanOrEqual, + MoreThanOrEqual, + type Repository, +} from 'typeorm'; + +import { PatientRequirement } from '@/domain/entities/patient-requirement'; +import type { PatientRequirementsOrderBy } from '@/domain/enums/patient-requirements'; +import type { PatientRequirementItem } from '@/domain/schemas/patient-requirement/responses'; + +import type { GetPatientRequirementsQuery } from '../patient-requirements.dtos'; + +interface GetPatientRequirementsUseCaseRequest { + query: GetPatientRequirementsQuery; +} + +type GetPatientRequirementsUseCaseResponse = Promise<{ + requirements: PatientRequirementItem[]; + total: number; +}>; + +@Injectable() +export class GetPatientRequirementsUseCase { + constructor( + @InjectRepository(PatientRequirement) + private readonly patientRequirementsRepository: Repository, + ) {} + + async execute({ + query, + }: GetPatientRequirementsUseCaseRequest): GetPatientRequirementsUseCaseResponse { + const { search, status, startDate, endDate, page, perPage } = query; + + const ORDER_BY_MAPPING: Record< + PatientRequirementsOrderBy, + keyof PatientRequirement + > = { + patient: 'patient', + type: 'type', + status: 'status', + approved_at: 'approved_at', + submitted_at: 'submitted_at', + date: 'created_at', + }; + + const where: FindOptionsWhere = {}; + + if (search) { + where.title = ILike(`%${search}%`); + } + + if (status) { + where.status = status; + } + + if (startDate && endDate) { + where.created_at = Between(new Date(startDate), new Date(endDate)); + } + + if (startDate && !endDate) { + where.created_at = MoreThanOrEqual(new Date(startDate)); + } + + if (endDate && !startDate) { + where.created_at = LessThanOrEqual(new Date(endDate)); + } + + const total = await this.patientRequirementsRepository.count({ where }); + + const orderBy = ORDER_BY_MAPPING[query.orderBy]; + const order = + orderBy === 'patient' + ? { patient: { name: query.order } } + : { [orderBy]: query.order }; + + const requirements = await this.patientRequirementsRepository.find({ + relations: { patient: true }, + select: { patient: { id: true, name: true, avatar_url: true } }, + skip: (page - 1) * perPage, + take: perPage, + order, + where, + }); + + return { requirements, total }; + } +} diff --git a/src/domain/enums/appointments.ts b/src/domain/enums/appointments.ts index e6c0937..df7601d 100644 --- a/src/domain/enums/appointments.ts +++ b/src/domain/enums/appointments.ts @@ -6,7 +6,7 @@ export const APPOINTMENT_STATUSES = [ ] as const; export type AppointmentStatus = (typeof APPOINTMENT_STATUSES)[number]; -export const APPOINTMENT_ORDER_BY = [ +export const APPOINTMENTS_ORDER_BY = [ 'date', 'patient', 'status', @@ -14,4 +14,4 @@ export const APPOINTMENT_ORDER_BY = [ 'condition', 'professional', ] as const; -export type AppointmentOrderBy = (typeof APPOINTMENT_ORDER_BY)[number]; +export type AppointmentsOrderBy = (typeof APPOINTMENTS_ORDER_BY)[number]; diff --git a/src/domain/enums/patient-requirements.ts b/src/domain/enums/patient-requirements.ts index 1182a85..24bbd1f 100644 --- a/src/domain/enums/patient-requirements.ts +++ b/src/domain/enums/patient-requirements.ts @@ -14,12 +14,12 @@ export type PatientRequirementStatus = (typeof PATIENT_REQUIREMENT_STATUSES)[number]; export const PATIENT_REQUIREMENTS_ORDER_BY = [ - 'name', + 'patient', 'status', 'type', 'date', 'approved_at', 'submitted_at', ] as const; -export type PatientRequirementOrderBy = +export type PatientRequirementsOrderBy = (typeof PATIENT_REQUIREMENTS_ORDER_BY)[number]; diff --git a/src/domain/schemas/appointments/requests.ts b/src/domain/schemas/appointments/requests.ts index 07730bf..868ca90 100644 --- a/src/domain/schemas/appointments/requests.ts +++ b/src/domain/schemas/appointments/requests.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; import { - APPOINTMENT_ORDER_BY, APPOINTMENT_STATUSES, + APPOINTMENTS_ORDER_BY, } from '@/domain/enums/appointments'; import { PATIENT_CONDITIONS } from '@/domain/enums/patients'; import { QUERY_ORDERS } from '@/domain/enums/queries'; @@ -40,7 +40,7 @@ export const getAppointmentsQuerySchema = baseQuerySchema status: z.enum(APPOINTMENT_STATUSES).optional(), category: z.enum(SPECIALTY_CATEGORIES).optional(), condition: z.enum(PATIENT_CONDITIONS).optional(), - orderBy: z.enum(APPOINTMENT_ORDER_BY).optional().default('date'), + orderBy: z.enum(APPOINTMENTS_ORDER_BY).optional().default('date'), order: z.enum(QUERY_ORDERS).optional().default('DESC'), }) .refine( diff --git a/src/domain/schemas/patient-requirement/requests.ts b/src/domain/schemas/patient-requirement/requests.ts index 0397e61..f996bab 100644 --- a/src/domain/schemas/patient-requirement/requests.ts +++ b/src/domain/schemas/patient-requirement/requests.ts @@ -29,10 +29,7 @@ export const getPatientRequirementsQuerySchema = baseQuerySchema }) .extend({ status: z.enum(PATIENT_REQUIREMENT_STATUSES).optional(), - orderBy: z - .enum(PATIENT_REQUIREMENTS_ORDER_BY) - .optional() - .default('approved_at'), + orderBy: z.enum(PATIENT_REQUIREMENTS_ORDER_BY).optional().default('date'), }) .refine( (data) => { diff --git a/src/domain/schemas/patient-requirement/responses.ts b/src/domain/schemas/patient-requirement/responses.ts index dad7d38..a954b88 100644 --- a/src/domain/schemas/patient-requirement/responses.ts +++ b/src/domain/schemas/patient-requirement/responses.ts @@ -36,6 +36,7 @@ export const patientRequirementByPatientIdSchema = type: true, title: true, status: true, + description: true, submitted_at: true, approved_at: true, created_at: true, From 305d7a59ffa404e2ef39c557b417f972da275066 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Fri, 19 Dec 2025 21:42:09 -0300 Subject: [PATCH 07/21] refactor(users): update module to match new use-case pattern --- .github/copilot-instructions.md | 15 +-- .../users/use-cases/create-user.use-case.ts | 51 ++++++++++ .../http/users/use-cases/get-user.use-case.ts | 39 ++++++++ .../users/use-cases/update-user.use-case.ts | 44 +++++++++ src/app/http/users/users.controller.ts | 14 ++- src/app/http/users/users.module.ts | 8 +- src/app/http/users/users.repository.ts | 45 --------- src/app/http/users/users.service.ts | 94 ------------------- src/domain/schemas/users/requests.ts | 5 - src/domain/schemas/users/responses.ts | 11 ++- 10 files changed, 159 insertions(+), 167 deletions(-) create mode 100644 src/app/http/users/use-cases/create-user.use-case.ts create mode 100644 src/app/http/users/use-cases/get-user.use-case.ts create mode 100644 src/app/http/users/use-cases/update-user.use-case.ts delete mode 100644 src/app/http/users/users.repository.ts delete mode 100644 src/app/http/users/users.service.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b4284b3..2650349 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,10 +13,7 @@ Module structure (`/src/app/http/{featureName}`): ├── {feature}.controller.ts # Routes and request handling ├── {feature}.dtos.ts # DTOs created from Zod schemas └── use-cases/ - ├── get-{feature}.use-case.ts # Read operations - ├── create-{feature}.use-case.ts # Create operations - ├── update-{feature}.use-case.ts # Update operations - └── cancel-{feature}.use-case.ts # Soft delete/cancel operations + └── {action}-{feature}.use-case.ts # Action operations, such as get, create, update, remove, cancel, delete ``` ## Module Organization @@ -32,8 +29,7 @@ Register entities, inject TypeORM repositories, and declare use-case providers: providers: [ GetFeatureUseCase, CreateFeatureUseCase, - UpdateFeatureUseCase, - CancelFeatureUseCase, + ... ], }) export class FeatureModule {} @@ -64,8 +60,7 @@ export class AppointmentsController { constructor( private readonly getAppointmentsUseCase: GetAppointmentsUseCase, private readonly createAppointmentUseCase: CreateAppointmentUseCase, - private readonly updateAppointmentUseCase: UpdateAppointmentUseCase, - private readonly cancelAppointmentUseCase: CancelAppointmentUseCase, + ... ) {} @Get() @@ -122,7 +117,7 @@ export const APPOINTMENT_STATUSES = ['scheduled', 'canceled', 'completed'] as co export type AppointmentStatus = (typeof APPOINTMENT_STATUSES)[number]; ``` -DTOs inherit validation directly from schemas—no manual definition needed. +DTOs inherit validation directly from schemas, no manual definition needed. ## Naming Conventions @@ -145,9 +140,9 @@ Files should match their exports: `get-total-patients.use-case.ts` exports `GetT ```typescript const appointments = await this.appointmentsRepository.find({ - where: { patientId: id }, select: { id: true, date: true, status: true }, relations: { patient: true }, + where: { patientId: id }, }); ``` diff --git a/src/app/http/users/use-cases/create-user.use-case.ts b/src/app/http/users/use-cases/create-user.use-case.ts new file mode 100644 index 0000000..b7a1028 --- /dev/null +++ b/src/app/http/users/use-cases/create-user.use-case.ts @@ -0,0 +1,51 @@ +import { ConflictException, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { User } from '@/domain/entities/user'; + +import type { CreateUserDto } from '../users.dtos'; + +interface CreateUserUseCaseRequest { + createUserDto: CreateUserDto; +} + +type CreateUserUseCaseResponse = Promise; + +@Injectable() +export class CreateUserUseCase { + private readonly logger = new Logger(CreateUserUseCase.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + private readonly cryptographyService: CryptographyService, + ) {} + + async execute({ + createUserDto, + }: CreateUserUseCaseRequest): CreateUserUseCaseResponse { + const { name, email, password } = createUserDto; + + const userExists = await this.usersRepository.findOne({ where: { email } }); + + if (userExists) { + throw new ConflictException( + 'Já existe uma conta cadastrada com este e-mail. Tente fazer login ou clique em "Esqueceu sua senha?" para recuperar o acesso.', + ); + } + + const hashedPassword = await this.cryptographyService.createHash(password); + + const user = await this.usersRepository.save({ + name, + email, + password: hashedPassword, + }); + + this.logger.log({ userId: user.id, email }, 'User created successfully'); + + return user; + } +} diff --git a/src/app/http/users/use-cases/get-user.use-case.ts b/src/app/http/users/use-cases/get-user.use-case.ts new file mode 100644 index 0000000..a9c907a --- /dev/null +++ b/src/app/http/users/use-cases/get-user.use-case.ts @@ -0,0 +1,39 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { User } from '@/domain/entities/user'; +import type { UserResponse } from '@/domain/schemas/users/responses'; + +interface GetUserUseCaseRequest { + id: string; +} + +type GetUserUseCaseResponse = Promise; + +@Injectable() +export class GetUserUseCase { + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + ) {} + + async execute({ id }: GetUserUseCaseRequest): GetUserUseCaseResponse { + const user = await this.usersRepository.findOne({ + where: { id }, + select: { + id: true, + name: true, + email: true, + avatar_url: true, + status: true, + }, + }); + + if (!user) { + throw new NotFoundException('Usuário não encontrado.'); + } + + return user; + } +} diff --git a/src/app/http/users/use-cases/update-user.use-case.ts b/src/app/http/users/use-cases/update-user.use-case.ts new file mode 100644 index 0000000..50a36b1 --- /dev/null +++ b/src/app/http/users/use-cases/update-user.use-case.ts @@ -0,0 +1,44 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { User } from '@/domain/entities/user'; + +import type { UpdateUserDto } from '../users.dtos'; + +interface UpdateUserUseCaseRequest { + id: string; + updateUserDto: UpdateUserDto; +} + +type UpdateUserUseCaseResponse = Promise; + +@Injectable() +export class UpdateUserUseCase { + private readonly logger = new Logger(UpdateUserUseCase.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + ) {} + + async execute({ + id, + updateUserDto, + }: UpdateUserUseCaseRequest): UpdateUserUseCaseResponse { + const user = await this.usersRepository.findOne({ where: { id } }); + + if (!user) { + throw new NotFoundException('Usuário não encontrado.'); + } + + Object.assign(user, updateUserDto); + + await this.usersRepository.save(user); + + this.logger.log( + { userId: id, email: updateUserDto.email }, + 'User updated successfully', + ); + } +} diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index d320c66..6c47e76 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -6,24 +6,22 @@ import { Roles } from '@/common/decorators/roles.decorator'; import type { GetUserResponse } from '@/domain/schemas/users/responses'; import type { AuthUserDto } from '../auth/auth.dtos'; -import { UsersService } from './users.service'; +import { GetUserUseCase } from './use-cases/get-user.use-case'; @ApiTags('Usuários') @Controller('users') export class UsersController { - constructor(private readonly usersService: UsersService) {} + constructor(private readonly getUserUseCase: GetUserUseCase) {} @Get('profile') - @Roles(['manager', 'nurse', 'specialist', 'patient']) - async getProfile( - @AuthUser() authUser: AuthUserDto, - ): Promise { - const user = await this.usersService.getProfile(authUser.id); + @Roles(['manager', 'nurse', 'specialist']) + async getProfile(@AuthUser() user: AuthUserDto): Promise { + const data = await this.getUserUseCase.execute({ id: user.id }); return { success: true, message: 'Dados do usuário retornado com sucesso.', - data: user, + data, }; } } diff --git a/src/app/http/users/users.module.ts b/src/app/http/users/users.module.ts index 43f2178..d99e635 100644 --- a/src/app/http/users/users.module.ts +++ b/src/app/http/users/users.module.ts @@ -4,14 +4,14 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CryptographyModule } from '@/app/cryptography/cryptography.module'; import { User } from '@/domain/entities/user'; +import { CreateUserUseCase } from './use-cases/create-user.use-case'; +import { GetUserUseCase } from './use-cases/get-user.use-case'; +import { UpdateUserUseCase } from './use-cases/update-user.use-case'; import { UsersController } from './users.controller'; -import { UsersRepository } from './users.repository'; -import { UsersService } from './users.service'; @Module({ imports: [TypeOrmModule.forFeature([User]), CryptographyModule], - providers: [UsersRepository, UsersService], + providers: [CreateUserUseCase, UpdateUserUseCase, GetUserUseCase], controllers: [UsersController], - exports: [UsersRepository, UsersService], }) export class UsersModule {} diff --git a/src/app/http/users/users.repository.ts b/src/app/http/users/users.repository.ts deleted file mode 100644 index b9ee869..0000000 --- a/src/app/http/users/users.repository.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { User } from '@/domain/entities/user'; - -import type { CreateUserDto, UpdateUserDto } from './users.dtos'; - -@Injectable() -export class UsersRepository { - constructor( - @InjectRepository(User) - private readonly usersRepository: Repository, - ) {} - - public async findAll(): Promise { - return await this.usersRepository.find(); - } - - public async findById(id: string): Promise { - return await this.usersRepository.findOne({ where: { id } }); - } - - public async findByEmail(email: string): Promise { - return await this.usersRepository.findOne({ where: { email } }); - } - - public async create(user: CreateUserDto): Promise { - const userCreated = this.usersRepository.create(user); - - return await this.usersRepository.save(userCreated); - } - - public async update(user: UpdateUserDto): Promise { - return await this.usersRepository.save(user); - } - - public async remove(user: User): Promise { - return await this.usersRepository.remove(user); - } - - public async updatePassword(id: string, hashedPassword: string) { - await this.usersRepository.update(id, { password: hashedPassword }); - } -} diff --git a/src/app/http/users/users.service.ts b/src/app/http/users/users.service.ts deleted file mode 100644 index b09e6ef..0000000 --- a/src/app/http/users/users.service.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - ConflictException, - Injectable, - Logger, - NotFoundException, -} from '@nestjs/common'; - -import { CryptographyService } from '@/app/cryptography/crypography.service'; -import type { User } from '@/domain/entities/user'; - -import type { CreateUserDto, UpdateUserDto } from './users.dtos'; -import { UsersRepository } from './users.repository'; - -@Injectable() -export class UsersService { - private readonly logger = new Logger(UsersService.name); - - constructor( - private readonly usersRepository: UsersRepository, - private readonly cryptographyService: CryptographyService, - ) {} - - async create(createUserDto: CreateUserDto): Promise { - const userExists = await this.usersRepository.findByEmail( - createUserDto.email, - ); - - if (userExists) { - throw new ConflictException( - 'Já existe uma conta cadastrada com este e-mail. Tente fazer login ou clique em "Esqueceu sua senha?" para recuperar o acesso.', - ); - } - - const hashPassword = await this.cryptographyService.createHash( - createUserDto.password, - ); - createUserDto.password = hashPassword; - - const user = await this.usersRepository.create(createUserDto); - - this.logger.log( - { id: user.id, email: user.email }, - 'User registered successfully', - ); - - return user; - } - - async update(id: string, updateUserDto: UpdateUserDto): Promise { - const user = await this.usersRepository.findById(id); - - if (!user) { - throw new NotFoundException('Usuário não encontrado.'); - } - - Object.assign(user, updateUserDto); - - this.logger.log( - { id: user.id, email: user.email }, - 'User updated successfully', - ); - - return await this.usersRepository.update(user); - } - - async remove(id: string): Promise { - const user = await this.usersRepository.findById(id); - - if (!user) { - throw new NotFoundException('Usuário não encontrado.'); - } - - this.logger.log( - { id: user.id, email: user.email }, - 'User removed successfully', - ); - - return await this.usersRepository.remove(user); - } - - async getProfile(id: string): Promise> { - const user = await this.usersRepository.findById(id); - - if (!user) { - throw new NotFoundException('Usuário não encontrado.'); - } - - // IMPORTANT: DO NOT RETURN USER PASSWORD - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { password, ...userWithoutPassword } = user; - - return userWithoutPassword; - } -} diff --git a/src/domain/schemas/users/requests.ts b/src/domain/schemas/users/requests.ts index 68cc580..5f97701 100644 --- a/src/domain/schemas/users/requests.ts +++ b/src/domain/schemas/users/requests.ts @@ -9,11 +9,6 @@ export const createUserSchema = userSchema.pick({ }); export type CreateUser = z.infer; -export const updateUserParamsSchema = z.object({ - id: z.string().uuid(), -}); -export type UpdateUserParams = z.infer; - export const updateUserSchema = userSchema.omit({ id: true, password: true, diff --git a/src/domain/schemas/users/responses.ts b/src/domain/schemas/users/responses.ts index cde22e6..8240677 100644 --- a/src/domain/schemas/users/responses.ts +++ b/src/domain/schemas/users/responses.ts @@ -3,7 +3,16 @@ import { z } from 'zod'; import { baseResponseSchema } from '../base'; import { userSchema } from '.'; +export const userResponseSchema = userSchema.pick({ + id: true, + name: true, + email: true, + avatar_url: true, + status: true, +}); +export type UserResponse = z.infer; + export const getUserResponseSchema = baseResponseSchema.extend({ - data: userSchema.omit({ password: true }), + data: userResponseSchema, }); export type GetUserResponse = z.infer; From 4d43c945f47942bfb9eaa38c378b2620db2cea49 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Sat, 20 Dec 2025 22:34:24 -0300 Subject: [PATCH 08/21] refactor(auth): update module to match new use-case pattern --- src/app/http/auth/auth.controller.ts | 164 ++++++++------ src/app/http/auth/auth.dtos.ts | 10 +- src/app/http/auth/auth.module.ts | 19 +- src/app/http/auth/auth.service.ts | 206 ------------------ src/app/http/auth/tokens.repository.ts | 27 --- .../http/auth/use-cases/logout.use-case.ts | 27 +++ .../use-cases/recover-password.use-case.ts | 77 +++++++ .../use-cases/register-patient.use-case.ts | 81 +++++++ .../auth/use-cases/register-user.use-case.ts | 100 +++++++++ .../auth/use-cases/reset-password.use-case.ts | 111 ++++++++++ .../use-cases/sign-in-with-email.use-case.ts | 102 +++++++++ .../users/use-cases/create-user.use-case.ts | 51 ----- src/app/http/users/users.dtos.ts | 7 +- src/app/http/users/users.module.ts | 3 +- src/domain/entities/token.ts | 2 +- src/domain/enums/auth.ts | 2 + src/domain/schemas/auth.ts | 39 +++- src/domain/schemas/tokens.ts | 12 +- src/domain/types/form-types.ts | 26 --- 19 files changed, 651 insertions(+), 415 deletions(-) delete mode 100644 src/app/http/auth/auth.service.ts delete mode 100644 src/app/http/auth/tokens.repository.ts create mode 100644 src/app/http/auth/use-cases/logout.use-case.ts create mode 100644 src/app/http/auth/use-cases/recover-password.use-case.ts create mode 100644 src/app/http/auth/use-cases/register-patient.use-case.ts create mode 100644 src/app/http/auth/use-cases/register-user.use-case.ts create mode 100644 src/app/http/auth/use-cases/reset-password.use-case.ts create mode 100644 src/app/http/auth/use-cases/sign-in-with-email.use-case.ts delete mode 100644 src/app/http/users/use-cases/create-user.use-case.ts create mode 100644 src/domain/enums/auth.ts delete mode 100644 src/domain/types/form-types.ts diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index 3a08e0b..711ea5a 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -2,55 +2,61 @@ import { Body, Controller, Post, - Req, Res, UnauthorizedException, } from '@nestjs/common'; import { ApiOperation } from '@nestjs/swagger'; -import type { Request, Response } from 'express'; +import type { Response } from 'express'; -import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Cookies } from '@/common/decorators/cookies.decorator'; import { Public } from '@/common/decorators/public.decorator'; -import { Roles } from '@/common/decorators/roles.decorator'; import { COOKIES_MAPPING } from '@/domain/cookies'; import type { BaseResponse } from '@/domain/schemas/base'; -import { UserSchema } from '@/domain/schemas/users'; import { UtilsService } from '@/utils/utils.service'; -import { CreateUserDto } from '../users/users.dtos'; import { - ChangePasswordDto, RecoverPasswordDto, + RegisterPatientDto, + RegisterUserDto, ResetPasswordDto, SignInWithEmailDto, } from './auth.dtos'; -import { AuthService } from './auth.service'; - +import { LogoutUseCase } from './use-cases/logout.use-case'; +import { RecoverPasswordUseCase } from './use-cases/recover-password.use-case'; +import { RegisterPatientUseCase } from './use-cases/register-patient.use-case'; +import { RegisterUserUseCase } from './use-cases/register-user.use-case'; +import { ResetPasswordUseCase } from './use-cases/reset-password.use-case'; +import { SignInWithEmailUseCase } from './use-cases/sign-in-with-email.use-case'; + +@Public() @Controller() export class AuthController { constructor( - private authService: AuthService, private utilsService: UtilsService, + private signInUseCase: SignInWithEmailUseCase, + private logoutUseCase: LogoutUseCase, + private recoverPasswordUseCase: RecoverPasswordUseCase, + private resetPasswordUseCase: ResetPasswordUseCase, + private registerPatientUseCase: RegisterPatientUseCase, + private registerUserUseCase: RegisterUserUseCase, ) {} - @Public() @Post('login') - @ApiOperation({ summary: 'Login do usuário' }) - async signIn( - @Req() request: Request, + @ApiOperation({ summary: 'Login de usuário ou paciente' }) + async login( @Body() signInWithEmailDto: SignInWithEmailDto, @Res({ passthrough: true }) response: Response, ): Promise { const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; - const { accessToken } = - await this.authService.signInUser(signInWithEmailDto); + const { accessToken } = await this.signInUseCase.execute({ + signInWithEmailDto, + }); this.utilsService.setCookie(response, { name: COOKIES_MAPPING.access_token, value: accessToken, - maxAge: signInWithEmailDto.rememberMe + maxAge: signInWithEmailDto.keep_logged_in ? TWELVE_HOURS_IN_MS * 60 : TWELVE_HOURS_IN_MS, }); @@ -61,59 +67,41 @@ export class AuthController { }; } - @Public() - @Post('register') - @ApiOperation({ summary: 'Registro de um novo usuário' }) - async register(@Body() createUserDto: CreateUserDto): Promise { - await this.authService.registerUser(createUserDto); - - return { - success: true, - message: 'Conta registrada com sucesso.', - }; - } - - @Public() - @Post('logout') - @ApiOperation({ summary: 'Logout do usuário' }) - async logout( - @Req() request: Request, - @Cookies('access_token') accessToken: string, + @Post('register/patient') + @ApiOperation({ summary: 'Registro de novo paciente' }) + async registerPatient( + @Body() registerPatientDto: RegisterPatientDto, @Res({ passthrough: true }) response: Response, - ) { - if (!accessToken) { - throw new UnauthorizedException('Token de acesso ausente.'); - } + ): Promise { + const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; - await this.authService.logout(accessToken); + const { accessToken } = await this.registerPatientUseCase.execute({ + registerPatientDto, + }); - this.utilsService.deleteCookie(response, COOKIES_MAPPING.access_token); + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.access_token, + value: accessToken, + maxAge: TWELVE_HOURS_IN_MS, + }); return { success: true, - message: 'Logout realizado com sucesso.', + message: 'Conta de paciente registrada com sucesso.', }; } - @Public() - @Post('reset-password') - async resetPassword( - @Req() request: Request, - @Cookies(COOKIES_MAPPING.password_reset) - passwordResetToken: string, - @Body() resetPasswordDto: ResetPasswordDto, + @Post('register/user') + @ApiOperation({ summary: 'Registro de novo usuário via convite' }) + async registerUser( + @Body() registerUserDto: RegisterUserDto, @Res({ passthrough: true }) response: Response, - ) { + ): Promise { const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; - if (!passwordResetToken) { - throw new UnauthorizedException('Token de redefinição de senha ausente.'); - } - - const { accessToken } = await this.authService.resetPassword( - passwordResetToken, - resetPasswordDto.password, - ); + const { accessToken } = await this.registerUserUseCase.execute({ + registerUserDto, + }); this.utilsService.setCookie(response, { name: COOKIES_MAPPING.access_token, @@ -123,28 +111,26 @@ export class AuthController { return { success: true, - message: 'Senha atualizada com sucesso.', + message: 'Conta de usuário registrada com sucesso.', }; } - @Public() @Post('recover-password') @ApiOperation({ summary: 'Recuperação de senha' }) async recoverPassword( - @Req() request: Request, @Body() recoverPasswordDto: RecoverPasswordDto, @Res({ passthrough: true }) response: Response, ): Promise { - const { passwordResetToken } = await this.authService.forgotPassword( - recoverPasswordDto.email, - ); + const { resetToken } = await this.recoverPasswordUseCase.execute({ + recoverPasswordDto, + }); const FOUR_HOURS_IN_MS = 1000 * 60 * 60 * 4; this.utilsService.setCookie(response, { name: COOKIES_MAPPING.password_reset, - value: passwordResetToken, maxAge: FOUR_HOURS_IN_MS, + value: resetToken, }); return { @@ -154,17 +140,53 @@ export class AuthController { }; } - @Post('change-password') - @Roles(['nurse', 'manager', 'specialist', 'admin']) - async changePassword( - @Body() changePasswordDto: ChangePasswordDto, - @AuthUser() user: UserSchema, + @Post('reset-password') + @ApiOperation({ summary: 'Redefinição de senha' }) + async resetPassword( + @Cookies(COOKIES_MAPPING.password_reset) token: string, + @Body() resetPasswordDto: ResetPasswordDto, + @Res({ passthrough: true }) response: Response, ): Promise { - await this.authService.changePassword(user, changePasswordDto); + if (!token) { + throw new UnauthorizedException('Token de redefinição de senha ausente.'); + } + + const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; + + const { accessToken } = await this.resetPasswordUseCase.execute({ + resetPasswordDto, + token, + }); + + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.access_token, + maxAge: TWELVE_HOURS_IN_MS, + value: accessToken, + }); return { success: true, message: 'Senha atualizada com sucesso.', }; } + + @Post('logout') + @ApiOperation({ summary: 'Logout' }) + async logout( + @Cookies('access_token') accessToken: string, + @Res({ passthrough: true }) response: Response, + ): Promise { + if (!accessToken) { + throw new UnauthorizedException('Token de acesso ausente.'); + } + + await this.logoutUseCase.execute({ token: accessToken }); + + this.utilsService.deleteCookie(response, COOKIES_MAPPING.access_token); + + return { + success: true, + message: 'Logout realizado com sucesso.', + }; + } } diff --git a/src/app/http/auth/auth.dtos.ts b/src/app/http/auth/auth.dtos.ts index ea8d235..26404e7 100644 --- a/src/app/http/auth/auth.dtos.ts +++ b/src/app/http/auth/auth.dtos.ts @@ -4,6 +4,8 @@ import { authUserSchema, changePasswordSchema, recoverPasswordSchema, + registerPatientSchema, + registerUserSchema, resetPasswordSchema, signInWithEmailSchema, } from '@/domain/schemas/auth'; @@ -11,12 +13,16 @@ import { createAuthTokenSchema } from '@/domain/schemas/tokens'; export class AuthUserDto extends createZodDto(authUserSchema) {} -export class SignInWithEmailDto extends createZodDto(signInWithEmailSchema) {} +export class RegisterPatientDto extends createZodDto(registerPatientSchema) {} -export class CreateAuthTokenDto extends createZodDto(createAuthTokenSchema) {} +export class RegisterUserDto extends createZodDto(registerUserSchema) {} + +export class SignInWithEmailDto extends createZodDto(signInWithEmailSchema) {} export class RecoverPasswordDto extends createZodDto(recoverPasswordSchema) {} export class ResetPasswordDto extends createZodDto(resetPasswordSchema) {} export class ChangePasswordDto extends createZodDto(changePasswordSchema) {} + +export class CreateAuthTokenDto extends createZodDto(createAuthTokenSchema) {} diff --git a/src/app/http/auth/auth.module.ts b/src/app/http/auth/auth.module.ts index d42f58e..1d2db59 100644 --- a/src/app/http/auth/auth.module.ts +++ b/src/app/http/auth/auth.module.ts @@ -3,7 +3,6 @@ import { APP_GUARD } from '@nestjs/core'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CryptographyModule } from '@/app/cryptography/cryptography.module'; -import { MailModule } from '@/app/mail/mail.module'; import { AuthGuard } from '@/common/guards/auth.guard'; import { RolesGuard } from '@/common/guards/roles.guard'; import { Patient } from '@/domain/entities/patient'; @@ -14,8 +13,12 @@ import { UtilsModule } from '@/utils/utils.module'; import { UsersModule } from '../users/users.module'; import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { TokensRepository } from './tokens.repository'; +import { LogoutUseCase } from './use-cases/logout.use-case'; +import { RecoverPasswordUseCase } from './use-cases/recover-password.use-case'; +import { RegisterPatientUseCase } from './use-cases/register-patient.use-case'; +import { RegisterUserUseCase } from './use-cases/register-user.use-case'; +import { ResetPasswordUseCase } from './use-cases/reset-password.use-case'; +import { SignInWithEmailUseCase } from './use-cases/sign-in-with-email.use-case'; @Module({ imports: [ @@ -23,16 +26,18 @@ import { TokensRepository } from './tokens.repository'; CryptographyModule, UsersModule, UtilsModule, - MailModule, EnvModule, ], providers: [ - AuthService, - TokensRepository, + SignInWithEmailUseCase, + LogoutUseCase, + RecoverPasswordUseCase, + ResetPasswordUseCase, + RegisterPatientUseCase, + RegisterUserUseCase, { provide: APP_GUARD, useClass: AuthGuard }, { provide: APP_GUARD, useClass: RolesGuard }, ], controllers: [AuthController], - exports: [AuthService, TokensRepository], }) export class AuthModule {} diff --git a/src/app/http/auth/auth.service.ts b/src/app/http/auth/auth.service.ts deleted file mode 100644 index eefb49d..0000000 --- a/src/app/http/auth/auth.service.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; - -import { CryptographyService } from '@/app/cryptography/crypography.service'; -import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; -import { UserSchema } from '@/domain/schemas/users'; -import { EnvService } from '@/env/env.service'; - -import type { CreateUserDto } from '../users/users.dtos'; -import { UsersRepository } from '../users/users.repository'; -import { UsersService } from '../users/users.service'; -import type { ChangePasswordDto, SignInWithEmailDto } from './auth.dtos'; -import { TokensRepository } from './tokens.repository'; - -@Injectable() -export class AuthService { - private readonly logger = new Logger(AuthService.name); - - constructor( - private readonly usersRepository: UsersRepository, - private readonly usersService: UsersService, - private readonly cryptographyService: CryptographyService, - private readonly tokensRepository: TokensRepository, - private readonly envService: EnvService, - ) {} - - async registerUser(createUserDto: CreateUserDto): Promise { - await this.usersService.create(createUserDto); - - // TODO: create e-mail template builder - // const subject = 'Verifique seu e-mail de cadastro'; - // const body = `Confirmar e-mail`; - - // await this.mailService.sendEmail(createUserDto.email, subject, body); - } - - async signInUser({ - email, - password, - rememberMe, - }: SignInWithEmailDto): Promise<{ accessToken: string }> { - const user = await this.usersRepository.findByEmail(email); - - if (!user) { - throw new UnauthorizedException( - 'Credenciais inválidas. Por favor, tente novamente.', - ); - } - - const verifyPassword = await this.cryptographyService.compareHash( - password, - user.password, - ); - - if (!verifyPassword) { - throw new UnauthorizedException( - 'Credenciais inválidas. Por favor, tente novamente.', - ); - } - - const expiresIn = rememberMe ? '30d' : '12h'; - - const accessToken = await this.cryptographyService.createToken( - AUTH_TOKENS_MAPPING.access_token, - { sub: user.id, role: user.role }, - { expiresIn }, - ); - - const expiration = new Date(); - expiration.setHours(expiration.getHours() + (rememberMe ? 24 * 30 : 12)); - - await this.tokensRepository.saveToken({ - user_id: user.id, - email: null, - token: accessToken, - type: AUTH_TOKENS_MAPPING.access_token, - expires_at: expiration, - }); - - this.logger.log({ id: user.id, email: user.email }, 'User logged in'); - - return { accessToken }; - } - - async logout(token: string): Promise { - await this.tokensRepository.deleteToken(token); - } - - async forgotPassword(email: string): Promise<{ passwordResetToken: string }> { - const user = await this.usersRepository.findByEmail(email); - - if (!user) { - this.logger.warn( - { email }, - 'Attempt to recover password for non-registered email failed', - ); - return { passwordResetToken: 'dummy_token' }; - } - - const payload = { sub: user.id, role: user.role }; - - const passwordResetToken = await this.cryptographyService.createToken( - AUTH_TOKENS_MAPPING.password_reset, - payload, - { expiresIn: '4h' }, - ); - - const expiration = new Date(); - expiration.setHours(expiration.getHours() + 4); - - await this.tokensRepository.saveToken({ - user_id: user.id, - email: null, - token: passwordResetToken, - type: AUTH_TOKENS_MAPPING.password_reset, - expires_at: expiration, - }); - - const appUrl = this.envService.get('APP_URL'); - const resetUrl = `${appUrl}/conta/nova-senha?token=${passwordResetToken}`; - - this.logger.log( - { id: user.id, email: user.email }, - 'Reset password token generated successfully', - ); - - // Log da URL (substituindo o envio de email por enquanto) - this.logger.log({ url: resetUrl }, 'Password reset URL'); - - return { passwordResetToken }; - } - - async resetPassword( - token: string, - newPassword: string, - ): Promise<{ accessToken: string }> { - const resetToken = await this.tokensRepository.findToken(token); - - if ( - !resetToken || - !resetToken.user_id || - resetToken.type !== AUTH_TOKENS_MAPPING.password_reset || - (resetToken.expires_at && resetToken.expires_at < new Date()) - ) { - throw new UnauthorizedException( - 'Token de redefinição de senha inválido ou expirado.', - ); - } - - const user = await this.usersRepository.findById(resetToken.user_id); - - if (!user) { - throw new UnauthorizedException('Usuário não encontrado.'); - } - - const hashedPassword = - await this.cryptographyService.createHash(newPassword); - - await this.usersRepository.updatePassword(user.id, hashedPassword); - - this.logger.log( - { userId: user.id, email: user.email }, - 'Password update successfully', - ); - - await this.tokensRepository.deleteToken(token); - - const { accessToken } = await this.signInUser({ - email: user.email, - password: newPassword, - rememberMe: false, - }); - - return { accessToken }; - } - - async changePassword(user: UserSchema, changePasswordDto: ChangePasswordDto) { - const { password, newPassword } = changePasswordDto; - - const userFound = await this.usersRepository.findById(user.id); - - if (!userFound) { - throw new UnauthorizedException('Usuário não encontrado.'); - } - - const verifyPassword = await this.cryptographyService.compareHash( - password, - userFound.password, - ); - - if (!verifyPassword) { - throw new UnauthorizedException( - 'Credenciais inválidas. Por favor, tente novamente.', - ); - } - - const hashedPassword = - await this.cryptographyService.createHash(newPassword); - - await this.usersRepository.updatePassword(user.id, hashedPassword); - - this.logger.log( - { userId: user.id, email: user.email }, - 'Password update successfully', - ); - } -} diff --git a/src/app/http/auth/tokens.repository.ts b/src/app/http/auth/tokens.repository.ts deleted file mode 100644 index 1ec71b1..0000000 --- a/src/app/http/auth/tokens.repository.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { Token } from '@/domain/entities/token'; - -import type { CreateAuthTokenDto } from './auth.dtos'; - -@Injectable() -export class TokensRepository { - constructor( - @InjectRepository(Token) - private readonly tokensRepository: Repository, - ) {} - - async saveToken(data: CreateAuthTokenDto) { - await this.tokensRepository.save(data); - } - - async findToken(token: string) { - return this.tokensRepository.findOne({ where: { token } }); - } - - async deleteToken(token: string) { - await this.tokensRepository.delete({ token }); - } -} diff --git a/src/app/http/auth/use-cases/logout.use-case.ts b/src/app/http/auth/use-cases/logout.use-case.ts new file mode 100644 index 0000000..8b87218 --- /dev/null +++ b/src/app/http/auth/use-cases/logout.use-case.ts @@ -0,0 +1,27 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { Token } from '@/domain/entities/token'; + +interface LogoutUseCaseRequest { + token: string; +} + +type LogoutUseCaseResponse = Promise; + +@Injectable() +export class LogoutUseCase { + private readonly logger = new Logger(LogoutUseCase.name); + + constructor( + @InjectRepository(Token) + private readonly tokensRepository: Repository, + ) {} + + async execute({ token }: LogoutUseCaseRequest): LogoutUseCaseResponse { + await this.tokensRepository.delete({ token }); + + this.logger.log({}, 'User logged out'); + } +} diff --git a/src/app/http/auth/use-cases/recover-password.use-case.ts b/src/app/http/auth/use-cases/recover-password.use-case.ts new file mode 100644 index 0000000..2b97f0a --- /dev/null +++ b/src/app/http/auth/use-cases/recover-password.use-case.ts @@ -0,0 +1,77 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { Patient } from '@/domain/entities/patient'; +import { Token } from '@/domain/entities/token'; +import { User } from '@/domain/entities/user'; +import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; + +import type { RecoverPasswordDto } from '../auth.dtos'; + +interface RecoverPasswordUseCaseRequest { + recoverPasswordDto: RecoverPasswordDto; +} + +type RecoverPasswordUseCaseResponse = Promise<{ resetToken: string }>; + +@Injectable() +export class RecoverPasswordUseCase { + private readonly logger = new Logger(RecoverPasswordUseCase.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + @InjectRepository(Token) + private readonly tokensRepository: Repository, + private readonly cryptographyService: CryptographyService, + ) {} + + async execute({ + recoverPasswordDto, + }: RecoverPasswordUseCaseRequest): RecoverPasswordUseCaseResponse { + const { email, account_type: accountType } = recoverPasswordDto; + + const findOptions = { select: { id: true }, where: { email } }; + + const entity = + accountType === 'patient' + ? await this.patientsRepository.findOne(findOptions) + : await this.usersRepository.findOne(findOptions); + + if (!entity) { + this.logger.warn( + { email, accountType }, + 'Attempt to recover password for non-registered email', + ); + + return { resetToken: 'dummy_token' }; + } + + const resetToken = await this.cryptographyService.createToken( + AUTH_TOKENS_MAPPING.password_reset, + { sub: entity.id, accountType }, + { expiresIn: '4h' }, + ); + + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 4); + + await this.tokensRepository.save({ + type: AUTH_TOKENS_MAPPING.password_reset, + expires_at: expiresAt, + entity_id: entity.id, + token: resetToken, + }); + + this.logger.log( + { entityId: entity.id, email, accountType }, + 'Password reset token generated successfully', + ); + + return { resetToken }; + } +} diff --git a/src/app/http/auth/use-cases/register-patient.use-case.ts b/src/app/http/auth/use-cases/register-patient.use-case.ts new file mode 100644 index 0000000..752075b --- /dev/null +++ b/src/app/http/auth/use-cases/register-patient.use-case.ts @@ -0,0 +1,81 @@ +import { ConflictException, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { Patient } from '@/domain/entities/patient'; +import { Token } from '@/domain/entities/token'; +import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; + +import type { RegisterPatientDto } from '../auth.dtos'; + +interface RegisterPatientUseCaseRequest { + registerPatientDto: RegisterPatientDto; +} + +type RegisterPatientUseCaseResponse = Promise<{ accessToken: string }>; + +@Injectable() +export class RegisterPatientUseCase { + private readonly logger = new Logger(RegisterPatientUseCase.name); + + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + @InjectRepository(Token) + private readonly tokensRepository: Repository, + private readonly cryptographyService: CryptographyService, + ) {} + + async execute({ + registerPatientDto, + }: RegisterPatientUseCaseRequest): RegisterPatientUseCaseResponse { + const { email, name, password } = registerPatientDto; + + const patient = await this.patientsRepository.findOne({ + select: { id: true }, + where: { email }, + }); + + if (patient) { + this.logger.error( + { email }, + 'Patient registration failed: Email already registered', + ); + throw new ConflictException( + 'Já existe uma conta cadastrada com este e-mail. Tente fazer login ou clique em "Esqueceu sua senha?" para recuperar o acesso.', + ); + } + + const hashedPassword = await this.cryptographyService.createHash(password); + + const newPatient = await this.patientsRepository.save({ + password: hashedPassword, + email, + name, + }); + + this.logger.log( + { patientId: newPatient.id, email }, + 'Patient registered successfully', + ); + + const accessToken = await this.cryptographyService.createToken( + AUTH_TOKENS_MAPPING.access_token, + { sub: newPatient.id, role: 'patient' }, + { expiresIn: '12h' }, + ); + + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 12); + + await this.tokensRepository.save({ + type: AUTH_TOKENS_MAPPING.access_token, + entity_id: newPatient.id, + expires_at: expiresAt, + token: accessToken, + }); + + return { accessToken }; + } +} diff --git a/src/app/http/auth/use-cases/register-user.use-case.ts b/src/app/http/auth/use-cases/register-user.use-case.ts new file mode 100644 index 0000000..4346e68 --- /dev/null +++ b/src/app/http/auth/use-cases/register-user.use-case.ts @@ -0,0 +1,100 @@ +import { + ConflictException, + Injectable, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { Token } from '@/domain/entities/token'; +import { User } from '@/domain/entities/user'; +import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; + +import { RegisterUserDto } from '../auth.dtos'; + +interface RegisterUserUseCaseRequest { + registerUserDto: RegisterUserDto; +} + +type RegisterUserUseCaseResponse = Promise<{ accessToken: string }>; + +@Injectable() +export class RegisterUserUseCase { + private readonly logger = new Logger(RegisterUserUseCase.name); + + constructor( + @InjectRepository(Token) + private readonly tokensRepository: Repository, + @InjectRepository(User) + private readonly usersRepository: Repository, + private readonly cryptographyService: CryptographyService, + ) {} + + async execute({ + registerUserDto, + }: RegisterUserUseCaseRequest): RegisterUserUseCaseResponse { + const { invite_token: token, name, password } = registerUserDto; + + const inviteToken = await this.tokensRepository.findOne({ + where: { token }, + }); + + if ( + !inviteToken || + inviteToken.type !== AUTH_TOKENS_MAPPING.invite_token || + (inviteToken.expires_at && inviteToken.expires_at < new Date()) + ) { + throw new UnauthorizedException('Token de convite inválido ou expirado.'); + } + + const email = inviteToken.email; + + if (!email) { + throw new UnauthorizedException('Token de convite inválido.'); + } + + const user = await this.usersRepository.findOne({ + select: { id: true }, + where: { email }, + }); + + if (user) { + throw new ConflictException('Este e-mail já está cadastrado.'); + } + + const hashedPassword = await this.cryptographyService.createHash(password); + + const newUser = await this.usersRepository.save({ + password: hashedPassword, + email, + name, + }); + + this.logger.log( + { userId: newUser.id, email, role: newUser.role }, + 'User registered successfully', + ); + + await this.tokensRepository.delete({ token }); + + const accessToken = await this.cryptographyService.createToken( + AUTH_TOKENS_MAPPING.access_token, + { sub: newUser.id, role: newUser.role }, + { expiresIn: '12h' }, + ); + + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 12); + + await this.tokensRepository.save({ + type: AUTH_TOKENS_MAPPING.access_token, + expires_at: expiresAt, + entity_id: newUser.id, + token: accessToken, + }); + + return { accessToken }; + } +} diff --git a/src/app/http/auth/use-cases/reset-password.use-case.ts b/src/app/http/auth/use-cases/reset-password.use-case.ts new file mode 100644 index 0000000..95cae6e --- /dev/null +++ b/src/app/http/auth/use-cases/reset-password.use-case.ts @@ -0,0 +1,111 @@ +import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { Patient } from '@/domain/entities/patient'; +import { Token } from '@/domain/entities/token'; +import { User } from '@/domain/entities/user'; +import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; +import type { UserRole } from '@/domain/enums/users'; + +import type { ResetPasswordDto } from '../auth.dtos'; + +interface ResetPasswordUseCaseRequest { + token: string; + resetPasswordDto: ResetPasswordDto; +} + +type ResetPasswordUseCaseResponse = Promise<{ accessToken: string }>; + +@Injectable() +export class ResetPasswordUseCase { + private readonly logger = new Logger(ResetPasswordUseCase.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + @InjectRepository(Token) + private readonly tokensRepository: Repository, + private readonly cryptographyService: CryptographyService, + ) {} + + async execute({ + token, + resetPasswordDto, + }: ResetPasswordUseCaseRequest): ResetPasswordUseCaseResponse { + const resetToken = await this.tokensRepository.findOne({ + where: { token }, + }); + + if ( + !resetToken || + !resetToken.entity_id || + resetToken.type !== AUTH_TOKENS_MAPPING.password_reset || + (resetToken.expires_at && resetToken.expires_at < new Date()) + ) { + throw new UnauthorizedException( + 'Token de redefinição de senha inválido ou expirado.', + ); + } + + const { account_type: accountType, password } = resetPasswordDto; + + const findOptions = { + select: { id: true, email: true, role: true }, + where: { id: resetToken.entity_id }, + }; + + const entity: { id: string; email: string; role?: UserRole } | null = + accountType === 'patient' + ? await this.patientsRepository.findOne(findOptions) + : await this.usersRepository.findOne(findOptions); + + if (!entity) { + this.logger.warn( + { id: resetToken.entity_id, accountType }, + 'Reset password failed: Entity not registered', + ); + throw new UnauthorizedException('Usuário não encontrado.'); + } + + const hashedPassword = await this.cryptographyService.createHash(password); + + if (accountType === 'patient') { + await this.usersRepository.update(entity.id, { + password: hashedPassword, + }); + } else { + await this.patientsRepository.update(entity.id, { + password: hashedPassword, + }); + } + + await this.tokensRepository.delete({ token }); + + this.logger.log( + { id: entity.id, email: entity.email, accountType }, + 'Password reseted successfully', + ); + + const accessToken = await this.cryptographyService.createToken( + AUTH_TOKENS_MAPPING.access_token, + { sub: entity.id, role: entity.role ?? 'patient' }, + { expiresIn: '12h' }, + ); + + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 12); + + await this.tokensRepository.save({ + type: AUTH_TOKENS_MAPPING.access_token, + expires_at: expiresAt, + entity_id: entity.id, + token: accessToken, + }); + + return { accessToken }; + } +} diff --git a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts new file mode 100644 index 0000000..91955c6 --- /dev/null +++ b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts @@ -0,0 +1,102 @@ +import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { Patient } from '@/domain/entities/patient'; +import { Token } from '@/domain/entities/token'; +import { User } from '@/domain/entities/user'; +import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; +import type { UserRole } from '@/domain/enums/users'; + +import type { SignInWithEmailDto } from '../auth.dtos'; + +interface SignInWithEmailUseCaseRequest { + signInWithEmailDto: SignInWithEmailDto; +} + +type SignInWithEmailUseCaseResponse = Promise<{ accessToken: string }>; + +@Injectable() +export class SignInWithEmailUseCase { + private readonly logger = new Logger(SignInWithEmailUseCase.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + @InjectRepository(Token) + private readonly tokensRepository: Repository, + private readonly cryptographyService: CryptographyService, + ) {} + + async execute({ + signInWithEmailDto, + }: SignInWithEmailUseCaseRequest): SignInWithEmailUseCaseResponse { + const { + email, + password, + keep_logged_in: keepLoggedIn, + } = signInWithEmailDto; + + const findOptions = { + select: { id: true, password: true, role: true }, + where: { email }, + }; + + const entity: { + id: string; + password: string | null; + role?: UserRole; + } | null = + signInWithEmailDto.account_type === 'patient' + ? await this.patientsRepository.findOne(findOptions) + : await this.usersRepository.findOne(findOptions); + + if (!entity || !entity.password) { + throw new UnauthorizedException( + 'Credenciais inválidas. Por favor, tente novamente.', + ); + } + + const passwordMatches = await this.cryptographyService.compareHash( + password, + entity.password, + ); + + if (!passwordMatches) { + throw new UnauthorizedException( + 'Credenciais inválidas. Por favor, tente novamente.', + ); + } + + const accessToken = await this.cryptographyService.createToken( + AUTH_TOKENS_MAPPING.access_token, + { sub: entity.id, role: entity.role ?? 'patient' }, + { expiresIn: keepLoggedIn ? '30d' : '12h' }, + ); + + const expiresAt = new Date(); + + if (keepLoggedIn) { + expiresAt.setDate(expiresAt.getDate() + 30); + } else { + expiresAt.setHours(expiresAt.getHours() + 12); + } + + await this.tokensRepository.save({ + type: AUTH_TOKENS_MAPPING.access_token, + expires_at: expiresAt, + entity_id: entity.id, + token: accessToken, + }); + + this.logger.log( + { entityId: entity.id, email }, + 'Entity signed in with e-mail', + ); + + return { accessToken }; + } +} diff --git a/src/app/http/users/use-cases/create-user.use-case.ts b/src/app/http/users/use-cases/create-user.use-case.ts deleted file mode 100644 index b7a1028..0000000 --- a/src/app/http/users/use-cases/create-user.use-case.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ConflictException, Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { CryptographyService } from '@/app/cryptography/crypography.service'; -import { User } from '@/domain/entities/user'; - -import type { CreateUserDto } from '../users.dtos'; - -interface CreateUserUseCaseRequest { - createUserDto: CreateUserDto; -} - -type CreateUserUseCaseResponse = Promise; - -@Injectable() -export class CreateUserUseCase { - private readonly logger = new Logger(CreateUserUseCase.name); - - constructor( - @InjectRepository(User) - private readonly usersRepository: Repository, - private readonly cryptographyService: CryptographyService, - ) {} - - async execute({ - createUserDto, - }: CreateUserUseCaseRequest): CreateUserUseCaseResponse { - const { name, email, password } = createUserDto; - - const userExists = await this.usersRepository.findOne({ where: { email } }); - - if (userExists) { - throw new ConflictException( - 'Já existe uma conta cadastrada com este e-mail. Tente fazer login ou clique em "Esqueceu sua senha?" para recuperar o acesso.', - ); - } - - const hashedPassword = await this.cryptographyService.createHash(password); - - const user = await this.usersRepository.save({ - name, - email, - password: hashedPassword, - }); - - this.logger.log({ userId: user.id, email }, 'User created successfully'); - - return user; - } -} diff --git a/src/app/http/users/users.dtos.ts b/src/app/http/users/users.dtos.ts index 3f9d9ff..64caeb1 100644 --- a/src/app/http/users/users.dtos.ts +++ b/src/app/http/users/users.dtos.ts @@ -1,10 +1,5 @@ import { createZodDto } from 'nestjs-zod'; -import { - createUserSchema, - updateUserSchema, -} from '@/domain/schemas/users/requests'; - -export class CreateUserDto extends createZodDto(createUserSchema) {} +import { updateUserSchema } from '@/domain/schemas/users/requests'; export class UpdateUserDto extends createZodDto(updateUserSchema) {} diff --git a/src/app/http/users/users.module.ts b/src/app/http/users/users.module.ts index d99e635..cd437a0 100644 --- a/src/app/http/users/users.module.ts +++ b/src/app/http/users/users.module.ts @@ -4,14 +4,13 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CryptographyModule } from '@/app/cryptography/cryptography.module'; import { User } from '@/domain/entities/user'; -import { CreateUserUseCase } from './use-cases/create-user.use-case'; import { GetUserUseCase } from './use-cases/get-user.use-case'; import { UpdateUserUseCase } from './use-cases/update-user.use-case'; import { UsersController } from './users.controller'; @Module({ imports: [TypeOrmModule.forFeature([User]), CryptographyModule], - providers: [CreateUserUseCase, UpdateUserUseCase, GetUserUseCase], + providers: [UpdateUserUseCase, GetUserUseCase], controllers: [UsersController], }) export class UsersModule {} diff --git a/src/domain/entities/token.ts b/src/domain/entities/token.ts index 38a12ea..625c308 100644 --- a/src/domain/entities/token.ts +++ b/src/domain/entities/token.ts @@ -14,7 +14,7 @@ export class Token implements AuthToken { id: number; @Column({ type: 'uuid', nullable: true }) - user_id: string | null; + entity_id: string | null; @Column({ type: 'varchar', nullable: true }) email: string | null; diff --git a/src/domain/enums/auth.ts b/src/domain/enums/auth.ts new file mode 100644 index 0000000..250f7e1 --- /dev/null +++ b/src/domain/enums/auth.ts @@ -0,0 +1,2 @@ +export const AUTH_ACCOUNT_TYPES = ['user', 'patient'] as const; +export type AuthAccountType = (typeof AUTH_ACCOUNT_TYPES)[number]; diff --git a/src/domain/schemas/auth.ts b/src/domain/schemas/auth.ts index 8b413db..7e23cf4 100644 --- a/src/domain/schemas/auth.ts +++ b/src/domain/schemas/auth.ts @@ -1,33 +1,48 @@ import { z } from 'zod'; +import { AUTH_ACCOUNT_TYPES } from '../enums/auth'; import { AUTH_TOKEN_ROLES } from '../enums/tokens'; +import { emailSchema, nameSchema, passwordSchema } from './shared'; export const authUserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(AUTH_TOKEN_ROLES), }); -export type AuthUserSchema = z.infer; + +const accountTypeSchema = z.enum(AUTH_ACCOUNT_TYPES); + +export const registerPatientSchema = z.object({ + name: nameSchema, + email: emailSchema, + password: passwordSchema, +}); + +export const registerUserSchema = z.object({ + name: nameSchema, + password: passwordSchema, + invite_token: z.string().min(1), +}); export const signInWithEmailSchema = z.object({ - email: z.string().email(), - password: z.string().min(8), - rememberMe: z.boolean().default(false), + email: emailSchema, + password: passwordSchema, + keep_logged_in: z.boolean().default(false), + account_type: accountTypeSchema, }); -export type SignInWithEmailSchema = z.infer; export const recoverPasswordSchema = z.object({ - email: z.string().email('E-mail inválido'), + email: emailSchema, + account_type: accountTypeSchema, }); -export type RecoverPasswordSchema = z.infer; export const resetPasswordSchema = z.object({ - password: z.string().min(8).max(255), + password: passwordSchema, + account_type: accountTypeSchema, }); -export type ResetPasswordSchema = z.infer; export const changePasswordSchema = z.object({ - password: z.string().min(8).max(255), - newPassword: z.string().min(8).max(255), + password: passwordSchema, + new_password: passwordSchema, + account_type: accountTypeSchema, }); -export type ChangePasswordSchema = z.infer; diff --git a/src/domain/schemas/tokens.ts b/src/domain/schemas/tokens.ts index c96873e..1090095 100644 --- a/src/domain/schemas/tokens.ts +++ b/src/domain/schemas/tokens.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; +import type { AuthAccountType } from '../enums/auth'; import { AUTH_TOKENS, type AUTH_TOKENS_MAPPING, @@ -9,7 +10,7 @@ import { export const authTokenSchema = z .object({ id: z.number().int().positive(), - user_id: z.string().uuid().nullable(), + entity_id: z.string().uuid().nullable(), email: z.string().email().nullable(), token: z.string(), type: z.enum(AUTH_TOKENS), @@ -20,7 +21,7 @@ export const authTokenSchema = z export type AuthToken = z.infer; export const createAuthTokenSchema = authTokenSchema.pick({ - user_id: true, + entity_id: true, email: true, token: true, type: true, @@ -28,11 +29,14 @@ export const createAuthTokenSchema = authTokenSchema.pick({ }); export type AccessTokenPayload = { sub: string; role: AuthTokenRole }; -export type PasswordResetPayload = { sub: string; role: AuthTokenRole }; export type InviteTokenPayload = { sub: string; role: AuthTokenRole }; +export type PasswordResetPayload = { + sub: string; + accountType: AuthAccountType; +}; export type AuthTokenPayloads = { [AUTH_TOKENS_MAPPING.access_token]: AccessTokenPayload; - [AUTH_TOKENS_MAPPING.password_reset]: PasswordResetPayload; [AUTH_TOKENS_MAPPING.invite_token]: InviteTokenPayload; + [AUTH_TOKENS_MAPPING.password_reset]: PasswordResetPayload; }; diff --git a/src/domain/types/form-types.ts b/src/domain/types/form-types.ts deleted file mode 100644 index f1b2fda..0000000 --- a/src/domain/types/form-types.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type FormType = 'triagem'; // Adicione outros no futuro: | 'anamnese' | 'consentimento' - -export type PendingForm = { - formType: FormType; - missingFields: Array< - | 'desc_gender' - | 'birth_of_date' - | 'city' - | 'state' - | 'whatsapp' - | 'cpf' - | 'url_photo' - | 'have_disability' - | 'need_legal_help' - | 'use_medicine' - | 'id_diagnostic' - | 'support' - >; -}; - -export type PatientFormsStatus = { - patientId: number; - patientName: string; - pendingForms: PendingForm[]; - completedForms: FormType[]; -}; From e0bf4f3d2f9ecb1e491619b73db22da9d5929038 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Wed, 24 Dec 2025 16:17:03 -0300 Subject: [PATCH 09/21] refactor: improve readability and simplify use-cases --- .github/copilot-instructions.md | 48 ++++++---- ...37-Initial.ts => 1766604246802-Initial.ts} | 8 +- infra/database/seed-dev.ts | 95 +++++++------------ .../appointments/appointments.controller.ts | 6 +- .../use-cases/cancel-appointment.use-case.ts | 3 +- .../use-cases/create-appointment.use-case.ts | 13 ++- .../use-cases/get-appointments.use-case.ts | 4 +- .../use-cases/update-appointment.use-case.ts | 2 +- .../use-cases/register-patient.use-case.ts | 14 ++- .../auth/use-cases/register-user.use-case.ts | 14 +-- .../auth/use-cases/reset-password.use-case.ts | 29 +++--- .../use-cases/sign-in-with-email.use-case.ts | 9 +- .../patient-requirements.controller.ts | 36 +++---- .../approve-patient-requirement.use-case.ts | 13 +-- .../create-patient-requirement.use-case.ts | 19 ++-- .../decline-patient-requirement.use-case.ts | 17 ++-- ...ent-requirements-by-patient-id.use-case.ts | 17 ++-- .../get-patient-requirements.use-case.ts | 10 +- .../patient-supports.controller.ts | 21 +--- .../create-patient-support.use-case.ts | 35 ++++++- .../delete-patient-support.use-case.ts | 24 ++++- .../update-patient-support.use-case.ts | 29 +++++- src/app/http/patients/patients.controller.ts | 21 ++-- .../use-cases/create-patient.use-case.ts | 35 ++++--- .../use-cases/deactivate-patient.use-case.ts | 22 +++-- .../use-cases/get-patients.use-case.ts | 19 ++-- .../use-cases/update-patient.use-case.ts | 58 ++++++++--- .../use-cases/get-referrals.use-case.ts | 4 +- .../users/use-cases/update-user.use-case.ts | 30 ++++-- src/domain/entities/patient-requirement.ts | 6 ++ .../schemas/patient-requirement/index.ts | 2 + .../schemas/patient-requirement/responses.ts | 2 + 32 files changed, 391 insertions(+), 274 deletions(-) rename infra/database/migrations/{1766169704537-Initial.ts => 1766604246802-Initial.ts} (89%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2650349..576492d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,6 +7,7 @@ NestJS SaaS API managing patients, referrals, appointments, and health tracking. **Stack**: NestJS + TypeORM + MySQL + Zod Module structure (`/src/app/http/{featureName}`): + ``` {feature}/ ├── {feature}.module.ts # Module definition with imports/providers @@ -41,11 +42,21 @@ Create DTOs exclusively from Zod schemas in `/domain/schemas`. Use `createZodDto ```typescript import { createZodDto } from 'nestjs-zod'; -import { createAppointmentSchema, updateAppointmentSchema, getAppointmentsQuerySchema } from '@/domain/schemas/appointments/requests'; - -export class CreateAppointmentDto extends createZodDto(createAppointmentSchema) {} -export class UpdateAppointmentDto extends createZodDto(updateAppointmentSchema) {} -export class GetAppointmentsQuery extends createZodDto(getAppointmentsQuerySchema) {} +import { + createAppointmentSchema, + updateAppointmentSchema, + getAppointmentsQuerySchema, +} from '@/domain/schemas/appointments/requests'; + +export class CreateAppointmentDto extends createZodDto( + createAppointmentSchema, +) {} +export class UpdateAppointmentDto extends createZodDto( + updateAppointmentSchema, +) {} +export class GetAppointmentsQuery extends createZodDto( + getAppointmentsQuerySchema, +) {} ``` **Naming**: `{Action}{Entity}Dto` (e.g., `CreateAppointmentDto`, `GetAppointmentsQuery`) @@ -70,8 +81,8 @@ export class AppointmentsController { } @Post() - async create(@Body() dto: CreateAppointmentDto): Promise { - await this.createAppointmentUseCase.execute(dto); + async create(@Body() createAppointmentDto: CreateAppointmentDto): Promise { + await this.createAppointmentUseCase.execute(createAppointmentDto); return { success: true }; } } @@ -96,7 +107,9 @@ export class GetAppointmentsUseCase { private readonly appointmentsRepository: Repository, ) {} - async execute(request: GetAppointmentsUseCaseRequest): GetAppointmentsUseCaseResponse { + async execute( + request: GetAppointmentsUseCaseRequest, + ): GetAppointmentsUseCaseResponse { // Implementation } } @@ -112,8 +125,13 @@ Centralize validation and types in `/domain/schemas` and `/domain/enums`: - **Enums** (`/domain/enums/{entity}.ts`): Define constants and types Example enum pattern: + ```typescript -export const APPOINTMENT_STATUSES = ['scheduled', 'canceled', 'completed'] as const; +export const APPOINTMENT_STATUSES = [ + 'scheduled', + 'canceled', + 'completed', +] as const; export type AppointmentStatus = (typeof APPOINTMENT_STATUSES)[number]; ``` @@ -134,7 +152,7 @@ Files should match their exports: `get-total-patients.use-case.ts` exports `GetT ### Queries -- **Always select fields**: `select: { id: true, name: true }`—avoid over-fetching +- **Always select fields**: `select: { id: true, name: true }` — avoid over-fetching - **Count operations**: Select only `id` for performance - **Relations**: Destructure explicitly: `relations: { user: true }` @@ -175,7 +193,9 @@ if (!patient) { } if (date > maxDate) { - throw new BadRequestException('A data de atendimento deve estar dentro dos próximos 3 meses.'); + throw new BadRequestException( + 'A data de atendimento deve estar dentro dos próximos 3 meses.', + ); } ``` @@ -186,11 +206,7 @@ Log significant events in use-cases with English messages: ```typescript private readonly logger = new Logger(CreateAppointmentUseCase.name); -this.logger.log({ - patientId, - appointmentId, - userId, -}, 'Appointment created successfully'); +this.logger.log({ patientId, appointmentId, createdBy }, 'Appointment created successfully'); ``` ### Query Builders diff --git a/infra/database/migrations/1766169704537-Initial.ts b/infra/database/migrations/1766604246802-Initial.ts similarity index 89% rename from infra/database/migrations/1766169704537-Initial.ts rename to infra/database/migrations/1766604246802-Initial.ts index 949dadf..a8af669 100644 --- a/infra/database/migrations/1766169704537-Initial.ts +++ b/infra/database/migrations/1766604246802-Initial.ts @@ -1,15 +1,15 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class Initial1766169704537 implements MigrationInterface { - name = 'Initial1766169704537' +export class Initial1766604246802 implements MigrationInterface { + name = 'Initial1766604246802' public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE \`patient_requirements\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`type\` enum ('screening', 'medical_report') NOT NULL, \`title\` varchar(255) NOT NULL, \`description\` varchar(500) NULL, \`status\` enum ('pending', 'under_review', 'approved', 'declined') NOT NULL DEFAULT 'pending', \`submitted_at\` datetime NULL, \`approved_by\` varchar(255) NULL, \`approved_at\` datetime NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`patient_requirements\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`type\` enum ('screening', 'medical_report') NOT NULL, \`title\` varchar(255) NOT NULL, \`description\` varchar(500) NULL, \`status\` enum ('pending', 'under_review', 'approved', 'declined') NOT NULL DEFAULT 'pending', \`submitted_at\` datetime NULL, \`approved_by\` varchar(255) NULL, \`approved_at\` datetime NULL, \`declined_by\` varchar(255) NULL, \`declined_at\` datetime NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`patient_supports\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`name\` varchar(64) NOT NULL, \`phone\` varchar(11) NOT NULL, \`kinship\` varchar(50) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`referrals\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(2000) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`patients\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NULL, \`avatar_url\` varchar(255) NULL, \`status\` enum ('active', 'inactive', 'pending') NOT NULL DEFAULT 'pending', \`gender\` enum ('male_cis', 'female_cis', 'male_trans', 'female_trans', 'non_binary', 'prefer_not_to_say') NOT NULL DEFAULT 'prefer_not_to_say', \`date_of_birth\` datetime NULL, \`phone\` varchar(11) NULL, \`cpf\` varchar(11) NULL, \`state\` enum ('AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO') NULL, \`city\` varchar(255) NULL, \`has_disability\` tinyint(1) NOT NULL DEFAULT '0', \`disability_desc\` varchar(500) NULL, \`need_legal_assistance\` tinyint(1) NOT NULL DEFAULT '0', \`take_medication\` tinyint(1) NOT NULL DEFAULT '0', \`medication_desc\` varchar(500) NULL, \`nmo_diagnosis\` enum ('anti_aqp4_positive', 'anti_mog_positive', 'both_negative', 'no_diagnosis') NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_5947301223f5a908fd5e372b0f\` (\`cpf\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`appointments\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(500) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`user_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'password_reset', 'invite_token') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`entity_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'password_reset', 'invite_token') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`users\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NOT NULL, \`avatar_url\` varchar(255) NULL, \`role\` enum ('admin', 'manager', 'nurse', 'specialist') NOT NULL, \`status\` enum ('active', 'inactive') NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`ALTER TABLE \`patient_requirements\` ADD CONSTRAINT \`FK_77b87c61cff4793ae6a4ac50070\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE \`patient_supports\` ADD CONSTRAINT \`FK_62c23ddd34837a0c09faf875425\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); diff --git a/infra/database/seed-dev.ts b/infra/database/seed-dev.ts index f80a1cf..7393b12 100644 --- a/infra/database/seed-dev.ts +++ b/infra/database/seed-dev.ts @@ -26,7 +26,6 @@ import { REFERRAL_STATUSES } from '@/domain/enums/referrals'; import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; import { USER_ROLES } from '@/domain/enums/users'; -// import { SPECIALIST_STATUS } from '@/domain/schemas/specialist'; import dataSource from './data.source'; const DATABASE_DEV_NAME = 'abnmo_dev'; @@ -70,65 +69,31 @@ async function main() { await dataSource.manager.clear(Referral); await dataSource.manager.clear(PatientRequirement); await dataSource.manager.clear(Patient); - // await dataSource.manager.clear(Specialist); await dataSource.manager.clear(User); await dataSource.query('SET FOREIGN_KEY_CHECKS = 1'); console.log('✅ Old data deleted.'); - const userRepository = dataSource.getRepository(User); - const patientRepository = dataSource.getRepository(Patient); - const supportNetworkRepository = dataSource.getRepository(PatientSupport); - // const specialistRepository = dataSource.getRepository(Specialist); - const appointmentRepository = dataSource.getRepository(Appointment); - const patientRequirementRepository = + const usersRepository = dataSource.getRepository(User); + const patientsRepository = dataSource.getRepository(Patient); + const patientSupportsRepository = dataSource.getRepository(PatientSupport); + const appointmentsRepository = dataSource.getRepository(Appointment); + const referralsRepository = dataSource.getRepository(Referral); + const patientRequirementsRepository = dataSource.getRepository(PatientRequirement); - const referralRepository = dataSource.getRepository(Referral); console.log('👤 Creating users...'); for (const role of USER_ROLES) { - const user = userRepository.create({ + const user = usersRepository.create({ name: faker.person.fullName(), email: `${role}@ipecode.com.br`, password, role, avatar_url: faker.image.avatar(), }); - await userRepository.save(user); + await usersRepository.save(user); } console.log('👤 Users created successfully...'); - // const specialties = [ - // 'Cirurgia oncológica', - // 'Cirurgia geral', - // 'Cirurgia plástica', - // 'Geriatria', - // 'Mastologia', - // 'Medicina preventiva e social', - // ]; - - // console.log('👨‍⚕️ Creating 5 specialists...'); - // const specialists: Specialist[] = []; - // for (let i = 0; i < 5; i++) { - // const user = userRepository.create({ - // name: faker.person.fullName(), - // email: faker.internet.email().toLocaleLowerCase(), - // password, - // role: 'specialist', - // avatar_url: faker.image.avatar(), - // }); - // await userRepository.save(user); - - // const specialist = specialistRepository.create({ - // user_id: user.id, - // specialty: faker.helpers.arrayElement(specialties), - // registry: faker.string.numeric(10), - // status: faker.helpers.arrayElement(SPECIALIST_STATUS), - // }); - // const savedSpecialist = await specialistRepository.save(specialist); - // specialists.push(savedSpecialist); - // } - // console.log('👨‍⚕️ Specialists created successfully...'); - const oneMonthAgo = new Date(); oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); const twoMonthsAgo = new Date(); @@ -139,7 +104,7 @@ async function main() { const twoMonthsAhead = new Date(); twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() + 2); - await patientRepository.save({ + const patient = patientsRepository.create({ name: faker.person.fullName(), email: 'patient@ipecode.com.br', password, @@ -159,6 +124,7 @@ async function main() { nmo_diagnosis: faker.helpers.arrayElement(PATIENT_NMO_DIAGNOSTICS), created_at: faker.date.between({ from: fourMonthsAgo, to: new Date() }), }); + await patientsRepository.save(patient); const totalOfPatients = 100; for (let i = 0; i < totalOfPatients; i++) { @@ -178,7 +144,7 @@ async function main() { ); } - const patient = patientRepository.create({ + const newPatient = patientsRepository.create({ name: faker.person.fullName(), email: faker.internet.email().toLowerCase(), password, @@ -198,12 +164,12 @@ async function main() { nmo_diagnosis: faker.helpers.arrayElement(PATIENT_NMO_DIAGNOSTICS), created_at: faker.date.between({ from: fourMonthsAgo, to: new Date() }), }); - await patientRepository.save(patient); + await patientsRepository.save(newPatient); const supportNetworkCount = faker.number.int({ min: 0, max: 3 }); for (let j = 0; j < supportNetworkCount; j++) { - const support = supportNetworkRepository.create({ - patient: patient, + const support = patientSupportsRepository.create({ + patient: newPatient, name: faker.person.fullName(), phone: faker.string.numeric(11), kinship: faker.helpers.arrayElement([ @@ -214,16 +180,16 @@ async function main() { 'Avó', ]), }); - await supportNetworkRepository.save(support); + await patientSupportsRepository.save(support); } - // Create between 0 and 2 referrals for each patient - const referralCount = faker.number.int({ min: 0, max: 2 }); - for (let j = 0; j < referralCount; j++) { - await referralRepository.save({ - patient_id: patient.id, + // Create between 0 and 2 appointments for each patient + const appointmentCount = faker.number.int({ min: 0, max: 2 }); + for (let j = 0; j < appointmentCount; j++) { + const appointment = appointmentsRepository.create({ + patient_id: newPatient.id, date: faker.date.between({ from: twoMonthsAgo, to: twoMonthsAhead }), - status: faker.helpers.arrayElement(REFERRAL_STATUSES), + status: faker.helpers.arrayElement(APPOINTMENT_STATUSES), category: faker.helpers.arrayElement(SPECIALTY_CATEGORIES), condition: faker.helpers.arrayElement(PATIENT_CONDITIONS), annotation: faker.datatype.boolean() ? faker.lorem.sentence() : null, @@ -234,15 +200,16 @@ async function main() { to: new Date(), }), }); + await appointmentsRepository.save(appointment); } - // Create between 0 and 2 appointments for each patient - const appointmentCount = faker.number.int({ min: 0, max: 2 }); - for (let j = 0; j < appointmentCount; j++) { - await appointmentRepository.save({ - patient_id: patient.id, + // Create between 0 and 2 referrals for each patient + const referralCount = faker.number.int({ min: 0, max: 2 }); + for (let j = 0; j < referralCount; j++) { + const referral = referralsRepository.create({ + patient_id: newPatient.id, date: faker.date.between({ from: twoMonthsAgo, to: twoMonthsAhead }), - status: faker.helpers.arrayElement(APPOINTMENT_STATUSES), + status: faker.helpers.arrayElement(REFERRAL_STATUSES), category: faker.helpers.arrayElement(SPECIALTY_CATEGORIES), condition: faker.helpers.arrayElement(PATIENT_CONDITIONS), annotation: faker.datatype.boolean() ? faker.lorem.sentence() : null, @@ -253,14 +220,15 @@ async function main() { to: new Date(), }), }); + await referralsRepository.save(referral); } // Create between 0 and 2 requirements for each patient const requirementCount = faker.number.int({ min: 0, max: 2 }); for (let j = 0; j < requirementCount; j++) { const status = faker.helpers.arrayElement(PATIENT_REQUIREMENT_STATUSES); - await patientRequirementRepository.save({ - patient_id: patient.id, + const patientRequirement = patientRequirementsRepository.create({ + patient_id: newPatient.id, type: faker.helpers.arrayElement(PATIENT_REQUIREMENT_TYPES), title: faker.lorem.words(3), description: faker.lorem.sentence(), @@ -279,6 +247,7 @@ async function main() { to: new Date(), }), }); + await patientRequirementsRepository.save(patientRequirement); } } diff --git a/src/app/http/appointments/appointments.controller.ts b/src/app/http/appointments/appointments.controller.ts index 7cd3fa8..3f85153 100644 --- a/src/app/http/appointments/appointments.controller.ts +++ b/src/app/http/appointments/appointments.controller.ts @@ -54,12 +54,12 @@ export class AppointmentsController { @Post() @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Cadastra novo atendimento' }) + @ApiOperation({ summary: 'Cadastra um novo atendimento' }) async create( @AuthUser() user: AuthUserDto, @Body() createAppointmentDto: CreateAppointmentDto, ): Promise { - await this.createAppointmentUseCase.execute({ createAppointmentDto, user }); + await this.createAppointmentUseCase.execute({ user, createAppointmentDto }); return { success: true, @@ -76,8 +76,8 @@ export class AppointmentsController { ): Promise { await this.updateAppointmentUseCase.execute({ id, - updateAppointmentDto, user, + updateAppointmentDto, }); return { diff --git a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts index 6904c6f..271334f 100644 --- a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts @@ -32,6 +32,7 @@ export class CancelAppointmentUseCase { user, }: CancelAppointmentUseCaseRequest): CancelAppointmentUseCaseResponse { const appointment = await this.appointmentsRepository.findOne({ + select: { id: true, status: true }, where: { id }, }); @@ -46,7 +47,7 @@ export class CancelAppointmentUseCase { await this.appointmentsRepository.save({ id, status: 'canceled' }); this.logger.log( - { appointmentId: id, userId: user.id, userEmail: user.email }, + { id, userId: user.id, userEmail: user.email, role: user.role }, 'Appointment canceled successfully.', ); } diff --git a/src/app/http/appointments/use-cases/create-appointment.use-case.ts b/src/app/http/appointments/use-cases/create-appointment.use-case.ts index 4fb920f..eae8d4b 100644 --- a/src/app/http/appointments/use-cases/create-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/create-appointment.use-case.ts @@ -30,7 +30,7 @@ export class CreateAppointmentUseCase { createAppointmentDto, user, }: CreateAppointmentUseCaseRequest): CreateAppointmentUseCaseResponse { - const { patient_id, date } = createAppointmentDto; + const { patient_id: patientId, date } = createAppointmentDto; const MAX_APPOINTMENT_MONTHS_LIMIT = 3; const appointmentDate = new Date(date); @@ -47,25 +47,28 @@ export class CreateAppointmentUseCase { } const patient = await this.patientsRepository.findOne({ + where: { id: patientId }, select: { id: true }, - where: { id: patient_id }, }); if (!patient) { throw new BadRequestException('Paciente não encontrado.'); } - const appointment = await this.appointmentsRepository.save({ + const appointment = this.appointmentsRepository.create({ ...createAppointmentDto, created_by: user.id, }); + await this.appointmentsRepository.save(appointment); + this.logger.log( { - patientId: patient_id, - appointmentId: appointment.id, + id: appointment.id, + patientId: patientId, userId: user.id, userEmail: user.email, + role: user.role, }, 'Appointment created successfully', ); diff --git a/src/app/http/appointments/use-cases/get-appointments.use-case.ts b/src/app/http/appointments/use-cases/get-appointments.use-case.ts index a3dcf61..d87cec1 100644 --- a/src/app/http/appointments/use-cases/get-appointments.use-case.ts +++ b/src/app/http/appointments/use-cases/get-appointments.use-case.ts @@ -38,6 +38,8 @@ export class GetAppointmentsUseCase { query, }: GetAppointmentsUseCaseRequest): GetAppointmentsUseCaseResponse { const { search, status, category, condition, page, perPage } = query; + const startDate = query.startDate ? new Date(query.startDate) : null; + const endDate = query.endDate ? new Date(query.endDate) : null; const ORDER_BY_MAPPING: Record = { date: 'created_at', @@ -49,8 +51,6 @@ export class GetAppointmentsUseCase { }; const where: FindOptionsWhere = {}; - const startDate = query.startDate ? new Date(query.startDate) : null; - const endDate = query.endDate ? new Date(query.endDate) : null; if (user.role === 'patient') { where.patient = { id: user.id }; diff --git a/src/app/http/appointments/use-cases/update-appointment.use-case.ts b/src/app/http/appointments/use-cases/update-appointment.use-case.ts index f491cd3..8478e90 100644 --- a/src/app/http/appointments/use-cases/update-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/update-appointment.use-case.ts @@ -53,7 +53,7 @@ export class UpdateAppointmentUseCase { await this.appointmentsRepository.save(appointment); this.logger.log( - { appointmentId: id, userId: user.id, userEmail: user.email }, + { id, userId: user.id, userEmail: user.email, role: user.role }, 'Appointment updated successfully.', ); } diff --git a/src/app/http/auth/use-cases/register-patient.use-case.ts b/src/app/http/auth/use-cases/register-patient.use-case.ts index 752075b..2ff18b5 100644 --- a/src/app/http/auth/use-cases/register-patient.use-case.ts +++ b/src/app/http/auth/use-cases/register-patient.use-case.ts @@ -30,7 +30,7 @@ export class RegisterPatientUseCase { async execute({ registerPatientDto, }: RegisterPatientUseCaseRequest): RegisterPatientUseCaseResponse { - const { email, name, password } = registerPatientDto; + const { email, name } = registerPatientDto; const patient = await this.patientsRepository.findOne({ select: { id: true }, @@ -47,14 +47,18 @@ export class RegisterPatientUseCase { ); } - const hashedPassword = await this.cryptographyService.createHash(password); + const password = await this.cryptographyService.createHash( + registerPatientDto.password, + ); - const newPatient = await this.patientsRepository.save({ - password: hashedPassword, - email, + const newPatient = this.patientsRepository.create({ name, + email, + password, }); + await this.patientsRepository.save(newPatient); + this.logger.log( { patientId: newPatient.id, email }, 'Patient registered successfully', diff --git a/src/app/http/auth/use-cases/register-user.use-case.ts b/src/app/http/auth/use-cases/register-user.use-case.ts index 4346e68..23e5ef4 100644 --- a/src/app/http/auth/use-cases/register-user.use-case.ts +++ b/src/app/http/auth/use-cases/register-user.use-case.ts @@ -35,7 +35,7 @@ export class RegisterUserUseCase { async execute({ registerUserDto, }: RegisterUserUseCaseRequest): RegisterUserUseCaseResponse { - const { invite_token: token, name, password } = registerUserDto; + const { invite_token: token, name } = registerUserDto; const inviteToken = await this.tokensRepository.findOne({ where: { token }, @@ -64,13 +64,13 @@ export class RegisterUserUseCase { throw new ConflictException('Este e-mail já está cadastrado.'); } - const hashedPassword = await this.cryptographyService.createHash(password); + const password = await this.cryptographyService.createHash( + registerUserDto.password, + ); - const newUser = await this.usersRepository.save({ - password: hashedPassword, - email, - name, - }); + const newUser = this.usersRepository.create({ name, email, password }); + + await this.usersRepository.save(newUser); this.logger.log( { userId: newUser.id, email, role: newUser.role }, diff --git a/src/app/http/auth/use-cases/reset-password.use-case.ts b/src/app/http/auth/use-cases/reset-password.use-case.ts index 95cae6e..02d80af 100644 --- a/src/app/http/auth/use-cases/reset-password.use-case.ts +++ b/src/app/http/auth/use-cases/reset-password.use-case.ts @@ -1,4 +1,9 @@ -import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; +import { + Injectable, + Logger, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -51,10 +56,14 @@ export class ResetPasswordUseCase { ); } - const { account_type: accountType, password } = resetPasswordDto; + const { account_type: accountType } = resetPasswordDto; const findOptions = { - select: { id: true, email: true, role: true }, + select: { + id: true, + email: true, + role: accountType === 'patient' ? undefined : true, + }, where: { id: resetToken.entity_id }, }; @@ -68,19 +77,17 @@ export class ResetPasswordUseCase { { id: resetToken.entity_id, accountType }, 'Reset password failed: Entity not registered', ); - throw new UnauthorizedException('Usuário não encontrado.'); + throw new NotFoundException('Usuário não encontrado.'); } - const hashedPassword = await this.cryptographyService.createHash(password); + const password = await this.cryptographyService.createHash( + resetPasswordDto.password, + ); if (accountType === 'patient') { - await this.usersRepository.update(entity.id, { - password: hashedPassword, - }); + await this.usersRepository.update(entity.id, { password }); } else { - await this.patientsRepository.update(entity.id, { - password: hashedPassword, - }); + await this.patientsRepository.update(entity.id, { password }); } await this.tokensRepository.delete({ token }); diff --git a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts index 91955c6..1fa0e9e 100644 --- a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts +++ b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts @@ -38,10 +38,15 @@ export class SignInWithEmailUseCase { email, password, keep_logged_in: keepLoggedIn, + account_type: accountType, } = signInWithEmailDto; const findOptions = { - select: { id: true, password: true, role: true }, + select: { + id: true, + password: true, + role: accountType === 'patient' ? undefined : true, + }, where: { email }, }; @@ -50,7 +55,7 @@ export class SignInWithEmailUseCase { password: string | null; role?: UserRole; } | null = - signInWithEmailDto.account_type === 'patient' + accountType === 'patient' ? await this.patientsRepository.findOne(findOptions) : await this.usersRepository.findOne(findOptions); diff --git a/src/app/http/patient-requirements/patient-requirements.controller.ts b/src/app/http/patient-requirements/patient-requirements.controller.ts index 0a36d8b..e444652 100644 --- a/src/app/http/patient-requirements/patient-requirements.controller.ts +++ b/src/app/http/patient-requirements/patient-requirements.controller.ts @@ -40,24 +40,6 @@ export class PatientRequirementsController { private readonly getPatientRequirementsByPatientIdUseCase: GetPatientRequirementsByPatientIdUseCase, ) {} - @Post() - @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Adiciona uma nova solicitação.' }) - async create( - @AuthUser() user: AuthUserDto, - @Body() createPatientRequirementDto: CreatePatientRequirementDto, - ): Promise { - await this.createPatientRequirementUseCase.execute({ - createPatientRequirementDto, - user, - }); - - return { - success: true, - message: 'Solicitação adicionada com sucesso.', - }; - } - @Get() @Roles(['nurse', 'manager']) @ApiOperation({ @@ -75,6 +57,24 @@ export class PatientRequirementsController { }; } + @Post() + @Roles(['nurse', 'manager']) + @ApiOperation({ summary: 'Adiciona uma nova solicitação.' }) + async create( + @AuthUser() user: AuthUserDto, + @Body() createPatientRequirementDto: CreatePatientRequirementDto, + ): Promise { + await this.createPatientRequirementUseCase.execute({ + user, + createPatientRequirementDto, + }); + + return { + success: true, + message: 'Solicitação adicionada com sucesso.', + }; + } + @Patch(':id/approve') @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Aprova uma solicitação pelo ID.' }) diff --git a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts index b2cf28b..35f115d 100644 --- a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts @@ -37,18 +37,10 @@ export class ApprovePatientRequirementUseCase { }); if (!requirement) { - this.logger.error( - { id }, - 'Approve patient requirement failed: Requirement not found', - ); throw new NotFoundException('Solicitação não encontrada.'); } if (requirement.status !== 'under_review') { - this.logger.error( - { id, status: requirement.status }, - 'Approve patient requirement failed: Invalid status', - ); throw new ConflictException( 'A solicitação deve estar aguardando aprovação.', ); @@ -61,6 +53,9 @@ export class ApprovePatientRequirementUseCase { approved_at: new Date(), }); - this.logger.log({ id }, 'Patient requirement approved successfully'); + this.logger.log( + { id, userId: user.id, userEmail: user.email, role: user.role }, + 'Patient requirement approved successfully', + ); } } diff --git a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts index e04b000..ff31256 100644 --- a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts @@ -38,21 +38,24 @@ export class CreatePatientRequirementUseCase { }); if (!patient) { - this.logger.error( - { patientId: patient_id }, - 'Create requirement failed: Patient not found', - ); throw new NotFoundException('Paciente não encontrado.'); } - await this.patientRequirementsRepository.save({ + const patientRequirement = this.patientRequirementsRepository.create({ ...createPatientRequirementDto, - required_by: user.id, - status: 'under_review', + created_by: user.id, }); + await this.patientRequirementsRepository.save(patientRequirement); + this.logger.log( - { patientId: patient_id, userId: user.id }, + { + id: patientRequirement.id, + patientId: patient_id, + userId: user.id, + userEmail: user.email, + role: user.role, + }, 'Requirement created successfully', ); } diff --git a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts index 1792736..0c3afff 100644 --- a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts @@ -37,18 +37,10 @@ export class DeclinePatientRequirementUseCase { }); if (!requirement) { - this.logger.error( - { id }, - 'Decline patient requirement failed: Requirement not found', - ); throw new NotFoundException('Solicitação não encontrada.'); } if (requirement.status !== 'under_review') { - this.logger.error( - { id, status: requirement.status }, - 'Decline patient requirement failed: Invalid status', - ); throw new ConflictException( 'A solicitação deve estar aguardando aprovação.', ); @@ -57,10 +49,13 @@ export class DeclinePatientRequirementUseCase { await this.patientRequirementsRepository.save({ id, status: 'declined', - approved_by: user.id, - approved_at: new Date(), + declined_by: user.id, + declined_at: new Date(), }); - this.logger.log({ id }, 'Patient requirement declined successfully'); + this.logger.log( + { id, userId: user.id, userEmail: user.email, role: user.role }, + 'Patient requirement declined successfully', + ); } } diff --git a/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts b/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts index f27834c..c839bb8 100644 --- a/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Between, @@ -25,10 +25,6 @@ type GetPatientRequirementsByPatientIdUseCaseResponse = Promise<{ @Injectable() export class GetPatientRequirementsByPatientIdUseCase { - private readonly logger = new Logger( - GetPatientRequirementsByPatientIdUseCase.name, - ); - constructor( @InjectRepository(PatientRequirement) private readonly patientRequirementsRepository: Repository, @@ -38,7 +34,9 @@ export class GetPatientRequirementsByPatientIdUseCase { patientId, query, }: GetPatientRequirementsByPatientIdUseCaseRequest): GetPatientRequirementsByPatientIdUseCaseResponse { - const { status, startDate, endDate, page, perPage } = query; + const { status, page, perPage } = query; + const startDate = query.startDate ? new Date(query.startDate) : null; + const endDate = query.endDate ? new Date(query.endDate) : null; const where: FindOptionsWhere = { patient_id: patientId, @@ -49,15 +47,15 @@ export class GetPatientRequirementsByPatientIdUseCase { } if (startDate && endDate) { - where.created_at = Between(new Date(startDate), new Date(endDate)); + where.created_at = Between(startDate, endDate); } if (startDate && !endDate) { - where.created_at = MoreThanOrEqual(new Date(startDate)); + where.created_at = MoreThanOrEqual(startDate); } if (endDate && !startDate) { - where.created_at = LessThanOrEqual(new Date(endDate)); + where.created_at = LessThanOrEqual(endDate); } const total = await this.patientRequirementsRepository.count({ where }); @@ -71,6 +69,7 @@ export class GetPatientRequirementsByPatientIdUseCase { status: true, submitted_at: true, approved_at: true, + declined_at: true, created_at: true, }, skip: (page - 1) * perPage, diff --git a/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts b/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts index 885fd71..675fe2c 100644 --- a/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts @@ -34,7 +34,9 @@ export class GetPatientRequirementsUseCase { async execute({ query, }: GetPatientRequirementsUseCaseRequest): GetPatientRequirementsUseCaseResponse { - const { search, status, startDate, endDate, page, perPage } = query; + const { search, status, page, perPage } = query; + const startDate = query.startDate ? new Date(query.startDate) : null; + const endDate = query.endDate ? new Date(query.endDate) : null; const ORDER_BY_MAPPING: Record< PatientRequirementsOrderBy, @@ -59,15 +61,15 @@ export class GetPatientRequirementsUseCase { } if (startDate && endDate) { - where.created_at = Between(new Date(startDate), new Date(endDate)); + where.created_at = Between(startDate, endDate); } if (startDate && !endDate) { - where.created_at = MoreThanOrEqual(new Date(startDate)); + where.created_at = MoreThanOrEqual(startDate); } if (endDate && !startDate) { - where.created_at = LessThanOrEqual(new Date(endDate)); + where.created_at = LessThanOrEqual(endDate); } const total = await this.patientRequirementsRepository.count({ where }); diff --git a/src/app/http/patient-supports/patient-supports.controller.ts b/src/app/http/patient-supports/patient-supports.controller.ts index 7a65436..33418e1 100644 --- a/src/app/http/patient-supports/patient-supports.controller.ts +++ b/src/app/http/patient-supports/patient-supports.controller.ts @@ -1,12 +1,4 @@ -import { - Body, - Controller, - Delete, - ForbiddenException, - Param, - Post, - Put, -} from '@nestjs/common'; +import { Body, Controller, Delete, Param, Post, Put } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; @@ -28,7 +20,7 @@ export class PatientSupportsController { constructor( private readonly createPatientSupportUseCase: CreatePatientSupportUseCase, private readonly updatePatientSupportUseCase: UpdatePatientSupportUseCase, - private readonly cancelPatientSupportUseCase: DeletePatientSupportUseCase, + private readonly deletePatientSupportUseCase: DeletePatientSupportUseCase, ) {} @Post(':patientId') @@ -41,13 +33,8 @@ export class PatientSupportsController { @AuthUser() user: AuthUserDto, @Body() createPatientSupportDto: CreatePatientSupportDto, ): Promise { - if (user.role === 'patient' && user.id !== patientId) { - throw new ForbiddenException( - 'Você não tem permissão para registrar contatos de apoio para este paciente.', - ); - } - await this.createPatientSupportUseCase.execute({ + user, patientId, createPatientSupportDto, }); @@ -85,7 +72,7 @@ export class PatientSupportsController { @Param('id') id: string, @AuthUser() user: AuthUserDto, ): Promise { - await this.cancelPatientSupportUseCase.execute({ id, user }); + await this.deletePatientSupportUseCase.execute({ id, user }); return { success: true, diff --git a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts index 550ef9d..1e6a90a 100644 --- a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts @@ -1,13 +1,20 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { + ForbiddenException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import type { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; import { PatientSupport } from '@/domain/entities/patient-support'; +import type { AuthUserDto } from '../../auth/auth.dtos'; import type { CreatePatientSupportDto } from '../patient-supports.dtos'; interface CreatePatientSupportUseCaseRequest { + user: AuthUserDto; patientId: string; createPatientSupportDto: CreatePatientSupportDto; } @@ -26,9 +33,20 @@ export class CreatePatientSupportUseCase { ) {} async execute({ + user, patientId, createPatientSupportDto, }: CreatePatientSupportUseCaseRequest): CreatePatientSupportUseCaseResponse { + if (user.id !== patientId) { + this.logger.log( + { patientId, userId: user.id, role: user.role }, + 'Create patient support failed: User does not have permission to create patient support for this patient', + ); + throw new ForbiddenException( + 'Você não tem permissão para criar um contato de apoio para este paciente.', + ); + } + const patient = await this.patientsRepository.findOne({ where: { id: patientId }, select: { id: true }, @@ -38,11 +56,22 @@ export class CreatePatientSupportUseCase { throw new NotFoundException('Paciente não encontrado.'); } - await this.patientSupportsRepository.save({ + const patientSupport = this.patientSupportsRepository.create({ ...createPatientSupportDto, patient_id: patientId, }); - this.logger.log({ patientId }, 'Support network created successfully'); + await this.patientSupportsRepository.save(patientSupport); + + this.logger.log( + { + id: patientSupport.id, + patientId, + userId: user.id, + userEmail: user.email, + role: user.role, + }, + 'Patient support created successfully', + ); } } diff --git a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts index 96696a3..576ea99 100644 --- a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts @@ -40,7 +40,19 @@ export class DeletePatientSupportUseCase { throw new NotFoundException('Contato de apoio não encontrado.'); } - if (user.role === 'patient' && user.id !== patientSupport.patient_id) { + const patientId = patientSupport.patient_id; + + if (user.role === 'patient' && user.id !== patientId) { + this.logger.log( + { + id, + patientId, + userId: user.id, + userEmail: user.email, + role: user.role, + }, + 'Remove patient support failed: User does not have permission to remove this patient support', + ); throw new ForbiddenException( 'Você não tem permissão para remover este contato de apoio.', ); @@ -49,8 +61,14 @@ export class DeletePatientSupportUseCase { await this.patientSupportsRepository.remove(patientSupport); this.logger.log( - { id, patientId: patientSupport.patient_id }, - 'Support network removed successfully', + { + id, + patientId, + userId: user.id, + userEmail: user.email, + role: user.role, + }, + 'Patient support removed successfully', ); } } diff --git a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts index bbaa775..7930ba3 100644 --- a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts @@ -14,8 +14,8 @@ import type { UpdatePatientSupportDto } from '../patient-supports.dtos'; interface UpdatePatientSupportUseCaseRequest { id: string; - updatePatientSupportDto: UpdatePatientSupportDto; user: AuthUserDto; + updatePatientSupportDto: UpdatePatientSupportDto; } type UpdatePatientSupportUseCaseResponse = Promise; @@ -31,8 +31,8 @@ export class UpdatePatientSupportUseCase { async execute({ id, - updatePatientSupportDto, user, + updatePatientSupportDto, }: UpdatePatientSupportUseCaseRequest): UpdatePatientSupportUseCaseResponse { const patientSupport = await this.patientSupportsRepository.findOne({ select: { id: true, patient_id: true }, @@ -43,7 +43,19 @@ export class UpdatePatientSupportUseCase { throw new NotFoundException('Contato de apoio não encontrado.'); } - if (user.role === 'patient' && user.id !== patientSupport.patient_id) { + const patientId = patientSupport.patient_id; + + if (user.role === 'patient' && user.id !== patientId) { + this.logger.log( + { + id, + patientId, + userId: user.id, + userEmail: user.email, + role: user.role, + }, + 'Update patient support failed: User does not have permission to update this patient support', + ); throw new ForbiddenException( 'Você não tem permissão para atualizar este contato de apoio.', ); @@ -54,6 +66,15 @@ export class UpdatePatientSupportUseCase { ...updatePatientSupportDto, }); - this.logger.log({ id }, 'Support network updated successfully'); + this.logger.log( + { + id, + patientId, + userId: user.id, + userEmail: user.email, + role: user.role, + }, + 'Patient support updated successfully', + ); } } diff --git a/src/app/http/patients/patients.controller.ts b/src/app/http/patients/patients.controller.ts index c65390b..e2b4796 100644 --- a/src/app/http/patients/patients.controller.ts +++ b/src/app/http/patients/patients.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, - ForbiddenException, Get, Param, Patch, @@ -39,7 +38,7 @@ export class PatientsController { private readonly getPatientUseCase: GetPatientUseCase, private readonly createPatientUseCase: CreatePatientUseCase, private readonly updatePatientUseCase: UpdatePatientUseCase, - private readonly deativatePatientUseCase: DeactivatePatientUseCase, + private readonly deactivatePatientUseCase: DeactivatePatientUseCase, ) {} @Get() @@ -74,9 +73,10 @@ export class PatientsController { @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Cadastra um novo paciente' }) async create( + @AuthUser() user: AuthUserDto, @Body() createPatientDto: CreatePatientDto, ): Promise { - await this.createPatientUseCase.execute({ createPatientDto }); + await this.createPatientUseCase.execute({ user, createPatientDto }); return { success: true, @@ -92,13 +92,7 @@ export class PatientsController { @AuthUser() user: AuthUserDto, @Body() updatePatientDto: UpdatePatientDto, ): Promise { - if (user.role === 'patient' && user.id !== id) { - throw new ForbiddenException( - 'Você não tem permissão para atualizar este paciente.', - ); - } - - await this.updatePatientUseCase.execute({ id, updatePatientDto }); + await this.updatePatientUseCase.execute({ id, user, updatePatientDto }); return { success: true, @@ -109,8 +103,11 @@ export class PatientsController { @Patch(':id/deactivate') @Roles(['manager']) @ApiOperation({ summary: 'Inativa um paciente pelo ID' }) - async deactivatePatient(@Param('id') id: string): Promise { - await this.deativatePatientUseCase.execute({ id }); + async deactivatePatient( + @Param('id') id: string, + @AuthUser() user: AuthUserDto, + ): Promise { + await this.deactivatePatientUseCase.execute({ id, user }); return { success: true, diff --git a/src/app/http/patients/use-cases/create-patient.use-case.ts b/src/app/http/patients/use-cases/create-patient.use-case.ts index c4e1748..528bdab 100644 --- a/src/app/http/patients/use-cases/create-patient.use-case.ts +++ b/src/app/http/patients/use-cases/create-patient.use-case.ts @@ -6,9 +6,11 @@ import { DataSource } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; import { PatientSupport } from '@/domain/entities/patient-support'; +import type { AuthUserDto } from '../../auth/auth.dtos'; import type { CreatePatientDto } from '../patients.dtos'; interface CreatePatientUseCaseRequest { + user: AuthUserDto; createPatientDto: CreatePatientDto; } @@ -26,31 +28,32 @@ export class CreatePatientUseCase { ) {} async execute({ + user, createPatientDto, }: CreatePatientUseCaseRequest): CreatePatientUseCaseResponse { const { email, cpf, supports, ...patientData } = createPatientDto; - const patientWithEmail = await this.patientsRepository.findOne({ + const patientWithSameEmail = await this.patientsRepository.findOne({ select: { id: true }, where: { email }, }); - if (patientWithEmail) { + if (patientWithSameEmail) { this.logger.error( - { email }, + { email, userId: user.id, userEmail: user.email, role: user.role }, 'Create patient failed: Email already registered', ); throw new ConflictException('O e-mail informado já está registrado.'); } - const patientWithCpf = await this.patientsRepository.findOne({ + const patientWithSameCpf = await this.patientsRepository.findOne({ select: { id: true }, where: { cpf }, }); - if (patientWithCpf) { + if (patientWithSameCpf) { this.logger.error( - { cpf }, + { cpf, userId: user.id, userEmail: user.email, role: user.role }, 'Create patient failed: CPF already registered', ); throw new ConflictException('O CPF informado já está registrado.'); @@ -60,20 +63,22 @@ export class CreatePatientUseCase { const patientsDataSource = manager.getRepository(Patient); const patientSupportsDataSource = manager.getRepository(PatientSupport); - const patient = await patientsDataSource.save({ + const patient = patientsDataSource.create({ ...patientData, email, cpf, status: 'active', }); + await patientsDataSource.save(patient); + if (supports && supports.length > 0) { - const patientSupports = supports.map((support) => + const patientSupports = supports.map(({ name, phone, kinship }) => patientSupportsDataSource.create({ - name: support.name, - phone: support.phone, - kinship: support.kinship, patient_id: patient.id, + name, + phone, + kinship, }), ); @@ -81,7 +86,13 @@ export class CreatePatientUseCase { } this.logger.log( - { patientId: patient.id, email }, + { + patientId: patient.id, + email, + userId: user.id, + userEmail: user.email, + role: user.role, + }, 'Patient created successfully', ); }); diff --git a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts index 9f90c05..dd4cd6a 100644 --- a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts +++ b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts @@ -9,8 +9,11 @@ import type { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; +import type { AuthUserDto } from '../../auth/auth.dtos'; + interface DeactivatePatientUseCaseRequest { id: string; + user: AuthUserDto; } type DeactivatePatientUseCaseResponse = Promise; @@ -26,6 +29,7 @@ export class DeactivatePatientUseCase { async execute({ id, + user, }: DeactivatePatientUseCaseRequest): DeactivatePatientUseCaseResponse { const patient = await this.patientsRepository.findOne({ select: { id: true, status: true }, @@ -33,23 +37,23 @@ export class DeactivatePatientUseCase { }); if (!patient) { - this.logger.error( - { patientId: id }, - 'Cancel patient failed: Patient not found', - ); throw new NotFoundException('Paciente não encontrado.'); } if (patient.status === 'inactive') { - this.logger.error( - { patientId: id }, - 'Cancel patient failed: Patient already inactive', - ); throw new ConflictException('Este paciente já está inativo.'); } await this.patientsRepository.save({ id, status: 'inactive' }); - this.logger.log({ patientId: id }, 'Patient deactivated successfully'); + this.logger.log( + { + patientId: id, + userId: user.id, + userEmail: user.email, + role: user.role, + }, + 'Patient deactivated successfully', + ); } } diff --git a/src/app/http/patients/use-cases/get-patients.use-case.ts b/src/app/http/patients/use-cases/get-patients.use-case.ts index 0863dc1..69b3550 100644 --- a/src/app/http/patients/use-cases/get-patients.use-case.ts +++ b/src/app/http/patients/use-cases/get-patients.use-case.ts @@ -35,16 +35,9 @@ export class GetPatientsUseCase { async execute({ query, }: GetPatientsUseCaseRequest): GetPatientsUseCaseResponse { - const { - search, - order, - orderBy, - status, - startDate, - endDate, - page, - perPage, - } = query; + const { search, order, orderBy, status, page, perPage } = query; + const startDate = query.startDate ? new Date(query.startDate) : null; + const endDate = query.endDate ? new Date(query.endDate) : null; const ORDER_BY_MAPPING: Record = { name: 'name', @@ -62,15 +55,15 @@ export class GetPatientsUseCase { } if (startDate && endDate) { - where.created_at = Between(new Date(startDate), new Date(endDate)); + where.created_at = Between(startDate, endDate); } if (startDate && !endDate) { - where.created_at = MoreThanOrEqual(new Date(startDate)); + where.created_at = MoreThanOrEqual(startDate); } if (endDate && !startDate) { - where.created_at = LessThanOrEqual(new Date(endDate)); + where.created_at = LessThanOrEqual(endDate); } const total = await this.patientsRepository.count({ where }); diff --git a/src/app/http/patients/use-cases/update-patient.use-case.ts b/src/app/http/patients/use-cases/update-patient.use-case.ts index 8584c98..f3c76f2 100644 --- a/src/app/http/patients/use-cases/update-patient.use-case.ts +++ b/src/app/http/patients/use-cases/update-patient.use-case.ts @@ -1,5 +1,6 @@ import { ConflictException, + ForbiddenException, Injectable, Logger, NotFoundException, @@ -9,11 +10,13 @@ import type { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; +import type { AuthUserDto } from '../../auth/auth.dtos'; import type { UpdatePatientDto } from '../patients.dtos'; interface UpdatePatientUseCaseRequest { - updatePatientDto: UpdatePatientDto; id: string; + user: AuthUserDto; + updatePatientDto: UpdatePatientDto; } type UpdatePatientUseCaseResponse = Promise; @@ -29,45 +32,64 @@ export class UpdatePatientUseCase { async execute({ id, + user, updatePatientDto, }: UpdatePatientUseCaseRequest): UpdatePatientUseCaseResponse { + if (user.role === 'patient' && user.id !== id) { + this.logger.log( + { id, userId: user.id, userEmail: user.email, role: user.role }, + 'Update patient failed: User does not have permission to update this patient', + ); + throw new ForbiddenException( + 'Você não tem permissão para atualizar este paciente.', + ); + } + const patient = await this.patientsRepository.findOne({ select: { id: true, email: true, cpf: true }, where: { id }, }); if (!patient) { - this.logger.error( - { patientId: id }, - 'Update patient failed: Patient not found', - ); throw new NotFoundException('Paciente não encontrado.'); } - if (updatePatientDto.cpf && updatePatientDto.cpf !== patient.cpf) { - const patientWithCpf = await this.patientsRepository.findOne({ + if (updatePatientDto.cpf !== patient.cpf) { + const patientWithSameCpf = await this.patientsRepository.findOne({ where: { cpf: updatePatientDto.cpf }, select: { id: true }, }); - if (patientWithCpf && patientWithCpf.id !== id) { + if (patientWithSameCpf && patientWithSameCpf.id !== id) { this.logger.error( - { patientId: id, cpf: updatePatientDto.cpf }, + { + patientId: id, + cpf: updatePatientDto.cpf, + userId: user.id, + userEmail: user.email, + role: user.role, + }, 'Update patient failed: CPF already registered', ); throw new ConflictException('O CPF informado já está registrado.'); } } - if (updatePatientDto.email && updatePatientDto.email !== patient.email) { - const patientWithEmail = await this.patientsRepository.findOne({ + if (updatePatientDto.email !== patient.email) { + const patientWithSameEmail = await this.patientsRepository.findOne({ where: { email: updatePatientDto.email }, select: { id: true }, }); - if (patientWithEmail && patientWithEmail.id !== id) { + if (patientWithSameEmail && patientWithSameEmail.id !== id) { this.logger.error( - { patientId: id, email: updatePatientDto.email }, + { + patientId: id, + email: updatePatientDto.email, + userId: user.id, + userEmail: user.email, + role: user.role, + }, 'Update patient failed: Email already registered', ); throw new ConflictException('O e-mail informado já está registrado.'); @@ -76,6 +98,14 @@ export class UpdatePatientUseCase { await this.patientsRepository.save({ id, ...updatePatientDto }); - this.logger.log({ patientId: id }, 'Patient updated successfully'); + this.logger.log( + { + patientId: id, + userId: user.id, + userEmail: user.email, + role: user.role, + }, + 'Patient updated successfully', + ); } } diff --git a/src/app/http/referrals/use-cases/get-referrals.use-case.ts b/src/app/http/referrals/use-cases/get-referrals.use-case.ts index f954154..f1c5e6b 100644 --- a/src/app/http/referrals/use-cases/get-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/get-referrals.use-case.ts @@ -32,6 +32,8 @@ export class GetReferralsUseCase { query, }: GetReferralsUseCaseRequest): GetReferralsUseCaseResponse { const { search, status, category, condition, page, perPage } = query; + const startDate = query.startDate ? new Date(query.startDate) : null; + const endDate = query.endDate ? new Date(query.endDate) : null; const ORDER_BY_MAPPING: Record = { date: 'created_at', @@ -43,8 +45,6 @@ export class GetReferralsUseCase { }; const where: FindOptionsWhere = {}; - const startDate = query.startDate ? new Date(query.startDate) : null; - const endDate = query.endDate ? new Date(query.endDate) : null; if (status) { where.status = status; diff --git a/src/app/http/users/use-cases/update-user.use-case.ts b/src/app/http/users/use-cases/update-user.use-case.ts index 50a36b1..625c353 100644 --- a/src/app/http/users/use-cases/update-user.use-case.ts +++ b/src/app/http/users/use-cases/update-user.use-case.ts @@ -1,13 +1,20 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { + ForbiddenException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from '@/domain/entities/user'; +import type { AuthUserDto } from '../../auth/auth.dtos'; import type { UpdateUserDto } from '../users.dtos'; interface UpdateUserUseCaseRequest { id: string; + user: AuthUserDto; updateUserDto: UpdateUserDto; } @@ -24,20 +31,31 @@ export class UpdateUserUseCase { async execute({ id, + user, updateUserDto, }: UpdateUserUseCaseRequest): UpdateUserUseCaseResponse { - const user = await this.usersRepository.findOne({ where: { id } }); + if (user.role !== 'admin' && user.id !== id) { + this.logger.log( + { id, userId: user.id, role: user.role }, + 'Update user failed: User does not have permission to update this user', + ); + throw new ForbiddenException( + 'Você não tem permissão para atualizar este usuário.', + ); + } + + const userToUpdate = await this.usersRepository.findOne({ where: { id } }); - if (!user) { + if (!userToUpdate) { throw new NotFoundException('Usuário não encontrado.'); } - Object.assign(user, updateUserDto); + Object.assign(userToUpdate, updateUserDto); - await this.usersRepository.save(user); + await this.usersRepository.save(userToUpdate); this.logger.log( - { userId: id, email: updateUserDto.email }, + { id, email: updateUserDto.email }, 'User updated successfully', ); } diff --git a/src/domain/entities/patient-requirement.ts b/src/domain/entities/patient-requirement.ts index 02dbc0c..9378d3d 100644 --- a/src/domain/entities/patient-requirement.ts +++ b/src/domain/entities/patient-requirement.ts @@ -50,6 +50,12 @@ export class PatientRequirement implements PatientRequirementSchema { @Column({ type: 'datetime', nullable: true }) approved_at: Date | null; + @Column({ type: 'uuid', nullable: true }) + declined_by: string | null; + + @Column({ type: 'datetime', nullable: true }) + declined_at: Date | null; + @Column({ type: 'uuid' }) created_by: string; diff --git a/src/domain/schemas/patient-requirement/index.ts b/src/domain/schemas/patient-requirement/index.ts index 9484605..8d310f8 100644 --- a/src/domain/schemas/patient-requirement/index.ts +++ b/src/domain/schemas/patient-requirement/index.ts @@ -16,6 +16,8 @@ export const patientRequirementSchema = z submitted_at: z.coerce.date().nullable(), approved_by: z.string().uuid().nullable(), approved_at: z.coerce.date().nullable(), + declined_by: z.string().uuid().nullable(), + declined_at: z.coerce.date().nullable(), created_by: z.string().uuid(), created_at: z.coerce.date(), updated_at: z.coerce.date(), diff --git a/src/domain/schemas/patient-requirement/responses.ts b/src/domain/schemas/patient-requirement/responses.ts index a954b88..9b3191c 100644 --- a/src/domain/schemas/patient-requirement/responses.ts +++ b/src/domain/schemas/patient-requirement/responses.ts @@ -13,6 +13,7 @@ export const patientRequirementItemSchema = patientRequirementSchema description: true, submitted_at: true, approved_at: true, + declined_at: true, created_at: true, }) .extend({ patient: patientSchema.pick({ id: true, name: true }) }); @@ -39,6 +40,7 @@ export const patientRequirementByPatientIdSchema = description: true, submitted_at: true, approved_at: true, + declined_at: true, created_at: true, }); export type PatientRequirementByPatientId = z.infer< From bb109aa824a2f6e474ad4772079520cd5a549afd Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Thu, 15 Jan 2026 23:15:36 -0300 Subject: [PATCH 10/21] refactor(auth): apply use-case pattern to auth module --- ...02-Initial.ts => 1766614884730-Initial.ts} | 12 +- infra/database/seed-dev.ts | 3 +- src/app/app.module.ts | 2 - src/app/cryptography/crypography.service.ts | 17 +- src/app/cryptography/cryptography.module.ts | 7 +- .../use-cases/create-token.use-case.ts | 62 +++++++ .../use-cases/cancel-appointment.use-case.ts | 7 +- .../use-cases/create-appointment.use-case.ts | 4 +- .../use-cases/get-appointments.use-case.ts | 6 +- .../use-cases/update-appointment.use-case.ts | 4 +- src/app/http/auth/auth.controller.ts | 99 ++--------- .../http/auth/use-cases/logout.use-case.ts | 37 +++- .../use-cases/recover-password.use-case.ts | 41 +++-- .../use-cases/register-patient.use-case.ts | 56 +++--- .../auth/use-cases/register-user.use-case.ts | 61 +++---- .../auth/use-cases/reset-password.use-case.ts | 68 ++++---- .../use-cases/sign-in-with-email.use-case.ts | 72 +++++--- .../approve-patient-requirement.use-case.ts | 4 +- .../create-patient-requirement.use-case.ts | 4 +- .../decline-patient-requirement.use-case.ts | 4 +- ...ent-requirements-by-patient-id.use-case.ts | 6 +- .../get-patient-requirements.use-case.ts | 6 +- .../create-patient-support.use-case.ts | 4 +- .../delete-patient-support.use-case.ts | 4 +- .../update-patient-support.use-case.ts | 4 +- src/app/http/patients/patients.controller.ts | 4 +- .../use-cases/create-patient.use-case.ts | 4 +- .../use-cases/deactivate-patient.use-case.ts | 7 +- .../use-cases/get-patient.use-case.ts | 10 +- .../use-cases/get-patients.use-case.ts | 6 +- .../use-cases/update-patient.use-case.ts | 4 +- .../use-cases/cancel-referral.use-case.ts | 7 +- .../use-cases/create-referrals.use-case.ts | 4 +- .../use-cases/get-referrals.use-case.ts | 9 +- .../http/statistics/statistics.controller.ts | 13 +- .../get-total-appointments.use-case.ts | 4 +- .../get-total-patients-by-field.use-case.ts | 8 +- .../get-total-patients-by-status.use-case.ts | 6 +- .../use-cases/get-total-patients.use-case.ts | 4 +- ...d-referred-patients-percentage.use-case.ts | 6 +- ...et-total-referrals-by-category.use-case.ts | 6 +- .../use-cases/get-total-referrals.use-case.ts | 4 +- ...tal-referred-patients-by-state.use-case.ts | 6 +- .../get-total-referred-patients.use-case.ts | 4 +- .../http/users/use-cases/get-user.use-case.ts | 10 +- .../users/use-cases/update-user.use-case.ts | 4 +- src/app/http/users/users.controller.ts | 10 +- src/common/guards/auth.guard.ts | 165 ++++++++++++++---- src/domain/cookies.ts | 11 +- src/domain/entities/database.ts | 2 - src/domain/entities/patient.ts | 2 +- src/domain/entities/token.ts | 4 +- src/domain/entities/user.ts | 2 +- src/domain/enums/tokens.ts | 8 +- src/domain/modules/cryptography.ts | 17 -- src/domain/schemas/statistics/responses.ts | 7 + src/domain/schemas/tokens.ts | 23 ++- 57 files changed, 533 insertions(+), 442 deletions(-) rename infra/database/migrations/{1766604246802-Initial.ts => 1766614884730-Initial.ts} (88%) create mode 100644 src/app/cryptography/use-cases/create-token.use-case.ts delete mode 100644 src/domain/modules/cryptography.ts diff --git a/infra/database/migrations/1766604246802-Initial.ts b/infra/database/migrations/1766614884730-Initial.ts similarity index 88% rename from infra/database/migrations/1766604246802-Initial.ts rename to infra/database/migrations/1766614884730-Initial.ts index a8af669..89de75c 100644 --- a/infra/database/migrations/1766604246802-Initial.ts +++ b/infra/database/migrations/1766614884730-Initial.ts @@ -1,16 +1,16 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class Initial1766604246802 implements MigrationInterface { - name = 'Initial1766604246802' +export class Initial1766614884730 implements MigrationInterface { + name = 'Initial1766614884730' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`CREATE TABLE \`patient_requirements\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`type\` enum ('screening', 'medical_report') NOT NULL, \`title\` varchar(255) NOT NULL, \`description\` varchar(500) NULL, \`status\` enum ('pending', 'under_review', 'approved', 'declined') NOT NULL DEFAULT 'pending', \`submitted_at\` datetime NULL, \`approved_by\` varchar(255) NULL, \`approved_at\` datetime NULL, \`declined_by\` varchar(255) NULL, \`declined_at\` datetime NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`patient_supports\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`name\` varchar(64) NOT NULL, \`phone\` varchar(11) NOT NULL, \`kinship\` varchar(50) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`referrals\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(2000) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`patients\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NULL, \`avatar_url\` varchar(255) NULL, \`status\` enum ('active', 'inactive', 'pending') NOT NULL DEFAULT 'pending', \`gender\` enum ('male_cis', 'female_cis', 'male_trans', 'female_trans', 'non_binary', 'prefer_not_to_say') NOT NULL DEFAULT 'prefer_not_to_say', \`date_of_birth\` datetime NULL, \`phone\` varchar(11) NULL, \`cpf\` varchar(11) NULL, \`state\` enum ('AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO') NULL, \`city\` varchar(255) NULL, \`has_disability\` tinyint(1) NOT NULL DEFAULT '0', \`disability_desc\` varchar(500) NULL, \`need_legal_assistance\` tinyint(1) NOT NULL DEFAULT '0', \`take_medication\` tinyint(1) NOT NULL DEFAULT '0', \`medication_desc\` varchar(500) NULL, \`nmo_diagnosis\` enum ('anti_aqp4_positive', 'anti_mog_positive', 'both_negative', 'no_diagnosis') NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_5947301223f5a908fd5e372b0f\` (\`cpf\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`patients\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NULL, \`avatar_url\` varchar(255) NULL, \`status\` enum ('active', 'inactive', 'pending') NOT NULL DEFAULT 'pending', \`gender\` enum ('male_cis', 'female_cis', 'male_trans', 'female_trans', 'non_binary', 'prefer_not_to_say') NOT NULL DEFAULT 'prefer_not_to_say', \`date_of_birth\` datetime NULL, \`phone\` varchar(11) NULL, \`cpf\` varchar(11) NULL, \`state\` enum ('AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO') NULL, \`city\` varchar(255) NULL, \`has_disability\` tinyint(1) NOT NULL DEFAULT '0', \`disability_desc\` varchar(500) NULL, \`need_legal_assistance\` tinyint(1) NOT NULL DEFAULT '0', \`take_medication\` tinyint(1) NOT NULL DEFAULT '0', \`medication_desc\` varchar(500) NULL, \`nmo_diagnosis\` enum ('anti_aqp4_positive', 'anti_mog_positive', 'both_negative', 'no_diagnosis') NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_64e2031265399f5690b0beba6a\` (\`email\`), UNIQUE INDEX \`IDX_5947301223f5a908fd5e372b0f\` (\`cpf\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`appointments\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(500) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`entity_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'password_reset', 'invite_token') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`users\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NOT NULL, \`avatar_url\` varchar(255) NULL, \`role\` enum ('admin', 'manager', 'nurse', 'specialist') NOT NULL, \`status\` enum ('active', 'inactive') NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`entity_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'refresh_token', 'password_reset', 'invite_token') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`users\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NOT NULL, \`avatar_url\` varchar(255) NULL, \`role\` enum ('admin', 'manager', 'nurse', 'specialist') NOT NULL, \`status\` enum ('active', 'inactive') NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_97672ac88f789774dd47f7c8be\` (\`email\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`ALTER TABLE \`patient_requirements\` ADD CONSTRAINT \`FK_77b87c61cff4793ae6a4ac50070\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE \`patient_supports\` ADD CONSTRAINT \`FK_62c23ddd34837a0c09faf875425\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE \`referrals\` ADD CONSTRAINT \`FK_bb61873c1c10fe8662f540f0625\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); @@ -22,10 +22,12 @@ export class Initial1766604246802 implements MigrationInterface { await queryRunner.query(`ALTER TABLE \`referrals\` DROP FOREIGN KEY \`FK_bb61873c1c10fe8662f540f0625\``); await queryRunner.query(`ALTER TABLE \`patient_supports\` DROP FOREIGN KEY \`FK_62c23ddd34837a0c09faf875425\``); await queryRunner.query(`ALTER TABLE \`patient_requirements\` DROP FOREIGN KEY \`FK_77b87c61cff4793ae6a4ac50070\``); + await queryRunner.query(`DROP INDEX \`IDX_97672ac88f789774dd47f7c8be\` ON \`users\``); await queryRunner.query(`DROP TABLE \`users\``); await queryRunner.query(`DROP TABLE \`tokens\``); await queryRunner.query(`DROP TABLE \`appointments\``); await queryRunner.query(`DROP INDEX \`IDX_5947301223f5a908fd5e372b0f\` ON \`patients\``); + await queryRunner.query(`DROP INDEX \`IDX_64e2031265399f5690b0beba6a\` ON \`patients\``); await queryRunner.query(`DROP TABLE \`patients\``); await queryRunner.query(`DROP TABLE \`referrals\``); await queryRunner.query(`DROP TABLE \`patient_supports\``); diff --git a/infra/database/seed-dev.ts b/infra/database/seed-dev.ts index 7393b12..08e89a4 100644 --- a/infra/database/seed-dev.ts +++ b/infra/database/seed-dev.ts @@ -8,7 +8,7 @@ import { Patient } from '@/domain/entities/patient'; import { PatientRequirement } from '@/domain/entities/patient-requirement'; import { PatientSupport } from '@/domain/entities/patient-support'; import { Referral } from '@/domain/entities/referral'; -// import { Specialist } from '@/domain/entities/specialist'; +import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; import { APPOINTMENT_STATUSES } from '@/domain/enums/appointments'; import { @@ -70,6 +70,7 @@ async function main() { await dataSource.manager.clear(PatientRequirement); await dataSource.manager.clear(Patient); await dataSource.manager.clear(User); + await dataSource.manager.clear(Token); await dataSource.query('SET FOREIGN_KEY_CHECKS = 1'); console.log('✅ Old data deleted.'); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b1f6a27..aba16b3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -15,7 +15,6 @@ import { PatientRequirementsModule } from './http/patient-requirements/patient-r import { PatientSupportsModule } from './http/patient-supports/patient-supports.module'; import { PatientsModule } from './http/patients/patients.module'; import { ReferralsModule } from './http/referrals/referrals.module'; -// import { SpecialistsModule } from './http/specialists/specialists.module'; import { StatisticsModule } from './http/statistics/statistics.module'; import { UsersModule } from './http/users/users.module'; @@ -55,7 +54,6 @@ import { UsersModule } from './http/users/users.module'; UsersModule, PatientsModule, PatientSupportsModule, - // SpecialistsModule, AppointmentsModule, StatisticsModule, PatientRequirementsModule, diff --git a/src/app/cryptography/crypography.service.ts b/src/app/cryptography/crypography.service.ts index 121c315..068ce74 100644 --- a/src/app/cryptography/crypography.service.ts +++ b/src/app/cryptography/crypography.service.ts @@ -1,13 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { JwtService, type JwtSignOptions } from '@nestjs/jwt'; +import { JwtService } from '@nestjs/jwt'; import { compare, hash } from 'bcryptjs'; -import type { Cryptography } from '@/domain/modules/cryptography'; -import type { AuthTokenPayloads } from '@/domain/schemas/tokens'; - @Injectable() -export class CryptographyService implements Cryptography { - private HASH_SALT_LENGTH = 10; +export class CryptographyService { + private readonly HASH_SALT_LENGTH = 10; constructor(private readonly jwtService: JwtService) {} @@ -19,14 +16,6 @@ export class CryptographyService implements Cryptography { return compare(plain, hash); } - async createToken( - _type: T, - payload: AuthTokenPayloads[T], - options?: JwtSignOptions, - ): Promise { - return this.jwtService.signAsync(payload, options); - } - async verifyToken(token: string): Promise { return this.jwtService.verifyAsync(token); } diff --git a/src/app/cryptography/cryptography.module.ts b/src/app/cryptography/cryptography.module.ts index eddd124..1d9eba8 100644 --- a/src/app/cryptography/cryptography.module.ts +++ b/src/app/cryptography/cryptography.module.ts @@ -5,6 +5,7 @@ import { EnvModule } from '@/env/env.module'; import { EnvService } from '@/env/env.service'; import { CryptographyService } from './crypography.service'; +import { CreateTokenUseCase } from './use-cases/create-token.use-case'; @Module({ imports: [ @@ -13,11 +14,11 @@ import { CryptographyService } from './crypography.service'; inject: [EnvService], useFactory: (envService: EnvService) => ({ secret: envService.get('JWT_SECRET'), - signOptions: { expiresIn: '12h' }, + signOptions: { expiresIn: '8h' }, }), }), ], - providers: [CryptographyService], - exports: [CryptographyService], + providers: [CryptographyService, CreateTokenUseCase], + exports: [CryptographyService, CreateTokenUseCase], }) export class CryptographyModule {} diff --git a/src/app/cryptography/use-cases/create-token.use-case.ts b/src/app/cryptography/use-cases/create-token.use-case.ts new file mode 100644 index 0000000..61cd804 --- /dev/null +++ b/src/app/cryptography/use-cases/create-token.use-case.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService, type JwtSignOptions } from '@nestjs/jwt'; + +import type { AuthTokenType } from '@/domain/enums/tokens'; +import type { AuthTokenPayloads } from '@/domain/schemas/tokens'; + +interface CreateTokenUseCaseInput { + type: T; + payload: AuthTokenPayloads[T]; + options?: JwtSignOptions; +} + +interface CreateAccessTokenUseCaseOutput { + token: string; + maxAge: number; + expiresAt: Date; +} + +type TokenExpiryTime = Record< + AuthTokenType, + { value: number; time: 'h' | 'd' } +>; + +type TokenMaxAge = Record; + +@Injectable() +export class CreateTokenUseCase { + constructor(private readonly jwtService: JwtService) {} + + async execute({ + type, + payload, + options, + }: CreateTokenUseCaseInput): Promise { + const EXPIRY_TIME: TokenExpiryTime = { + access_token: { value: 8, time: 'h' }, + invite_user_token: { value: 8, time: 'h' }, + password_reset: { value: 4, time: 'h' }, + refresh_token: { value: 30, time: 'd' }, + }; + + const expiryTime = EXPIRY_TIME[type]; + + const MAX_AGES: TokenMaxAge = { + access_token: 1000 * 60 * 60 * expiryTime.value, + invite_user_token: 1000 * 60 * 60 * expiryTime.value, + password_reset: 1000 * 60 * 60 * expiryTime.value, + refresh_token: 1000 * 60 * 60 * 24 * expiryTime.value, + }; + + const maxAge = MAX_AGES[type]; + const expiresIn = `${expiryTime.value}${expiryTime.time}`; + const expiresAt = new Date(Date.now() + maxAge); + + const token = await this.jwtService.signAsync(payload, { + expiresIn, + ...options, + }); + + return { token, maxAge, expiresAt }; + } +} diff --git a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts index 271334f..6319538 100644 --- a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts @@ -16,8 +16,6 @@ interface CancelAppointmentUseCaseRequest { user: AuthUserDto; } -type CancelAppointmentUseCaseResponse = Promise; - @Injectable() export class CancelAppointmentUseCase { private readonly logger = new Logger(CancelAppointmentUseCase.name); @@ -27,10 +25,7 @@ export class CancelAppointmentUseCase { private readonly appointmentsRepository: Repository, ) {} - async execute({ - id, - user, - }: CancelAppointmentUseCaseRequest): CancelAppointmentUseCaseResponse { + async execute({ id, user }: CancelAppointmentUseCaseRequest): Promise { const appointment = await this.appointmentsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/appointments/use-cases/create-appointment.use-case.ts b/src/app/http/appointments/use-cases/create-appointment.use-case.ts index eae8d4b..0cebf74 100644 --- a/src/app/http/appointments/use-cases/create-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/create-appointment.use-case.ts @@ -13,8 +13,6 @@ interface CreateAppointmentUseCaseRequest { user: AuthUserDto; } -type CreateAppointmentUseCaseResponse = Promise; - @Injectable() export class CreateAppointmentUseCase { private readonly logger = new Logger(CreateAppointmentUseCase.name); @@ -29,7 +27,7 @@ export class CreateAppointmentUseCase { async execute({ createAppointmentDto, user, - }: CreateAppointmentUseCaseRequest): CreateAppointmentUseCaseResponse { + }: CreateAppointmentUseCaseRequest): Promise { const { patient_id: patientId, date } = createAppointmentDto; const MAX_APPOINTMENT_MONTHS_LIMIT = 3; diff --git a/src/app/http/appointments/use-cases/get-appointments.use-case.ts b/src/app/http/appointments/use-cases/get-appointments.use-case.ts index d87cec1..79d4037 100644 --- a/src/app/http/appointments/use-cases/get-appointments.use-case.ts +++ b/src/app/http/appointments/use-cases/get-appointments.use-case.ts @@ -21,10 +21,10 @@ interface GetAppointmentsUseCaseRequest { query: GetAppointmentsQuery; } -type GetAppointmentsUseCaseResponse = Promise<{ +interface GetAppointmentsUseCaseResponse { appointments: AppointmentResponse[]; total: number; -}>; +} @Injectable() export class GetAppointmentsUseCase { @@ -36,7 +36,7 @@ export class GetAppointmentsUseCase { async execute({ user, query, - }: GetAppointmentsUseCaseRequest): GetAppointmentsUseCaseResponse { + }: GetAppointmentsUseCaseRequest): Promise { const { search, status, category, condition, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/appointments/use-cases/update-appointment.use-case.ts b/src/app/http/appointments/use-cases/update-appointment.use-case.ts index 8478e90..60be71b 100644 --- a/src/app/http/appointments/use-cases/update-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/update-appointment.use-case.ts @@ -18,8 +18,6 @@ interface UpdateAppointmentUseCaseRequest { updateAppointmentDto: UpdateAppointmentDto; } -type UpdateAppointmentUseCaseResponse = Promise; - @Injectable() export class UpdateAppointmentUseCase { private readonly logger = new Logger(UpdateAppointmentUseCase.name); @@ -33,7 +31,7 @@ export class UpdateAppointmentUseCase { id, user, updateAppointmentDto, - }: UpdateAppointmentUseCaseRequest): UpdateAppointmentUseCaseResponse { + }: UpdateAppointmentUseCaseRequest): Promise { const appointment = await this.appointmentsRepository.findOne({ where: { id }, }); diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index 711ea5a..a1522c7 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -1,10 +1,4 @@ -import { - Body, - Controller, - Post, - Res, - UnauthorizedException, -} from '@nestjs/common'; +import { Body, Controller, Post, Res } from '@nestjs/common'; import { ApiOperation } from '@nestjs/swagger'; import type { Response } from 'express'; @@ -12,7 +6,6 @@ import { Cookies } from '@/common/decorators/cookies.decorator'; import { Public } from '@/common/decorators/public.decorator'; import { COOKIES_MAPPING } from '@/domain/cookies'; import type { BaseResponse } from '@/domain/schemas/base'; -import { UtilsService } from '@/utils/utils.service'; import { RecoverPasswordDto, @@ -32,13 +25,12 @@ import { SignInWithEmailUseCase } from './use-cases/sign-in-with-email.use-case' @Controller() export class AuthController { constructor( - private utilsService: UtilsService, - private signInUseCase: SignInWithEmailUseCase, - private logoutUseCase: LogoutUseCase, - private recoverPasswordUseCase: RecoverPasswordUseCase, - private resetPasswordUseCase: ResetPasswordUseCase, - private registerPatientUseCase: RegisterPatientUseCase, - private registerUserUseCase: RegisterUserUseCase, + private readonly signInUseCase: SignInWithEmailUseCase, + private readonly logoutUseCase: LogoutUseCase, + private readonly recoverPasswordUseCase: RecoverPasswordUseCase, + private readonly resetPasswordUseCase: ResetPasswordUseCase, + private readonly registerPatientUseCase: RegisterPatientUseCase, + private readonly registerUserUseCase: RegisterUserUseCase, ) {} @Post('login') @@ -47,19 +39,7 @@ export class AuthController { @Body() signInWithEmailDto: SignInWithEmailDto, @Res({ passthrough: true }) response: Response, ): Promise { - const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; - - const { accessToken } = await this.signInUseCase.execute({ - signInWithEmailDto, - }); - - this.utilsService.setCookie(response, { - name: COOKIES_MAPPING.access_token, - value: accessToken, - maxAge: signInWithEmailDto.keep_logged_in - ? TWELVE_HOURS_IN_MS * 60 - : TWELVE_HOURS_IN_MS, - }); + await this.signInUseCase.execute({ signInWithEmailDto, response }); return { success: true, @@ -73,17 +53,7 @@ export class AuthController { @Body() registerPatientDto: RegisterPatientDto, @Res({ passthrough: true }) response: Response, ): Promise { - const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; - - const { accessToken } = await this.registerPatientUseCase.execute({ - registerPatientDto, - }); - - this.utilsService.setCookie(response, { - name: COOKIES_MAPPING.access_token, - value: accessToken, - maxAge: TWELVE_HOURS_IN_MS, - }); + await this.registerPatientUseCase.execute({ registerPatientDto, response }); return { success: true, @@ -97,17 +67,7 @@ export class AuthController { @Body() registerUserDto: RegisterUserDto, @Res({ passthrough: true }) response: Response, ): Promise { - const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; - - const { accessToken } = await this.registerUserUseCase.execute({ - registerUserDto, - }); - - this.utilsService.setCookie(response, { - name: COOKIES_MAPPING.access_token, - value: accessToken, - maxAge: TWELVE_HOURS_IN_MS, - }); + await this.registerUserUseCase.execute({ registerUserDto, response }); return { success: true, @@ -121,17 +81,7 @@ export class AuthController { @Body() recoverPasswordDto: RecoverPasswordDto, @Res({ passthrough: true }) response: Response, ): Promise { - const { resetToken } = await this.recoverPasswordUseCase.execute({ - recoverPasswordDto, - }); - - const FOUR_HOURS_IN_MS = 1000 * 60 * 60 * 4; - - this.utilsService.setCookie(response, { - name: COOKIES_MAPPING.password_reset, - maxAge: FOUR_HOURS_IN_MS, - value: resetToken, - }); + await this.recoverPasswordUseCase.execute({ response, recoverPasswordDto }); return { success: true, @@ -147,23 +97,12 @@ export class AuthController { @Body() resetPasswordDto: ResetPasswordDto, @Res({ passthrough: true }) response: Response, ): Promise { - if (!token) { - throw new UnauthorizedException('Token de redefinição de senha ausente.'); - } - - const TWELVE_HOURS_IN_MS = 1000 * 60 * 60 * 12; - - const { accessToken } = await this.resetPasswordUseCase.execute({ + await this.resetPasswordUseCase.execute({ resetPasswordDto, + response, token, }); - this.utilsService.setCookie(response, { - name: COOKIES_MAPPING.access_token, - maxAge: TWELVE_HOURS_IN_MS, - value: accessToken, - }); - return { success: true, message: 'Senha atualizada com sucesso.', @@ -171,18 +110,12 @@ export class AuthController { } @Post('logout') - @ApiOperation({ summary: 'Logout' }) + @ApiOperation({ summary: 'Logout do usuário ou paciente' }) async logout( - @Cookies('access_token') accessToken: string, + @Cookies(COOKIES_MAPPING.refresh_token) refreshToken: string, @Res({ passthrough: true }) response: Response, ): Promise { - if (!accessToken) { - throw new UnauthorizedException('Token de acesso ausente.'); - } - - await this.logoutUseCase.execute({ token: accessToken }); - - this.utilsService.deleteCookie(response, COOKIES_MAPPING.access_token); + await this.logoutUseCase.execute({ response, refreshToken }); return { success: true, diff --git a/src/app/http/auth/use-cases/logout.use-case.ts b/src/app/http/auth/use-cases/logout.use-case.ts index 8b87218..4932943 100644 --- a/src/app/http/auth/use-cases/logout.use-case.ts +++ b/src/app/http/auth/use-cases/logout.use-case.ts @@ -1,15 +1,19 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import type { Response } from 'express'; import { Repository } from 'typeorm'; +import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { COOKIES_MAPPING } from '@/domain/cookies'; import { Token } from '@/domain/entities/token'; +import type { RefreshTokenPayload } from '@/domain/schemas/tokens'; +import { UtilsService } from '@/utils/utils.service'; -interface LogoutUseCaseRequest { - token: string; +interface LogoutUseCaseInput { + refreshToken?: string; + response: Response; } -type LogoutUseCaseResponse = Promise; - @Injectable() export class LogoutUseCase { private readonly logger = new Logger(LogoutUseCase.name); @@ -17,11 +21,30 @@ export class LogoutUseCase { constructor( @InjectRepository(Token) private readonly tokensRepository: Repository, + private readonly cryptographyService: CryptographyService, + private readonly utilsService: UtilsService, ) {} - async execute({ token }: LogoutUseCaseRequest): LogoutUseCaseResponse { - await this.tokensRepository.delete({ token }); + async execute({ response, refreshToken }: LogoutUseCaseInput): Promise { + this.utilsService.deleteCookie(response, COOKIES_MAPPING.access_token); + + if (!refreshToken) { + return; + } + + const payload = + await this.cryptographyService.verifyToken( + refreshToken, + ); + + // Delete ALL refresh tokens for this entity + await this.tokensRepository.delete({ entity_id: payload.sub }); + + this.utilsService.deleteCookie(response, COOKIES_MAPPING.refresh_token); - this.logger.log({}, 'User logged out'); + this.logger.log( + { id: payload.sub, accountType: payload.accountType }, + 'User logged out', + ); } } diff --git a/src/app/http/auth/use-cases/recover-password.use-case.ts b/src/app/http/auth/use-cases/recover-password.use-case.ts index 2b97f0a..54c3867 100644 --- a/src/app/http/auth/use-cases/recover-password.use-case.ts +++ b/src/app/http/auth/use-cases/recover-password.use-case.ts @@ -1,21 +1,23 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import type { Response } from 'express'; import { Repository } from 'typeorm'; -import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { CreateTokenUseCase } from '@/app/cryptography/use-cases/create-token.use-case'; +import { COOKIES_MAPPING } from '@/domain/cookies'; import { Patient } from '@/domain/entities/patient'; import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; +import { UtilsService } from '@/utils/utils.service'; import type { RecoverPasswordDto } from '../auth.dtos'; -interface RecoverPasswordUseCaseRequest { +interface RecoverPasswordUseCaseInput { recoverPasswordDto: RecoverPasswordDto; + response: Response; } -type RecoverPasswordUseCaseResponse = Promise<{ resetToken: string }>; - @Injectable() export class RecoverPasswordUseCase { private readonly logger = new Logger(RecoverPasswordUseCase.name); @@ -27,12 +29,14 @@ export class RecoverPasswordUseCase { private readonly patientsRepository: Repository, @InjectRepository(Token) private readonly tokensRepository: Repository, - private readonly cryptographyService: CryptographyService, + private readonly createTokenUseCase: CreateTokenUseCase, + private readonly utilsService: UtilsService, ) {} async execute({ recoverPasswordDto, - }: RecoverPasswordUseCaseRequest): RecoverPasswordUseCaseResponse { + response, + }: RecoverPasswordUseCaseInput): Promise { const { email, account_type: accountType } = recoverPasswordDto; const findOptions = { select: { id: true }, where: { email } }; @@ -47,31 +51,30 @@ export class RecoverPasswordUseCase { { email, accountType }, 'Attempt to recover password for non-registered email', ); - - return { resetToken: 'dummy_token' }; + return; } - const resetToken = await this.cryptographyService.createToken( - AUTH_TOKENS_MAPPING.password_reset, - { sub: entity.id, accountType }, - { expiresIn: '4h' }, - ); - - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 4); + const { expiresAt, maxAge, token } = await this.createTokenUseCase.execute({ + type: COOKIES_MAPPING.password_reset, + payload: { sub: entity.id, accountType }, + }); await this.tokensRepository.save({ type: AUTH_TOKENS_MAPPING.password_reset, expires_at: expiresAt, entity_id: entity.id, - token: resetToken, + token, + }); + + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.password_reset, + value: token, + maxAge, }); this.logger.log( { entityId: entity.id, email, accountType }, 'Password reset token generated successfully', ); - - return { resetToken }; } } diff --git a/src/app/http/auth/use-cases/register-patient.use-case.ts b/src/app/http/auth/use-cases/register-patient.use-case.ts index 2ff18b5..3dbb368 100644 --- a/src/app/http/auth/use-cases/register-patient.use-case.ts +++ b/src/app/http/auth/use-cases/register-patient.use-case.ts @@ -1,20 +1,22 @@ import { ConflictException, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import type { Response } from 'express'; import { Repository } from 'typeorm'; import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { CreateTokenUseCase } from '@/app/cryptography/use-cases/create-token.use-case'; +import { COOKIES_MAPPING } from '@/domain/cookies'; import { Patient } from '@/domain/entities/patient'; -import { Token } from '@/domain/entities/token'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; +import { UtilsService } from '@/utils/utils.service'; import type { RegisterPatientDto } from '../auth.dtos'; -interface RegisterPatientUseCaseRequest { +interface RegisterPatientUseCaseInput { registerPatientDto: RegisterPatientDto; + response: Response; } -type RegisterPatientUseCaseResponse = Promise<{ accessToken: string }>; - @Injectable() export class RegisterPatientUseCase { private readonly logger = new Logger(RegisterPatientUseCase.name); @@ -22,26 +24,23 @@ export class RegisterPatientUseCase { constructor( @InjectRepository(Patient) private readonly patientsRepository: Repository, - @InjectRepository(Token) - private readonly tokensRepository: Repository, + private readonly createTokenUseCase: CreateTokenUseCase, private readonly cryptographyService: CryptographyService, + private readonly utilsService: UtilsService, ) {} async execute({ registerPatientDto, - }: RegisterPatientUseCaseRequest): RegisterPatientUseCaseResponse { + response, + }: RegisterPatientUseCaseInput): Promise { const { email, name } = registerPatientDto; - const patient = await this.patientsRepository.findOne({ + const patientWithSameEmail = await this.patientsRepository.findOne({ select: { id: true }, where: { email }, }); - if (patient) { - this.logger.error( - { email }, - 'Patient registration failed: Email already registered', - ); + if (patientWithSameEmail) { throw new ConflictException( 'Já existe uma conta cadastrada com este e-mail. Tente fazer login ou clique em "Esqueceu sua senha?" para recuperar o acesso.', ); @@ -51,35 +50,24 @@ export class RegisterPatientUseCase { registerPatientDto.password, ); - const newPatient = this.patientsRepository.create({ - name, - email, - password, - }); + const patient = this.patientsRepository.create({ name, email, password }); - await this.patientsRepository.save(newPatient); + await this.patientsRepository.save(patient); this.logger.log( - { patientId: newPatient.id, email }, + { patientId: patient.id, email }, 'Patient registered successfully', ); - const accessToken = await this.cryptographyService.createToken( - AUTH_TOKENS_MAPPING.access_token, - { sub: newPatient.id, role: 'patient' }, - { expiresIn: '12h' }, - ); - - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 12); - - await this.tokensRepository.save({ + const { maxAge, token } = await this.createTokenUseCase.execute({ type: AUTH_TOKENS_MAPPING.access_token, - entity_id: newPatient.id, - expires_at: expiresAt, - token: accessToken, + payload: { sub: patient.id, accountType: 'patient' }, }); - return { accessToken }; + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.access_token, + value: token, + maxAge, + }); } } diff --git a/src/app/http/auth/use-cases/register-user.use-case.ts b/src/app/http/auth/use-cases/register-user.use-case.ts index 23e5ef4..43b54bd 100644 --- a/src/app/http/auth/use-cases/register-user.use-case.ts +++ b/src/app/http/auth/use-cases/register-user.use-case.ts @@ -5,21 +5,25 @@ import { UnauthorizedException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import type { Response } from 'express'; import { Repository } from 'typeorm'; import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { CreateTokenUseCase } from '@/app/cryptography/use-cases/create-token.use-case'; +import { COOKIES_MAPPING } from '@/domain/cookies'; import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; +import type { InviteUserTokenPayload } from '@/domain/schemas/tokens'; +import { UtilsService } from '@/utils/utils.service'; import { RegisterUserDto } from '../auth.dtos'; -interface RegisterUserUseCaseRequest { +interface RegisterUserUseCaseInput { registerUserDto: RegisterUserDto; + response: Response; } -type RegisterUserUseCaseResponse = Promise<{ accessToken: string }>; - @Injectable() export class RegisterUserUseCase { private readonly logger = new Logger(RegisterUserUseCase.name); @@ -29,38 +33,41 @@ export class RegisterUserUseCase { private readonly tokensRepository: Repository, @InjectRepository(User) private readonly usersRepository: Repository, + private readonly createTokenUseCase: CreateTokenUseCase, private readonly cryptographyService: CryptographyService, + private readonly utilsService: UtilsService, ) {} async execute({ registerUserDto, - }: RegisterUserUseCaseRequest): RegisterUserUseCaseResponse { + response, + }: RegisterUserUseCaseInput): Promise { const { invite_token: token, name } = registerUserDto; const inviteToken = await this.tokensRepository.findOne({ where: { token }, }); + const payload = + await this.cryptographyService.verifyToken(token); + if ( + !payload || !inviteToken || - inviteToken.type !== AUTH_TOKENS_MAPPING.invite_token || + inviteToken.type !== AUTH_TOKENS_MAPPING.invite_user_token || (inviteToken.expires_at && inviteToken.expires_at < new Date()) ) { throw new UnauthorizedException('Token de convite inválido ou expirado.'); } - const email = inviteToken.email; - - if (!email) { - throw new UnauthorizedException('Token de convite inválido.'); - } + const { role, email } = payload; - const user = await this.usersRepository.findOne({ + const userWithSameEmail = await this.usersRepository.findOne({ select: { id: true }, where: { email }, }); - if (user) { + if (userWithSameEmail) { throw new ConflictException('Este e-mail já está cadastrado.'); } @@ -68,33 +75,27 @@ export class RegisterUserUseCase { registerUserDto.password, ); - const newUser = this.usersRepository.create({ name, email, password }); + const user = this.usersRepository.create({ name, email, password, role }); - await this.usersRepository.save(newUser); + await this.usersRepository.save(user); this.logger.log( - { userId: newUser.id, email, role: newUser.role }, + { userId: user.id, email, role }, 'User registered successfully', ); await this.tokensRepository.delete({ token }); - const accessToken = await this.cryptographyService.createToken( - AUTH_TOKENS_MAPPING.access_token, - { sub: newUser.id, role: newUser.role }, - { expiresIn: '12h' }, - ); - - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 12); + const { maxAge, token: accessToken } = + await this.createTokenUseCase.execute({ + type: AUTH_TOKENS_MAPPING.access_token, + payload: { sub: user.id, accountType: 'user' }, + }); - await this.tokensRepository.save({ - type: AUTH_TOKENS_MAPPING.access_token, - expires_at: expiresAt, - entity_id: newUser.id, - token: accessToken, + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.access_token, + value: accessToken, + maxAge, }); - - return { accessToken }; } } diff --git a/src/app/http/auth/use-cases/reset-password.use-case.ts b/src/app/http/auth/use-cases/reset-password.use-case.ts index 02d80af..5879916 100644 --- a/src/app/http/auth/use-cases/reset-password.use-case.ts +++ b/src/app/http/auth/use-cases/reset-password.use-case.ts @@ -5,24 +5,28 @@ import { UnauthorizedException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import type { Response } from 'express'; import { Repository } from 'typeorm'; import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { CreateTokenUseCase } from '@/app/cryptography/use-cases/create-token.use-case'; +import { COOKIES_MAPPING } from '@/domain/cookies'; import { Patient } from '@/domain/entities/patient'; import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; import type { UserRole } from '@/domain/enums/users'; +import type { PasswordResetPayload } from '@/domain/schemas/tokens'; +import { UtilsService } from '@/utils/utils.service'; import type { ResetPasswordDto } from '../auth.dtos'; -interface ResetPasswordUseCaseRequest { - token: string; +interface ResetPasswordUseCaseInput { resetPasswordDto: ResetPasswordDto; + response: Response; + token?: string; } -type ResetPasswordUseCaseResponse = Promise<{ accessToken: string }>; - @Injectable() export class ResetPasswordUseCase { private readonly logger = new Logger(ResetPasswordUseCase.name); @@ -34,20 +38,28 @@ export class ResetPasswordUseCase { private readonly patientsRepository: Repository, @InjectRepository(Token) private readonly tokensRepository: Repository, + private readonly createTokenUseCase: CreateTokenUseCase, private readonly cryptographyService: CryptographyService, + private readonly utilsService: UtilsService, ) {} async execute({ - token, resetPasswordDto, - }: ResetPasswordUseCaseRequest): ResetPasswordUseCaseResponse { - const resetToken = await this.tokensRepository.findOne({ - where: { token }, - }); + response, + token, + }: ResetPasswordUseCaseInput): Promise { + if (!token) { + throw new UnauthorizedException('Token de redefinição de senha ausente.'); + } + + const [resetToken, payload] = await Promise.all([ + this.tokensRepository.findOne({ where: { token } }), + this.cryptographyService.verifyToken(token), + ]); if ( + !payload || !resetToken || - !resetToken.entity_id || resetToken.type !== AUTH_TOKENS_MAPPING.password_reset || (resetToken.expires_at && resetToken.expires_at < new Date()) ) { @@ -56,15 +68,15 @@ export class ResetPasswordUseCase { ); } - const { account_type: accountType } = resetPasswordDto; + const { sub: id, accountType } = payload; const findOptions = { + where: { id }, select: { id: true, email: true, role: accountType === 'patient' ? undefined : true, }, - where: { id: resetToken.entity_id }, }; const entity: { id: string; email: string; role?: UserRole } | null = @@ -74,7 +86,7 @@ export class ResetPasswordUseCase { if (!entity) { this.logger.warn( - { id: resetToken.entity_id, accountType }, + { id, accountType }, 'Reset password failed: Entity not registered', ); throw new NotFoundException('Usuário não encontrado.'); @@ -85,34 +97,28 @@ export class ResetPasswordUseCase { ); if (accountType === 'patient') { - await this.usersRepository.update(entity.id, { password }); - } else { await this.patientsRepository.update(entity.id, { password }); + } else { + await this.usersRepository.update(entity.id, { password }); } - await this.tokensRepository.delete({ token }); - this.logger.log( { id: entity.id, email: entity.email, accountType }, 'Password reseted successfully', ); - const accessToken = await this.cryptographyService.createToken( - AUTH_TOKENS_MAPPING.access_token, - { sub: entity.id, role: entity.role ?? 'patient' }, - { expiresIn: '12h' }, - ); + await this.tokensRepository.delete({ token }); - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 12); + const { maxAge, token: accessToken } = + await this.createTokenUseCase.execute({ + type: AUTH_TOKENS_MAPPING.access_token, + payload: { sub: entity.id, accountType }, + }); - await this.tokensRepository.save({ - type: AUTH_TOKENS_MAPPING.access_token, - expires_at: expiresAt, - entity_id: entity.id, - token: accessToken, + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.access_token, + value: accessToken, + maxAge, }); - - return { accessToken }; } } diff --git a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts index 1fa0e9e..dc89c38 100644 --- a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts +++ b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts @@ -1,22 +1,25 @@ import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import type { Response } from 'express'; import { Repository } from 'typeorm'; import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { CreateTokenUseCase } from '@/app/cryptography/use-cases/create-token.use-case'; +import { COOKIES_MAPPING } from '@/domain/cookies'; import { Patient } from '@/domain/entities/patient'; import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; import type { UserRole } from '@/domain/enums/users'; +import { UtilsService } from '@/utils/utils.service'; import type { SignInWithEmailDto } from '../auth.dtos'; -interface SignInWithEmailUseCaseRequest { +interface SignInWithEmailUseCaseInput { signInWithEmailDto: SignInWithEmailDto; + response: Response; } -type SignInWithEmailUseCaseResponse = Promise<{ accessToken: string }>; - @Injectable() export class SignInWithEmailUseCase { private readonly logger = new Logger(SignInWithEmailUseCase.name); @@ -28,12 +31,15 @@ export class SignInWithEmailUseCase { private readonly patientsRepository: Repository, @InjectRepository(Token) private readonly tokensRepository: Repository, + private readonly createTokenUseCase: CreateTokenUseCase, private readonly cryptographyService: CryptographyService, + private readonly utilsService: UtilsService, ) {} async execute({ signInWithEmailDto, - }: SignInWithEmailUseCaseRequest): SignInWithEmailUseCaseResponse { + response, + }: SignInWithEmailUseCaseInput): Promise { const { email, password, @@ -42,12 +48,12 @@ export class SignInWithEmailUseCase { } = signInWithEmailDto; const findOptions = { + where: { email }, select: { id: true, password: true, role: accountType === 'patient' ? undefined : true, }, - where: { email }, }; const entity: { @@ -76,32 +82,50 @@ export class SignInWithEmailUseCase { ); } - const accessToken = await this.cryptographyService.createToken( - AUTH_TOKENS_MAPPING.access_token, - { sub: entity.id, role: entity.role ?? 'patient' }, - { expiresIn: keepLoggedIn ? '30d' : '12h' }, - ); + const role = entity.role ?? 'patient'; - const expiresAt = new Date(); + const { maxAge: accessTokenMaxAge, token: accessToken } = + await this.createTokenUseCase.execute({ + type: AUTH_TOKENS_MAPPING.access_token, + payload: { sub: entity.id, accountType }, + }); + + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.access_token, + maxAge: accessTokenMaxAge, + value: accessToken, + }); if (keepLoggedIn) { - expiresAt.setDate(expiresAt.getDate() + 30); - } else { - expiresAt.setHours(expiresAt.getHours() + 12); + // Delete ALL refresh tokens for this entity before generate a new one + await this.tokensRepository.delete({ entity_id: entity.id }); + + const { + maxAge: refreshTokenMaxAge, + token: refreshToken, + expiresAt, + } = await this.createTokenUseCase.execute({ + type: AUTH_TOKENS_MAPPING.refresh_token, + payload: { sub: entity.id, accountType }, + }); + + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.refresh_token, + maxAge: refreshTokenMaxAge, + value: refreshToken, + }); + + await this.tokensRepository.save({ + type: AUTH_TOKENS_MAPPING.refresh_token, + expires_at: expiresAt, + entity_id: entity.id, + token: refreshToken, + }); } - await this.tokensRepository.save({ - type: AUTH_TOKENS_MAPPING.access_token, - expires_at: expiresAt, - entity_id: entity.id, - token: accessToken, - }); - this.logger.log( - { entityId: entity.id, email }, + { entityId: entity.id, email, role, keepLoggedIn }, 'Entity signed in with e-mail', ); - - return { accessToken }; } } diff --git a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts index 35f115d..1272a69 100644 --- a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts @@ -16,8 +16,6 @@ interface ApprovePatientRequirementUseCaseRequest { user: AuthUserDto; } -type ApprovePatientRequirementUseCaseResponse = Promise; - @Injectable() export class ApprovePatientRequirementUseCase { private readonly logger = new Logger(ApprovePatientRequirementUseCase.name); @@ -30,7 +28,7 @@ export class ApprovePatientRequirementUseCase { async execute({ id, user, - }: ApprovePatientRequirementUseCaseRequest): ApprovePatientRequirementUseCaseResponse { + }: ApprovePatientRequirementUseCaseRequest): Promise { const requirement = await this.patientRequirementsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts index ff31256..d78263a 100644 --- a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts @@ -13,8 +13,6 @@ interface CreatePatientRequirementUseCaseRequest { user: AuthUserDto; } -type CreatePatientRequirementUseCaseResponse = Promise; - @Injectable() export class CreatePatientRequirementUseCase { private readonly logger = new Logger(CreatePatientRequirementUseCase.name); @@ -29,7 +27,7 @@ export class CreatePatientRequirementUseCase { async execute({ createPatientRequirementDto, user, - }: CreatePatientRequirementUseCaseRequest): CreatePatientRequirementUseCaseResponse { + }: CreatePatientRequirementUseCaseRequest): Promise { const { patient_id } = createPatientRequirementDto; const patient = await this.patientsRepository.findOne({ diff --git a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts index 0c3afff..fad529c 100644 --- a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts @@ -16,8 +16,6 @@ interface DeclinePatientRequirementUseCaseRequest { user: AuthUserDto; } -type DeclinePatientRequirementUseCaseResponse = Promise; - @Injectable() export class DeclinePatientRequirementUseCase { private readonly logger = new Logger(DeclinePatientRequirementUseCase.name); @@ -30,7 +28,7 @@ export class DeclinePatientRequirementUseCase { async execute({ id, user, - }: DeclinePatientRequirementUseCaseRequest): DeclinePatientRequirementUseCaseResponse { + }: DeclinePatientRequirementUseCaseRequest): Promise { const requirement = await this.patientRequirementsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts b/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts index c839bb8..5d13b7f 100644 --- a/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts @@ -18,10 +18,10 @@ interface GetPatientRequirementsByPatientIdUseCaseRequest { query: GetPatientRequirementsByPatientIdQuery; } -type GetPatientRequirementsByPatientIdUseCaseResponse = Promise<{ +interface GetPatientRequirementsByPatientIdUseCaseResponse { requirements: PatientRequirementByPatientId[]; total: number; -}>; +} @Injectable() export class GetPatientRequirementsByPatientIdUseCase { @@ -33,7 +33,7 @@ export class GetPatientRequirementsByPatientIdUseCase { async execute({ patientId, query, - }: GetPatientRequirementsByPatientIdUseCaseRequest): GetPatientRequirementsByPatientIdUseCaseResponse { + }: GetPatientRequirementsByPatientIdUseCaseRequest): Promise { const { status, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts b/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts index 675fe2c..7fbb644 100644 --- a/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts @@ -19,10 +19,10 @@ interface GetPatientRequirementsUseCaseRequest { query: GetPatientRequirementsQuery; } -type GetPatientRequirementsUseCaseResponse = Promise<{ +interface GetPatientRequirementsUseCaseResponse { requirements: PatientRequirementItem[]; total: number; -}>; +} @Injectable() export class GetPatientRequirementsUseCase { @@ -33,7 +33,7 @@ export class GetPatientRequirementsUseCase { async execute({ query, - }: GetPatientRequirementsUseCaseRequest): GetPatientRequirementsUseCaseResponse { + }: GetPatientRequirementsUseCaseRequest): Promise { const { search, status, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts index 1e6a90a..6a3cd9c 100644 --- a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts @@ -19,8 +19,6 @@ interface CreatePatientSupportUseCaseRequest { createPatientSupportDto: CreatePatientSupportDto; } -type CreatePatientSupportUseCaseResponse = Promise; - @Injectable() export class CreatePatientSupportUseCase { private readonly logger = new Logger(CreatePatientSupportUseCase.name); @@ -36,7 +34,7 @@ export class CreatePatientSupportUseCase { user, patientId, createPatientSupportDto, - }: CreatePatientSupportUseCaseRequest): CreatePatientSupportUseCaseResponse { + }: CreatePatientSupportUseCaseRequest): Promise { if (user.id !== patientId) { this.logger.log( { patientId, userId: user.id, role: user.role }, diff --git a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts index 576ea99..7f396ca 100644 --- a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts @@ -16,8 +16,6 @@ interface DeletePatientSupportUseCaseRequest { user: AuthUserDto; } -type DeletePatientSupportUseCaseResponse = Promise; - @Injectable() export class DeletePatientSupportUseCase { private readonly logger = new Logger(DeletePatientSupportUseCase.name); @@ -30,7 +28,7 @@ export class DeletePatientSupportUseCase { async execute({ id, user, - }: DeletePatientSupportUseCaseRequest): DeletePatientSupportUseCaseResponse { + }: DeletePatientSupportUseCaseRequest): Promise { const patientSupport = await this.patientSupportsRepository.findOne({ select: { id: true, patient_id: true }, where: { id }, diff --git a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts index 7930ba3..89db23a 100644 --- a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts @@ -18,8 +18,6 @@ interface UpdatePatientSupportUseCaseRequest { updatePatientSupportDto: UpdatePatientSupportDto; } -type UpdatePatientSupportUseCaseResponse = Promise; - @Injectable() export class UpdatePatientSupportUseCase { private readonly logger = new Logger(UpdatePatientSupportUseCase.name); @@ -33,7 +31,7 @@ export class UpdatePatientSupportUseCase { id, user, updatePatientSupportDto, - }: UpdatePatientSupportUseCaseRequest): UpdatePatientSupportUseCaseResponse { + }: UpdatePatientSupportUseCaseRequest): Promise { const patientSupport = await this.patientSupportsRepository.findOne({ select: { id: true, patient_id: true }, where: { id }, diff --git a/src/app/http/patients/patients.controller.ts b/src/app/http/patients/patients.controller.ts index e2b4796..71edbe9 100644 --- a/src/app/http/patients/patients.controller.ts +++ b/src/app/http/patients/patients.controller.ts @@ -60,12 +60,12 @@ export class PatientsController { @Roles(['manager', 'nurse', 'specialist']) @ApiOperation({ summary: 'Busca um paciente pelo ID' }) async getPatientById(@Param('id') id: string): Promise { - const data = await this.getPatientUseCase.execute({ id }); + const { patient } = await this.getPatientUseCase.execute({ id }); return { success: true, message: 'Paciente retornado com sucesso.', - data, + data: patient, }; } diff --git a/src/app/http/patients/use-cases/create-patient.use-case.ts b/src/app/http/patients/use-cases/create-patient.use-case.ts index 528bdab..b6ae120 100644 --- a/src/app/http/patients/use-cases/create-patient.use-case.ts +++ b/src/app/http/patients/use-cases/create-patient.use-case.ts @@ -14,8 +14,6 @@ interface CreatePatientUseCaseRequest { createPatientDto: CreatePatientDto; } -type CreatePatientUseCaseResponse = Promise; - @Injectable() export class CreatePatientUseCase { private readonly logger = new Logger(CreatePatientUseCase.name); @@ -30,7 +28,7 @@ export class CreatePatientUseCase { async execute({ user, createPatientDto, - }: CreatePatientUseCaseRequest): CreatePatientUseCaseResponse { + }: CreatePatientUseCaseRequest): Promise { const { email, cpf, supports, ...patientData } = createPatientDto; const patientWithSameEmail = await this.patientsRepository.findOne({ diff --git a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts index dd4cd6a..caac607 100644 --- a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts +++ b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts @@ -16,8 +16,6 @@ interface DeactivatePatientUseCaseRequest { user: AuthUserDto; } -type DeactivatePatientUseCaseResponse = Promise; - @Injectable() export class DeactivatePatientUseCase { private readonly logger = new Logger(DeactivatePatientUseCase.name); @@ -27,10 +25,7 @@ export class DeactivatePatientUseCase { private readonly patientsRepository: Repository, ) {} - async execute({ - id, - user, - }: DeactivatePatientUseCaseRequest): DeactivatePatientUseCaseResponse { + async execute({ id, user }: DeactivatePatientUseCaseRequest): Promise { const patient = await this.patientsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/patients/use-cases/get-patient.use-case.ts b/src/app/http/patients/use-cases/get-patient.use-case.ts index 0d5ed33..ad345b6 100644 --- a/src/app/http/patients/use-cases/get-patient.use-case.ts +++ b/src/app/http/patients/use-cases/get-patient.use-case.ts @@ -8,7 +8,9 @@ interface GetPatientUseCaseRequest { id: string; } -type GetPatientUseCaseResponse = Promise; +interface GetPatientUseCaseResponse { + patient: Patient; +} @Injectable() export class GetPatientUseCase { @@ -17,7 +19,9 @@ export class GetPatientUseCase { private readonly patientsRepository: Repository, ) {} - async execute({ id }: GetPatientUseCaseRequest): GetPatientUseCaseResponse { + async execute({ + id, + }: GetPatientUseCaseRequest): Promise { const patient = await this.patientsRepository.findOne({ relations: { supports: true }, where: { id }, @@ -47,6 +51,6 @@ export class GetPatientUseCase { throw new NotFoundException('Paciente não encontrado.'); } - return patient; + return { patient }; } } diff --git a/src/app/http/patients/use-cases/get-patients.use-case.ts b/src/app/http/patients/use-cases/get-patients.use-case.ts index 69b3550..534762a 100644 --- a/src/app/http/patients/use-cases/get-patients.use-case.ts +++ b/src/app/http/patients/use-cases/get-patients.use-case.ts @@ -20,10 +20,10 @@ interface GetPatientsUseCaseRequest { query: GetPatientsQuery; } -type GetPatientsUseCaseResponse = Promise<{ +interface GetPatientsUseCaseResponse { patients: PatientResponse[]; total: number; -}>; +} @Injectable() export class GetPatientsUseCase { @@ -34,7 +34,7 @@ export class GetPatientsUseCase { async execute({ query, - }: GetPatientsUseCaseRequest): GetPatientsUseCaseResponse { + }: GetPatientsUseCaseRequest): Promise { const { search, order, orderBy, status, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/patients/use-cases/update-patient.use-case.ts b/src/app/http/patients/use-cases/update-patient.use-case.ts index f3c76f2..b5c4859 100644 --- a/src/app/http/patients/use-cases/update-patient.use-case.ts +++ b/src/app/http/patients/use-cases/update-patient.use-case.ts @@ -19,8 +19,6 @@ interface UpdatePatientUseCaseRequest { updatePatientDto: UpdatePatientDto; } -type UpdatePatientUseCaseResponse = Promise; - @Injectable() export class UpdatePatientUseCase { private readonly logger = new Logger(UpdatePatientUseCase.name); @@ -34,7 +32,7 @@ export class UpdatePatientUseCase { id, user, updatePatientDto, - }: UpdatePatientUseCaseRequest): UpdatePatientUseCaseResponse { + }: UpdatePatientUseCaseRequest): Promise { if (user.role === 'patient' && user.id !== id) { this.logger.log( { id, userId: user.id, userEmail: user.email, role: user.role }, diff --git a/src/app/http/referrals/use-cases/cancel-referral.use-case.ts b/src/app/http/referrals/use-cases/cancel-referral.use-case.ts index ee3c29e..34a907c 100644 --- a/src/app/http/referrals/use-cases/cancel-referral.use-case.ts +++ b/src/app/http/referrals/use-cases/cancel-referral.use-case.ts @@ -14,8 +14,6 @@ interface CancelReferralUseCaseRequest { userId: string; } -type CancelReferralUseCaseResponse = Promise; - @Injectable() export class CancelReferralUseCase { private readonly logger = new Logger(CancelReferralUseCase.name); @@ -25,10 +23,7 @@ export class CancelReferralUseCase { private readonly referralsRepository: Repository, ) {} - async execute({ - id, - userId, - }: CancelReferralUseCaseRequest): CancelReferralUseCaseResponse { + async execute({ id, userId }: CancelReferralUseCaseRequest): Promise { const referral = await this.referralsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/referrals/use-cases/create-referrals.use-case.ts b/src/app/http/referrals/use-cases/create-referrals.use-case.ts index c8785f5..1bab2ae 100644 --- a/src/app/http/referrals/use-cases/create-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/create-referrals.use-case.ts @@ -12,8 +12,6 @@ interface CreateReferralUseCaseRequest { createReferralDto: CreateReferralDto; } -type CreateReferralUseCaseResponse = Promise; - @Injectable() export class CreateReferralUseCase { private readonly logger = new Logger(CreateReferralUseCase.name); @@ -27,7 +25,7 @@ export class CreateReferralUseCase { async execute({ createReferralDto, userId, - }: CreateReferralUseCaseRequest): CreateReferralUseCaseResponse { + }: CreateReferralUseCaseRequest): Promise { const { patient_id } = createReferralDto; const patient = await this.patientsRepository.findOne({ diff --git a/src/app/http/referrals/use-cases/get-referrals.use-case.ts b/src/app/http/referrals/use-cases/get-referrals.use-case.ts index f1c5e6b..c5b3b6e 100644 --- a/src/app/http/referrals/use-cases/get-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/get-referrals.use-case.ts @@ -11,7 +11,7 @@ import { import { Referral } from '@/domain/entities/referral'; import type { ReferralOrderBy } from '@/domain/enums/referrals'; -import type { GetReferralsResponse } from '@/domain/schemas/referrals/responses'; +import type { ReferralResponse } from '@/domain/schemas/referrals/responses'; import { GetReferralsQuery } from '../referrals.dtos'; @@ -19,7 +19,10 @@ interface GetReferralsUseCaseRequest { query: GetReferralsQuery; } -type GetReferralsUseCaseResponse = Promise; +interface GetReferralsUseCaseResponse { + referrals: ReferralResponse[]; + total: number; +} @Injectable() export class GetReferralsUseCase { @@ -30,7 +33,7 @@ export class GetReferralsUseCase { async execute({ query, - }: GetReferralsUseCaseRequest): GetReferralsUseCaseResponse { + }: GetReferralsUseCaseRequest): Promise { const { search, status, category, condition, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index 74457c6..fdc789f 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -6,6 +6,8 @@ import type { GetPatientsByCityResponse, GetPatientsByGenderResponse, GetReferredPatientsByStateResponse, + GetTotalAppointmentsResponse, + GetTotalPatientsByStatusResponse, GetTotalReferralsAndReferredPatientsPercentageResponse, GetTotalReferralsByCategoryResponse, TotalPatientsByCity, @@ -40,7 +42,7 @@ export class StatisticsController { @Get('appointments-total') @ApiOperation({ summary: 'Número total de atendimentos' }) - async getTotalAppointments() { + async getTotalAppointments(): Promise { const total = await this.getTotalAppointmentsUseCase.execute(); return { @@ -52,12 +54,12 @@ export class StatisticsController { @Get('patients-total') @ApiOperation({ summary: 'Estatísticas totais de pacientes' }) - async getPatientsTotal() { + async getTotalPatients(): Promise { const data = await this.getTotalPatientsByStatusUseCase.execute(); return { success: true, - message: 'Estatísticas com total de pacientes retornada com sucesso.', + message: 'Número total de pacientes retornado com sucesso.', data, }; } @@ -69,10 +71,7 @@ export class StatisticsController { ): Promise { const { items: genders, total } = await this.getTotalPatientsByPeriodUseCase.execute( - { - field: 'gender', - query, - }, + { field: 'gender', query }, ); return { diff --git a/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts b/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts index bdc7eaa..b45997f 100644 --- a/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts @@ -24,8 +24,6 @@ interface GetTotalAppointmentsUseCaseRequest { endDate?: Date; } -type GetTotalAppointmentsUseCaseResponse = Promise; - @Injectable() export class GetTotalAppointmentsUseCase { constructor( @@ -41,7 +39,7 @@ export class GetTotalAppointmentsUseCase { period, startDate, endDate, - }: GetTotalAppointmentsUseCaseRequest = {}): GetTotalAppointmentsUseCaseResponse { + }: GetTotalAppointmentsUseCaseRequest = {}): Promise { const where: FindOptionsWhere = {}; if (period) { diff --git a/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts index 03c043a..43f3f49 100644 --- a/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts @@ -14,10 +14,10 @@ interface GetTotalPatientsByFieldUseCaseRequest { query: GetTotalPatientsByFieldQuery; } -type GetTotalPatientsByFieldUseCaseResponse = Promise<{ +interface GetTotalPatientsByFieldUseCaseResponse { items: T[]; total: number; -}>; +} @Injectable() export class GetTotalPatientsByFieldUseCase { @@ -31,7 +31,9 @@ export class GetTotalPatientsByFieldUseCase { async execute({ field, query, - }: GetTotalPatientsByFieldUseCaseRequest): GetTotalPatientsByFieldUseCaseResponse { + }: GetTotalPatientsByFieldUseCaseRequest): Promise< + GetTotalPatientsByFieldUseCaseResponse + > { const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( query.period, ); diff --git a/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts index 8f7b312..e21347d 100644 --- a/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts @@ -4,11 +4,11 @@ import type { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -type GetTotalPatientsByStatusUseCaseResponse = Promise<{ +interface GetTotalPatientsByStatusUseCaseResponse { total: number; active: number; inactive: number; -}>; +} @Injectable() export class GetTotalPatientsByStatusUseCase { @@ -17,7 +17,7 @@ export class GetTotalPatientsByStatusUseCase { private readonly patientsRepository: Repository, ) {} - async execute(): GetTotalPatientsByStatusUseCaseResponse { + async execute(): Promise { const queryBuilder = await this.patientsRepository .createQueryBuilder('patient') .select('COUNT(patient.id)', 'total') diff --git a/src/app/http/statistics/use-cases/get-total-patients.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients.use-case.ts index 6320cd3..c94f697 100644 --- a/src/app/http/statistics/use-cases/get-total-patients.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients.use-case.ts @@ -21,8 +21,6 @@ interface GetTotalPatientsUseCaseRequest { endDate?: Date; } -type GetTotalPatientsUseCaseResponse = Promise; - @Injectable() export class GetTotalPatientsUseCase { constructor( @@ -36,7 +34,7 @@ export class GetTotalPatientsUseCase { period, startDate, endDate, - }: GetTotalPatientsUseCaseRequest = {}): GetTotalPatientsUseCaseResponse { + }: GetTotalPatientsUseCaseRequest = {}): Promise { const where: FindOptionsWhere = { status: status ?? Not('pending'), }; diff --git a/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts index 7491320..b569785 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts @@ -9,10 +9,10 @@ interface GetTotalReferralsAndReferredPatientsPercentageUseCaseRequest { query: GetTotalReferralsAndReferredPatientsPercentageQuery; } -type GetTotalReferralsAndReferredPatientsPercentageUseCaseResponse = Promise<{ +interface GetTotalReferralsAndReferredPatientsPercentageUseCaseResponse { totalReferrals: number; referredPatientsPercentage: number; -}>; +} @Injectable() export class GetTotalReferralsAndReferredPatientsPercentageUseCase { @@ -24,7 +24,7 @@ export class GetTotalReferralsAndReferredPatientsPercentageUseCase { async execute({ query, - }: GetTotalReferralsAndReferredPatientsPercentageUseCaseRequest): GetTotalReferralsAndReferredPatientsPercentageUseCaseResponse { + }: GetTotalReferralsAndReferredPatientsPercentageUseCaseRequest): Promise { const { period } = query; const [totalPatients, totalReferrals, totalReferredPatients] = diff --git a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts index ba9194d..a4431e4 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts @@ -12,10 +12,10 @@ interface GetTotalReferralsByCategoryUseCaseRequest { query: GetTotalReferralsByCategoryQuery; } -type GetTotalReferralsByCategoryUseCaseResponse = Promise<{ +interface GetTotalReferralsByCategoryUseCaseResponse { categories: TotalReferralsByCategory[]; total: number; -}>; +} @Injectable() export class GetTotalReferralsByCategoryUseCase { @@ -27,7 +27,7 @@ export class GetTotalReferralsByCategoryUseCase { async execute({ query, - }: GetTotalReferralsByCategoryUseCaseRequest): GetTotalReferralsByCategoryUseCaseResponse { + }: GetTotalReferralsByCategoryUseCaseRequest): Promise { const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( query.period, ); diff --git a/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts index d136161..33276f6 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts @@ -24,8 +24,6 @@ interface GetTotalReferralsUseCaseRequest { endDate?: Date; } -type GetTotalReferralsUseCaseResponse = Promise; - @Injectable() export class GetTotalReferralsUseCase { constructor( @@ -41,7 +39,7 @@ export class GetTotalReferralsUseCase { period, startDate, endDate, - }: GetTotalReferralsUseCaseRequest = {}): GetTotalReferralsUseCaseResponse { + }: GetTotalReferralsUseCaseRequest = {}): Promise { const where: FindOptionsWhere = {}; if (period) { diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts index 82fdb83..2c347f8 100644 --- a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts @@ -12,10 +12,10 @@ interface GetTotalReferredPatientsByStateUseCaseRequest { query: GetReferredPatientsByStateQuery; } -type GetTotalReferredPatientsByStateUseCaseResponse = Promise<{ +interface GetTotalReferredPatientsByStateUseCaseResponse { states: TotalReferredPatientsByStateSchema[]; total: number; -}>; +} @Injectable() export class GetTotalReferredPatientsByStateUseCase { @@ -27,7 +27,7 @@ export class GetTotalReferredPatientsByStateUseCase { async execute({ query, - }: GetTotalReferredPatientsByStateUseCaseRequest): GetTotalReferredPatientsByStateUseCaseResponse { + }: GetTotalReferredPatientsByStateUseCaseRequest): Promise { const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( query.period, ); diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts index da06d99..c2cf62b 100644 --- a/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts @@ -20,8 +20,6 @@ interface GetTotalReferredPatientsUseCaseRequest { endDate?: Date; } -type GetTotalReferredPatientsUseCaseResponse = Promise; - @Injectable() export class GetTotalReferredPatientsUseCase { constructor( @@ -34,7 +32,7 @@ export class GetTotalReferredPatientsUseCase { period, startDate, endDate, - }: GetTotalReferredPatientsUseCaseRequest = {}): GetTotalReferredPatientsUseCaseResponse { + }: GetTotalReferredPatientsUseCaseRequest = {}): Promise { const where: FindOptionsWhere = { referrals: { id: Not(IsNull()) }, }; diff --git a/src/app/http/users/use-cases/get-user.use-case.ts b/src/app/http/users/use-cases/get-user.use-case.ts index a9c907a..757c332 100644 --- a/src/app/http/users/use-cases/get-user.use-case.ts +++ b/src/app/http/users/use-cases/get-user.use-case.ts @@ -9,7 +9,9 @@ interface GetUserUseCaseRequest { id: string; } -type GetUserUseCaseResponse = Promise; +interface GetUserUseCaseResponse { + user: UserResponse; +} @Injectable() export class GetUserUseCase { @@ -18,7 +20,9 @@ export class GetUserUseCase { private readonly usersRepository: Repository, ) {} - async execute({ id }: GetUserUseCaseRequest): GetUserUseCaseResponse { + async execute({ + id, + }: GetUserUseCaseRequest): Promise { const user = await this.usersRepository.findOne({ where: { id }, select: { @@ -34,6 +38,6 @@ export class GetUserUseCase { throw new NotFoundException('Usuário não encontrado.'); } - return user; + return { user }; } } diff --git a/src/app/http/users/use-cases/update-user.use-case.ts b/src/app/http/users/use-cases/update-user.use-case.ts index 625c353..af602c3 100644 --- a/src/app/http/users/use-cases/update-user.use-case.ts +++ b/src/app/http/users/use-cases/update-user.use-case.ts @@ -18,8 +18,6 @@ interface UpdateUserUseCaseRequest { updateUserDto: UpdateUserDto; } -type UpdateUserUseCaseResponse = Promise; - @Injectable() export class UpdateUserUseCase { private readonly logger = new Logger(UpdateUserUseCase.name); @@ -33,7 +31,7 @@ export class UpdateUserUseCase { id, user, updateUserDto, - }: UpdateUserUseCaseRequest): UpdateUserUseCaseResponse { + }: UpdateUserUseCaseRequest): Promise { if (user.role !== 'admin' && user.id !== id) { this.logger.log( { id, userId: user.id, role: user.role }, diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index 6c47e76..f83cfe3 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -13,15 +13,17 @@ import { GetUserUseCase } from './use-cases/get-user.use-case'; export class UsersController { constructor(private readonly getUserUseCase: GetUserUseCase) {} - @Get('profile') + @Get('me') @Roles(['manager', 'nurse', 'specialist']) - async getProfile(@AuthUser() user: AuthUserDto): Promise { - const data = await this.getUserUseCase.execute({ id: user.id }); + async getProfile( + @AuthUser() authUser: AuthUserDto, + ): Promise { + const { user } = await this.getUserUseCase.execute({ id: authUser.id }); return { success: true, message: 'Dados do usuário retornado com sucesso.', - data, + data: user, }; } } diff --git a/src/common/guards/auth.guard.ts b/src/common/guards/auth.guard.ts index d7749a3..b3bb11c 100644 --- a/src/common/guards/auth.guard.ts +++ b/src/common/guards/auth.guard.ts @@ -6,26 +6,44 @@ import { } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { InjectRepository } from '@nestjs/typeorm'; +import type { Response } from 'express'; import type { Repository } from 'typeorm'; import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { CreateTokenUseCase } from '@/app/cryptography/use-cases/create-token.use-case'; import type { AuthUserDto } from '@/app/http/auth/auth.dtos'; import type { Cookie } from '@/domain/cookies'; +import { COOKIES_MAPPING } from '@/domain/cookies'; import { Patient } from '@/domain/entities/patient'; +import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; -import type { AccessTokenPayload } from '@/domain/schemas/tokens'; +import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; +import type { + AccessTokenPayload, + RefreshTokenPayload, +} from '@/domain/schemas/tokens'; +import { UtilsService } from '@/utils/utils.service'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; +interface AuthenticatedRequest { + signedCookies?: Record; + user?: AuthUserDto; +} + @Injectable() export class AuthGuard implements CanActivate { constructor( - private readonly reflector: Reflector, - private readonly cryptographyService: CryptographyService, @InjectRepository(User) private readonly usersRepository: Repository, @InjectRepository(Patient) private readonly patientsRepository: Repository, + @InjectRepository(Token) + private readonly tokensRepository: Repository, + private readonly cryptographyService: CryptographyService, + private readonly createTokenUseCase: CreateTokenUseCase, + private readonly utilsService: UtilsService, + private readonly reflector: Reflector, ) {} async canActivate(context: ExecutionContext): Promise { @@ -38,57 +56,136 @@ export class AuthGuard implements CanActivate { return true; } - const request = context.switchToHttp().getRequest<{ - signedCookies?: Record; - user?: AuthUserDto; - }>(); + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); - const token = request.signedCookies?.access_token; + const accessToken = request.signedCookies?.access_token; + const refreshToken = request.signedCookies?.refresh_token; - if (!token) { - throw new UnauthorizedException('Token de acesso ausente.'); + if (accessToken) { + try { + const user = await this.getUserFromToken(accessToken); + + if (!user) { + throw new UnauthorizedException( + 'Token de acesso inválido ou expirado.', + ); + } + + request.user = user; + return true; + } catch (error) { + this.utilsService.deleteCookie(response, COOKIES_MAPPING.access_token); + + if (error instanceof UnauthorizedException) { + throw error; + } + + throw new UnauthorizedException( + 'Token de acesso inválido ou expirado.', + ); + } + } + + if (!refreshToken) { + throw new UnauthorizedException('Token de atualização ausente.'); } try { - const tokenPayload = - await this.cryptographyService.verifyToken(token); + const user = await this.getUserFromToken(refreshToken); + + if (!user) { + throw new UnauthorizedException( + 'Token de atualização inválido ou expirado.', + ); + } + + const storedRefreshToken = await this.tokensRepository.findOne({ + where: { + type: AUTH_TOKENS_MAPPING.refresh_token, + token: refreshToken, + entity_id: user.id, + }, + }); + + if (!storedRefreshToken || !storedRefreshToken.expires_at) { + throw new UnauthorizedException('Token de atualização não encontrado.'); + } - const userId = tokenPayload.sub; - const role = tokenPayload.role; + if (storedRefreshToken.expires_at < new Date()) { + await this.tokensRepository.delete({ entity_id: user.id }); - if (!userId) { - throw new UnauthorizedException('Token inválido.'); + this.utilsService.deleteCookie(response, COOKIES_MAPPING.access_token); + this.utilsService.deleteCookie(response, COOKIES_MAPPING.refresh_token); + + throw new UnauthorizedException('Token de atualização expirado.'); } - if (role === 'patient') { - const user = await this.patientsRepository.findOne({ - select: { id: true, email: true }, - where: { id: userId }, + const { token: newAccessToken, maxAge } = + await this.createTokenUseCase.execute({ + type: COOKIES_MAPPING.access_token, + payload: { + sub: user.id, + accountType: user.role === 'patient' ? 'patient' : 'user', + }, }); - if (!user) { - throw new UnauthorizedException('Usuário não encontrado.'); - } + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.access_token, + value: newAccessToken, + maxAge, + }); - request.user = { id: user.id, email: user.email, role }; + request.user = user; + return true; + } catch (error) { + this.utilsService.deleteCookie(response, COOKIES_MAPPING.access_token); + this.utilsService.deleteCookie(response, COOKIES_MAPPING.refresh_token); - return true; + if (error instanceof UnauthorizedException) { + throw error; } - const user = await this.usersRepository.findOne({ - select: { id: true, email: true, role: true }, - where: { id: userId }, + throw new UnauthorizedException( + 'Token de atualização inválido ou expirado.', + ); + } + } + + private async getUserFromToken(token: string): Promise { + const payload = await this.cryptographyService.verifyToken< + AccessTokenPayload | RefreshTokenPayload + >(token); + + const entityId = payload.sub; + const accountType = payload.accountType; + + if (!entityId) { + return null; + } + + if (accountType === 'patient') { + const patient = await this.patientsRepository.findOne({ + select: { id: true, email: true }, + where: { id: entityId }, }); - if (!user) { - throw new UnauthorizedException('Usuário não encontrado.'); + if (!patient) { + return null; } - request.user = { id: user.id, email: user.email, role }; + return { id: patient.id, email: patient.email, role: 'patient' }; + } - return true; - } catch { - throw new UnauthorizedException('Token inválido ou expirado.'); + const user = await this.usersRepository.findOne({ + select: { id: true, email: true, role: true }, + where: { id: entityId }, + }); + + if (!user) { + return null; } + + return { id: user.id, email: user.email, role: user.role }; } } diff --git a/src/domain/cookies.ts b/src/domain/cookies.ts index 25096d4..1e8bf6a 100644 --- a/src/domain/cookies.ts +++ b/src/domain/cookies.ts @@ -1,10 +1,11 @@ -import type { AuthTokenKey } from './enums/tokens'; +import { AUTH_TOKENS_MAPPING, type AuthTokenType } from './enums/tokens'; -export type Cookie = AuthTokenKey; +export type Cookie = AuthTokenType; export type Cookies = Record; export const COOKIES_MAPPING: Cookies = { - access_token: 'access_token', - password_reset: 'password_reset', - invite_token: 'invite_token', + access_token: AUTH_TOKENS_MAPPING.access_token, + refresh_token: AUTH_TOKENS_MAPPING.refresh_token, + password_reset: AUTH_TOKENS_MAPPING.password_reset, + invite_user_token: AUTH_TOKENS_MAPPING.invite_user_token, } as const; diff --git a/src/domain/entities/database.ts b/src/domain/entities/database.ts index 20eb10d..63eee07 100644 --- a/src/domain/entities/database.ts +++ b/src/domain/entities/database.ts @@ -3,7 +3,6 @@ import { Patient } from './patient'; import { PatientRequirement } from './patient-requirement'; import { PatientSupport } from './patient-support'; import { Referral } from './referral'; -// import { Specialist } from './specialist'; import { Token } from './token'; import { User } from './user'; @@ -13,7 +12,6 @@ export const DATABASE_ENTITIES = [ Patient, PatientSupport, Appointment, - // Specialist, PatientRequirement, Referral, ]; diff --git a/src/domain/entities/patient.ts b/src/domain/entities/patient.ts index f43c577..a720e19 100644 --- a/src/domain/entities/patient.ts +++ b/src/domain/entities/patient.ts @@ -34,7 +34,7 @@ export class Patient implements PatientSchema { @Column({ type: 'varchar', length: 64 }) name: string; - @Column({ type: 'varchar', length: 64 }) + @Column({ type: 'varchar', length: 64, unique: true }) email: string; @Column({ type: 'varchar', nullable: true }) diff --git a/src/domain/entities/token.ts b/src/domain/entities/token.ts index 625c308..f13ca38 100644 --- a/src/domain/entities/token.ts +++ b/src/domain/entities/token.ts @@ -5,7 +5,7 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; -import { AUTH_TOKENS, type AuthTokenKey } from '../enums/tokens'; +import { AUTH_TOKENS, type AuthTokenType } from '../enums/tokens'; import type { AuthToken } from '../schemas/tokens'; @Entity('tokens') @@ -23,7 +23,7 @@ export class Token implements AuthToken { token: string; @Column({ type: 'enum', enum: AUTH_TOKENS }) - type: AuthTokenKey; + type: AuthTokenType; @CreateDateColumn({ type: 'datetime', nullable: true }) expires_at: Date | null; diff --git a/src/domain/entities/user.ts b/src/domain/entities/user.ts index 2299af1..f1cf1fd 100644 --- a/src/domain/entities/user.ts +++ b/src/domain/entities/user.ts @@ -22,7 +22,7 @@ export class User implements UserSchema { @Column({ type: 'varchar', length: 64 }) name: string; - @Column({ type: 'varchar', length: 64 }) + @Column({ type: 'varchar', length: 64, unique: true }) email: string; @Column({ type: 'varchar' }) diff --git a/src/domain/enums/tokens.ts b/src/domain/enums/tokens.ts index 92743ae..8bf5181 100644 --- a/src/domain/enums/tokens.ts +++ b/src/domain/enums/tokens.ts @@ -2,15 +2,17 @@ import { USER_ROLES } from './users'; export const AUTH_TOKENS_MAPPING = { access_token: 'access_token', + refresh_token: 'refresh_token', password_reset: 'password_reset', - invite_token: 'invite_token', + invite_user_token: 'invite_user_token', } as const; -export type AuthTokenKey = keyof typeof AUTH_TOKENS_MAPPING; +export type AuthTokenType = keyof typeof AUTH_TOKENS_MAPPING; export const AUTH_TOKENS = [ AUTH_TOKENS_MAPPING.access_token, + AUTH_TOKENS_MAPPING.refresh_token, AUTH_TOKENS_MAPPING.password_reset, - AUTH_TOKENS_MAPPING.invite_token, + AUTH_TOKENS_MAPPING.invite_user_token, ] as const; export const AUTH_TOKEN_ROLES = [...USER_ROLES, 'patient'] as const; diff --git a/src/domain/modules/cryptography.ts b/src/domain/modules/cryptography.ts deleted file mode 100644 index 6d7be30..0000000 --- a/src/domain/modules/cryptography.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { JwtSignOptions } from '@nestjs/jwt'; - -import type { AuthTokenPayloads } from '../schemas/tokens'; - -export abstract class Cryptography { - abstract createHash(plain: string): Promise; - - abstract compareHash(plain: string, hash: string): Promise; - - abstract createToken( - _type: T, - payload: AuthTokenPayloads[T], - options?: JwtSignOptions, - ): Promise; - - abstract verifyToken(token: string): Promise; -} diff --git a/src/domain/schemas/statistics/responses.ts b/src/domain/schemas/statistics/responses.ts index 6e12b16..fdd88c8 100644 --- a/src/domain/schemas/statistics/responses.ts +++ b/src/domain/schemas/statistics/responses.ts @@ -56,6 +56,13 @@ export type GetPatientsByCityResponse = z.infer< typeof getPatientsByCityResponseSchema >; +// Appointments + +export const getTotalAppointments = baseResponseSchema.extend({ + data: z.object({ total: z.number() }), +}); +export type GetTotalAppointmentsResponse = z.infer; + // Referrals export const getTotalReferralsAndReferredPatientsPercentageResponseSchema = diff --git a/src/domain/schemas/tokens.ts b/src/domain/schemas/tokens.ts index 1090095..35a8514 100644 --- a/src/domain/schemas/tokens.ts +++ b/src/domain/schemas/tokens.ts @@ -1,18 +1,15 @@ import { z } from 'zod'; import type { AuthAccountType } from '../enums/auth'; -import { - AUTH_TOKENS, - type AUTH_TOKENS_MAPPING, - type AuthTokenRole, -} from '../enums/tokens'; +import { AUTH_TOKENS, type AUTH_TOKENS_MAPPING } from '../enums/tokens'; +import type { UserRole } from '../enums/users'; export const authTokenSchema = z .object({ id: z.number().int().positive(), entity_id: z.string().uuid().nullable(), email: z.string().email().nullable(), - token: z.string(), + token: z.string().min(1), type: z.enum(AUTH_TOKENS), expires_at: z.coerce.date().nullable(), created_at: z.coerce.date(), @@ -28,15 +25,23 @@ export const createAuthTokenSchema = authTokenSchema.pick({ expires_at: true, }); -export type AccessTokenPayload = { sub: string; role: AuthTokenRole }; -export type InviteTokenPayload = { sub: string; role: AuthTokenRole }; +export type AccessTokenPayload = { sub: string; accountType: AuthAccountType }; +export type RefreshTokenPayload = { sub: string; accountType: AuthAccountType }; + export type PasswordResetPayload = { sub: string; accountType: AuthAccountType; }; +export type InviteUserTokenPayload = { + sub: string; + email: string; + role: UserRole; +}; + export type AuthTokenPayloads = { [AUTH_TOKENS_MAPPING.access_token]: AccessTokenPayload; - [AUTH_TOKENS_MAPPING.invite_token]: InviteTokenPayload; + [AUTH_TOKENS_MAPPING.refresh_token]: RefreshTokenPayload; [AUTH_TOKENS_MAPPING.password_reset]: PasswordResetPayload; + [AUTH_TOKENS_MAPPING.invite_user_token]: InviteUserTokenPayload; }; From 7bbe7d60471164e765e90cd7b3a47d914903efab Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Fri, 16 Jan 2026 23:48:28 -0300 Subject: [PATCH 11/21] feat(users): create `users/invite` endpoint and update register user `use-case` --- ...30-Initial.ts => 1768616081767-Initial.ts} | 6 +- src/app/http/auth/auth.controller.ts | 6 +- .../auth/use-cases/register-user.use-case.ts | 24 ++++-- .../use-cases/sign-in-with-email.use-case.ts | 4 +- .../use-cases/create-user-invite.use-case.ts | 78 +++++++++++++++++++ src/app/http/users/users.controller.ts | 32 ++++++-- src/app/http/users/users.dtos.ts | 7 +- src/app/http/users/users.module.ts | 6 +- src/domain/schemas/tokens.ts | 6 +- src/domain/schemas/users/requests.ts | 7 ++ 10 files changed, 147 insertions(+), 29 deletions(-) rename infra/database/migrations/{1766614884730-Initial.ts => 1768616081767-Initial.ts} (96%) create mode 100644 src/app/http/users/use-cases/create-user-invite.use-case.ts diff --git a/infra/database/migrations/1766614884730-Initial.ts b/infra/database/migrations/1768616081767-Initial.ts similarity index 96% rename from infra/database/migrations/1766614884730-Initial.ts rename to infra/database/migrations/1768616081767-Initial.ts index 89de75c..eab4511 100644 --- a/infra/database/migrations/1766614884730-Initial.ts +++ b/infra/database/migrations/1768616081767-Initial.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class Initial1766614884730 implements MigrationInterface { - name = 'Initial1766614884730' +export class Initial1768616081767 implements MigrationInterface { + name = 'Initial1768616081767' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`CREATE TABLE \`patient_requirements\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`type\` enum ('screening', 'medical_report') NOT NULL, \`title\` varchar(255) NOT NULL, \`description\` varchar(500) NULL, \`status\` enum ('pending', 'under_review', 'approved', 'declined') NOT NULL DEFAULT 'pending', \`submitted_at\` datetime NULL, \`approved_by\` varchar(255) NULL, \`approved_at\` datetime NULL, \`declined_by\` varchar(255) NULL, \`declined_at\` datetime NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); @@ -9,7 +9,7 @@ export class Initial1766614884730 implements MigrationInterface { await queryRunner.query(`CREATE TABLE \`referrals\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(2000) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`patients\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NULL, \`avatar_url\` varchar(255) NULL, \`status\` enum ('active', 'inactive', 'pending') NOT NULL DEFAULT 'pending', \`gender\` enum ('male_cis', 'female_cis', 'male_trans', 'female_trans', 'non_binary', 'prefer_not_to_say') NOT NULL DEFAULT 'prefer_not_to_say', \`date_of_birth\` datetime NULL, \`phone\` varchar(11) NULL, \`cpf\` varchar(11) NULL, \`state\` enum ('AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO') NULL, \`city\` varchar(255) NULL, \`has_disability\` tinyint(1) NOT NULL DEFAULT '0', \`disability_desc\` varchar(500) NULL, \`need_legal_assistance\` tinyint(1) NOT NULL DEFAULT '0', \`take_medication\` tinyint(1) NOT NULL DEFAULT '0', \`medication_desc\` varchar(500) NULL, \`nmo_diagnosis\` enum ('anti_aqp4_positive', 'anti_mog_positive', 'both_negative', 'no_diagnosis') NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_64e2031265399f5690b0beba6a\` (\`email\`), UNIQUE INDEX \`IDX_5947301223f5a908fd5e372b0f\` (\`cpf\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`appointments\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(500) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`entity_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'refresh_token', 'password_reset', 'invite_token') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`entity_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'refresh_token', 'password_reset', 'invite_user_token') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`users\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NOT NULL, \`avatar_url\` varchar(255) NULL, \`role\` enum ('admin', 'manager', 'nurse', 'specialist') NOT NULL, \`status\` enum ('active', 'inactive') NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_97672ac88f789774dd47f7c8be\` (\`email\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`ALTER TABLE \`patient_requirements\` ADD CONSTRAINT \`FK_77b87c61cff4793ae6a4ac50070\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE \`patient_supports\` ADD CONSTRAINT \`FK_62c23ddd34837a0c09faf875425\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index a1522c7..39b8bd0 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -57,7 +57,7 @@ export class AuthController { return { success: true, - message: 'Conta de paciente registrada com sucesso.', + message: 'Sua conta foi registrada com sucesso.', }; } @@ -71,7 +71,7 @@ export class AuthController { return { success: true, - message: 'Conta de usuário registrada com sucesso.', + message: 'Sua conta foi registrada com sucesso.', }; } @@ -86,7 +86,7 @@ export class AuthController { return { success: true, message: - 'O link para redefinição de senha foi enviado ao e-mail solicitado.', + 'O link para redefinição de senha foi enviado ao e-mail informado.', }; } diff --git a/src/app/http/auth/use-cases/register-user.use-case.ts b/src/app/http/auth/use-cases/register-user.use-case.ts index 43b54bd..2119816 100644 --- a/src/app/http/auth/use-cases/register-user.use-case.ts +++ b/src/app/http/auth/use-cases/register-user.use-case.ts @@ -2,6 +2,7 @@ import { ConflictException, Injectable, Logger, + NotFoundException, UnauthorizedException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -33,8 +34,8 @@ export class RegisterUserUseCase { private readonly tokensRepository: Repository, @InjectRepository(User) private readonly usersRepository: Repository, - private readonly createTokenUseCase: CreateTokenUseCase, private readonly cryptographyService: CryptographyService, + private readonly createTokenUseCase: CreateTokenUseCase, private readonly utilsService: UtilsService, ) {} @@ -48,19 +49,28 @@ export class RegisterUserUseCase { where: { token }, }); - const payload = - await this.cryptographyService.verifyToken(token); + if (!inviteToken) { + throw new NotFoundException('Token de convite não encontrado.'); + } if ( - !payload || - !inviteToken || + !inviteToken.email || inviteToken.type !== AUTH_TOKENS_MAPPING.invite_user_token || (inviteToken.expires_at && inviteToken.expires_at < new Date()) ) { + await this.tokensRepository.delete({ token }); + throw new UnauthorizedException('Token de convite inválido ou expirado.'); + } + + const payload = + await this.cryptographyService.verifyToken(token); + + if (!payload) { throw new UnauthorizedException('Token de convite inválido ou expirado.'); } - const { role, email } = payload; + const { email } = inviteToken; + const { role } = payload; const userWithSameEmail = await this.usersRepository.findOne({ select: { id: true }, @@ -80,7 +90,7 @@ export class RegisterUserUseCase { await this.usersRepository.save(user); this.logger.log( - { userId: user.id, email, role }, + { id: user.id, email, role }, 'User registered successfully', ); diff --git a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts index dc89c38..b7243a0 100644 --- a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts +++ b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts @@ -115,12 +115,14 @@ export class SignInWithEmailUseCase { value: refreshToken, }); - await this.tokensRepository.save({ + const token = this.tokensRepository.create({ type: AUTH_TOKENS_MAPPING.refresh_token, expires_at: expiresAt, entity_id: entity.id, token: refreshToken, }); + + await this.tokensRepository.save(token); } this.logger.log( diff --git a/src/app/http/users/use-cases/create-user-invite.use-case.ts b/src/app/http/users/use-cases/create-user-invite.use-case.ts new file mode 100644 index 0000000..d7f7e9f --- /dev/null +++ b/src/app/http/users/use-cases/create-user-invite.use-case.ts @@ -0,0 +1,78 @@ +import { ConflictException, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { CreateTokenUseCase } from '@/app/cryptography/use-cases/create-token.use-case'; +import { Token } from '@/domain/entities/token'; +import { User } from '@/domain/entities/user'; +import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; + +import type { AuthUserDto } from '../../auth/auth.dtos'; +import type { CreateUserInviteDto } from '../users.dtos'; + +interface CreateUserInviteUseCaseInput { + user: AuthUserDto; + createUserInviteDto: CreateUserInviteDto; +} + +@Injectable() +export class CreateUserInviteUseCase { + private readonly logger = new Logger(CreateUserInviteUseCase.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + @InjectRepository(Token) + private readonly tokensRepository: Repository, + private readonly createTokenUseCase: CreateTokenUseCase, + ) {} + + async execute({ + user, + createUserInviteDto, + }: CreateUserInviteUseCaseInput): Promise { + const { email, role } = createUserInviteDto; + + const [existingUser, existingInviteUserToken] = await Promise.all([ + this.usersRepository.findOne({ where: { email }, select: { id: true } }), + this.tokensRepository.findOne({ where: { email } }), + ]); + + if (existingUser) { + throw new ConflictException('Este e-mail já está cadastrado no sistema.'); + } + + const existingTokenExpiryDate = existingInviteUserToken?.expires_at; + + if (existingTokenExpiryDate && existingTokenExpiryDate > new Date()) { + throw new ConflictException( + 'Já existe um convite ativo para este e-mail.', + ); + } + + const [{ token: inviteUserToken, expiresAt }] = await Promise.all([ + this.createTokenUseCase.execute({ + type: AUTH_TOKENS_MAPPING.invite_user_token, + payload: { role }, + }), + // Delete all tokens for this email before creating a new one + this.tokensRepository.delete({ email }), + ]); + + const newInviteUserToken = this.tokensRepository.create({ + type: AUTH_TOKENS_MAPPING.invite_user_token, + token: inviteUserToken, + expires_at: expiresAt, + email, + }); + + await this.tokensRepository.save(newInviteUserToken); + + // TODO: send email with register user URL including invite token + + this.logger.log( + { id: newInviteUserToken.id, email, role, createdBy: user.id }, + 'Invite user token created successfully', + ); + } +} diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index f83cfe3..7b0c673 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -1,29 +1,47 @@ -import { Controller, Get } from '@nestjs/common'; +import { Body, Controller, Get, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; +import type { BaseResponse } from '@/domain/schemas/base'; import type { GetUserResponse } from '@/domain/schemas/users/responses'; import type { AuthUserDto } from '../auth/auth.dtos'; +import { CreateUserInviteUseCase } from './use-cases/create-user-invite.use-case'; import { GetUserUseCase } from './use-cases/get-user.use-case'; +import { CreateUserInviteDto } from './users.dtos'; @ApiTags('Usuários') @Controller('users') export class UsersController { - constructor(private readonly getUserUseCase: GetUserUseCase) {} + constructor( + private readonly createUserInviteUseCase: CreateUserInviteUseCase, + private readonly getUserUseCase: GetUserUseCase, + ) {} + + @Post('invite') + @Roles(['manager']) + async createUserInvite( + @AuthUser() user: AuthUserDto, + @Body() createUserInviteDto: CreateUserInviteDto, + ): Promise { + await this.createUserInviteUseCase.execute({ user, createUserInviteDto }); + + return { + success: true, + message: 'Convite do usuário criado com sucesso.', + }; + } @Get('me') @Roles(['manager', 'nurse', 'specialist']) - async getProfile( - @AuthUser() authUser: AuthUserDto, - ): Promise { - const { user } = await this.getUserUseCase.execute({ id: authUser.id }); + async getProfile(@AuthUser() user: AuthUserDto): Promise { + const { user: data } = await this.getUserUseCase.execute({ id: user.id }); return { success: true, message: 'Dados do usuário retornado com sucesso.', - data: user, + data, }; } } diff --git a/src/app/http/users/users.dtos.ts b/src/app/http/users/users.dtos.ts index 64caeb1..70893b5 100644 --- a/src/app/http/users/users.dtos.ts +++ b/src/app/http/users/users.dtos.ts @@ -1,5 +1,10 @@ import { createZodDto } from 'nestjs-zod'; -import { updateUserSchema } from '@/domain/schemas/users/requests'; +import { + createUserInviteSchema, + updateUserSchema, +} from '@/domain/schemas/users/requests'; + +export class CreateUserInviteDto extends createZodDto(createUserInviteSchema) {} export class UpdateUserDto extends createZodDto(updateUserSchema) {} diff --git a/src/app/http/users/users.module.ts b/src/app/http/users/users.module.ts index cd437a0..56b5ae2 100644 --- a/src/app/http/users/users.module.ts +++ b/src/app/http/users/users.module.ts @@ -2,15 +2,17 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CryptographyModule } from '@/app/cryptography/cryptography.module'; +import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; +import { CreateUserInviteUseCase } from './use-cases/create-user-invite.use-case'; import { GetUserUseCase } from './use-cases/get-user.use-case'; import { UpdateUserUseCase } from './use-cases/update-user.use-case'; import { UsersController } from './users.controller'; @Module({ - imports: [TypeOrmModule.forFeature([User]), CryptographyModule], - providers: [UpdateUserUseCase, GetUserUseCase], + imports: [TypeOrmModule.forFeature([User, Token]), CryptographyModule], + providers: [CreateUserInviteUseCase, UpdateUserUseCase, GetUserUseCase], controllers: [UsersController], }) export class UsersModule {} diff --git a/src/domain/schemas/tokens.ts b/src/domain/schemas/tokens.ts index 35a8514..dc45563 100644 --- a/src/domain/schemas/tokens.ts +++ b/src/domain/schemas/tokens.ts @@ -33,11 +33,7 @@ export type PasswordResetPayload = { accountType: AuthAccountType; }; -export type InviteUserTokenPayload = { - sub: string; - email: string; - role: UserRole; -}; +export type InviteUserTokenPayload = { role: UserRole }; export type AuthTokenPayloads = { [AUTH_TOKENS_MAPPING.access_token]: AccessTokenPayload; diff --git a/src/domain/schemas/users/requests.ts b/src/domain/schemas/users/requests.ts index 5f97701..e82ec48 100644 --- a/src/domain/schemas/users/requests.ts +++ b/src/domain/schemas/users/requests.ts @@ -2,10 +2,17 @@ import { z } from 'zod'; import { userSchema } from '.'; +export const createUserInviteSchema = userSchema.pick({ + email: true, + role: true, +}); +export type CreateUserInviteSchema = z.infer; + export const createUserSchema = userSchema.pick({ name: true, email: true, password: true, + avatar_url: true, }); export type CreateUser = z.infer; From db7c64b7df34cea0cb4de8093a0a1c3818b1b91e Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Fri, 16 Jan 2026 23:55:19 -0300 Subject: [PATCH 12/21] chore(use-cases): rename types to keep consistency --- .github/copilot-instructions.md | 10 ++++++---- .../use-cases/cancel-appointment.use-case.ts | 4 ++-- .../use-cases/create-appointment.use-case.ts | 4 ++-- .../use-cases/get-appointments.use-case.ts | 6 +++--- .../use-cases/update-appointment.use-case.ts | 4 ++-- .../use-cases/approve-patient-requirement.use-case.ts | 4 ++-- .../use-cases/create-patient-requirement.use-case.ts | 4 ++-- .../use-cases/decline-patient-requirement.use-case.ts | 4 ++-- .../get-patient-requirements-by-patient-id.use-case.ts | 6 +++--- .../use-cases/get-patient-requirements.use-case.ts | 6 +++--- .../use-cases/create-patient-support.use-case.ts | 4 ++-- .../use-cases/delete-patient-support.use-case.ts | 7 ++----- .../use-cases/update-patient-support.use-case.ts | 4 ++-- .../http/patients/use-cases/create-patient.use-case.ts | 4 ++-- .../patients/use-cases/deactivate-patient.use-case.ts | 4 ++-- .../http/patients/use-cases/get-patient.use-case.ts | 6 +++--- .../http/patients/use-cases/get-patients.use-case.ts | 6 +++--- .../http/patients/use-cases/update-patient.use-case.ts | 4 ++-- .../referrals/use-cases/cancel-referral.use-case.ts | 4 ++-- .../referrals/use-cases/create-referrals.use-case.ts | 4 ++-- .../http/referrals/use-cases/get-referrals.use-case.ts | 6 +++--- .../use-cases/get-total-appointments.use-case.ts | 4 ++-- .../use-cases/get-total-patients-by-field.use-case.ts | 8 ++++---- .../use-cases/get-total-patients-by-status.use-case.ts | 4 ++-- .../use-cases/get-total-patients.use-case.ts | 4 ++-- ...errals-and-referred-patients-percentage.use-case.ts | 6 +++--- .../get-total-referrals-by-category.use-case.ts | 6 +++--- .../use-cases/get-total-referrals.use-case.ts | 4 ++-- .../get-total-referred-patients-by-state.use-case.ts | 6 +++--- .../use-cases/get-total-referred-patients.use-case.ts | 4 ++-- src/app/http/users/use-cases/get-user.use-case.ts | 8 +++----- src/app/http/users/use-cases/update-user.use-case.ts | 4 ++-- 32 files changed, 80 insertions(+), 83 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 576492d..990ea2b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -93,12 +93,14 @@ export class AppointmentsController { One use-case per file, one responsibility. Define input/output types explicitly: ```typescript -interface GetAppointmentsUseCaseRequest { +interface GetAppointmentsUseCaseInput { query: GetAppointmentsQuery; user: AuthUserDto; } -type GetAppointmentsUseCaseResponse = Promise; +interface GetAppointmentsUseCaseOutput = { + appointments: Appointment[]> +} @Injectable() export class GetAppointmentsUseCase { @@ -108,8 +110,8 @@ export class GetAppointmentsUseCase { ) {} async execute( - request: GetAppointmentsUseCaseRequest, - ): GetAppointmentsUseCaseResponse { + request: GetAppointmentsUseCaseInput, + ): Promise { // Implementation } } diff --git a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts index 6319538..a5d3b51 100644 --- a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts @@ -11,7 +11,7 @@ import { Appointment } from '@/domain/entities/appointment'; import type { AuthUserDto } from '../../auth/auth.dtos'; -interface CancelAppointmentUseCaseRequest { +interface CancelAppointmentUseCaseInput { id: string; user: AuthUserDto; } @@ -25,7 +25,7 @@ export class CancelAppointmentUseCase { private readonly appointmentsRepository: Repository, ) {} - async execute({ id, user }: CancelAppointmentUseCaseRequest): Promise { + async execute({ id, user }: CancelAppointmentUseCaseInput): Promise { const appointment = await this.appointmentsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/appointments/use-cases/create-appointment.use-case.ts b/src/app/http/appointments/use-cases/create-appointment.use-case.ts index 0cebf74..ef8a2c4 100644 --- a/src/app/http/appointments/use-cases/create-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/create-appointment.use-case.ts @@ -8,7 +8,7 @@ import { Patient } from '@/domain/entities/patient'; import type { AuthUserDto } from '../../auth/auth.dtos'; import type { CreateAppointmentDto } from '../appointments.dtos'; -interface CreateAppointmentUseCaseRequest { +interface CreateAppointmentUseCaseInput { createAppointmentDto: CreateAppointmentDto; user: AuthUserDto; } @@ -27,7 +27,7 @@ export class CreateAppointmentUseCase { async execute({ createAppointmentDto, user, - }: CreateAppointmentUseCaseRequest): Promise { + }: CreateAppointmentUseCaseInput): Promise { const { patient_id: patientId, date } = createAppointmentDto; const MAX_APPOINTMENT_MONTHS_LIMIT = 3; diff --git a/src/app/http/appointments/use-cases/get-appointments.use-case.ts b/src/app/http/appointments/use-cases/get-appointments.use-case.ts index 79d4037..9962596 100644 --- a/src/app/http/appointments/use-cases/get-appointments.use-case.ts +++ b/src/app/http/appointments/use-cases/get-appointments.use-case.ts @@ -16,12 +16,12 @@ import type { AppointmentResponse } from '@/domain/schemas/appointments/response import type { AuthUserDto } from '../../auth/auth.dtos'; import type { GetAppointmentsQuery } from '../appointments.dtos'; -interface GetAppointmentsUseCaseRequest { +interface GetAppointmentsUseCaseInput { user: AuthUserDto; query: GetAppointmentsQuery; } -interface GetAppointmentsUseCaseResponse { +interface GetAppointmentsUseCaseOutput { appointments: AppointmentResponse[]; total: number; } @@ -36,7 +36,7 @@ export class GetAppointmentsUseCase { async execute({ user, query, - }: GetAppointmentsUseCaseRequest): Promise { + }: GetAppointmentsUseCaseInput): Promise { const { search, status, category, condition, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/appointments/use-cases/update-appointment.use-case.ts b/src/app/http/appointments/use-cases/update-appointment.use-case.ts index 60be71b..12a7dd5 100644 --- a/src/app/http/appointments/use-cases/update-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/update-appointment.use-case.ts @@ -12,7 +12,7 @@ import { Appointment } from '@/domain/entities/appointment'; import type { AuthUserDto } from '../../auth/auth.dtos'; import type { UpdateAppointmentDto } from '../appointments.dtos'; -interface UpdateAppointmentUseCaseRequest { +interface UpdateAppointmentUseCaseInput { id: string; user: AuthUserDto; updateAppointmentDto: UpdateAppointmentDto; @@ -31,7 +31,7 @@ export class UpdateAppointmentUseCase { id, user, updateAppointmentDto, - }: UpdateAppointmentUseCaseRequest): Promise { + }: UpdateAppointmentUseCaseInput): Promise { const appointment = await this.appointmentsRepository.findOne({ where: { id }, }); diff --git a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts index 1272a69..f11b07d 100644 --- a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts @@ -11,7 +11,7 @@ import { PatientRequirement } from '@/domain/entities/patient-requirement'; import type { AuthUserDto } from '../../auth/auth.dtos'; -interface ApprovePatientRequirementUseCaseRequest { +interface ApprovePatientRequirementUseCaseInput { id: string; user: AuthUserDto; } @@ -28,7 +28,7 @@ export class ApprovePatientRequirementUseCase { async execute({ id, user, - }: ApprovePatientRequirementUseCaseRequest): Promise { + }: ApprovePatientRequirementUseCaseInput): Promise { const requirement = await this.patientRequirementsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts index d78263a..4c1e85e 100644 --- a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts @@ -8,7 +8,7 @@ import { PatientRequirement } from '@/domain/entities/patient-requirement'; import type { AuthUserDto } from '../../auth/auth.dtos'; import type { CreatePatientRequirementDto } from '../patient-requirements.dtos'; -interface CreatePatientRequirementUseCaseRequest { +interface CreatePatientRequirementUseCaseInput { createPatientRequirementDto: CreatePatientRequirementDto; user: AuthUserDto; } @@ -27,7 +27,7 @@ export class CreatePatientRequirementUseCase { async execute({ createPatientRequirementDto, user, - }: CreatePatientRequirementUseCaseRequest): Promise { + }: CreatePatientRequirementUseCaseInput): Promise { const { patient_id } = createPatientRequirementDto; const patient = await this.patientsRepository.findOne({ diff --git a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts index fad529c..8a23cc9 100644 --- a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts @@ -11,7 +11,7 @@ import { PatientRequirement } from '@/domain/entities/patient-requirement'; import type { AuthUserDto } from '../../auth/auth.dtos'; -interface DeclinePatientRequirementUseCaseRequest { +interface DeclinePatientRequirementUseCaseInput { id: string; user: AuthUserDto; } @@ -28,7 +28,7 @@ export class DeclinePatientRequirementUseCase { async execute({ id, user, - }: DeclinePatientRequirementUseCaseRequest): Promise { + }: DeclinePatientRequirementUseCaseInput): Promise { const requirement = await this.patientRequirementsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts b/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts index 5d13b7f..618728c 100644 --- a/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/get-patient-requirements-by-patient-id.use-case.ts @@ -13,12 +13,12 @@ import type { PatientRequirementByPatientId } from '@/domain/schemas/patient-req import type { GetPatientRequirementsByPatientIdQuery } from '../patient-requirements.dtos'; -interface GetPatientRequirementsByPatientIdUseCaseRequest { +interface GetPatientRequirementsByPatientIdUseCaseInput { patientId: string; query: GetPatientRequirementsByPatientIdQuery; } -interface GetPatientRequirementsByPatientIdUseCaseResponse { +interface GetPatientRequirementsByPatientIdUseCaseOutput { requirements: PatientRequirementByPatientId[]; total: number; } @@ -33,7 +33,7 @@ export class GetPatientRequirementsByPatientIdUseCase { async execute({ patientId, query, - }: GetPatientRequirementsByPatientIdUseCaseRequest): Promise { + }: GetPatientRequirementsByPatientIdUseCaseInput): Promise { const { status, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts b/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts index 7fbb644..78086ed 100644 --- a/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/get-patient-requirements.use-case.ts @@ -15,11 +15,11 @@ import type { PatientRequirementItem } from '@/domain/schemas/patient-requiremen import type { GetPatientRequirementsQuery } from '../patient-requirements.dtos'; -interface GetPatientRequirementsUseCaseRequest { +interface GetPatientRequirementsUseCaseInput { query: GetPatientRequirementsQuery; } -interface GetPatientRequirementsUseCaseResponse { +interface GetPatientRequirementsUseCaseOutput { requirements: PatientRequirementItem[]; total: number; } @@ -33,7 +33,7 @@ export class GetPatientRequirementsUseCase { async execute({ query, - }: GetPatientRequirementsUseCaseRequest): Promise { + }: GetPatientRequirementsUseCaseInput): Promise { const { search, status, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts index 6a3cd9c..edf60f3 100644 --- a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts @@ -13,7 +13,7 @@ import { PatientSupport } from '@/domain/entities/patient-support'; import type { AuthUserDto } from '../../auth/auth.dtos'; import type { CreatePatientSupportDto } from '../patient-supports.dtos'; -interface CreatePatientSupportUseCaseRequest { +interface CreatePatientSupportUseCaseInput { user: AuthUserDto; patientId: string; createPatientSupportDto: CreatePatientSupportDto; @@ -34,7 +34,7 @@ export class CreatePatientSupportUseCase { user, patientId, createPatientSupportDto, - }: CreatePatientSupportUseCaseRequest): Promise { + }: CreatePatientSupportUseCaseInput): Promise { if (user.id !== patientId) { this.logger.log( { patientId, userId: user.id, role: user.role }, diff --git a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts index 7f396ca..5d22072 100644 --- a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts @@ -11,7 +11,7 @@ import { PatientSupport } from '@/domain/entities/patient-support'; import type { AuthUserDto } from '../../auth/auth.dtos'; -interface DeletePatientSupportUseCaseRequest { +interface DeletePatientSupportUseCaseInput { id: string; user: AuthUserDto; } @@ -25,10 +25,7 @@ export class DeletePatientSupportUseCase { private readonly patientSupportsRepository: Repository, ) {} - async execute({ - id, - user, - }: DeletePatientSupportUseCaseRequest): Promise { + async execute({ id, user }: DeletePatientSupportUseCaseInput): Promise { const patientSupport = await this.patientSupportsRepository.findOne({ select: { id: true, patient_id: true }, where: { id }, diff --git a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts index 89db23a..39c5dd8 100644 --- a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts @@ -12,7 +12,7 @@ import { PatientSupport } from '@/domain/entities/patient-support'; import type { AuthUserDto } from '../../auth/auth.dtos'; import type { UpdatePatientSupportDto } from '../patient-supports.dtos'; -interface UpdatePatientSupportUseCaseRequest { +interface UpdatePatientSupportUseCaseInput { id: string; user: AuthUserDto; updatePatientSupportDto: UpdatePatientSupportDto; @@ -31,7 +31,7 @@ export class UpdatePatientSupportUseCase { id, user, updatePatientSupportDto, - }: UpdatePatientSupportUseCaseRequest): Promise { + }: UpdatePatientSupportUseCaseInput): Promise { const patientSupport = await this.patientSupportsRepository.findOne({ select: { id: true, patient_id: true }, where: { id }, diff --git a/src/app/http/patients/use-cases/create-patient.use-case.ts b/src/app/http/patients/use-cases/create-patient.use-case.ts index b6ae120..94d15df 100644 --- a/src/app/http/patients/use-cases/create-patient.use-case.ts +++ b/src/app/http/patients/use-cases/create-patient.use-case.ts @@ -9,7 +9,7 @@ import { PatientSupport } from '@/domain/entities/patient-support'; import type { AuthUserDto } from '../../auth/auth.dtos'; import type { CreatePatientDto } from '../patients.dtos'; -interface CreatePatientUseCaseRequest { +interface CreatePatientUseCaseInput { user: AuthUserDto; createPatientDto: CreatePatientDto; } @@ -28,7 +28,7 @@ export class CreatePatientUseCase { async execute({ user, createPatientDto, - }: CreatePatientUseCaseRequest): Promise { + }: CreatePatientUseCaseInput): Promise { const { email, cpf, supports, ...patientData } = createPatientDto; const patientWithSameEmail = await this.patientsRepository.findOne({ diff --git a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts index caac607..3c75595 100644 --- a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts +++ b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts @@ -11,7 +11,7 @@ import { Patient } from '@/domain/entities/patient'; import type { AuthUserDto } from '../../auth/auth.dtos'; -interface DeactivatePatientUseCaseRequest { +interface DeactivatePatientUseCaseInput { id: string; user: AuthUserDto; } @@ -25,7 +25,7 @@ export class DeactivatePatientUseCase { private readonly patientsRepository: Repository, ) {} - async execute({ id, user }: DeactivatePatientUseCaseRequest): Promise { + async execute({ id, user }: DeactivatePatientUseCaseInput): Promise { const patient = await this.patientsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/patients/use-cases/get-patient.use-case.ts b/src/app/http/patients/use-cases/get-patient.use-case.ts index ad345b6..b027190 100644 --- a/src/app/http/patients/use-cases/get-patient.use-case.ts +++ b/src/app/http/patients/use-cases/get-patient.use-case.ts @@ -4,11 +4,11 @@ import type { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -interface GetPatientUseCaseRequest { +interface GetPatientUseCaseInput { id: string; } -interface GetPatientUseCaseResponse { +interface GetPatientUseCaseOutput { patient: Patient; } @@ -21,7 +21,7 @@ export class GetPatientUseCase { async execute({ id, - }: GetPatientUseCaseRequest): Promise { + }: GetPatientUseCaseInput): Promise { const patient = await this.patientsRepository.findOne({ relations: { supports: true }, where: { id }, diff --git a/src/app/http/patients/use-cases/get-patients.use-case.ts b/src/app/http/patients/use-cases/get-patients.use-case.ts index 534762a..9234515 100644 --- a/src/app/http/patients/use-cases/get-patients.use-case.ts +++ b/src/app/http/patients/use-cases/get-patients.use-case.ts @@ -16,11 +16,11 @@ import type { PatientResponse } from '@/domain/schemas/patients/responses'; import type { GetPatientsQuery } from '../patients.dtos'; -interface GetPatientsUseCaseRequest { +interface GetPatientsUseCaseInput { query: GetPatientsQuery; } -interface GetPatientsUseCaseResponse { +interface GetPatientsUseCaseOutput { patients: PatientResponse[]; total: number; } @@ -34,7 +34,7 @@ export class GetPatientsUseCase { async execute({ query, - }: GetPatientsUseCaseRequest): Promise { + }: GetPatientsUseCaseInput): Promise { const { search, order, orderBy, status, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/patients/use-cases/update-patient.use-case.ts b/src/app/http/patients/use-cases/update-patient.use-case.ts index b5c4859..0b1a924 100644 --- a/src/app/http/patients/use-cases/update-patient.use-case.ts +++ b/src/app/http/patients/use-cases/update-patient.use-case.ts @@ -13,7 +13,7 @@ import { Patient } from '@/domain/entities/patient'; import type { AuthUserDto } from '../../auth/auth.dtos'; import type { UpdatePatientDto } from '../patients.dtos'; -interface UpdatePatientUseCaseRequest { +interface UpdatePatientUseCaseInput { id: string; user: AuthUserDto; updatePatientDto: UpdatePatientDto; @@ -32,7 +32,7 @@ export class UpdatePatientUseCase { id, user, updatePatientDto, - }: UpdatePatientUseCaseRequest): Promise { + }: UpdatePatientUseCaseInput): Promise { if (user.role === 'patient' && user.id !== id) { this.logger.log( { id, userId: user.id, userEmail: user.email, role: user.role }, diff --git a/src/app/http/referrals/use-cases/cancel-referral.use-case.ts b/src/app/http/referrals/use-cases/cancel-referral.use-case.ts index 34a907c..853d7ac 100644 --- a/src/app/http/referrals/use-cases/cancel-referral.use-case.ts +++ b/src/app/http/referrals/use-cases/cancel-referral.use-case.ts @@ -9,7 +9,7 @@ import type { Repository } from 'typeorm'; import { Referral } from '@/domain/entities/referral'; -interface CancelReferralUseCaseRequest { +interface CancelReferralUseCaseInput { id: string; userId: string; } @@ -23,7 +23,7 @@ export class CancelReferralUseCase { private readonly referralsRepository: Repository, ) {} - async execute({ id, userId }: CancelReferralUseCaseRequest): Promise { + async execute({ id, userId }: CancelReferralUseCaseInput): Promise { const referral = await this.referralsRepository.findOne({ select: { id: true, status: true }, where: { id }, diff --git a/src/app/http/referrals/use-cases/create-referrals.use-case.ts b/src/app/http/referrals/use-cases/create-referrals.use-case.ts index 1bab2ae..cc71abe 100644 --- a/src/app/http/referrals/use-cases/create-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/create-referrals.use-case.ts @@ -7,7 +7,7 @@ import { Referral } from '@/domain/entities/referral'; import { CreateReferralDto } from '../referrals.dtos'; -interface CreateReferralUseCaseRequest { +interface CreateReferralUseCaseInput { userId: string; createReferralDto: CreateReferralDto; } @@ -25,7 +25,7 @@ export class CreateReferralUseCase { async execute({ createReferralDto, userId, - }: CreateReferralUseCaseRequest): Promise { + }: CreateReferralUseCaseInput): Promise { const { patient_id } = createReferralDto; const patient = await this.patientsRepository.findOne({ diff --git a/src/app/http/referrals/use-cases/get-referrals.use-case.ts b/src/app/http/referrals/use-cases/get-referrals.use-case.ts index c5b3b6e..e97cc4a 100644 --- a/src/app/http/referrals/use-cases/get-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/get-referrals.use-case.ts @@ -15,11 +15,11 @@ import type { ReferralResponse } from '@/domain/schemas/referrals/responses'; import { GetReferralsQuery } from '../referrals.dtos'; -interface GetReferralsUseCaseRequest { +interface GetReferralsUseCaseInput { query: GetReferralsQuery; } -interface GetReferralsUseCaseResponse { +interface GetReferralsUseCaseOutput { referrals: ReferralResponse[]; total: number; } @@ -33,7 +33,7 @@ export class GetReferralsUseCase { async execute({ query, - }: GetReferralsUseCaseRequest): Promise { + }: GetReferralsUseCaseInput): Promise { const { search, status, category, condition, page, perPage } = query; const startDate = query.startDate ? new Date(query.startDate) : null; const endDate = query.endDate ? new Date(query.endDate) : null; diff --git a/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts b/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts index b45997f..8d68bdc 100644 --- a/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-appointments.use-case.ts @@ -15,7 +15,7 @@ import type { QueryPeriod } from '@/domain/enums/queries'; import type { SpecialtyCategory } from '@/domain/enums/shared'; import { UtilsService } from '@/utils/utils.service'; -interface GetTotalAppointmentsUseCaseRequest { +interface GetTotalAppointmentsUseCaseInput { status?: AppointmentStatus; category?: SpecialtyCategory; condition?: PatientCondition; @@ -39,7 +39,7 @@ export class GetTotalAppointmentsUseCase { period, startDate, endDate, - }: GetTotalAppointmentsUseCaseRequest = {}): Promise { + }: GetTotalAppointmentsUseCaseInput = {}): Promise { const where: FindOptionsWhere = {}; if (period) { diff --git a/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts index 43f3f49..519cebf 100644 --- a/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts @@ -9,12 +9,12 @@ import { UtilsService } from '@/utils/utils.service'; import type { GetTotalPatientsByFieldQuery } from '../statistics.dtos'; import { GetTotalPatientsUseCase } from './get-total-patients.use-case'; -interface GetTotalPatientsByFieldUseCaseRequest { +interface GetTotalPatientsByFieldUseCaseInput { field: PatientsStatisticField; query: GetTotalPatientsByFieldQuery; } -interface GetTotalPatientsByFieldUseCaseResponse { +interface GetTotalPatientsByFieldUseCaseOutput { items: T[]; total: number; } @@ -31,8 +31,8 @@ export class GetTotalPatientsByFieldUseCase { async execute({ field, query, - }: GetTotalPatientsByFieldUseCaseRequest): Promise< - GetTotalPatientsByFieldUseCaseResponse + }: GetTotalPatientsByFieldUseCaseInput): Promise< + GetTotalPatientsByFieldUseCaseOutput > { const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( query.period, diff --git a/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts index e21347d..6c52d3e 100644 --- a/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts @@ -4,7 +4,7 @@ import type { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -interface GetTotalPatientsByStatusUseCaseResponse { +interface GetTotalPatientsByStatusUseCaseOutput { total: number; active: number; inactive: number; @@ -17,7 +17,7 @@ export class GetTotalPatientsByStatusUseCase { private readonly patientsRepository: Repository, ) {} - async execute(): Promise { + async execute(): Promise { const queryBuilder = await this.patientsRepository .createQueryBuilder('patient') .select('COUNT(patient.id)', 'total') diff --git a/src/app/http/statistics/use-cases/get-total-patients.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients.use-case.ts index c94f697..7fb7173 100644 --- a/src/app/http/statistics/use-cases/get-total-patients.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients.use-case.ts @@ -14,7 +14,7 @@ import type { PatientStatus } from '@/domain/enums/patients'; import type { QueryPeriod } from '@/domain/enums/queries'; import { UtilsService } from '@/utils/utils.service'; -interface GetTotalPatientsUseCaseRequest { +interface GetTotalPatientsUseCaseInput { status?: PatientStatus; period?: QueryPeriod; startDate?: Date; @@ -34,7 +34,7 @@ export class GetTotalPatientsUseCase { period, startDate, endDate, - }: GetTotalPatientsUseCaseRequest = {}): Promise { + }: GetTotalPatientsUseCaseInput = {}): Promise { const where: FindOptionsWhere = { status: status ?? Not('pending'), }; diff --git a/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts index b569785..158a429 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts @@ -5,11 +5,11 @@ import { GetTotalPatientsUseCase } from './get-total-patients.use-case'; import { GetTotalReferralsUseCase } from './get-total-referrals.use-case'; import { GetTotalReferredPatientsUseCase } from './get-total-referred-patients.use-case'; -interface GetTotalReferralsAndReferredPatientsPercentageUseCaseRequest { +interface GetTotalReferralsAndReferredPatientsPercentageUseCaseInput { query: GetTotalReferralsAndReferredPatientsPercentageQuery; } -interface GetTotalReferralsAndReferredPatientsPercentageUseCaseResponse { +interface GetTotalReferralsAndReferredPatientsPercentageUseCaseOutput { totalReferrals: number; referredPatientsPercentage: number; } @@ -24,7 +24,7 @@ export class GetTotalReferralsAndReferredPatientsPercentageUseCase { async execute({ query, - }: GetTotalReferralsAndReferredPatientsPercentageUseCaseRequest): Promise { + }: GetTotalReferralsAndReferredPatientsPercentageUseCaseInput): Promise { const { period } = query; const [totalPatients, totalReferrals, totalReferredPatients] = diff --git a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts index a4431e4..ba39920 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts @@ -8,11 +8,11 @@ import { UtilsService } from '@/utils/utils.service'; import type { GetTotalReferralsByCategoryQuery } from '../statistics.dtos'; -interface GetTotalReferralsByCategoryUseCaseRequest { +interface GetTotalReferralsByCategoryUseCaseInput { query: GetTotalReferralsByCategoryQuery; } -interface GetTotalReferralsByCategoryUseCaseResponse { +interface GetTotalReferralsByCategoryUseCaseOutput { categories: TotalReferralsByCategory[]; total: number; } @@ -27,7 +27,7 @@ export class GetTotalReferralsByCategoryUseCase { async execute({ query, - }: GetTotalReferralsByCategoryUseCaseRequest): Promise { + }: GetTotalReferralsByCategoryUseCaseInput): Promise { const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( query.period, ); diff --git a/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts index 33276f6..a78355b 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts @@ -15,7 +15,7 @@ import type { ReferralStatus } from '@/domain/enums/referrals'; import type { SpecialtyCategory } from '@/domain/enums/shared'; import { UtilsService } from '@/utils/utils.service'; -interface GetTotalReferralsUseCaseRequest { +interface GetTotalReferralsUseCaseInput { status?: ReferralStatus; category?: SpecialtyCategory; condition?: PatientCondition; @@ -39,7 +39,7 @@ export class GetTotalReferralsUseCase { period, startDate, endDate, - }: GetTotalReferralsUseCaseRequest = {}): Promise { + }: GetTotalReferralsUseCaseInput = {}): Promise { const where: FindOptionsWhere = {}; if (period) { diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts index 2c347f8..2aea8bb 100644 --- a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts @@ -8,11 +8,11 @@ import { UtilsService } from '@/utils/utils.service'; import type { GetReferredPatientsByStateQuery } from '../statistics.dtos'; -interface GetTotalReferredPatientsByStateUseCaseRequest { +interface GetTotalReferredPatientsByStateUseCaseInput { query: GetReferredPatientsByStateQuery; } -interface GetTotalReferredPatientsByStateUseCaseResponse { +interface GetTotalReferredPatientsByStateUseCaseOutput { states: TotalReferredPatientsByStateSchema[]; total: number; } @@ -27,7 +27,7 @@ export class GetTotalReferredPatientsByStateUseCase { async execute({ query, - }: GetTotalReferredPatientsByStateUseCaseRequest): Promise { + }: GetTotalReferredPatientsByStateUseCaseInput): Promise { const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( query.period, ); diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts index c2cf62b..9d7b7c5 100644 --- a/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts @@ -14,7 +14,7 @@ import { Patient } from '@/domain/entities/patient'; import type { QueryPeriod } from '@/domain/enums/queries'; import { UtilsService } from '@/utils/utils.service'; -interface GetTotalReferredPatientsUseCaseRequest { +interface GetTotalReferredPatientsUseCaseInput { period?: QueryPeriod; startDate?: Date; endDate?: Date; @@ -32,7 +32,7 @@ export class GetTotalReferredPatientsUseCase { period, startDate, endDate, - }: GetTotalReferredPatientsUseCaseRequest = {}): Promise { + }: GetTotalReferredPatientsUseCaseInput = {}): Promise { const where: FindOptionsWhere = { referrals: { id: Not(IsNull()) }, }; diff --git a/src/app/http/users/use-cases/get-user.use-case.ts b/src/app/http/users/use-cases/get-user.use-case.ts index 757c332..6fbf661 100644 --- a/src/app/http/users/use-cases/get-user.use-case.ts +++ b/src/app/http/users/use-cases/get-user.use-case.ts @@ -5,11 +5,11 @@ import { Repository } from 'typeorm'; import { User } from '@/domain/entities/user'; import type { UserResponse } from '@/domain/schemas/users/responses'; -interface GetUserUseCaseRequest { +interface GetUserUseCaseInput { id: string; } -interface GetUserUseCaseResponse { +interface GetUserUseCaseOutput { user: UserResponse; } @@ -20,9 +20,7 @@ export class GetUserUseCase { private readonly usersRepository: Repository, ) {} - async execute({ - id, - }: GetUserUseCaseRequest): Promise { + async execute({ id }: GetUserUseCaseInput): Promise { const user = await this.usersRepository.findOne({ where: { id }, select: { diff --git a/src/app/http/users/use-cases/update-user.use-case.ts b/src/app/http/users/use-cases/update-user.use-case.ts index af602c3..885ba89 100644 --- a/src/app/http/users/use-cases/update-user.use-case.ts +++ b/src/app/http/users/use-cases/update-user.use-case.ts @@ -12,7 +12,7 @@ import { User } from '@/domain/entities/user'; import type { AuthUserDto } from '../../auth/auth.dtos'; import type { UpdateUserDto } from '../users.dtos'; -interface UpdateUserUseCaseRequest { +interface UpdateUserUseCaseInput { id: string; user: AuthUserDto; updateUserDto: UpdateUserDto; @@ -31,7 +31,7 @@ export class UpdateUserUseCase { id, user, updateUserDto, - }: UpdateUserUseCaseRequest): Promise { + }: UpdateUserUseCaseInput): Promise { if (user.role !== 'admin' && user.id !== id) { this.logger.log( { id, userId: user.id, role: user.role }, From 6de59e3406f8a55890fa45fcab381222fc293550 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Sun, 18 Jan 2026 19:55:24 -0300 Subject: [PATCH 13/21] chore(auth): improve readability and remove unused schema --- ...67-Initial.ts => 1768776807973-Initial.ts} | 6 ++-- src/app/app.module.ts | 4 +-- .../use-cases/create-token.use-case.ts | 8 ++--- src/app/http/auth/auth.controller.ts | 10 ++---- src/app/http/auth/auth.dtos.ts | 3 -- .../use-cases/recover-password.use-case.ts | 31 ++++++++++--------- .../use-cases/register-patient.use-case.ts | 8 +++-- .../auth/use-cases/register-user.use-case.ts | 15 +++++---- .../auth/use-cases/reset-password.use-case.ts | 23 ++++++++------ .../use-cases/sign-in-with-email.use-case.ts | 31 ++++++++++++------- .../use-cases/create-user-invite.use-case.ts | 4 +-- src/domain/cookies.ts | 2 +- src/domain/enums/tokens.ts | 4 +-- src/domain/schemas/auth.ts | 3 +- src/domain/schemas/tokens.ts | 16 +++------- 15 files changed, 84 insertions(+), 84 deletions(-) rename infra/database/migrations/{1768616081767-Initial.ts => 1768776807973-Initial.ts} (96%) diff --git a/infra/database/migrations/1768616081767-Initial.ts b/infra/database/migrations/1768776807973-Initial.ts similarity index 96% rename from infra/database/migrations/1768616081767-Initial.ts rename to infra/database/migrations/1768776807973-Initial.ts index eab4511..4aa6920 100644 --- a/infra/database/migrations/1768616081767-Initial.ts +++ b/infra/database/migrations/1768776807973-Initial.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class Initial1768616081767 implements MigrationInterface { - name = 'Initial1768616081767' +export class Initial1768776807973 implements MigrationInterface { + name = 'Initial1768776807973' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`CREATE TABLE \`patient_requirements\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`type\` enum ('screening', 'medical_report') NOT NULL, \`title\` varchar(255) NOT NULL, \`description\` varchar(500) NULL, \`status\` enum ('pending', 'under_review', 'approved', 'declined') NOT NULL DEFAULT 'pending', \`submitted_at\` datetime NULL, \`approved_by\` varchar(255) NULL, \`approved_at\` datetime NULL, \`declined_by\` varchar(255) NULL, \`declined_at\` datetime NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); @@ -9,7 +9,7 @@ export class Initial1768616081767 implements MigrationInterface { await queryRunner.query(`CREATE TABLE \`referrals\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(2000) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`patients\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NULL, \`avatar_url\` varchar(255) NULL, \`status\` enum ('active', 'inactive', 'pending') NOT NULL DEFAULT 'pending', \`gender\` enum ('male_cis', 'female_cis', 'male_trans', 'female_trans', 'non_binary', 'prefer_not_to_say') NOT NULL DEFAULT 'prefer_not_to_say', \`date_of_birth\` datetime NULL, \`phone\` varchar(11) NULL, \`cpf\` varchar(11) NULL, \`state\` enum ('AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO') NULL, \`city\` varchar(255) NULL, \`has_disability\` tinyint(1) NOT NULL DEFAULT '0', \`disability_desc\` varchar(500) NULL, \`need_legal_assistance\` tinyint(1) NOT NULL DEFAULT '0', \`take_medication\` tinyint(1) NOT NULL DEFAULT '0', \`medication_desc\` varchar(500) NULL, \`nmo_diagnosis\` enum ('anti_aqp4_positive', 'anti_mog_positive', 'both_negative', 'no_diagnosis') NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_64e2031265399f5690b0beba6a\` (\`email\`), UNIQUE INDEX \`IDX_5947301223f5a908fd5e372b0f\` (\`cpf\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`appointments\` (\`id\` varchar(36) NOT NULL, \`patient_id\` varchar(255) NOT NULL, \`date\` datetime NOT NULL, \`status\` enum ('scheduled', 'canceled', 'completed', 'no_show') NOT NULL DEFAULT 'scheduled', \`category\` enum ('medical_care', 'legal', 'nursing', 'psychology', 'nutrition', 'physical_training', 'social_work', 'psychiatry', 'neurology', 'ophthalmology') NOT NULL, \`condition\` enum ('in_crisis', 'stable') NOT NULL, \`annotation\` varchar(500) NULL, \`professional_name\` varchar(64) NULL, \`created_by\` varchar(255) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); - await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`entity_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'refresh_token', 'password_reset', 'invite_user_token') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`tokens\` (\`id\` int NOT NULL AUTO_INCREMENT, \`entity_id\` varchar(255) NULL, \`email\` varchar(255) NULL, \`token\` varchar(255) NOT NULL, \`type\` enum ('access_token', 'refresh_token', 'password_reset', 'invite_user') NOT NULL, \`expires_at\` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6), \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`CREATE TABLE \`users\` (\`id\` varchar(36) NOT NULL, \`name\` varchar(64) NOT NULL, \`email\` varchar(64) NOT NULL, \`password\` varchar(255) NOT NULL, \`avatar_url\` varchar(255) NULL, \`role\` enum ('admin', 'manager', 'nurse', 'specialist') NOT NULL, \`status\` enum ('active', 'inactive') NOT NULL DEFAULT 'active', \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_97672ac88f789774dd47f7c8be\` (\`email\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); await queryRunner.query(`ALTER TABLE \`patient_requirements\` ADD CONSTRAINT \`FK_77b87c61cff4793ae6a4ac50070\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); await queryRunner.query(`ALTER TABLE \`patient_supports\` ADD CONSTRAINT \`FK_62c23ddd34837a0c09faf875425\` FOREIGN KEY (\`patient_id\`) REFERENCES \`patients\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index aba16b3..9e430bb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -53,11 +53,11 @@ import { UsersModule } from './http/users/users.module'; AuthModule, UsersModule, PatientsModule, - PatientSupportsModule, + ReferralsModule, AppointmentsModule, StatisticsModule, + PatientSupportsModule, PatientRequirementsModule, - ReferralsModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/app/cryptography/use-cases/create-token.use-case.ts b/src/app/cryptography/use-cases/create-token.use-case.ts index 61cd804..669bdbf 100644 --- a/src/app/cryptography/use-cases/create-token.use-case.ts +++ b/src/app/cryptography/use-cases/create-token.use-case.ts @@ -34,18 +34,18 @@ export class CreateTokenUseCase { }: CreateTokenUseCaseInput): Promise { const EXPIRY_TIME: TokenExpiryTime = { access_token: { value: 8, time: 'h' }, - invite_user_token: { value: 8, time: 'h' }, - password_reset: { value: 4, time: 'h' }, refresh_token: { value: 30, time: 'd' }, + password_reset: { value: 4, time: 'h' }, + invite_user: { value: 8, time: 'h' }, }; const expiryTime = EXPIRY_TIME[type]; const MAX_AGES: TokenMaxAge = { access_token: 1000 * 60 * 60 * expiryTime.value, - invite_user_token: 1000 * 60 * 60 * expiryTime.value, - password_reset: 1000 * 60 * 60 * expiryTime.value, refresh_token: 1000 * 60 * 60 * 24 * expiryTime.value, + password_reset: 1000 * 60 * 60 * expiryTime.value, + invite_user: 1000 * 60 * 60 * expiryTime.value, }; const maxAge = MAX_AGES[type]; diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index 39b8bd0..7430aa7 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -79,9 +79,8 @@ export class AuthController { @ApiOperation({ summary: 'Recuperação de senha' }) async recoverPassword( @Body() recoverPasswordDto: RecoverPasswordDto, - @Res({ passthrough: true }) response: Response, ): Promise { - await this.recoverPasswordUseCase.execute({ response, recoverPasswordDto }); + await this.recoverPasswordUseCase.execute({ recoverPasswordDto }); return { success: true, @@ -93,15 +92,10 @@ export class AuthController { @Post('reset-password') @ApiOperation({ summary: 'Redefinição de senha' }) async resetPassword( - @Cookies(COOKIES_MAPPING.password_reset) token: string, @Body() resetPasswordDto: ResetPasswordDto, @Res({ passthrough: true }) response: Response, ): Promise { - await this.resetPasswordUseCase.execute({ - resetPasswordDto, - response, - token, - }); + await this.resetPasswordUseCase.execute({ resetPasswordDto, response }); return { success: true, diff --git a/src/app/http/auth/auth.dtos.ts b/src/app/http/auth/auth.dtos.ts index 26404e7..01ee049 100644 --- a/src/app/http/auth/auth.dtos.ts +++ b/src/app/http/auth/auth.dtos.ts @@ -9,7 +9,6 @@ import { resetPasswordSchema, signInWithEmailSchema, } from '@/domain/schemas/auth'; -import { createAuthTokenSchema } from '@/domain/schemas/tokens'; export class AuthUserDto extends createZodDto(authUserSchema) {} @@ -24,5 +23,3 @@ export class RecoverPasswordDto extends createZodDto(recoverPasswordSchema) {} export class ResetPasswordDto extends createZodDto(resetPasswordSchema) {} export class ChangePasswordDto extends createZodDto(changePasswordSchema) {} - -export class CreateAuthTokenDto extends createZodDto(createAuthTokenSchema) {} diff --git a/src/app/http/auth/use-cases/recover-password.use-case.ts b/src/app/http/auth/use-cases/recover-password.use-case.ts index 54c3867..732ecef 100644 --- a/src/app/http/auth/use-cases/recover-password.use-case.ts +++ b/src/app/http/auth/use-cases/recover-password.use-case.ts @@ -1,6 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import type { Response } from 'express'; import { Repository } from 'typeorm'; import { CreateTokenUseCase } from '@/app/cryptography/use-cases/create-token.use-case'; @@ -9,15 +8,18 @@ import { Patient } from '@/domain/entities/patient'; import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; -import { UtilsService } from '@/utils/utils.service'; import type { RecoverPasswordDto } from '../auth.dtos'; interface RecoverPasswordUseCaseInput { recoverPasswordDto: RecoverPasswordDto; - response: Response; } +type PasswordResetToken = Pick< + Token, + 'entity_id' | 'email' | 'token' | 'expires_at' +> & { type: typeof AUTH_TOKENS_MAPPING.password_reset }; + @Injectable() export class RecoverPasswordUseCase { private readonly logger = new Logger(RecoverPasswordUseCase.name); @@ -30,12 +32,10 @@ export class RecoverPasswordUseCase { @InjectRepository(Token) private readonly tokensRepository: Repository, private readonly createTokenUseCase: CreateTokenUseCase, - private readonly utilsService: UtilsService, ) {} async execute({ recoverPasswordDto, - response, }: RecoverPasswordUseCaseInput): Promise { const { email, account_type: accountType } = recoverPasswordDto; @@ -54,23 +54,24 @@ export class RecoverPasswordUseCase { return; } - const { expiresAt, maxAge, token } = await this.createTokenUseCase.execute({ - type: COOKIES_MAPPING.password_reset, - payload: { sub: entity.id, accountType }, - }); + const [{ token, expiresAt }] = await Promise.all([ + this.createTokenUseCase.execute({ + type: COOKIES_MAPPING.password_reset, + payload: { sub: entity.id, accountType }, + }), + // Delete all tokens for this email before creating a new one + this.tokensRepository.delete({ email }), + ]); - await this.tokensRepository.save({ + await this.tokensRepository.save({ type: AUTH_TOKENS_MAPPING.password_reset, expires_at: expiresAt, entity_id: entity.id, token, + email, }); - this.utilsService.setCookie(response, { - name: COOKIES_MAPPING.password_reset, - value: token, - maxAge, - }); + // TODO: send email with password reset URL including reset token this.logger.log( { entityId: entity.id, email, accountType }, diff --git a/src/app/http/auth/use-cases/register-patient.use-case.ts b/src/app/http/auth/use-cases/register-patient.use-case.ts index 3dbb368..20b805e 100644 --- a/src/app/http/auth/use-cases/register-patient.use-case.ts +++ b/src/app/http/auth/use-cases/register-patient.use-case.ts @@ -50,9 +50,11 @@ export class RegisterPatientUseCase { registerPatientDto.password, ); - const patient = this.patientsRepository.create({ name, email, password }); - - await this.patientsRepository.save(patient); + const patient = await this.patientsRepository.save({ + name, + email, + password, + }); this.logger.log( { patientId: patient.id, email }, diff --git a/src/app/http/auth/use-cases/register-user.use-case.ts b/src/app/http/auth/use-cases/register-user.use-case.ts index 2119816..14b46bb 100644 --- a/src/app/http/auth/use-cases/register-user.use-case.ts +++ b/src/app/http/auth/use-cases/register-user.use-case.ts @@ -15,7 +15,7 @@ import { COOKIES_MAPPING } from '@/domain/cookies'; import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; -import type { InviteUserTokenPayload } from '@/domain/schemas/tokens'; +import type { InviteUserPayload } from '@/domain/schemas/tokens'; import { UtilsService } from '@/utils/utils.service'; import { RegisterUserDto } from '../auth.dtos'; @@ -55,7 +55,7 @@ export class RegisterUserUseCase { if ( !inviteToken.email || - inviteToken.type !== AUTH_TOKENS_MAPPING.invite_user_token || + inviteToken.type !== AUTH_TOKENS_MAPPING.invite_user || (inviteToken.expires_at && inviteToken.expires_at < new Date()) ) { await this.tokensRepository.delete({ token }); @@ -63,7 +63,7 @@ export class RegisterUserUseCase { } const payload = - await this.cryptographyService.verifyToken(token); + await this.cryptographyService.verifyToken(token); if (!payload) { throw new UnauthorizedException('Token de convite inválido ou expirado.'); @@ -85,9 +85,12 @@ export class RegisterUserUseCase { registerUserDto.password, ); - const user = this.usersRepository.create({ name, email, password, role }); - - await this.usersRepository.save(user); + const user = await this.usersRepository.save({ + name, + email, + password, + role, + }); this.logger.log( { id: user.id, email, role }, diff --git a/src/app/http/auth/use-cases/reset-password.use-case.ts b/src/app/http/auth/use-cases/reset-password.use-case.ts index 5879916..b18af57 100644 --- a/src/app/http/auth/use-cases/reset-password.use-case.ts +++ b/src/app/http/auth/use-cases/reset-password.use-case.ts @@ -16,7 +16,7 @@ import { Token } from '@/domain/entities/token'; import { User } from '@/domain/entities/user'; import { AUTH_TOKENS_MAPPING } from '@/domain/enums/tokens'; import type { UserRole } from '@/domain/enums/users'; -import type { PasswordResetPayload } from '@/domain/schemas/tokens'; +import type { ResetPasswordPayload } from '@/domain/schemas/tokens'; import { UtilsService } from '@/utils/utils.service'; import type { ResetPasswordDto } from '../auth.dtos'; @@ -24,7 +24,6 @@ import type { ResetPasswordDto } from '../auth.dtos'; interface ResetPasswordUseCaseInput { resetPasswordDto: ResetPasswordDto; response: Response; - token?: string; } @Injectable() @@ -46,20 +45,24 @@ export class ResetPasswordUseCase { async execute({ resetPasswordDto, response, - token, }: ResetPasswordUseCaseInput): Promise { - if (!token) { - throw new UnauthorizedException('Token de redefinição de senha ausente.'); + const { reset_token: token } = resetPasswordDto; + + const resetToken = await this.tokensRepository.findOne({ + where: { token }, + }); + + if (!resetToken) { + throw new NotFoundException( + 'Token de redefinição de senha não encontrado.', + ); } - const [resetToken, payload] = await Promise.all([ - this.tokensRepository.findOne({ where: { token } }), - this.cryptographyService.verifyToken(token), - ]); + const payload = + await this.cryptographyService.verifyToken(token); if ( !payload || - !resetToken || resetToken.type !== AUTH_TOKENS_MAPPING.password_reset || (resetToken.expires_at && resetToken.expires_at < new Date()) ) { diff --git a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts index b7243a0..a1eed59 100644 --- a/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts +++ b/src/app/http/auth/use-cases/sign-in-with-email.use-case.ts @@ -20,6 +20,13 @@ interface SignInWithEmailUseCaseInput { response: Response; } +type RefreshToken = Pick< + Token, + 'entity_id' | 'email' | 'token' | 'expires_at' +> & { + type: typeof AUTH_TOKENS_MAPPING.refresh_token; +}; + @Injectable() export class SignInWithEmailUseCase { private readonly logger = new Logger(SignInWithEmailUseCase.name); @@ -82,8 +89,6 @@ export class SignInWithEmailUseCase { ); } - const role = entity.role ?? 'patient'; - const { maxAge: accessTokenMaxAge, token: accessToken } = await this.createTokenUseCase.execute({ type: AUTH_TOKENS_MAPPING.access_token, @@ -109,24 +114,28 @@ export class SignInWithEmailUseCase { payload: { sub: entity.id, accountType }, }); - this.utilsService.setCookie(response, { - name: COOKIES_MAPPING.refresh_token, - maxAge: refreshTokenMaxAge, - value: refreshToken, - }); - - const token = this.tokensRepository.create({ + await this.tokensRepository.save({ type: AUTH_TOKENS_MAPPING.refresh_token, expires_at: expiresAt, entity_id: entity.id, token: refreshToken, + email, }); - await this.tokensRepository.save(token); + this.utilsService.setCookie(response, { + name: COOKIES_MAPPING.refresh_token, + maxAge: refreshTokenMaxAge, + value: refreshToken, + }); } this.logger.log( - { entityId: entity.id, email, role, keepLoggedIn }, + { + entityId: entity.id, + email, + role: entity.role ?? 'patient', + keepLoggedIn, + }, 'Entity signed in with e-mail', ); } diff --git a/src/app/http/users/use-cases/create-user-invite.use-case.ts b/src/app/http/users/use-cases/create-user-invite.use-case.ts index d7f7e9f..f165d05 100644 --- a/src/app/http/users/use-cases/create-user-invite.use-case.ts +++ b/src/app/http/users/use-cases/create-user-invite.use-case.ts @@ -52,7 +52,7 @@ export class CreateUserInviteUseCase { const [{ token: inviteUserToken, expiresAt }] = await Promise.all([ this.createTokenUseCase.execute({ - type: AUTH_TOKENS_MAPPING.invite_user_token, + type: AUTH_TOKENS_MAPPING.invite_user, payload: { role }, }), // Delete all tokens for this email before creating a new one @@ -60,7 +60,7 @@ export class CreateUserInviteUseCase { ]); const newInviteUserToken = this.tokensRepository.create({ - type: AUTH_TOKENS_MAPPING.invite_user_token, + type: AUTH_TOKENS_MAPPING.invite_user, token: inviteUserToken, expires_at: expiresAt, email, diff --git a/src/domain/cookies.ts b/src/domain/cookies.ts index 1e8bf6a..fd2855e 100644 --- a/src/domain/cookies.ts +++ b/src/domain/cookies.ts @@ -7,5 +7,5 @@ export const COOKIES_MAPPING: Cookies = { access_token: AUTH_TOKENS_MAPPING.access_token, refresh_token: AUTH_TOKENS_MAPPING.refresh_token, password_reset: AUTH_TOKENS_MAPPING.password_reset, - invite_user_token: AUTH_TOKENS_MAPPING.invite_user_token, + invite_user: AUTH_TOKENS_MAPPING.invite_user, } as const; diff --git a/src/domain/enums/tokens.ts b/src/domain/enums/tokens.ts index 8bf5181..c789ef1 100644 --- a/src/domain/enums/tokens.ts +++ b/src/domain/enums/tokens.ts @@ -4,7 +4,7 @@ export const AUTH_TOKENS_MAPPING = { access_token: 'access_token', refresh_token: 'refresh_token', password_reset: 'password_reset', - invite_user_token: 'invite_user_token', + invite_user: 'invite_user', } as const; export type AuthTokenType = keyof typeof AUTH_TOKENS_MAPPING; @@ -12,7 +12,7 @@ export const AUTH_TOKENS = [ AUTH_TOKENS_MAPPING.access_token, AUTH_TOKENS_MAPPING.refresh_token, AUTH_TOKENS_MAPPING.password_reset, - AUTH_TOKENS_MAPPING.invite_user_token, + AUTH_TOKENS_MAPPING.invite_user, ] as const; export const AUTH_TOKEN_ROLES = [...USER_ROLES, 'patient'] as const; diff --git a/src/domain/schemas/auth.ts b/src/domain/schemas/auth.ts index 7e23cf4..6ea6866 100644 --- a/src/domain/schemas/auth.ts +++ b/src/domain/schemas/auth.ts @@ -38,11 +38,10 @@ export const recoverPasswordSchema = z.object({ export const resetPasswordSchema = z.object({ password: passwordSchema, - account_type: accountTypeSchema, + reset_token: z.string().min(1), }); export const changePasswordSchema = z.object({ password: passwordSchema, new_password: passwordSchema, - account_type: accountTypeSchema, }); diff --git a/src/domain/schemas/tokens.ts b/src/domain/schemas/tokens.ts index dc45563..a1ff58e 100644 --- a/src/domain/schemas/tokens.ts +++ b/src/domain/schemas/tokens.ts @@ -17,27 +17,19 @@ export const authTokenSchema = z .strict(); export type AuthToken = z.infer; -export const createAuthTokenSchema = authTokenSchema.pick({ - entity_id: true, - email: true, - token: true, - type: true, - expires_at: true, -}); - export type AccessTokenPayload = { sub: string; accountType: AuthAccountType }; export type RefreshTokenPayload = { sub: string; accountType: AuthAccountType }; -export type PasswordResetPayload = { +export type ResetPasswordPayload = { sub: string; accountType: AuthAccountType; }; -export type InviteUserTokenPayload = { role: UserRole }; +export type InviteUserPayload = { role: UserRole }; export type AuthTokenPayloads = { [AUTH_TOKENS_MAPPING.access_token]: AccessTokenPayload; [AUTH_TOKENS_MAPPING.refresh_token]: RefreshTokenPayload; - [AUTH_TOKENS_MAPPING.password_reset]: PasswordResetPayload; - [AUTH_TOKENS_MAPPING.invite_user_token]: InviteUserTokenPayload; + [AUTH_TOKENS_MAPPING.password_reset]: ResetPasswordPayload; + [AUTH_TOKENS_MAPPING.invite_user]: InviteUserPayload; }; From 26b1f8ac8e5a9a90cb4ed606bb666a159a0d5d47 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Sun, 18 Jan 2026 21:33:02 -0300 Subject: [PATCH 14/21] feat(auth): create `change-password` endpoint --- src/app/http/auth/auth.controller.ts | 30 +++++- src/app/http/auth/auth.module.ts | 2 + .../use-cases/change-password.use-case.ts | 96 +++++++++++++++++++ src/common/decorators/roles.decorator.ts | 4 +- src/common/guards/roles.guard.ts | 6 +- src/domain/enums/tokens.ts | 3 + 6 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 src/app/http/auth/use-cases/change-password.use-case.ts diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index 7430aa7..6bff32d 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -2,18 +2,23 @@ import { Body, Controller, Post, Res } from '@nestjs/common'; import { ApiOperation } from '@nestjs/swagger'; import type { Response } from 'express'; +import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Cookies } from '@/common/decorators/cookies.decorator'; import { Public } from '@/common/decorators/public.decorator'; +import { Roles } from '@/common/decorators/roles.decorator'; import { COOKIES_MAPPING } from '@/domain/cookies'; import type { BaseResponse } from '@/domain/schemas/base'; import { + AuthUserDto, + ChangePasswordDto, RecoverPasswordDto, RegisterPatientDto, RegisterUserDto, ResetPasswordDto, SignInWithEmailDto, } from './auth.dtos'; +import { ChangePasswordUseCase } from './use-cases/change-password.use-case'; import { LogoutUseCase } from './use-cases/logout.use-case'; import { RecoverPasswordUseCase } from './use-cases/recover-password.use-case'; import { RegisterPatientUseCase } from './use-cases/register-patient.use-case'; @@ -21,7 +26,6 @@ import { RegisterUserUseCase } from './use-cases/register-user.use-case'; import { ResetPasswordUseCase } from './use-cases/reset-password.use-case'; import { SignInWithEmailUseCase } from './use-cases/sign-in-with-email.use-case'; -@Public() @Controller() export class AuthController { constructor( @@ -31,8 +35,10 @@ export class AuthController { private readonly resetPasswordUseCase: ResetPasswordUseCase, private readonly registerPatientUseCase: RegisterPatientUseCase, private readonly registerUserUseCase: RegisterUserUseCase, + private readonly changePasswordUseCase: ChangePasswordUseCase, ) {} + @Public() @Post('login') @ApiOperation({ summary: 'Login de usuário ou paciente' }) async login( @@ -47,6 +53,7 @@ export class AuthController { }; } + @Public() @Post('register/patient') @ApiOperation({ summary: 'Registro de novo paciente' }) async registerPatient( @@ -61,6 +68,7 @@ export class AuthController { }; } + @Public() @Post('register/user') @ApiOperation({ summary: 'Registro de novo usuário via convite' }) async registerUser( @@ -75,6 +83,7 @@ export class AuthController { }; } + @Public() @Post('recover-password') @ApiOperation({ summary: 'Recuperação de senha' }) async recoverPassword( @@ -89,6 +98,7 @@ export class AuthController { }; } + @Public() @Post('reset-password') @ApiOperation({ summary: 'Redefinição de senha' }) async resetPassword( @@ -103,6 +113,24 @@ export class AuthController { }; } + @Roles(['all']) + @Post('change-password') + @ApiOperation({ + summary: 'Alteração de senha do usuário ou paciente autenticado', + }) + async changePassword( + @AuthUser() user: AuthUserDto, + @Body() changePasswordDto: ChangePasswordDto, + ): Promise { + await this.changePasswordUseCase.execute({ user, changePasswordDto }); + + return { + success: true, + message: 'Senha alterada com sucesso.', + }; + } + + @Roles(['all']) @Post('logout') @ApiOperation({ summary: 'Logout do usuário ou paciente' }) async logout( diff --git a/src/app/http/auth/auth.module.ts b/src/app/http/auth/auth.module.ts index 1d2db59..6977b32 100644 --- a/src/app/http/auth/auth.module.ts +++ b/src/app/http/auth/auth.module.ts @@ -13,6 +13,7 @@ import { UtilsModule } from '@/utils/utils.module'; import { UsersModule } from '../users/users.module'; import { AuthController } from './auth.controller'; +import { ChangePasswordUseCase } from './use-cases/change-password.use-case'; import { LogoutUseCase } from './use-cases/logout.use-case'; import { RecoverPasswordUseCase } from './use-cases/recover-password.use-case'; import { RegisterPatientUseCase } from './use-cases/register-patient.use-case'; @@ -35,6 +36,7 @@ import { SignInWithEmailUseCase } from './use-cases/sign-in-with-email.use-case' ResetPasswordUseCase, RegisterPatientUseCase, RegisterUserUseCase, + ChangePasswordUseCase, { provide: APP_GUARD, useClass: AuthGuard }, { provide: APP_GUARD, useClass: RolesGuard }, ], diff --git a/src/app/http/auth/use-cases/change-password.use-case.ts b/src/app/http/auth/use-cases/change-password.use-case.ts new file mode 100644 index 0000000..d2b99c2 --- /dev/null +++ b/src/app/http/auth/use-cases/change-password.use-case.ts @@ -0,0 +1,96 @@ +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { CryptographyService } from '@/app/cryptography/crypography.service'; +import { Patient } from '@/domain/entities/patient'; +import { User } from '@/domain/entities/user'; + +import type { AuthUserDto, ChangePasswordDto } from '../auth.dtos'; + +interface ChangePasswordUseCaseInput { + user: AuthUserDto; + changePasswordDto: ChangePasswordDto; +} + +@Injectable() +export class ChangePasswordUseCase { + private readonly logger = new Logger(ChangePasswordUseCase.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + private readonly cryptographyService: CryptographyService, + ) {} + + async execute({ + user, + changePasswordDto, + }: ChangePasswordUseCaseInput): Promise { + const { id, role } = user; + const { password: currentPassword, new_password: newPassword } = + changePasswordDto; + + const findOptions = { + where: { id }, + select: { id: true, email: true, password: true }, + }; + + const entity: { + id: string; + email: string; + password: string | null; + } | null = + role === 'patient' + ? await this.patientsRepository.findOne(findOptions) + : await this.usersRepository.findOne(findOptions); + + if (!entity) { + throw new NotFoundException('Usuário não encontrado.'); + } + + if (!entity.password) { + this.logger.warn( + { id, email: entity.email, role }, + 'Change password failed: Entity does not have password', + ); + throw new BadRequestException('Usuário não encontrado.'); + } + + const passwordMatches = await this.cryptographyService.compareHash( + currentPassword, + entity.password, + ); + + if (!passwordMatches) { + throw new UnauthorizedException('Senha atual inválida.'); + } + + if (currentPassword === newPassword) { + throw new BadRequestException( + 'A nova senha deve ser diferente da senha atual.', + ); + } + + const password = await this.cryptographyService.createHash(newPassword); + + if (role === 'patient') { + await this.patientsRepository.update({ id }, { password }); + } else { + await this.usersRepository.update({ id }, { password }); + } + + this.logger.log( + { id, email: entity.email, role }, + 'Password changed successfully', + ); + } +} diff --git a/src/common/decorators/roles.decorator.ts b/src/common/decorators/roles.decorator.ts index 3b3f6e7..d058546 100644 --- a/src/common/decorators/roles.decorator.ts +++ b/src/common/decorators/roles.decorator.ts @@ -1,5 +1,5 @@ import { Reflector } from '@nestjs/core'; -import type { AuthTokenRole } from '@/domain/enums/tokens'; +import type { AllowedRole } from '@/domain/enums/tokens'; -export const Roles = Reflector.createDecorator(); +export const Roles = Reflector.createDecorator(); diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts index d9d1877..261a77d 100644 --- a/src/common/guards/roles.guard.ts +++ b/src/common/guards/roles.guard.ts @@ -31,7 +31,6 @@ export class RolesGuard implements CanActivate { } const request = context.switchToHttp().getRequest<{ user?: AuthUserDto }>(); - const user = request.user; if (!user) { @@ -40,7 +39,10 @@ export class RolesGuard implements CanActivate { ); } - const isAllowed = roles.includes(user.role) || user.role === 'admin'; + const isAllowed = + roles.includes(user.role) || + roles.includes('all') || + user.role === 'admin'; if (!isAllowed) { throw new UnauthorizedException( diff --git a/src/domain/enums/tokens.ts b/src/domain/enums/tokens.ts index c789ef1..cd0848d 100644 --- a/src/domain/enums/tokens.ts +++ b/src/domain/enums/tokens.ts @@ -17,3 +17,6 @@ export const AUTH_TOKENS = [ export const AUTH_TOKEN_ROLES = [...USER_ROLES, 'patient'] as const; export type AuthTokenRole = (typeof AUTH_TOKEN_ROLES)[number]; + +export const ALLOWED_ROLES = ['all', ...AUTH_TOKEN_ROLES] as const; +export type AllowedRole = (typeof ALLOWED_ROLES)[number]; From 2831f1a955a2fccd4ecab9da6063f5c08189205f Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Sun, 18 Jan 2026 23:43:59 -0300 Subject: [PATCH 15/21] feat(users): create `GET /users` endpoint --- .../http/users/use-cases/get-user.use-case.ts | 1 + .../users/use-cases/get-users.use-case.ts | 95 +++++++++++++++++++ src/app/http/users/users.controller.ts | 28 +++++- src/app/http/users/users.dtos.ts | 3 + src/app/http/users/users.module.ts | 8 +- src/domain/enums/users.ts | 3 + src/domain/schemas/users/requests.ts | 36 +++++++ src/domain/schemas/users/responses.ts | 9 ++ 8 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 src/app/http/users/use-cases/get-users.use-case.ts diff --git a/src/app/http/users/use-cases/get-user.use-case.ts b/src/app/http/users/use-cases/get-user.use-case.ts index 6fbf661..3822139 100644 --- a/src/app/http/users/use-cases/get-user.use-case.ts +++ b/src/app/http/users/use-cases/get-user.use-case.ts @@ -29,6 +29,7 @@ export class GetUserUseCase { email: true, avatar_url: true, status: true, + role: true, }, }); diff --git a/src/app/http/users/use-cases/get-users.use-case.ts b/src/app/http/users/use-cases/get-users.use-case.ts new file mode 100644 index 0000000..865eb20 --- /dev/null +++ b/src/app/http/users/use-cases/get-users.use-case.ts @@ -0,0 +1,95 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Between, + type FindOptionsWhere, + ILike, + LessThanOrEqual, + MoreThanOrEqual, + type Repository, +} from 'typeorm'; + +import { User } from '@/domain/entities/user'; +import type { UsersOrderBy } from '@/domain/enums/users'; +import type { GetUsersQuery } from '@/domain/schemas/users/requests'; +import type { UserResponse } from '@/domain/schemas/users/responses'; + +interface GetUsersUseCaseInput { + query: GetUsersQuery; +} + +interface GetUsersUseCaseOutput { + users: UserResponse[]; + total: number; +} + +@Injectable() +export class GetUsersUseCase { + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + ) {} + + async execute({ + query, + }: GetUsersUseCaseInput): Promise { + const { search, role, status, page, perPage } = query; + const startDate = query.startDate ? new Date(query.startDate) : null; + const endDate = query.endDate ? new Date(query.endDate) : null; + + const ORDER_BY_MAPPING: Record = { + name: 'name', + date: 'created_at', + role: 'role', + status: 'status', + }; + + const where: FindOptionsWhere = {}; + + if (startDate && !endDate) { + where.created_at = MoreThanOrEqual(startDate); + } + + if (endDate && !startDate) { + where.created_at = LessThanOrEqual(endDate); + } + + if (startDate && endDate) { + where.created_at = Between(startDate, endDate); + } + + if (role) { + where.role = role; + } + + if (status) { + where.status = status; + } + + if (search) { + where.name = ILike(`%${search}%`); + } + + const total = await this.usersRepository.count({ where }); + + const orderBy = ORDER_BY_MAPPING[query.orderBy]; + const order = { [orderBy]: query.order }; + + const users = await this.usersRepository.find({ + select: { + id: true, + name: true, + email: true, + avatar_url: true, + status: true, + role: true, + }, + skip: (page - 1) * perPage, + take: perPage, + order, + where, + }); + + return { users, total }; + } +} diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index 7b0c673..c0f2afa 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -1,15 +1,19 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import type { BaseResponse } from '@/domain/schemas/base'; -import type { GetUserResponse } from '@/domain/schemas/users/responses'; +import type { + GetUserResponse, + GetUsersResponse, +} from '@/domain/schemas/users/responses'; import type { AuthUserDto } from '../auth/auth.dtos'; import { CreateUserInviteUseCase } from './use-cases/create-user-invite.use-case'; import { GetUserUseCase } from './use-cases/get-user.use-case'; -import { CreateUserInviteDto } from './users.dtos'; +import { GetUsersUseCase } from './use-cases/get-users.use-case'; +import { CreateUserInviteDto, GetUsersQuery } from './users.dtos'; @ApiTags('Usuários') @Controller('users') @@ -17,10 +21,12 @@ export class UsersController { constructor( private readonly createUserInviteUseCase: CreateUserInviteUseCase, private readonly getUserUseCase: GetUserUseCase, + private readonly getUsersUseCase: GetUsersUseCase, ) {} @Post('invite') @Roles(['manager']) + @ApiOperation({ summary: 'Cria convite para registro de usuário' }) async createUserInvite( @AuthUser() user: AuthUserDto, @Body() createUserInviteDto: CreateUserInviteDto, @@ -33,8 +39,22 @@ export class UsersController { }; } + @Get() + @Roles(['manager']) + @ApiOperation({ summary: 'Lista todos os usuários' }) + async getUsers(@Query() query: GetUsersQuery): Promise { + const data = await this.getUsersUseCase.execute({ query }); + + return { + success: true, + message: 'Lista de usuários retornada com sucesso.', + data, + }; + } + @Get('me') @Roles(['manager', 'nurse', 'specialist']) + @ApiOperation({ summary: 'Retorna os dados do usuário autenticado' }) async getProfile(@AuthUser() user: AuthUserDto): Promise { const { user: data } = await this.getUserUseCase.execute({ id: user.id }); diff --git a/src/app/http/users/users.dtos.ts b/src/app/http/users/users.dtos.ts index 70893b5..3ec837f 100644 --- a/src/app/http/users/users.dtos.ts +++ b/src/app/http/users/users.dtos.ts @@ -2,9 +2,12 @@ import { createZodDto } from 'nestjs-zod'; import { createUserInviteSchema, + getUsersQuerySchema, updateUserSchema, } from '@/domain/schemas/users/requests'; export class CreateUserInviteDto extends createZodDto(createUserInviteSchema) {} export class UpdateUserDto extends createZodDto(updateUserSchema) {} + +export class GetUsersQuery extends createZodDto(getUsersQuerySchema) {} diff --git a/src/app/http/users/users.module.ts b/src/app/http/users/users.module.ts index 56b5ae2..4aa0f36 100644 --- a/src/app/http/users/users.module.ts +++ b/src/app/http/users/users.module.ts @@ -7,12 +7,18 @@ import { User } from '@/domain/entities/user'; import { CreateUserInviteUseCase } from './use-cases/create-user-invite.use-case'; import { GetUserUseCase } from './use-cases/get-user.use-case'; +import { GetUsersUseCase } from './use-cases/get-users.use-case'; import { UpdateUserUseCase } from './use-cases/update-user.use-case'; import { UsersController } from './users.controller'; @Module({ imports: [TypeOrmModule.forFeature([User, Token]), CryptographyModule], - providers: [CreateUserInviteUseCase, UpdateUserUseCase, GetUserUseCase], + providers: [ + CreateUserInviteUseCase, + UpdateUserUseCase, + GetUserUseCase, + GetUsersUseCase, + ], controllers: [UsersController], }) export class UsersModule {} diff --git a/src/domain/enums/users.ts b/src/domain/enums/users.ts index f6f04b8..3099b5d 100644 --- a/src/domain/enums/users.ts +++ b/src/domain/enums/users.ts @@ -3,3 +3,6 @@ export type UserRole = (typeof USER_ROLES)[number]; export const USER_STATUSES = ['active', 'inactive'] as const; export type UserStatus = (typeof USER_STATUSES)[number]; + +export const USERS_ORDER_BY = ['name', 'date', 'role', 'status'] as const; +export type UsersOrderBy = (typeof USERS_ORDER_BY)[number]; diff --git a/src/domain/schemas/users/requests.ts b/src/domain/schemas/users/requests.ts index e82ec48..f6f1b03 100644 --- a/src/domain/schemas/users/requests.ts +++ b/src/domain/schemas/users/requests.ts @@ -1,5 +1,13 @@ import { z } from 'zod'; +import { QUERY_ORDERS } from '@/domain/enums/queries'; +import { + USER_ROLES, + USER_STATUSES, + USERS_ORDER_BY, +} from '@/domain/enums/users'; + +import { baseQuerySchema } from '../query'; import { userSchema } from '.'; export const createUserInviteSchema = userSchema.pick({ @@ -23,3 +31,31 @@ export const updateUserSchema = userSchema.omit({ updated_at: true, }); export type UpdateUser = z.infer; + +export const getUsersQuerySchema = baseQuerySchema + .pick({ + search: true, + startDate: true, + endDate: true, + page: true, + perPage: true, + }) + .extend({ + role: z.enum(USER_ROLES).optional(), + status: z.enum(USER_STATUSES).optional(), + orderBy: z.enum(USERS_ORDER_BY).optional().default('name'), + order: z.enum(QUERY_ORDERS).optional().default('ASC'), + }) + .refine( + (data) => { + if (data.startDate && data.endDate) { + return data.startDate < data.endDate; + } + return true; + }, + { + message: 'It should be greater than `startDate`', + path: ['endDate'], + }, + ); +export type GetUsersQuery = z.infer; diff --git a/src/domain/schemas/users/responses.ts b/src/domain/schemas/users/responses.ts index 8240677..a4ffbc0 100644 --- a/src/domain/schemas/users/responses.ts +++ b/src/domain/schemas/users/responses.ts @@ -9,6 +9,7 @@ export const userResponseSchema = userSchema.pick({ email: true, avatar_url: true, status: true, + role: true, }); export type UserResponse = z.infer; @@ -16,3 +17,11 @@ export const getUserResponseSchema = baseResponseSchema.extend({ data: userResponseSchema, }); export type GetUserResponse = z.infer; + +export const getUsersResponseSchema = baseResponseSchema.extend({ + data: z.object({ + users: z.array(userResponseSchema), + total: z.number(), + }), +}); +export type GetUsersResponse = z.infer; From 568ae7b03e833ddddd3d5ae2aec12b95f3667fc6 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Mon, 19 Jan 2026 00:07:37 -0300 Subject: [PATCH 16/21] chore(docs): update swagger endpoints descriptions --- src/app/app.controller.ts | 16 ------ src/app/app.module.ts | 6 +-- src/app/app.service.ts | 8 --- .../appointments/appointments.controller.ts | 2 + src/app/http/auth/auth.controller.ts | 14 ++--- .../patient-requirements.controller.ts | 53 +++++++++---------- .../patient-supports.controller.ts | 8 +-- src/app/http/patients/patients.controller.ts | 6 +-- .../http/referrals/referrals.controller.ts | 4 +- .../http/statistics/statistics.controller.ts | 14 ++--- src/app/http/users/users.controller.ts | 2 +- 11 files changed, 52 insertions(+), 81 deletions(-) delete mode 100644 src/app/app.controller.ts delete mode 100644 src/app/app.service.ts diff --git a/src/app/app.controller.ts b/src/app/app.controller.ts deleted file mode 100644 index e4c9a82..0000000 --- a/src/app/app.controller.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -import { Public } from '@/common/decorators/public.decorator'; - -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Public() - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9e430bb..35be2e3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -6,8 +6,6 @@ import { envSchema } from '@/env/env'; import { EnvModule } from '@/env/env.module'; import { EnvService } from '@/env/env.service'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { DatabaseModule } from './database/database.module'; import { AppointmentsModule } from './http/appointments/appointments.module'; import { AuthModule } from './http/auth/auth.module'; @@ -56,10 +54,8 @@ import { UsersModule } from './http/users/users.module'; ReferralsModule, AppointmentsModule, StatisticsModule, - PatientSupportsModule, PatientRequirementsModule, + PatientSupportsModule, ], - controllers: [AppController], - providers: [AppService], }) export class AppModule {} diff --git a/src/app/app.service.ts b/src/app/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/src/app/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/app/http/appointments/appointments.controller.ts b/src/app/http/appointments/appointments.controller.ts index 3f85153..f74dd9e 100644 --- a/src/app/http/appointments/appointments.controller.ts +++ b/src/app/http/appointments/appointments.controller.ts @@ -69,6 +69,7 @@ export class AppointmentsController { @Put(':id') @Roles(['nurse', 'manager', 'specialist']) + @ApiOperation({ summary: 'Atualiza os dados do atendimento' }) public async update( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -88,6 +89,7 @@ export class AppointmentsController { @Roles(['nurse', 'manager', 'specialist']) @Patch(':id/cancel') + @ApiOperation({ summary: 'Cancela o atendimento' }) async cancel( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index 6bff32d..b886c67 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -40,7 +40,7 @@ export class AuthController { @Public() @Post('login') - @ApiOperation({ summary: 'Login de usuário ou paciente' }) + @ApiOperation({ summary: 'Inicia a sessão do usuário ou paciente' }) async login( @Body() signInWithEmailDto: SignInWithEmailDto, @Res({ passthrough: true }) response: Response, @@ -55,7 +55,7 @@ export class AuthController { @Public() @Post('register/patient') - @ApiOperation({ summary: 'Registro de novo paciente' }) + @ApiOperation({ summary: 'Registra um novo paciente' }) async registerPatient( @Body() registerPatientDto: RegisterPatientDto, @Res({ passthrough: true }) response: Response, @@ -70,7 +70,7 @@ export class AuthController { @Public() @Post('register/user') - @ApiOperation({ summary: 'Registro de novo usuário via convite' }) + @ApiOperation({ summary: 'Registro um novo usuário via convite' }) async registerUser( @Body() registerUserDto: RegisterUserDto, @Res({ passthrough: true }) response: Response, @@ -85,7 +85,7 @@ export class AuthController { @Public() @Post('recover-password') - @ApiOperation({ summary: 'Recuperação de senha' }) + @ApiOperation({ summary: 'Solicita recuperação de senha' }) async recoverPassword( @Body() recoverPasswordDto: RecoverPasswordDto, ): Promise { @@ -100,7 +100,7 @@ export class AuthController { @Public() @Post('reset-password') - @ApiOperation({ summary: 'Redefinição de senha' }) + @ApiOperation({ summary: 'Solicita redefinição de senha' }) async resetPassword( @Body() resetPasswordDto: ResetPasswordDto, @Res({ passthrough: true }) response: Response, @@ -116,7 +116,7 @@ export class AuthController { @Roles(['all']) @Post('change-password') @ApiOperation({ - summary: 'Alteração de senha do usuário ou paciente autenticado', + summary: 'Altera a senha do usuário ou paciente autenticado', }) async changePassword( @AuthUser() user: AuthUserDto, @@ -132,7 +132,7 @@ export class AuthController { @Roles(['all']) @Post('logout') - @ApiOperation({ summary: 'Logout do usuário ou paciente' }) + @ApiOperation({ summary: 'Encerra a sessão do usuário ou paciente' }) async logout( @Cookies(COOKIES_MAPPING.refresh_token) refreshToken: string, @Res({ passthrough: true }) response: Response, diff --git a/src/app/http/patient-requirements/patient-requirements.controller.ts b/src/app/http/patient-requirements/patient-requirements.controller.ts index e444652..c90897f 100644 --- a/src/app/http/patient-requirements/patient-requirements.controller.ts +++ b/src/app/http/patient-requirements/patient-requirements.controller.ts @@ -42,9 +42,7 @@ export class PatientRequirementsController { @Get() @Roles(['nurse', 'manager']) - @ApiOperation({ - summary: 'Lista as solicitações com paginação e filtros', - }) + @ApiOperation({ summary: 'Lista todas as solicitações' }) async getPatientRequirements( @Query() query: GetPatientRequirementsQuery, ): Promise { @@ -57,9 +55,29 @@ export class PatientRequirementsController { }; } + @Get('me') + @ApiOperation({ + summary: 'Lista todas as solicitações do paciente autenticado', + }) + async getPatientRequirementsLogged( + @AuthUser() user: AuthUserDto, + @Query() query: GetPatientRequirementsByPatientIdQuery, + ): Promise { + const data = await this.getPatientRequirementsByPatientIdUseCase.execute({ + patientId: user.id, + query, + }); + + return { + success: true, + message: 'Lista de solicitações retornada com sucesso.', + data, + }; + } + @Post() @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Adiciona uma nova solicitação.' }) + @ApiOperation({ summary: 'Cadastra uma nova solicitação' }) async create( @AuthUser() user: AuthUserDto, @Body() createPatientRequirementDto: CreatePatientRequirementDto, @@ -71,13 +89,13 @@ export class PatientRequirementsController { return { success: true, - message: 'Solicitação adicionada com sucesso.', + message: 'Solicitação cadastrada com sucesso.', }; } @Patch(':id/approve') @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Aprova uma solicitação pelo ID.' }) + @ApiOperation({ summary: 'Aprova a solicitação' }) async approve( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -92,7 +110,7 @@ export class PatientRequirementsController { @Patch(':id/decline') @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Recusa uma solicitação pelo ID.' }) + @ApiOperation({ summary: 'Recusa a solicitação' }) async decline( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -104,25 +122,4 @@ export class PatientRequirementsController { message: 'Solicitação recusada com sucesso.', }; } - - @Get('me') - @ApiOperation({ - summary: - 'Lista as solicitações do paciente logado com paginação e filtros.', - }) - async getPatientRequirementsLogged( - @AuthUser() user: AuthUserDto, - @Query() query: GetPatientRequirementsByPatientIdQuery, - ): Promise { - const data = await this.getPatientRequirementsByPatientIdUseCase.execute({ - patientId: user.id, - query, - }); - - return { - success: true, - message: 'Lista de solicitações retornada com sucesso.', - data, - }; - } } diff --git a/src/app/http/patient-supports/patient-supports.controller.ts b/src/app/http/patient-supports/patient-supports.controller.ts index 33418e1..636e282 100644 --- a/src/app/http/patient-supports/patient-supports.controller.ts +++ b/src/app/http/patient-supports/patient-supports.controller.ts @@ -26,7 +26,7 @@ export class PatientSupportsController { @Post(':patientId') @Roles(['nurse', 'manager', 'patient']) @ApiOperation({ - summary: 'Registra um novo contato de apoio para um paciente', + summary: 'Cadastra um novo contato de apoio para o paciente', }) async createPatientSupport( @Param('patientId') patientId: string, @@ -41,13 +41,13 @@ export class PatientSupportsController { return { success: true, - message: 'Contato de apoio registrado com sucesso.', + message: 'Contato de apoio cadastrado com sucesso.', }; } @Put(':id') @Roles(['nurse', 'manager', 'patient']) - @ApiOperation({ summary: 'Atualiza um contato de apoio pelo ID' }) + @ApiOperation({ summary: 'Atualiza os dados do contato de apoio' }) async updatePatientSupport( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -67,7 +67,7 @@ export class PatientSupportsController { @Delete(':id') @Roles(['nurse', 'manager', 'patient']) - @ApiOperation({ summary: 'Remove um contato de apoio pelo ID' }) + @ApiOperation({ summary: 'Remove o contato de apoio' }) async removePatientSupport( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/patients/patients.controller.ts b/src/app/http/patients/patients.controller.ts index 71edbe9..d5316d7 100644 --- a/src/app/http/patients/patients.controller.ts +++ b/src/app/http/patients/patients.controller.ts @@ -58,7 +58,7 @@ export class PatientsController { @Get(':id') @Roles(['manager', 'nurse', 'specialist']) - @ApiOperation({ summary: 'Busca um paciente pelo ID' }) + @ApiOperation({ summary: 'Retorna os dados do paciente' }) async getPatientById(@Param('id') id: string): Promise { const { patient } = await this.getPatientUseCase.execute({ id }); @@ -86,7 +86,7 @@ export class PatientsController { @Put(':id') @Roles(['manager', 'nurse', 'patient']) - @ApiOperation({ summary: 'Atualiza um paciente pelo ID' }) + @ApiOperation({ summary: 'Atualiza os dados do paciente' }) async update( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -102,7 +102,7 @@ export class PatientsController { @Patch(':id/deactivate') @Roles(['manager']) - @ApiOperation({ summary: 'Inativa um paciente pelo ID' }) + @ApiOperation({ summary: 'Inativa o paciente' }) async deactivatePatient( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/referrals/referrals.controller.ts b/src/app/http/referrals/referrals.controller.ts index cb4d081..90868b9 100644 --- a/src/app/http/referrals/referrals.controller.ts +++ b/src/app/http/referrals/referrals.controller.ts @@ -31,7 +31,7 @@ export class ReferralsController { @Get() @Roles(['manager', 'nurse']) - @ApiOperation({ summary: 'Lista encaminhamentos cadastrados no sistema' }) + @ApiOperation({ summary: 'Lista todos os encaminhamentos' }) async getReferrals( @Query() query: GetReferralsQuery, ): Promise { @@ -61,7 +61,7 @@ export class ReferralsController { @Patch(':id/cancel') @Roles(['nurse', 'manager']) - @ApiOperation({ summary: 'Cancela um encaminhamento' }) + @ApiOperation({ summary: 'Cancela o encaminhamento' }) async cancel( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index fdc789f..8e5b471 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -41,7 +41,7 @@ export class StatisticsController { ) {} @Get('appointments-total') - @ApiOperation({ summary: 'Número total de atendimentos' }) + @ApiOperation({ summary: 'Retorna o número total de atendimentos' }) async getTotalAppointments(): Promise { const total = await this.getTotalAppointmentsUseCase.execute(); @@ -53,7 +53,7 @@ export class StatisticsController { } @Get('patients-total') - @ApiOperation({ summary: 'Estatísticas totais de pacientes' }) + @ApiOperation({ summary: 'Retorna o número total de pacientes' }) async getTotalPatients(): Promise { const data = await this.getTotalPatientsByStatusUseCase.execute(); @@ -65,7 +65,7 @@ export class StatisticsController { } @Get('patients-by-gender') - @ApiOperation({ summary: 'Estatísticas de pacientes por gênero' }) + @ApiOperation({ summary: 'Retorna o número total de pacientes por gênero' }) async getPatientsByGender( @Query() query: GetTotalPatientsByFieldQuery, ): Promise { @@ -82,7 +82,7 @@ export class StatisticsController { } @Get('patients-by-city') - @ApiOperation({ summary: 'Estatísticas de pacientes por cidade' }) + @ApiOperation({ summary: 'Retorna o número total de pacientes por cidade' }) async getPatientsByCity( @Query() query: GetTotalPatientsByFieldQuery, ): Promise { @@ -100,7 +100,7 @@ export class StatisticsController { } @Get('referrals-total') - @ApiOperation({ summary: 'Estatísticas do total de encaminhamentos' }) + @ApiOperation({ summary: 'Retorna o número total de encaminhamentos' }) async getTotalReferralsAndReferredPatientsPercentage( @Query() query: GetTotalReferralsAndReferredPatientsPercentageQuery, ): Promise { @@ -119,7 +119,7 @@ export class StatisticsController { @Get('referrals-by-category') @ApiOperation({ - summary: 'Lista com o total de encaminhamentos por categoria', + summary: 'Retorna o número total de encaminhamentos por categoria', }) async getTotalReferralsByCategory( @Query() query: GetTotalReferralsByCategoryQuery, @@ -137,7 +137,7 @@ export class StatisticsController { @Get('referrals-by-state') @ApiOperation({ - summary: 'Lista com o total de pacientes encaminhados por estado', + summary: 'Retorna o número total de encaminhamentos por estado', }) async getReferredPatientsByState( @Query() query: GetReferredPatientsByStateQuery, diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index c0f2afa..ee10c38 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -60,7 +60,7 @@ export class UsersController { return { success: true, - message: 'Dados do usuário retornado com sucesso.', + message: 'Dados do usuário retornados com sucesso.', data, }; } From 1b929fee947dc525ac0c847f04e90c86ffba2b12 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Mon, 19 Jan 2026 00:21:39 -0300 Subject: [PATCH 17/21] chore(users): add response DTOs to swagger docs --- src/app/http/users/users.controller.ts | 19 +++++++++++-------- src/app/http/users/users.dtos.ts | 8 ++++++++ src/domain/schemas/users/responses.ts | 2 -- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index ee10c38..467fcf1 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -1,19 +1,20 @@ import { Body, Controller, Get, Post, Query } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import type { BaseResponse } from '@/domain/schemas/base'; -import type { - GetUserResponse, - GetUsersResponse, -} from '@/domain/schemas/users/responses'; import type { AuthUserDto } from '../auth/auth.dtos'; import { CreateUserInviteUseCase } from './use-cases/create-user-invite.use-case'; import { GetUserUseCase } from './use-cases/get-user.use-case'; import { GetUsersUseCase } from './use-cases/get-users.use-case'; -import { CreateUserInviteDto, GetUsersQuery } from './users.dtos'; +import { + CreateUserInviteDto, + GetUserResponseDto, + GetUsersQuery, + GetUsersResponseDto, +} from './users.dtos'; @ApiTags('Usuários') @Controller('users') @@ -42,7 +43,8 @@ export class UsersController { @Get() @Roles(['manager']) @ApiOperation({ summary: 'Lista todos os usuários' }) - async getUsers(@Query() query: GetUsersQuery): Promise { + @ApiResponse({ status: 200, type: GetUsersResponseDto }) + async getUsers(@Query() query: GetUsersQuery): Promise { const data = await this.getUsersUseCase.execute({ query }); return { @@ -55,7 +57,8 @@ export class UsersController { @Get('me') @Roles(['manager', 'nurse', 'specialist']) @ApiOperation({ summary: 'Retorna os dados do usuário autenticado' }) - async getProfile(@AuthUser() user: AuthUserDto): Promise { + @ApiResponse({ status: 200, type: GetUserResponseDto }) + async getProfile(@AuthUser() user: AuthUserDto): Promise { const { user: data } = await this.getUserUseCase.execute({ id: user.id }); return { diff --git a/src/app/http/users/users.dtos.ts b/src/app/http/users/users.dtos.ts index 3ec837f..4170a55 100644 --- a/src/app/http/users/users.dtos.ts +++ b/src/app/http/users/users.dtos.ts @@ -5,9 +5,17 @@ import { getUsersQuerySchema, updateUserSchema, } from '@/domain/schemas/users/requests'; +import { + getUserResponseSchema, + getUsersResponseSchema, +} from '@/domain/schemas/users/responses'; export class CreateUserInviteDto extends createZodDto(createUserInviteSchema) {} export class UpdateUserDto extends createZodDto(updateUserSchema) {} export class GetUsersQuery extends createZodDto(getUsersQuerySchema) {} + +export class GetUserResponseDto extends createZodDto(getUserResponseSchema) {} + +export class GetUsersResponseDto extends createZodDto(getUsersResponseSchema) {} diff --git a/src/domain/schemas/users/responses.ts b/src/domain/schemas/users/responses.ts index a4ffbc0..bf0ec7e 100644 --- a/src/domain/schemas/users/responses.ts +++ b/src/domain/schemas/users/responses.ts @@ -16,7 +16,6 @@ export type UserResponse = z.infer; export const getUserResponseSchema = baseResponseSchema.extend({ data: userResponseSchema, }); -export type GetUserResponse = z.infer; export const getUsersResponseSchema = baseResponseSchema.extend({ data: z.object({ @@ -24,4 +23,3 @@ export const getUsersResponseSchema = baseResponseSchema.extend({ total: z.number(), }), }); -export type GetUsersResponse = z.infer; From e5b5463a6c2fd20c4b06702370ad899d55ea71fb Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Mon, 19 Jan 2026 17:56:42 -0300 Subject: [PATCH 18/21] feat(statistics): create `GET /referred-patients-total` endpoint --- src/app/app.ts | 2 +- .../http/statistics/statistics.controller.ts | 119 ++++++++++++------ src/app/http/statistics/statistics.dtos.ts | 65 +++++++++- src/app/http/statistics/statistics.module.ts | 2 - .../get-total-patients-by-field.use-case.ts | 50 +++++--- .../get-total-patients-by-status.use-case.ts | 8 +- ...d-referred-patients-percentage.use-case.ts | 44 ------- ...et-total-referrals-by-category.use-case.ts | 69 +++++----- ...tal-referred-patients-by-state.use-case.ts | 73 ++++++----- src/app/http/users/users.controller.ts | 12 +- src/app/http/users/users.dtos.ts | 7 +- src/domain/schemas/query.ts | 19 ++- src/domain/schemas/statistics/requests.ts | 44 +++++-- src/domain/schemas/statistics/responses.ts | 97 ++++++-------- 14 files changed, 347 insertions(+), 264 deletions(-) delete mode 100644 src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts diff --git a/src/app/app.ts b/src/app/app.ts index 8e6f50a..c9a084f 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -23,7 +23,7 @@ export async function createNestApp(adapter?: ExpressAdapter) { app.useGlobalFilters(new HttpExceptionFilter()); const envService = app.get(EnvService); - const allowLocalRequests = false; + const allowLocalRequests = true; app.enableCors({ origin: (origin, callback) => { diff --git a/src/app/http/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index 8e5b471..6b4a491 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -1,30 +1,33 @@ import { Controller, Get, Query } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Roles } from '@/common/decorators/roles.decorator'; import type { - GetPatientsByCityResponse, - GetPatientsByGenderResponse, - GetReferredPatientsByStateResponse, - GetTotalAppointmentsResponse, - GetTotalPatientsByStatusResponse, - GetTotalReferralsAndReferredPatientsPercentageResponse, - GetTotalReferralsByCategoryResponse, TotalPatientsByCity, TotalPatientsByGender, } from '@/domain/schemas/statistics/responses'; import { - GetReferredPatientsByStateQuery, + GetTotalAppointmentsResponse, + GetTotalPatientsByCityResponse, GetTotalPatientsByFieldQuery, - GetTotalReferralsAndReferredPatientsPercentageQuery, + GetTotalPatientsByGenderResponse, + GetTotalPatientsByStatusResponse, GetTotalReferralsByCategoryQuery, + GetTotalReferralsByCategoryResponse, + GetTotalReferralsQuery, + GetTotalReferralsResponse, + GetTotalReferredPatientsByStateQuery, + GetTotalReferredPatientsByStateResponse, + GetTotalReferredPatientsQuery, + GetTotalReferredPatientsResponse, } from './statistics.dtos'; import { GetTotalAppointmentsUseCase } from './use-cases/get-total-appointments.use-case'; import { GetTotalPatientsByFieldUseCase } from './use-cases/get-total-patients-by-field.use-case'; import { GetTotalPatientsByStatusUseCase } from './use-cases/get-total-patients-by-status.use-case'; -import { GetTotalReferralsAndReferredPatientsPercentageUseCase } from './use-cases/get-total-referrals-and-referred-patients-percentage.use-case'; +import { GetTotalReferralsUseCase } from './use-cases/get-total-referrals.use-case'; import { GetTotalReferralsByCategoryUseCase } from './use-cases/get-total-referrals-by-category.use-case'; +import { GetTotalReferredPatientsUseCase } from './use-cases/get-total-referred-patients.use-case'; import { GetTotalReferredPatientsByStateUseCase } from './use-cases/get-total-referred-patients-by-state.use-case'; @ApiTags('Estatísticas') @@ -32,18 +35,24 @@ import { GetTotalReferredPatientsByStateUseCase } from './use-cases/get-total-re @Controller('statistics') export class StatisticsController { constructor( + private readonly getTotalAppointmentsUseCase: GetTotalAppointmentsUseCase, private readonly getTotalPatientsByStatusUseCase: GetTotalPatientsByStatusUseCase, private readonly getTotalPatientsByPeriodUseCase: GetTotalPatientsByFieldUseCase, + private readonly getTotalReferralsUseCase: GetTotalReferralsUseCase, private readonly getTotalReferredPatientsByStateUseCase: GetTotalReferredPatientsByStateUseCase, + private readonly getTotalReferredPatientsUseCase: GetTotalReferredPatientsUseCase, private readonly getTotalReferralsByCategoryUseCase: GetTotalReferralsByCategoryUseCase, - private readonly getTotalReferralsAndReferredPatientsPercentageUseCase: GetTotalReferralsAndReferredPatientsPercentageUseCase, - private readonly getTotalAppointmentsUseCase: GetTotalAppointmentsUseCase, ) {} @Get('appointments-total') @ApiOperation({ summary: 'Retorna o número total de atendimentos' }) - async getTotalAppointments(): Promise { - const total = await this.getTotalAppointmentsUseCase.execute(); + @ApiResponse({ status: 200, type: GetTotalAppointmentsResponse }) + async getTotalAppointments( + @Query() query: GetTotalReferralsQuery, + ): Promise { + const { period } = query; + + const total = await this.getTotalAppointmentsUseCase.execute({ period }); return { success: true, @@ -54,6 +63,7 @@ export class StatisticsController { @Get('patients-total') @ApiOperation({ summary: 'Retorna o número total de pacientes' }) + @ApiResponse({ status: 200, type: GetTotalPatientsByStatusResponse }) async getTotalPatients(): Promise { const data = await this.getTotalPatientsByStatusUseCase.execute(); @@ -66,54 +76,64 @@ export class StatisticsController { @Get('patients-by-gender') @ApiOperation({ summary: 'Retorna o número total de pacientes por gênero' }) + @ApiResponse({ status: 200, type: GetTotalPatientsByGenderResponse }) async getPatientsByGender( @Query() query: GetTotalPatientsByFieldQuery, - ): Promise { + ): Promise { + const { period, limit, order, withPercentage } = query; + const { items: genders, total } = await this.getTotalPatientsByPeriodUseCase.execute( - { field: 'gender', query }, + { field: 'gender', period, limit, order, withPercentage }, ); return { success: true, - message: 'Estatísticas de pacientes por gênero retornada com sucesso.', + message: + 'Lista com o total de pacientes por gênero retornado com sucesso.', data: { genders, total }, }; } @Get('patients-by-city') @ApiOperation({ summary: 'Retorna o número total de pacientes por cidade' }) + @ApiResponse({ status: 200, type: GetTotalPatientsByCityResponse }) async getPatientsByCity( @Query() query: GetTotalPatientsByFieldQuery, - ): Promise { + ): Promise { + const { period, limit, order, withPercentage } = query; + const { items: cities, total } = await this.getTotalPatientsByPeriodUseCase.execute({ field: 'city', - query, + period, + order, + limit, + withPercentage, }); return { success: true, - message: 'Estatísticas de pacientes por cidade retornada com sucesso.', + message: + 'Lista com o total de pacientes por cidade retornado com sucesso.', data: { cities, total }, }; } @Get('referrals-total') @ApiOperation({ summary: 'Retorna o número total de encaminhamentos' }) - async getTotalReferralsAndReferredPatientsPercentage( - @Query() query: GetTotalReferralsAndReferredPatientsPercentageQuery, - ): Promise { - const data = - await this.getTotalReferralsAndReferredPatientsPercentageUseCase.execute({ - query, - }); + @ApiResponse({ status: 200, type: GetTotalReferralsResponse }) + async getTotalReferrals( + @Query() query: GetTotalReferralsQuery, + ): Promise { + const { period } = query; + + const total = await this.getTotalReferralsUseCase.execute({ period }); return { success: true, - message: - 'Estatísticas com total de encaminhamentos retornada com sucesso.', - data, + message: 'Número total de encaminhamentos retornado com sucesso.', + data: { total }, }; } @@ -121,11 +141,14 @@ export class StatisticsController { @ApiOperation({ summary: 'Retorna o número total de encaminhamentos por categoria', }) + @ApiResponse({ status: 200, type: GetTotalReferralsByCategoryResponse }) async getTotalReferralsByCategory( @Query() query: GetTotalReferralsByCategoryQuery, ): Promise { + const { period } = query; + const { categories, total } = - await this.getTotalReferralsByCategoryUseCase.execute({ query }); + await this.getTotalReferralsByCategoryUseCase.execute({ period }); return { success: true, @@ -139,11 +162,17 @@ export class StatisticsController { @ApiOperation({ summary: 'Retorna o número total de encaminhamentos por estado', }) + @ApiResponse({ status: 200, type: GetTotalReferredPatientsByStateResponse }) async getReferredPatientsByState( - @Query() query: GetReferredPatientsByStateQuery, - ): Promise { + @Query() query: GetTotalReferredPatientsByStateQuery, + ): Promise { + const { period, limit } = query; + const { states, total } = - await this.getTotalReferredPatientsByStateUseCase.execute({ query }); + await this.getTotalReferredPatientsByStateUseCase.execute({ + period, + limit, + }); return { success: true, @@ -152,4 +181,24 @@ export class StatisticsController { data: { states, total }, }; } + + @Get('referred-patients-total') + @ApiOperation({ summary: 'Retorna o número total de pacientes encaminhados' }) + @ApiResponse({ status: 200, type: GetTotalReferredPatientsResponse }) + async getTotalReferredPatients( + @Query() query: GetTotalReferredPatientsQuery, + ): Promise { + const { period } = query; + + const total = await this.getTotalReferredPatientsUseCase.execute({ + period, + }); + + return { + success: true, + message: + 'Número total de pacientes encaminhamados retornado com sucesso.', + data: { total }, + }; + } } diff --git a/src/app/http/statistics/statistics.dtos.ts b/src/app/http/statistics/statistics.dtos.ts index 312c8e5..9a98fcf 100644 --- a/src/app/http/statistics/statistics.dtos.ts +++ b/src/app/http/statistics/statistics.dtos.ts @@ -1,24 +1,77 @@ import { createZodDto } from 'nestjs-zod'; import { - getReferredPatientsByStateQuerySchema, + getTotalAppointmentsQuerySchema, getTotalPatientsByFieldQuerySchema, - getTotalReferralsAndReferredPatientsPercentageQuerySchema, getTotalReferralsByCategoryQuerySchema, + getTotalReferralsQuerySchema, + getTotalReferredPatientsByStateQuerySchema, + getTotalReferredPatientsQuerySchema, } from '@/domain/schemas/statistics/requests'; +import { + getTotalAppointmentsResponseSchema, + getTotalPatientsByCityResponseSchema, + getTotalPatientsByGenderResponseSchema, + getTotalPatientsByStatusResponseSchema, + getTotalReferralsByCategoryResponseSchema, + getTotalReferralsResponseSchema, + getTotalReferredPatientsByStateResponseSchema, + getTotalReferredPatientsResponseSchema, +} from '@/domain/schemas/statistics/responses'; + +// Appointments + +export class GetTotalAppointmentsQuery extends createZodDto( + getTotalAppointmentsQuerySchema, +) {} +export class GetTotalAppointmentsResponse extends createZodDto( + getTotalAppointmentsResponseSchema, +) {} + +// Patients + +export class GetTotalPatientsByStatusResponse extends createZodDto( + getTotalPatientsByStatusResponseSchema, +) {} + +export class GetTotalPatientsByGenderResponse extends createZodDto( + getTotalPatientsByGenderResponseSchema, +) {} + +export class GetTotalPatientsByCityResponse extends createZodDto( + getTotalPatientsByCityResponseSchema, +) {} export class GetTotalPatientsByFieldQuery extends createZodDto( getTotalPatientsByFieldQuerySchema, ) {} -export class GetTotalReferralsAndReferredPatientsPercentageQuery extends createZodDto( - getTotalReferralsAndReferredPatientsPercentageQuerySchema, +// Referrals + +export class GetTotalReferralsQuery extends createZodDto( + getTotalReferralsQuerySchema, +) {} +export class GetTotalReferralsResponse extends createZodDto( + getTotalReferralsResponseSchema, ) {} export class GetTotalReferralsByCategoryQuery extends createZodDto( getTotalReferralsByCategoryQuerySchema, ) {} +export class GetTotalReferralsByCategoryResponse extends createZodDto( + getTotalReferralsByCategoryResponseSchema, +) {} -export class GetReferredPatientsByStateQuery extends createZodDto( - getReferredPatientsByStateQuerySchema, +export class GetTotalReferredPatientsQuery extends createZodDto( + getTotalReferredPatientsQuerySchema, +) {} +export class GetTotalReferredPatientsResponse extends createZodDto( + getTotalReferredPatientsResponseSchema, +) {} + +export class GetTotalReferredPatientsByStateQuery extends createZodDto( + getTotalReferredPatientsByStateQuerySchema, +) {} +export class GetTotalReferredPatientsByStateResponse extends createZodDto( + getTotalReferredPatientsByStateResponseSchema, ) {} diff --git a/src/app/http/statistics/statistics.module.ts b/src/app/http/statistics/statistics.module.ts index 396d05e..fe08f0d 100644 --- a/src/app/http/statistics/statistics.module.ts +++ b/src/app/http/statistics/statistics.module.ts @@ -12,7 +12,6 @@ import { GetTotalPatientsUseCase } from './use-cases/get-total-patients.use-case import { GetTotalPatientsByFieldUseCase } from './use-cases/get-total-patients-by-field.use-case'; import { GetTotalPatientsByStatusUseCase } from './use-cases/get-total-patients-by-status.use-case'; import { GetTotalReferralsUseCase } from './use-cases/get-total-referrals.use-case'; -import { GetTotalReferralsAndReferredPatientsPercentageUseCase } from './use-cases/get-total-referrals-and-referred-patients-percentage.use-case'; import { GetTotalReferralsByCategoryUseCase } from './use-cases/get-total-referrals-by-category.use-case'; import { GetTotalReferredPatientsUseCase } from './use-cases/get-total-referred-patients.use-case'; import { GetTotalReferredPatientsByStateUseCase } from './use-cases/get-total-referred-patients-by-state.use-case'; @@ -30,7 +29,6 @@ import { GetTotalReferredPatientsByStateUseCase } from './use-cases/get-total-re GetTotalPatientsByStatusUseCase, GetTotalReferralsUseCase, GetTotalReferralsByCategoryUseCase, - GetTotalReferralsAndReferredPatientsPercentageUseCase, GetTotalReferredPatientsUseCase, GetTotalReferredPatientsByStateUseCase, ], diff --git a/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts index 519cebf..5b253f1 100644 --- a/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts @@ -3,15 +3,20 @@ import { InjectRepository } from '@nestjs/typeorm'; import type { Repository, SelectQueryBuilder } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; +import type { QueryOrder, QueryPeriod } from '@/domain/enums/queries'; import type { PatientsStatisticField } from '@/domain/enums/statistics'; import { UtilsService } from '@/utils/utils.service'; -import type { GetTotalPatientsByFieldQuery } from '../statistics.dtos'; import { GetTotalPatientsUseCase } from './get-total-patients.use-case'; interface GetTotalPatientsByFieldUseCaseInput { field: PatientsStatisticField; - query: GetTotalPatientsByFieldQuery; + period?: QueryPeriod; + startDate?: Date; + endDate?: Date; + order?: QueryOrder; + limit?: number; + withPercentage?: boolean; } interface GetTotalPatientsByFieldUseCaseOutput { @@ -30,29 +35,38 @@ export class GetTotalPatientsByFieldUseCase { async execute({ field, - query, + period, + startDate, + endDate, + order, + limit, + withPercentage, }: GetTotalPatientsByFieldUseCaseInput): Promise< GetTotalPatientsByFieldUseCaseOutput > { - const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( - query.period, - ); + const dateRange = period + ? this.utilsService.getDateRangeForPeriod(period) + : { startDate, endDate }; const totalPatients = await this.getTotalPatientsUseCase.execute({ - startDate, - endDate, + startDate: dateRange.startDate, + endDate: dateRange.endDate, }); const createBaseQuery = (): SelectQueryBuilder => { - return this.patientsRepository - .createQueryBuilder('patient') - .where('patient.created_at BETWEEN :start AND :end', { - start: startDate, - end: endDate, + const baseQuery = this.patientsRepository.createQueryBuilder('patient'); + + if (dateRange.startDate && dateRange.endDate) { + baseQuery.where('patient.created_at BETWEEN :start AND :end', { + start: dateRange.startDate, + end: dateRange.endDate, }); + } + + return baseQuery; }; - const totalFieldQuery = createBaseQuery().select( + const totalQuery = createBaseQuery().select( `COUNT(DISTINCT patient.${field})`, 'total', ); @@ -61,10 +75,10 @@ export class GetTotalPatientsByFieldUseCase { .select(`patient.${field}`, field) .addSelect('COUNT(patient.id)', 'total') .groupBy(`patient.${field}`) - .orderBy('total', query.order) - .limit(query.limit); + .orderBy('total', order) + .limit(limit); - if (query.withPercentage) { + if (withPercentage) { fieldQuery.addSelect( `ROUND((COUNT(patient.id) * 100.0 / ${totalPatients}), 1)`, 'percentage', @@ -73,7 +87,7 @@ export class GetTotalPatientsByFieldUseCase { const [items, totalResult] = await Promise.all([ fieldQuery.getRawMany(), - totalFieldQuery.getRawOne<{ total: string }>(), + totalQuery.getRawOne<{ total: string }>(), ]); return { items, total: Number(totalResult?.total || 0) }; diff --git a/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts index 6c52d3e..e5fc5f8 100644 --- a/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts @@ -18,7 +18,7 @@ export class GetTotalPatientsByStatusUseCase { ) {} async execute(): Promise { - const queryBuilder = await this.patientsRepository + const query = await this.patientsRepository .createQueryBuilder('patient') .select('COUNT(patient.id)', 'total') .where('patient.status != :status', { status: 'pending' }) @@ -33,9 +33,9 @@ export class GetTotalPatientsByStatusUseCase { .getRawOne<{ total: string; active: string; inactive: string }>(); return { - total: Number(queryBuilder?.total ?? 0), - active: Number(queryBuilder?.active ?? 0), - inactive: Number(queryBuilder?.inactive ?? 0), + total: Number(query?.total ?? 0), + active: Number(query?.active ?? 0), + inactive: Number(query?.inactive ?? 0), }; } } diff --git a/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts deleted file mode 100644 index 158a429..0000000 --- a/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import type { GetTotalReferralsAndReferredPatientsPercentageQuery } from '../statistics.dtos'; -import { GetTotalPatientsUseCase } from './get-total-patients.use-case'; -import { GetTotalReferralsUseCase } from './get-total-referrals.use-case'; -import { GetTotalReferredPatientsUseCase } from './get-total-referred-patients.use-case'; - -interface GetTotalReferralsAndReferredPatientsPercentageUseCaseInput { - query: GetTotalReferralsAndReferredPatientsPercentageQuery; -} - -interface GetTotalReferralsAndReferredPatientsPercentageUseCaseOutput { - totalReferrals: number; - referredPatientsPercentage: number; -} - -@Injectable() -export class GetTotalReferralsAndReferredPatientsPercentageUseCase { - constructor( - private readonly getTotalPatientsUseCase: GetTotalPatientsUseCase, - private readonly getTotalReferralsUseCase: GetTotalReferralsUseCase, - private readonly getTotalReferredPatientsUseCase: GetTotalReferredPatientsUseCase, - ) {} - - async execute({ - query, - }: GetTotalReferralsAndReferredPatientsPercentageUseCaseInput): Promise { - const { period } = query; - - const [totalPatients, totalReferrals, totalReferredPatients] = - await Promise.all([ - this.getTotalPatientsUseCase.execute({ period }), - this.getTotalReferralsUseCase.execute({ period }), - this.getTotalReferredPatientsUseCase.execute({ period }), - ]); - - const percentage = Number((totalReferredPatients / totalPatients) * 100); - - return { - totalReferrals, - referredPatientsPercentage: Number(percentage.toFixed(2)), - }; - } -} diff --git a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts index ba39920..0b87855 100644 --- a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts @@ -3,13 +3,15 @@ import { InjectRepository } from '@nestjs/typeorm'; import type { Repository, SelectQueryBuilder } from 'typeorm'; import { Referral } from '@/domain/entities/referral'; +import type { QueryPeriod } from '@/domain/enums/queries'; import type { TotalReferralsByCategory } from '@/domain/schemas/statistics/responses'; import { UtilsService } from '@/utils/utils.service'; -import type { GetTotalReferralsByCategoryQuery } from '../statistics.dtos'; - interface GetTotalReferralsByCategoryUseCaseInput { - query: GetTotalReferralsByCategoryQuery; + period?: QueryPeriod; + startDate?: Date; + endDate?: Date; + limit?: number; } interface GetTotalReferralsByCategoryUseCaseOutput { @@ -26,50 +28,45 @@ export class GetTotalReferralsByCategoryUseCase { ) {} async execute({ - query, - }: GetTotalReferralsByCategoryUseCaseInput): Promise { - const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( - query.period, - ); + period, + startDate, + endDate, + limit, + }: GetTotalReferralsByCategoryUseCaseInput = {}): Promise { + const dateRange = period + ? this.utilsService.getDateRangeForPeriod(period) + : { startDate, endDate }; - const createQueryBuilder = (): SelectQueryBuilder => { - return this.referralsRepository.createQueryBuilder('referral'); - }; + const createBaseQuery = (): SelectQueryBuilder => { + const baseQuery = this.referralsRepository.createQueryBuilder('referral'); - function getQueryBuilderWithFilters( - queryBuilder: SelectQueryBuilder, - ) { - if (startDate && endDate) { - queryBuilder.andWhere('referral.created_at BETWEEN :start AND :end', { - start: startDate, - end: endDate, + if (dateRange.startDate && dateRange.endDate) { + baseQuery.where('referral.created_at BETWEEN :start AND :end', { + start: dateRange.startDate, + end: dateRange.endDate, }); } - return queryBuilder; - } + return baseQuery; + }; - const categoryListQuery = getQueryBuilderWithFilters( - createQueryBuilder() - .select('referral.category', 'category') - .addSelect('COUNT(referral.id)', 'total') - .groupBy('referral.category') - .orderBy('COUNT(referral.id)', 'DESC') - .limit(query.limit), - ); + const listCategoriesQuery = createBaseQuery() + .select('referral.category', 'category') + .addSelect('COUNT(referral.id)', 'total') + .groupBy('referral.category') + .orderBy('COUNT(referral.id)', 'DESC') + .limit(limit); - const totalCategoriesQuery = getQueryBuilderWithFilters( - createQueryBuilder().select('COUNT(DISTINCT referral.category)', 'total'), + const totalQuery = createBaseQuery().select( + 'COUNT(DISTINCT referral.category)', + 'total', ); const [categories, totalResult] = await Promise.all([ - categoryListQuery.getRawMany(), - totalCategoriesQuery.getRawOne<{ total: string }>(), + listCategoriesQuery.getRawMany(), + totalQuery.getRawOne<{ total: string }>(), ]); - return { - categories, - total: Number(totalResult?.total || 0), - }; + return { categories, total: Number(totalResult?.total || 0) }; } } diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts index 2aea8bb..7377acb 100644 --- a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts +++ b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts @@ -3,17 +3,19 @@ import { InjectRepository } from '@nestjs/typeorm'; import type { Repository, SelectQueryBuilder } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -import type { TotalReferredPatientsByStateSchema } from '@/domain/schemas/statistics/responses'; +import type { QueryPeriod } from '@/domain/enums/queries'; +import type { TotalReferredPatientsByState } from '@/domain/schemas/statistics/responses'; import { UtilsService } from '@/utils/utils.service'; -import type { GetReferredPatientsByStateQuery } from '../statistics.dtos'; - interface GetTotalReferredPatientsByStateUseCaseInput { - query: GetReferredPatientsByStateQuery; + period?: QueryPeriod; + startDate?: Date; + endDate?: Date; + limit?: number; } interface GetTotalReferredPatientsByStateUseCaseOutput { - states: TotalReferredPatientsByStateSchema[]; + states: TotalReferredPatientsByState[]; total: number; } @@ -26,53 +28,48 @@ export class GetTotalReferredPatientsByStateUseCase { ) {} async execute({ - query, - }: GetTotalReferredPatientsByStateUseCaseInput): Promise { - const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( - query.period, - ); + period, + startDate, + endDate, + limit, + }: GetTotalReferredPatientsByStateUseCaseInput = {}): Promise { + const dateRange = period + ? this.utilsService.getDateRangeForPeriod(period) + : { startDate, endDate }; - const createQueryBuilder = (): SelectQueryBuilder => { - return this.patientsRepository + const createBaseQuery = (): SelectQueryBuilder => { + const baseQuery = this.patientsRepository .createQueryBuilder('patient') .innerJoin('patient.referrals', 'referral') .where('referral.id IS NOT NULL'); - }; - function getQueryBuilderWithFilters( - queryBuilder: SelectQueryBuilder, - ) { - if (startDate && endDate) { - queryBuilder.andWhere('referral.created_at BETWEEN :start AND :end', { - start: startDate, - end: endDate, + if (dateRange.startDate && dateRange.endDate) { + baseQuery.andWhere('referral.created_at BETWEEN :start AND :end', { + start: dateRange.startDate, + end: dateRange.endDate, }); } - return queryBuilder; - } + return baseQuery; + }; - const stateListQuery = getQueryBuilderWithFilters( - createQueryBuilder() - .select('patient.state', 'state') - .addSelect('COUNT(DISTINCT patient.id)', 'total') - .groupBy('patient.state') - .orderBy('COUNT(DISTINCT patient.id)', 'DESC') - .limit(query.limit), - ); + const listStatesQuery = createBaseQuery() + .select('patient.state', 'state') + .addSelect('COUNT(DISTINCT patient.id)', 'total') + .groupBy('patient.state') + .orderBy('COUNT(DISTINCT patient.id)', 'DESC') + .limit(limit); - const totalStatesQuery = getQueryBuilderWithFilters( - createQueryBuilder().select('COUNT(DISTINCT patient.state)', 'total'), + const totalQuery = createBaseQuery().select( + 'COUNT(DISTINCT patient.state)', + 'total', ); const [states, totalResult] = await Promise.all([ - stateListQuery.getRawMany(), - totalStatesQuery.getRawOne<{ total: string }>(), + listStatesQuery.getRawMany(), + totalQuery.getRawOne<{ total: string }>(), ]); - return { - states, - total: Number(totalResult?.total || 0), - }; + return { states, total: Number(totalResult?.total || 0) }; } } diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index 467fcf1..1c7c2dc 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -11,9 +11,9 @@ import { GetUserUseCase } from './use-cases/get-user.use-case'; import { GetUsersUseCase } from './use-cases/get-users.use-case'; import { CreateUserInviteDto, - GetUserResponseDto, + GetUserResponse, GetUsersQuery, - GetUsersResponseDto, + GetUsersResponse, } from './users.dtos'; @ApiTags('Usuários') @@ -43,8 +43,8 @@ export class UsersController { @Get() @Roles(['manager']) @ApiOperation({ summary: 'Lista todos os usuários' }) - @ApiResponse({ status: 200, type: GetUsersResponseDto }) - async getUsers(@Query() query: GetUsersQuery): Promise { + @ApiResponse({ status: 200, type: GetUsersResponse }) + async getUsers(@Query() query: GetUsersQuery): Promise { const data = await this.getUsersUseCase.execute({ query }); return { @@ -57,8 +57,8 @@ export class UsersController { @Get('me') @Roles(['manager', 'nurse', 'specialist']) @ApiOperation({ summary: 'Retorna os dados do usuário autenticado' }) - @ApiResponse({ status: 200, type: GetUserResponseDto }) - async getProfile(@AuthUser() user: AuthUserDto): Promise { + @ApiResponse({ status: 200, type: GetUserResponse }) + async getProfile(@AuthUser() user: AuthUserDto): Promise { const { user: data } = await this.getUserUseCase.execute({ id: user.id }); return { diff --git a/src/app/http/users/users.dtos.ts b/src/app/http/users/users.dtos.ts index 4170a55..0e54ca4 100644 --- a/src/app/http/users/users.dtos.ts +++ b/src/app/http/users/users.dtos.ts @@ -14,8 +14,7 @@ export class CreateUserInviteDto extends createZodDto(createUserInviteSchema) {} export class UpdateUserDto extends createZodDto(updateUserSchema) {} -export class GetUsersQuery extends createZodDto(getUsersQuerySchema) {} - -export class GetUserResponseDto extends createZodDto(getUserResponseSchema) {} +export class GetUserResponse extends createZodDto(getUserResponseSchema) {} -export class GetUsersResponseDto extends createZodDto(getUsersResponseSchema) {} +export class GetUsersQuery extends createZodDto(getUsersQuerySchema) {} +export class GetUsersResponse extends createZodDto(getUsersResponseSchema) {} diff --git a/src/domain/schemas/query.ts b/src/domain/schemas/query.ts index 6e265d1..665809a 100644 --- a/src/domain/schemas/query.ts +++ b/src/domain/schemas/query.ts @@ -13,4 +13,21 @@ export const baseQuerySchema = z.object({ endDate: z.string().datetime().optional(), withPercentage: z.coerce.boolean().optional().default(false), }); -export type BaseQuery = z.infer; + +export const querySearchSchema = z.string(); +export const queryOrderSchema = z.enum(QUERY_ORDERS); +export const queryPeriodSchema = z.enum(QUERY_PERIODS); +export const queryDateSchema = z.string().datetime(); + +export const queryLimitSchema = z.coerce.number().min(1).optional().default(10); +export const queryPageSchema = z.coerce.number().min(1).optional().default(1); +export const queryPerPageSchema = z.coerce + .number() + .min(1) + .max(50) + .optional() + .default(10); +export const queryPercentageSchema = z.coerce + .boolean() + .optional() + .default(false); diff --git a/src/domain/schemas/statistics/requests.ts b/src/domain/schemas/statistics/requests.ts index b197cdc..9ab2c03 100644 --- a/src/domain/schemas/statistics/requests.ts +++ b/src/domain/schemas/statistics/requests.ts @@ -1,22 +1,42 @@ -import { baseQuerySchema } from '../query'; +import { z } from 'zod'; + +import { + queryLimitSchema, + queryOrderSchema, + queryPercentageSchema, + queryPeriodSchema, +} from '../query'; + +// Appointments + +export const getTotalAppointmentsQuerySchema = z.object({ + period: queryPeriodSchema.optional(), +}); // Patients -export const getTotalPatientsByFieldQuerySchema = baseQuerySchema - .pick({ period: true, limit: true, order: true, withPercentage: true }) - .extend({ order: baseQuerySchema.shape.order.default('DESC') }); +export const getTotalPatientsByFieldQuerySchema = z.object({ + period: queryPeriodSchema.optional(), + order: queryOrderSchema.optional().default('DESC'), + limit: queryLimitSchema, + withPercentage: queryPercentageSchema, +}); // Referrals -export const getTotalReferralsAndReferredPatientsPercentageQuerySchema = - baseQuerySchema.pick({ period: true }); +export const getTotalReferralsQuerySchema = z.object({ + period: queryPeriodSchema.optional(), +}); + +export const getTotalReferralsByCategoryQuerySchema = z.object({ + period: queryPeriodSchema.optional(), +}); -export const getReferredPatientsByStateQuerySchema = baseQuerySchema.pick({ - period: true, - limit: true, +export const getTotalReferredPatientsQuerySchema = z.object({ + period: queryPeriodSchema.optional(), }); -export const getTotalReferralsByCategoryQuerySchema = baseQuerySchema.pick({ - period: true, - limit: true, +export const getTotalReferredPatientsByStateQuerySchema = z.object({ + period: queryPeriodSchema.optional(), + limit: queryLimitSchema, }); diff --git a/src/domain/schemas/statistics/responses.ts b/src/domain/schemas/statistics/responses.ts index fdd88c8..fd70461 100644 --- a/src/domain/schemas/statistics/responses.ts +++ b/src/domain/schemas/statistics/responses.ts @@ -6,6 +6,12 @@ import { SPECIALTY_CATEGORIES } from '@/domain/enums/shared'; import { baseResponseSchema } from '../base'; +// Appointments + +export const getTotalAppointmentsResponseSchema = baseResponseSchema.extend({ + data: z.object({ total: z.number() }), +}); + // Patients export const getTotalPatientsByStatusResponseSchema = baseResponseSchema.extend( @@ -17,9 +23,6 @@ export const getTotalPatientsByStatusResponseSchema = baseResponseSchema.extend( }), }, ); -export type GetTotalPatientsByStatusResponse = z.infer< - typeof getTotalPatientsByStatusResponseSchema ->; export const totalPatientsByGenderSchema = z.object({ gender: z.enum(PATIENT_GENDERS), @@ -27,89 +30,69 @@ export const totalPatientsByGenderSchema = z.object({ }); export type TotalPatientsByGender = z.infer; -export const getPatientsByGenderResponseSchema = baseResponseSchema.extend({ - data: z.object({ - genders: z.array(totalPatientsByGenderSchema), - total: z.number(), - }), -}); -export type GetPatientsByGenderResponse = z.infer< - typeof getPatientsByGenderResponseSchema ->; +export const getTotalPatientsByGenderResponseSchema = baseResponseSchema.extend( + { + data: z.object({ + genders: z.array(totalPatientsByGenderSchema), + total: z.number(), + }), + }, +); -export const totalPatientsByCitySchema = z - .object({ - city: z.string(), - total: z.number(), - percentage: z.number(), - }) - .strict(); +export const totalPatientsByCitySchema = z.object({ + city: z.string(), + total: z.number(), + percentage: z.number(), +}); export type TotalPatientsByCity = z.infer; -export const getPatientsByCityResponseSchema = baseResponseSchema.extend({ +export const getTotalPatientsByCityResponseSchema = baseResponseSchema.extend({ data: z.object({ cities: z.array(totalPatientsByCitySchema), total: z.number(), }), }); -export type GetPatientsByCityResponse = z.infer< - typeof getPatientsByCityResponseSchema ->; -// Appointments +// Referrals -export const getTotalAppointments = baseResponseSchema.extend({ +export const getTotalReferralsResponseSchema = baseResponseSchema.extend({ data: z.object({ total: z.number() }), }); -export type GetTotalAppointmentsResponse = z.infer; - -// Referrals -export const getTotalReferralsAndReferredPatientsPercentageResponseSchema = - baseResponseSchema.extend({ - data: z.object({ - totalReferrals: z.number(), - referredPatientsPercentage: z.number(), - }), - }); -export type GetTotalReferralsAndReferredPatientsPercentageResponse = z.infer< - typeof getTotalReferralsAndReferredPatientsPercentageResponseSchema ->; - -export const totalReferredPatientsByStateSchema = z.object({ - state: z.enum(BRAZILIAN_STATES), +export const totalReferralsByCategorySchema = z.object({ + category: z.enum(SPECIALTY_CATEGORIES), total: z.number(), }); -export type TotalReferredPatientsByStateSchema = z.infer< - typeof totalReferredPatientsByStateSchema +export type TotalReferralsByCategory = z.infer< + typeof totalReferralsByCategorySchema >; -export const getReferredPatientsByStateResponseSchema = +export const getTotalReferralsByCategoryResponseSchema = baseResponseSchema.extend({ data: z.object({ - states: z.array(totalReferredPatientsByStateSchema), + categories: z.array(totalReferralsByCategorySchema), total: z.number(), }), }); -export type GetReferredPatientsByStateResponse = z.infer< - typeof getReferredPatientsByStateResponseSchema ->; -export const totalReferralsByCategorySchema = z.object({ - category: z.enum(SPECIALTY_CATEGORIES), +export const getTotalReferredPatientsResponseSchema = baseResponseSchema.extend( + { + data: z.object({ total: z.number() }), + }, +); + +export const totalReferredPatientsByStateSchema = z.object({ + state: z.enum(BRAZILIAN_STATES), total: z.number(), }); -export type TotalReferralsByCategory = z.infer< - typeof totalReferralsByCategorySchema +export type TotalReferredPatientsByState = z.infer< + typeof totalReferredPatientsByStateSchema >; -export const getTotalReferralsByCategoryResponseSchema = +export const getTotalReferredPatientsByStateResponseSchema = baseResponseSchema.extend({ data: z.object({ - categories: z.array(totalReferralsByCategorySchema), + states: z.array(totalReferredPatientsByStateSchema), total: z.number(), }), }); -export type GetTotalReferralsByCategoryResponse = z.infer< - typeof getTotalReferralsByCategoryResponseSchema ->; From 9711a5366519de207002d9f18c63d45eef4bced8 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Mon, 19 Jan 2026 21:39:26 -0300 Subject: [PATCH 19/21] chore(docs): add response DTOs to each endpoint --- .env.example | 1 - .env.test | 1 - README.md | 101 ++++++------------ src/app/app.ts | 35 +++--- .../appointments/appointments.controller.ts | 12 ++- .../http/appointments/appointments.dtos.ts | 4 + src/app/http/auth/auth.controller.ts | 11 +- .../patient-requirements.controller.ts | 15 +-- .../patient-requirements.dtos.ts | 19 +++- .../patient-supports.controller.ts | 7 +- .../patient-supports/patient-supports.dtos.ts | 12 +++ src/app/http/patients/patients.controller.ts | 15 +-- src/app/http/patients/patients.dtos.ts | 15 +++ .../http/referrals/referrals.controller.ts | 14 ++- src/app/http/referrals/referrals.dtos.ts | 8 +- .../http/statistics/statistics.controller.ts | 21 ++-- .../users/use-cases/get-users.use-case.ts | 3 +- src/app/http/users/users.controller.ts | 7 +- src/app/http/users/users.dtos.ts | 10 +- src/common/dtos.ts | 5 + src/config/database.config.ts | 1 - src/domain/schemas/appointments/responses.ts | 3 - src/domain/schemas/base.ts | 5 +- .../schemas/patient-requirement/requests.ts | 9 -- .../schemas/patient-requirement/responses.ts | 6 -- .../schemas/patient-support/requests.ts | 4 - .../schemas/patient-support/responses.ts | 6 -- src/domain/schemas/patients/requests.ts | 3 - src/domain/schemas/patients/responses.ts | 5 - src/domain/schemas/referrals/requests.ts | 1 - src/domain/schemas/referrals/responses.ts | 1 - src/domain/schemas/users/requests.ts | 4 - src/env/env.ts | 7 +- 33 files changed, 185 insertions(+), 186 deletions(-) create mode 100644 src/common/dtos.ts diff --git a/.env.example b/.env.example index 7d97b84..99e779b 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,6 @@ API_PORT=3333 # APP APP_URL="http://localhost:3000" -APP_LOCAL_URL="http://localhost:3000" # Secrets COOKIE_DOMAIN="localhost" diff --git a/.env.test b/.env.test index 326aa11..6c1ed7b 100644 --- a/.env.test +++ b/.env.test @@ -8,7 +8,6 @@ API_PORT=3333 # APP APP_URL="http://localhost:3000" -APP_LOCAL_URL="http://localhost:3000" # Secrets COOKIE_DOMAIN="localhost" diff --git a/README.md b/README.md index 5aca2c7..544f6e5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -# 🧠 ABNMO Backend +# Sistema Viver Melhor (SVM) - ABNMO - Back-End -Este repositório contém a API do projeto ABNMO, construída com [NestJS](https://nestjs.com/), [TypeORM](https://typeorm.io/) e banco de dados MySQL. +Aplicação Back-End do Sistema Viver Melhor (SVM), desenvolvida para a ABNMO. Este sistema foi projetado para equipes multidisciplinares de saúde, proporcionando uma plataforma centralizada para acompanhamento de pacientes, gerenciamento de encaminhamentos e consolidação de informações clínicas. + +O sistema otimiza o fluxo de atendimento com integração de dados em uma interface responsiva, acessível e adaptável a diversos dispositivos. --- -## 🚀 Tecnologias Utilizadas +## Tecnologias utilizadas - Node.js - NestJS @@ -12,101 +14,68 @@ Este repositório contém a API do projeto ABNMO, construída com [NestJS](https - MySQL - Jest (testes) - ESLint + Prettier (linting e formatação) -- Zod (validação) +- Zod (schemas e validação) +- Swagger (documentação) +- Docker (containers com banco de dados e app de desenvolvimento) --- -## 📦 Instalação +## Instalação Clone o repositório e instale as dependências: ```bash -git clone https://github.com/seu-usuario/abnmo-backend.git +git clone https://github.com/ipecode-br/abnmo-backend.git cd abnmo-backend npm install ``` --- -## ⚙️ Ambiente de Desenvolvimento - -Para rodar o projeto localmente: +## Ambiente de desenvolvimento -1. Crie um arquivo `.env` na raiz do projeto com as credenciais de acesso ao banco de dados e outras variáveis necessárias. -2. Execute o comando: +### Executando pela primeira vez +1. Copie o arquivo `.env.example` e renomeie para `.env` ou execute o comando: ```bash -npm run start:dev +cp .env.example .env ``` -Isso iniciará o servidor em modo de desenvolvimento com `watch`. - ---- - -## 🧪 Testes - -Execute os testes unitários com: - +2. Com o Docker em execução, inicie a instância do banco de dados: ```bash -npm run test +npm run services:up ``` -Para ver a cobertura: - +3. Execute as migrações do banco de dados: ```bash -npm run test:cov +npm run db:migrate ``` ---- - -## 🧬 Migrations - -Para gerar uma nova migration: - +4. Popule o banco de dados com dados de exemplo: ```bash -npm run db:generate NomeDaMigration +npm run db:seed-dev ``` -Para rodar as migrations: - +5. Inicie a aplicação em modo de desenvolvimento: ```bash -npm run db:migrate +npm run dev ``` -## 👨‍💻 Scripts úteis - -- `npm run build`: Compila o projeto -- `npm run start`: Inicia o app em produção -- `npm run start:prod`: Inicia usando o `dist` -- `npm run lint:eslint:check`: Verifica problemas de lint -- `npm run lint:prettier:fix`: Corrige problemas de formatação - ---- - -## 📡 Padrão de Respostas da API +### Executando a aplicação -### ✅ Sucesso - -```json -{ - "success": true, - "message": "Mensagem descritiva do sucesso", - "data": { - // dados retornados - } -} +Para iniciar a aplicação novamente, execute o comando abaixo com o Docker em funcionamento: +```bash +npm run dev ``` -### ❌ Erro - -```json -{ - "success": false, - "message": "Mensagem descritiva do erro", - "data": null -} -``` +--- -## Para mais detalhes consulte o Wiki do projeto em: +## Scripts úteis -## https://github.com/ipecode-br/abnmo-backend/wiki +- `npm run dev`: Inicia o container do banco de dados (Docker), aguarda a conexão estar disponível, executa as migrações (se houver pendências) e inicia o app em desenvolvimento +- `npm run start:dev`: Inicia apenas o app em desenvolvimento +- `npm run services:stop`: Interrompe a execução do container do banco de dados (Docker) +- `npm run services:down`: Exclui o container do banco de dados (Docker) +- `npm run lint:eslint:check`: Verifica problemas de lint +- `npm run lint:prettier:check`: Verifica problemas de formatação +- `npm run lint:prettier:fix`: Corrige problemas de formatação diff --git a/src/app/app.ts b/src/app/app.ts index c9a084f..e752fcd 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -23,27 +23,28 @@ export async function createNestApp(adapter?: ExpressAdapter) { app.useGlobalFilters(new HttpExceptionFilter()); const envService = app.get(EnvService); - const allowLocalRequests = true; - app.enableCors({ - origin: (origin, callback) => { - const allowedOrigins = allowLocalRequests - ? [envService.get('APP_URL'), envService.get('APP_LOCAL_URL')] - : [envService.get('APP_URL')]; + // TODO: remove the block below after review + // app.enableCors({ + // origin: (origin, callback) => { + // const allowedOrigins = [ + // envService.get('APP_URL'), + // `${envService.get('API_BASE_URL')}:${envService.get('API_PORT')}`, + // ]; - // Allow requests with no origin (like mobile apps or curl requests) - if (!origin) return callback(null, true); + // // Allow requests with no origin (like mobile apps or curl requests) + // if (!origin) return callback(null, true); - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } + // if (allowedOrigins.includes(origin)) { + // return callback(null, true); + // } - return callback(new Error(`Origin ${origin} not allowed by CORS`)); - }, - allowedHeaders: ['Authorization', 'Content-Type', 'Content-Length'], - methods: ['OPTIONS', 'GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], - credentials: true, - }); + // return callback(new Error(`Origin ${origin} not allowed by CORS`)); + // }, + // allowedHeaders: ['Authorization', 'Content-Type', 'Content-Length'], + // methods: ['OPTIONS', 'GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], + // credentials: true, + // }); app.use(cookieParser(envService.get('COOKIE_SECRET'))); app.useLogger(app.get(Logger)); diff --git a/src/app/http/appointments/appointments.controller.ts b/src/app/http/appointments/appointments.controller.ts index f74dd9e..ceddfd3 100644 --- a/src/app/http/appointments/appointments.controller.ts +++ b/src/app/http/appointments/appointments.controller.ts @@ -8,17 +8,17 @@ import { Put, Query, } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import type { GetAppointmentsResponse } from '@/domain/schemas/appointments/responses'; -import { BaseResponse } from '@/domain/schemas/base'; +import { BaseResponse } from '@/common/dtos'; import type { AuthUserDto } from '../auth/auth.dtos'; -import { GetAppointmentsQuery } from './appointments.dtos'; import { CreateAppointmentDto, + GetAppointmentsQuery, + GetAppointmentsResponse, UpdateAppointmentDto, } from './appointments.dtos'; import { CancelAppointmentUseCase } from './use-cases/cancel-appointment.use-case'; @@ -39,6 +39,7 @@ export class AppointmentsController { @Get() @Roles(['manager', 'nurse', 'specialist', 'patient']) @ApiOperation({ summary: 'Lista todos os atendimentos' }) + @ApiResponse({ type: GetAppointmentsResponse }) async getAppointments( @Query() query: GetAppointmentsQuery, @AuthUser() user: AuthUserDto, @@ -55,6 +56,7 @@ export class AppointmentsController { @Post() @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Cadastra um novo atendimento' }) + @ApiResponse({ type: BaseResponse }) async create( @AuthUser() user: AuthUserDto, @Body() createAppointmentDto: CreateAppointmentDto, @@ -70,6 +72,7 @@ export class AppointmentsController { @Put(':id') @Roles(['nurse', 'manager', 'specialist']) @ApiOperation({ summary: 'Atualiza os dados do atendimento' }) + @ApiResponse({ type: BaseResponse }) public async update( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -90,6 +93,7 @@ export class AppointmentsController { @Roles(['nurse', 'manager', 'specialist']) @Patch(':id/cancel') @ApiOperation({ summary: 'Cancela o atendimento' }) + @ApiResponse({ type: BaseResponse }) async cancel( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/appointments/appointments.dtos.ts b/src/app/http/appointments/appointments.dtos.ts index 97c0d40..830535f 100644 --- a/src/app/http/appointments/appointments.dtos.ts +++ b/src/app/http/appointments/appointments.dtos.ts @@ -5,10 +5,14 @@ import { getAppointmentsQuerySchema, updateAppointmentSchema, } from '@/domain/schemas/appointments/requests'; +import { getAppointmentsResponseSchema } from '@/domain/schemas/appointments/responses'; export class GetAppointmentsQuery extends createZodDto( getAppointmentsQuerySchema, ) {} +export class GetAppointmentsResponse extends createZodDto( + getAppointmentsResponseSchema, +) {} export class CreateAppointmentDto extends createZodDto( createAppointmentSchema, diff --git a/src/app/http/auth/auth.controller.ts b/src/app/http/auth/auth.controller.ts index b886c67..34a2135 100644 --- a/src/app/http/auth/auth.controller.ts +++ b/src/app/http/auth/auth.controller.ts @@ -1,13 +1,13 @@ import { Body, Controller, Post, Res } from '@nestjs/common'; -import { ApiOperation } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; import type { Response } from 'express'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Cookies } from '@/common/decorators/cookies.decorator'; import { Public } from '@/common/decorators/public.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; +import { BaseResponse } from '@/common/dtos'; import { COOKIES_MAPPING } from '@/domain/cookies'; -import type { BaseResponse } from '@/domain/schemas/base'; import { AuthUserDto, @@ -41,6 +41,7 @@ export class AuthController { @Public() @Post('login') @ApiOperation({ summary: 'Inicia a sessão do usuário ou paciente' }) + @ApiResponse({ type: BaseResponse }) async login( @Body() signInWithEmailDto: SignInWithEmailDto, @Res({ passthrough: true }) response: Response, @@ -56,6 +57,7 @@ export class AuthController { @Public() @Post('register/patient') @ApiOperation({ summary: 'Registra um novo paciente' }) + @ApiResponse({ type: BaseResponse }) async registerPatient( @Body() registerPatientDto: RegisterPatientDto, @Res({ passthrough: true }) response: Response, @@ -71,6 +73,7 @@ export class AuthController { @Public() @Post('register/user') @ApiOperation({ summary: 'Registro um novo usuário via convite' }) + @ApiResponse({ type: BaseResponse }) async registerUser( @Body() registerUserDto: RegisterUserDto, @Res({ passthrough: true }) response: Response, @@ -86,6 +89,7 @@ export class AuthController { @Public() @Post('recover-password') @ApiOperation({ summary: 'Solicita recuperação de senha' }) + @ApiResponse({ type: BaseResponse }) async recoverPassword( @Body() recoverPasswordDto: RecoverPasswordDto, ): Promise { @@ -101,6 +105,7 @@ export class AuthController { @Public() @Post('reset-password') @ApiOperation({ summary: 'Solicita redefinição de senha' }) + @ApiResponse({ type: BaseResponse }) async resetPassword( @Body() resetPasswordDto: ResetPasswordDto, @Res({ passthrough: true }) response: Response, @@ -118,6 +123,7 @@ export class AuthController { @ApiOperation({ summary: 'Altera a senha do usuário ou paciente autenticado', }) + @ApiResponse({ type: BaseResponse }) async changePassword( @AuthUser() user: AuthUserDto, @Body() changePasswordDto: ChangePasswordDto, @@ -133,6 +139,7 @@ export class AuthController { @Roles(['all']) @Post('logout') @ApiOperation({ summary: 'Encerra a sessão do usuário ou paciente' }) + @ApiResponse({ type: BaseResponse }) async logout( @Cookies(COOKIES_MAPPING.refresh_token) refreshToken: string, @Res({ passthrough: true }) response: Response, diff --git a/src/app/http/patient-requirements/patient-requirements.controller.ts b/src/app/http/patient-requirements/patient-requirements.controller.ts index c90897f..ae5aa95 100644 --- a/src/app/http/patient-requirements/patient-requirements.controller.ts +++ b/src/app/http/patient-requirements/patient-requirements.controller.ts @@ -7,21 +7,19 @@ import { Post, Query, } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import { BaseResponse } from '@/domain/schemas/base'; -import type { - GetPatientRequirementsByPatientIdResponse, - GetPatientRequirementsResponse, -} from '@/domain/schemas/patient-requirement/responses'; +import { BaseResponse } from '@/common/dtos'; import type { AuthUserDto } from '../auth/auth.dtos'; import { CreatePatientRequirementDto, GetPatientRequirementsByPatientIdQuery, + GetPatientRequirementsByPatientIdResponse, GetPatientRequirementsQuery, + GetPatientRequirementsResponse, } from './patient-requirements.dtos'; import { ApprovePatientRequirementUseCase } from './use-cases/approve-patient-requirement.use-case'; import { CreatePatientRequirementUseCase } from './use-cases/create-patient-requirement.use-case'; @@ -43,6 +41,7 @@ export class PatientRequirementsController { @Get() @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Lista todas as solicitações' }) + @ApiResponse({ type: GetPatientRequirementsResponse }) async getPatientRequirements( @Query() query: GetPatientRequirementsQuery, ): Promise { @@ -59,6 +58,7 @@ export class PatientRequirementsController { @ApiOperation({ summary: 'Lista todas as solicitações do paciente autenticado', }) + @ApiResponse({ type: GetPatientRequirementsByPatientIdResponse }) async getPatientRequirementsLogged( @AuthUser() user: AuthUserDto, @Query() query: GetPatientRequirementsByPatientIdQuery, @@ -78,6 +78,7 @@ export class PatientRequirementsController { @Post() @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Cadastra uma nova solicitação' }) + @ApiResponse({ type: BaseResponse }) async create( @AuthUser() user: AuthUserDto, @Body() createPatientRequirementDto: CreatePatientRequirementDto, @@ -96,6 +97,7 @@ export class PatientRequirementsController { @Patch(':id/approve') @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Aprova a solicitação' }) + @ApiResponse({ type: BaseResponse }) async approve( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -111,6 +113,7 @@ export class PatientRequirementsController { @Patch(':id/decline') @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Recusa a solicitação' }) + @ApiResponse({ type: BaseResponse }) async decline( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/patient-requirements/patient-requirements.dtos.ts b/src/app/http/patient-requirements/patient-requirements.dtos.ts index 5cfe49f..d721f72 100644 --- a/src/app/http/patient-requirements/patient-requirements.dtos.ts +++ b/src/app/http/patient-requirements/patient-requirements.dtos.ts @@ -5,15 +5,26 @@ import { getPatientRequirementsByPatientIdQuerySchema, getPatientRequirementsQuerySchema, } from '@/domain/schemas/patient-requirement/requests'; - -export class CreatePatientRequirementDto extends createZodDto( - createPatientRequirementSchema, -) {} +import { + getPatientRequirementsByPatientIdResponseSchema, + getPatientRequirementsResponseSchema, +} from '@/domain/schemas/patient-requirement/responses'; export class GetPatientRequirementsQuery extends createZodDto( getPatientRequirementsQuerySchema, ) {} +export class GetPatientRequirementsResponse extends createZodDto( + getPatientRequirementsResponseSchema, +) {} export class GetPatientRequirementsByPatientIdQuery extends createZodDto( getPatientRequirementsByPatientIdQuerySchema, ) {} + +export class GetPatientRequirementsByPatientIdResponse extends createZodDto( + getPatientRequirementsByPatientIdResponseSchema, +) {} + +export class CreatePatientRequirementDto extends createZodDto( + createPatientRequirementSchema, +) {} diff --git a/src/app/http/patient-supports/patient-supports.controller.ts b/src/app/http/patient-supports/patient-supports.controller.ts index 636e282..1cfa131 100644 --- a/src/app/http/patient-supports/patient-supports.controller.ts +++ b/src/app/http/patient-supports/patient-supports.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Delete, Param, Post, Put } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import type { BaseResponse } from '@/domain/schemas/base'; +import { BaseResponse } from '@/common/dtos'; import type { AuthUserDto } from '../auth/auth.dtos'; import { @@ -28,6 +28,7 @@ export class PatientSupportsController { @ApiOperation({ summary: 'Cadastra um novo contato de apoio para o paciente', }) + @ApiResponse({ type: BaseResponse }) async createPatientSupport( @Param('patientId') patientId: string, @AuthUser() user: AuthUserDto, @@ -48,6 +49,7 @@ export class PatientSupportsController { @Put(':id') @Roles(['nurse', 'manager', 'patient']) @ApiOperation({ summary: 'Atualiza os dados do contato de apoio' }) + @ApiResponse({ type: BaseResponse }) async updatePatientSupport( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -68,6 +70,7 @@ export class PatientSupportsController { @Delete(':id') @Roles(['nurse', 'manager', 'patient']) @ApiOperation({ summary: 'Remove o contato de apoio' }) + @ApiResponse({ type: BaseResponse }) async removePatientSupport( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/patient-supports/patient-supports.dtos.ts b/src/app/http/patient-supports/patient-supports.dtos.ts index 4de716f..7d7a2cf 100644 --- a/src/app/http/patient-supports/patient-supports.dtos.ts +++ b/src/app/http/patient-supports/patient-supports.dtos.ts @@ -4,6 +4,18 @@ import { createPatientSupportSchema, updatePatientSupportSchema, } from '@/domain/schemas/patient-support/requests'; +import { + getPatientSupportResponseSchema, + getPatientSupportsResponseSchema, +} from '@/domain/schemas/patient-support/responses'; + +export class GetPatientSupportsResponse extends createZodDto( + getPatientSupportsResponseSchema, +) {} + +export class GetPatientSupportResponse extends createZodDto( + getPatientSupportResponseSchema, +) {} export class CreatePatientSupportDto extends createZodDto( createPatientSupportSchema, diff --git a/src/app/http/patients/patients.controller.ts b/src/app/http/patients/patients.controller.ts index d5316d7..96cd3e0 100644 --- a/src/app/http/patients/patients.controller.ts +++ b/src/app/http/patients/patients.controller.ts @@ -8,20 +8,18 @@ import { Put, Query, } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import { BaseResponse } from '@/domain/schemas/base'; -import type { - GetPatientResponse, - GetPatientsResponse, -} from '@/domain/schemas/patients/responses'; +import { BaseResponse } from '@/common/dtos'; import type { AuthUserDto } from '../auth/auth.dtos'; import { CreatePatientDto, + GetPatientResponse, GetPatientsQuery, + GetPatientsResponse, UpdatePatientDto, } from './patients.dtos'; import { CreatePatientUseCase } from './use-cases/create-patient.use-case'; @@ -44,6 +42,7 @@ export class PatientsController { @Get() @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Lista todos os pacientes' }) + @ApiResponse({ type: GetPatientsResponse }) async getPatients( @Query() query: GetPatientsQuery, ): Promise { @@ -59,6 +58,7 @@ export class PatientsController { @Get(':id') @Roles(['manager', 'nurse', 'specialist']) @ApiOperation({ summary: 'Retorna os dados do paciente' }) + @ApiResponse({ type: GetPatientResponse }) async getPatientById(@Param('id') id: string): Promise { const { patient } = await this.getPatientUseCase.execute({ id }); @@ -72,6 +72,7 @@ export class PatientsController { @Post() @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Cadastra um novo paciente' }) + @ApiResponse({ type: BaseResponse }) async create( @AuthUser() user: AuthUserDto, @Body() createPatientDto: CreatePatientDto, @@ -87,6 +88,7 @@ export class PatientsController { @Put(':id') @Roles(['manager', 'nurse', 'patient']) @ApiOperation({ summary: 'Atualiza os dados do paciente' }) + @ApiResponse({ type: BaseResponse }) async update( @Param('id') id: string, @AuthUser() user: AuthUserDto, @@ -103,6 +105,7 @@ export class PatientsController { @Patch(':id/deactivate') @Roles(['manager']) @ApiOperation({ summary: 'Inativa o paciente' }) + @ApiResponse({ type: BaseResponse }) async deactivatePatient( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/patients/patients.dtos.ts b/src/app/http/patients/patients.dtos.ts index c8afa6c..22f234d 100644 --- a/src/app/http/patients/patients.dtos.ts +++ b/src/app/http/patients/patients.dtos.ts @@ -5,8 +5,23 @@ import { getPatientsQuerySchema, updatePatientSchema, } from '@/domain/schemas/patients/requests'; +import { + getAllPatientsListResponseSchema, + getPatientResponseSchema, + getPatientsResponseSchema, +} from '@/domain/schemas/patients/responses'; export class GetPatientsQuery extends createZodDto(getPatientsQuerySchema) {} +export class GetPatientsResponse extends createZodDto( + getPatientsResponseSchema, +) {} +export class GetAllPatientsListResponse extends createZodDto( + getAllPatientsListResponseSchema, +) {} + +export class GetPatientResponse extends createZodDto( + getPatientResponseSchema, +) {} export class CreatePatientDto extends createZodDto(createPatientSchema) {} diff --git a/src/app/http/referrals/referrals.controller.ts b/src/app/http/referrals/referrals.controller.ts index 90868b9..519d68a 100644 --- a/src/app/http/referrals/referrals.controller.ts +++ b/src/app/http/referrals/referrals.controller.ts @@ -7,15 +7,18 @@ import { Post, Query, } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import { BaseResponse } from '@/domain/schemas/base'; -import type { GetReferralsResponse } from '@/domain/schemas/referrals/responses'; +import { BaseResponse } from '@/common/dtos'; import type { AuthUserDto } from '../auth/auth.dtos'; -import { CreateReferralDto, GetReferralsQuery } from './referrals.dtos'; +import { + CreateReferralDto, + GetReferralsQuery, + GetReferralsResponse, +} from './referrals.dtos'; import { CancelReferralUseCase } from './use-cases/cancel-referral.use-case'; import { CreateReferralUseCase } from './use-cases/create-referrals.use-case'; import { GetReferralsUseCase } from './use-cases/get-referrals.use-case'; @@ -32,6 +35,7 @@ export class ReferralsController { @Get() @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Lista todos os encaminhamentos' }) + @ApiResponse({ type: GetReferralsResponse }) async getReferrals( @Query() query: GetReferralsQuery, ): Promise { @@ -47,6 +51,7 @@ export class ReferralsController { @Post() @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Cadastra um novo encaminhamento' }) + @ApiResponse({ type: BaseResponse }) async create( @AuthUser() user: AuthUserDto, @Body() createReferralDto: CreateReferralDto, @@ -62,6 +67,7 @@ export class ReferralsController { @Patch(':id/cancel') @Roles(['nurse', 'manager']) @ApiOperation({ summary: 'Cancela o encaminhamento' }) + @ApiResponse({ type: BaseResponse }) async cancel( @Param('id') id: string, @AuthUser() user: AuthUserDto, diff --git a/src/app/http/referrals/referrals.dtos.ts b/src/app/http/referrals/referrals.dtos.ts index 9d06ab3..e305905 100644 --- a/src/app/http/referrals/referrals.dtos.ts +++ b/src/app/http/referrals/referrals.dtos.ts @@ -4,7 +4,11 @@ import { createReferralSchema, getReferralsQuerySchema, } from '@/domain/schemas/referrals/requests'; - -export class CreateReferralDto extends createZodDto(createReferralSchema) {} +import { getReferralsResponseSchema } from '@/domain/schemas/referrals/responses'; export class GetReferralsQuery extends createZodDto(getReferralsQuerySchema) {} +export class GetReferralsResponse extends createZodDto( + getReferralsResponseSchema, +) {} + +export class CreateReferralDto extends createZodDto(createReferralSchema) {} diff --git a/src/app/http/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index 6b4a491..1a6f682 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -46,7 +46,7 @@ export class StatisticsController { @Get('appointments-total') @ApiOperation({ summary: 'Retorna o número total de atendimentos' }) - @ApiResponse({ status: 200, type: GetTotalAppointmentsResponse }) + @ApiResponse({ type: GetTotalAppointmentsResponse }) async getTotalAppointments( @Query() query: GetTotalReferralsQuery, ): Promise { @@ -63,7 +63,7 @@ export class StatisticsController { @Get('patients-total') @ApiOperation({ summary: 'Retorna o número total de pacientes' }) - @ApiResponse({ status: 200, type: GetTotalPatientsByStatusResponse }) + @ApiResponse({ type: GetTotalPatientsByStatusResponse }) async getTotalPatients(): Promise { const data = await this.getTotalPatientsByStatusUseCase.execute(); @@ -76,7 +76,7 @@ export class StatisticsController { @Get('patients-by-gender') @ApiOperation({ summary: 'Retorna o número total de pacientes por gênero' }) - @ApiResponse({ status: 200, type: GetTotalPatientsByGenderResponse }) + @ApiResponse({ type: GetTotalPatientsByGenderResponse }) async getPatientsByGender( @Query() query: GetTotalPatientsByFieldQuery, ): Promise { @@ -97,7 +97,7 @@ export class StatisticsController { @Get('patients-by-city') @ApiOperation({ summary: 'Retorna o número total de pacientes por cidade' }) - @ApiResponse({ status: 200, type: GetTotalPatientsByCityResponse }) + @ApiResponse({ type: GetTotalPatientsByCityResponse }) async getPatientsByCity( @Query() query: GetTotalPatientsByFieldQuery, ): Promise { @@ -122,7 +122,7 @@ export class StatisticsController { @Get('referrals-total') @ApiOperation({ summary: 'Retorna o número total de encaminhamentos' }) - @ApiResponse({ status: 200, type: GetTotalReferralsResponse }) + @ApiResponse({ type: GetTotalReferralsResponse }) async getTotalReferrals( @Query() query: GetTotalReferralsQuery, ): Promise { @@ -141,7 +141,7 @@ export class StatisticsController { @ApiOperation({ summary: 'Retorna o número total de encaminhamentos por categoria', }) - @ApiResponse({ status: 200, type: GetTotalReferralsByCategoryResponse }) + @ApiResponse({ type: GetTotalReferralsByCategoryResponse }) async getTotalReferralsByCategory( @Query() query: GetTotalReferralsByCategoryQuery, ): Promise { @@ -160,9 +160,9 @@ export class StatisticsController { @Get('referrals-by-state') @ApiOperation({ - summary: 'Retorna o número total de encaminhamentos por estado', + summary: 'Retorna o número total de pacientes encaminhados por estado', }) - @ApiResponse({ status: 200, type: GetTotalReferredPatientsByStateResponse }) + @ApiResponse({ type: GetTotalReferredPatientsByStateResponse }) async getReferredPatientsByState( @Query() query: GetTotalReferredPatientsByStateQuery, ): Promise { @@ -184,7 +184,7 @@ export class StatisticsController { @Get('referred-patients-total') @ApiOperation({ summary: 'Retorna o número total de pacientes encaminhados' }) - @ApiResponse({ status: 200, type: GetTotalReferredPatientsResponse }) + @ApiResponse({ type: GetTotalReferredPatientsResponse }) async getTotalReferredPatients( @Query() query: GetTotalReferredPatientsQuery, ): Promise { @@ -196,8 +196,7 @@ export class StatisticsController { return { success: true, - message: - 'Número total de pacientes encaminhamados retornado com sucesso.', + message: 'Número total de pacientes encaminhados retornado com sucesso.', data: { total }, }; } diff --git a/src/app/http/users/use-cases/get-users.use-case.ts b/src/app/http/users/use-cases/get-users.use-case.ts index 865eb20..f5016ff 100644 --- a/src/app/http/users/use-cases/get-users.use-case.ts +++ b/src/app/http/users/use-cases/get-users.use-case.ts @@ -11,9 +11,10 @@ import { import { User } from '@/domain/entities/user'; import type { UsersOrderBy } from '@/domain/enums/users'; -import type { GetUsersQuery } from '@/domain/schemas/users/requests'; import type { UserResponse } from '@/domain/schemas/users/responses'; +import type { GetUsersQuery } from '../users.dtos'; + interface GetUsersUseCaseInput { query: GetUsersQuery; } diff --git a/src/app/http/users/users.controller.ts b/src/app/http/users/users.controller.ts index 1c7c2dc..62bf55e 100644 --- a/src/app/http/users/users.controller.ts +++ b/src/app/http/users/users.controller.ts @@ -3,7 +3,7 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthUser } from '@/common/decorators/auth-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; -import type { BaseResponse } from '@/domain/schemas/base'; +import { BaseResponse } from '@/common/dtos'; import type { AuthUserDto } from '../auth/auth.dtos'; import { CreateUserInviteUseCase } from './use-cases/create-user-invite.use-case'; @@ -28,6 +28,7 @@ export class UsersController { @Post('invite') @Roles(['manager']) @ApiOperation({ summary: 'Cria convite para registro de usuário' }) + @ApiResponse({ type: BaseResponse }) async createUserInvite( @AuthUser() user: AuthUserDto, @Body() createUserInviteDto: CreateUserInviteDto, @@ -43,7 +44,7 @@ export class UsersController { @Get() @Roles(['manager']) @ApiOperation({ summary: 'Lista todos os usuários' }) - @ApiResponse({ status: 200, type: GetUsersResponse }) + @ApiResponse({ type: GetUsersResponse }) async getUsers(@Query() query: GetUsersQuery): Promise { const data = await this.getUsersUseCase.execute({ query }); @@ -57,7 +58,7 @@ export class UsersController { @Get('me') @Roles(['manager', 'nurse', 'specialist']) @ApiOperation({ summary: 'Retorna os dados do usuário autenticado' }) - @ApiResponse({ status: 200, type: GetUserResponse }) + @ApiResponse({ type: GetUserResponse }) async getProfile(@AuthUser() user: AuthUserDto): Promise { const { user: data } = await this.getUserUseCase.execute({ id: user.id }); diff --git a/src/app/http/users/users.dtos.ts b/src/app/http/users/users.dtos.ts index 0e54ca4..46ab6e2 100644 --- a/src/app/http/users/users.dtos.ts +++ b/src/app/http/users/users.dtos.ts @@ -10,11 +10,11 @@ import { getUsersResponseSchema, } from '@/domain/schemas/users/responses'; -export class CreateUserInviteDto extends createZodDto(createUserInviteSchema) {} - -export class UpdateUserDto extends createZodDto(updateUserSchema) {} +export class GetUsersQuery extends createZodDto(getUsersQuerySchema) {} +export class GetUsersResponse extends createZodDto(getUsersResponseSchema) {} export class GetUserResponse extends createZodDto(getUserResponseSchema) {} -export class GetUsersQuery extends createZodDto(getUsersQuerySchema) {} -export class GetUsersResponse extends createZodDto(getUsersResponseSchema) {} +export class CreateUserInviteDto extends createZodDto(createUserInviteSchema) {} + +export class UpdateUserDto extends createZodDto(updateUserSchema) {} diff --git a/src/common/dtos.ts b/src/common/dtos.ts new file mode 100644 index 0000000..ec9877f --- /dev/null +++ b/src/common/dtos.ts @@ -0,0 +1,5 @@ +import { createZodDto } from 'nestjs-zod'; + +import { baseResponseSchema } from '@/domain/schemas/base'; + +export class BaseResponse extends createZodDto(baseResponseSchema) {} diff --git a/src/config/database.config.ts b/src/config/database.config.ts index 8d19a83..92368b6 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -13,5 +13,4 @@ export const databaseConfig = (): DatabaseConfig => ({ username: process.env.DB_USERNAME || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_DATABASE || 'test', - schema: process.env.DB_SCHEMA, }); diff --git a/src/domain/schemas/appointments/responses.ts b/src/domain/schemas/appointments/responses.ts index 80f5927..b18824e 100644 --- a/src/domain/schemas/appointments/responses.ts +++ b/src/domain/schemas/appointments/responses.ts @@ -15,6 +15,3 @@ export const getAppointmentsResponseSchema = baseResponseSchema.extend({ total: z.number(), }), }); -export type GetAppointmentsResponse = z.infer< - typeof getAppointmentsResponseSchema ->; diff --git a/src/domain/schemas/base.ts b/src/domain/schemas/base.ts index d759bba..7d74ed4 100644 --- a/src/domain/schemas/base.ts +++ b/src/domain/schemas/base.ts @@ -1,7 +1,6 @@ import { z } from 'zod'; export const baseResponseSchema = z.object({ - success: z.boolean().describe('Confirma se a operação foi bem-sucedida.'), - message: z.string().describe('Mensagem de resposta pertinente à requisição.'), + success: z.boolean(), + message: z.string(), }); -export type BaseResponse = z.infer; diff --git a/src/domain/schemas/patient-requirement/requests.ts b/src/domain/schemas/patient-requirement/requests.ts index f996bab..405a6a2 100644 --- a/src/domain/schemas/patient-requirement/requests.ts +++ b/src/domain/schemas/patient-requirement/requests.ts @@ -14,9 +14,6 @@ export const createPatientRequirementSchema = patientRequirementSchema.pick({ title: true, description: true, }); -export type CreatePatientRequirement = z.infer< - typeof createPatientRequirementSchema ->; export const getPatientRequirementsQuerySchema = baseQuerySchema .pick({ @@ -43,9 +40,6 @@ export const getPatientRequirementsQuerySchema = baseQuerySchema path: ['endDate'], }, ); -export type GetPatientRequirementsQuery = z.infer< - typeof getPatientRequirementsQuerySchema ->; export const getPatientRequirementsByPatientIdQuerySchema = baseQuerySchema .pick({ @@ -68,6 +62,3 @@ export const getPatientRequirementsByPatientIdQuerySchema = baseQuerySchema path: ['endDate'], }, ); -export type GetPatientRequirementsByPatientIdQuery = z.infer< - typeof getPatientRequirementsByPatientIdQuerySchema ->; diff --git a/src/domain/schemas/patient-requirement/responses.ts b/src/domain/schemas/patient-requirement/responses.ts index 9b3191c..bd219d0 100644 --- a/src/domain/schemas/patient-requirement/responses.ts +++ b/src/domain/schemas/patient-requirement/responses.ts @@ -27,9 +27,6 @@ export const getPatientRequirementsResponseSchema = baseResponseSchema.extend({ total: z.number(), }), }); -export type GetPatientRequirementsResponse = z.infer< - typeof getPatientRequirementsResponseSchema ->; export const patientRequirementByPatientIdSchema = patientRequirementSchema.pick({ @@ -54,6 +51,3 @@ export const getPatientRequirementsByPatientIdResponseSchema = total: z.number(), }), }); -export type GetPatientRequirementsByPatientIdResponse = z.infer< - typeof getPatientRequirementsByPatientIdResponseSchema ->; diff --git a/src/domain/schemas/patient-support/requests.ts b/src/domain/schemas/patient-support/requests.ts index 93b0855..292c56e 100644 --- a/src/domain/schemas/patient-support/requests.ts +++ b/src/domain/schemas/patient-support/requests.ts @@ -1,5 +1,3 @@ -import { z } from 'zod'; - import { patientSupportSchema } from '.'; export const createPatientSupportSchema = patientSupportSchema.pick({ @@ -8,11 +6,9 @@ export const createPatientSupportSchema = patientSupportSchema.pick({ phone: true, kinship: true, }); -export type CreatePatientSupport = z.infer; export const updatePatientSupportSchema = patientSupportSchema.pick({ name: true, phone: true, kinship: true, }); -export type UpdatePatientSupport = z.infer; diff --git a/src/domain/schemas/patient-support/responses.ts b/src/domain/schemas/patient-support/responses.ts index 30cfa5d..9320928 100644 --- a/src/domain/schemas/patient-support/responses.ts +++ b/src/domain/schemas/patient-support/responses.ts @@ -9,13 +9,7 @@ export const getPatientSupportsResponseSchema = baseResponseSchema.extend({ total: z.number(), }), }); -export type GetPatientSupportsResponse = z.infer< - typeof getPatientSupportsResponseSchema ->; export const getPatientSupportResponseSchema = baseResponseSchema.extend({ data: patientSupportSchema, }); -export type GetPatientSupportResponse = z.infer< - typeof getPatientSupportResponseSchema ->; diff --git a/src/domain/schemas/patients/requests.ts b/src/domain/schemas/patients/requests.ts index 0899496..d8a0555 100644 --- a/src/domain/schemas/patients/requests.ts +++ b/src/domain/schemas/patients/requests.ts @@ -44,12 +44,10 @@ export const createPatientSchema = z medication_desc: true, }), ); -export type CreatePatient = z.infer; export const updatePatientSchema = createPatientSchema .omit({ supports: true }) .merge(patientSchema.pick({ status: true })); -export type UpdatePatient = z.infer; export const getPatientsQuerySchema = baseQuerySchema .pick({ @@ -76,4 +74,3 @@ export const getPatientsQuerySchema = baseQuerySchema path: ['endDate'], }, ); -export type GetPatientsQuery = z.infer; diff --git a/src/domain/schemas/patients/responses.ts b/src/domain/schemas/patients/responses.ts index 3c1f25a..1872112 100644 --- a/src/domain/schemas/patients/responses.ts +++ b/src/domain/schemas/patients/responses.ts @@ -21,20 +21,15 @@ export const getPatientsResponseSchema = baseResponseSchema.extend({ total: z.number(), }), }); -export type GetPatientsResponse = z.infer; export const getPatientResponseSchema = baseResponseSchema.extend({ data: patientSchema .omit({ password: true }) .extend({ supports: z.array(patientSupportSchema) }), }); -export type GetPatientResponse = z.infer; export const getAllPatientsListResponseSchema = baseResponseSchema.extend({ data: z.object({ patients: z.array(patientSchema.pick({ id: true, name: true, cpf: true })), }), }); -export type GetAllPatientsListResponse = z.infer< - typeof getAllPatientsListResponseSchema ->; diff --git a/src/domain/schemas/referrals/requests.ts b/src/domain/schemas/referrals/requests.ts index e425ff6..2d3d991 100644 --- a/src/domain/schemas/referrals/requests.ts +++ b/src/domain/schemas/referrals/requests.ts @@ -16,7 +16,6 @@ export const createReferralSchema = referralSchema.pick({ annotation: true, professional_name: true, }); -export type CreateReferral = z.infer; export const getReferralsQuerySchema = baseQuerySchema .pick({ diff --git a/src/domain/schemas/referrals/responses.ts b/src/domain/schemas/referrals/responses.ts index 656342d..34c98ba 100644 --- a/src/domain/schemas/referrals/responses.ts +++ b/src/domain/schemas/referrals/responses.ts @@ -15,4 +15,3 @@ export const getReferralsResponseSchema = baseResponseSchema.extend({ total: z.number(), }), }); -export type GetReferralsResponse = z.infer; diff --git a/src/domain/schemas/users/requests.ts b/src/domain/schemas/users/requests.ts index f6f1b03..7bb1f03 100644 --- a/src/domain/schemas/users/requests.ts +++ b/src/domain/schemas/users/requests.ts @@ -14,7 +14,6 @@ export const createUserInviteSchema = userSchema.pick({ email: true, role: true, }); -export type CreateUserInviteSchema = z.infer; export const createUserSchema = userSchema.pick({ name: true, @@ -22,7 +21,6 @@ export const createUserSchema = userSchema.pick({ password: true, avatar_url: true, }); -export type CreateUser = z.infer; export const updateUserSchema = userSchema.omit({ id: true, @@ -30,7 +28,6 @@ export const updateUserSchema = userSchema.omit({ created_at: true, updated_at: true, }); -export type UpdateUser = z.infer; export const getUsersQuerySchema = baseQuerySchema .pick({ @@ -58,4 +55,3 @@ export const getUsersQuerySchema = baseQuerySchema path: ['endDate'], }, ); -export type GetUsersQuery = z.infer; diff --git a/src/env/env.ts b/src/env/env.ts index 3ffc67a..6505356 100644 --- a/src/env/env.ts +++ b/src/env/env.ts @@ -2,9 +2,7 @@ import { z } from 'zod'; export const envSchema = z.object({ // Environment - NODE_ENV: z - .enum(['production', 'development', 'homolog', 'test']) - .default('development'), + NODE_ENV: z.enum(['production', 'development', 'homolog', 'test']), APP_ENVIRONMENT: z.enum(['production', 'development', 'homolog', 'local']), // API @@ -13,10 +11,9 @@ export const envSchema = z.object({ // APP APP_URL: z.string().url(), - APP_LOCAL_URL: z.string().url().optional().default(''), // Secrets - COOKIE_DOMAIN: z.string().optional(), + COOKIE_DOMAIN: z.string().min(1), COOKIE_SECRET: z.string().min(1), JWT_SECRET: z.string().min(1), From 4d1a9e59f41b125c9017318225a00d0307cf164b Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Mon, 19 Jan 2026 22:58:28 -0300 Subject: [PATCH 20/21] chore(logs): update logs message to match the pattern --- .../use-cases/cancel-appointment.use-case.ts | 4 ++-- .../use-cases/create-appointment.use-case.ts | 4 ++-- .../use-cases/update-appointment.use-case.ts | 2 +- .../approve-patient-requirement.use-case.ts | 2 +- .../create-patient-requirement.use-case.ts | 8 +++---- .../decline-patient-requirement.use-case.ts | 2 +- .../create-patient-support.use-case.ts | 4 ++-- .../delete-patient-support.use-case.ts | 4 ++-- .../update-patient-support.use-case.ts | 4 ++-- .../use-cases/create-patient.use-case.ts | 6 +++--- .../use-cases/deactivate-patient.use-case.ts | 4 ++-- .../use-cases/update-patient.use-case.ts | 15 +++++-------- .../http/referrals/referrals.controller.ts | 4 ++-- .../use-cases/cancel-referral.use-case.ts | 13 ++++++++---- .../use-cases/create-referrals.use-case.ts | 21 ++++++++++++------- .../use-cases/create-user-invite.use-case.ts | 9 +++++++- .../users/use-cases/update-user.use-case.ts | 10 +++++++-- 17 files changed, 68 insertions(+), 48 deletions(-) diff --git a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts index a5d3b51..bb93032 100644 --- a/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/cancel-appointment.use-case.ts @@ -39,10 +39,10 @@ export class CancelAppointmentUseCase { throw new BadRequestException('Este atendimento já está cancelado.'); } - await this.appointmentsRepository.save({ id, status: 'canceled' }); + await this.appointmentsRepository.update({ id }, { status: 'canceled' }); this.logger.log( - { id, userId: user.id, userEmail: user.email, role: user.role }, + { id, userId: user.id, userEmail: user.email, userRole: user.role }, 'Appointment canceled successfully.', ); } diff --git a/src/app/http/appointments/use-cases/create-appointment.use-case.ts b/src/app/http/appointments/use-cases/create-appointment.use-case.ts index ef8a2c4..89e58d9 100644 --- a/src/app/http/appointments/use-cases/create-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/create-appointment.use-case.ts @@ -63,10 +63,10 @@ export class CreateAppointmentUseCase { this.logger.log( { id: appointment.id, - patientId: patientId, + patientId, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Appointment created successfully', ); diff --git a/src/app/http/appointments/use-cases/update-appointment.use-case.ts b/src/app/http/appointments/use-cases/update-appointment.use-case.ts index 12a7dd5..85af918 100644 --- a/src/app/http/appointments/use-cases/update-appointment.use-case.ts +++ b/src/app/http/appointments/use-cases/update-appointment.use-case.ts @@ -51,7 +51,7 @@ export class UpdateAppointmentUseCase { await this.appointmentsRepository.save(appointment); this.logger.log( - { id, userId: user.id, userEmail: user.email, role: user.role }, + { id, userId: user.id, userEmail: user.email, userRole: user.role }, 'Appointment updated successfully.', ); } diff --git a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts index f11b07d..e85130a 100644 --- a/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/approve-patient-requirement.use-case.ts @@ -52,7 +52,7 @@ export class ApprovePatientRequirementUseCase { }); this.logger.log( - { id, userId: user.id, userEmail: user.email, role: user.role }, + { id, userId: user.id, userEmail: user.email, userRole: user.role }, 'Patient requirement approved successfully', ); } diff --git a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts index 4c1e85e..f4e76b7 100644 --- a/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/create-patient-requirement.use-case.ts @@ -28,10 +28,10 @@ export class CreatePatientRequirementUseCase { createPatientRequirementDto, user, }: CreatePatientRequirementUseCaseInput): Promise { - const { patient_id } = createPatientRequirementDto; + const { patient_id: patientId } = createPatientRequirementDto; const patient = await this.patientsRepository.findOne({ - where: { id: patient_id }, + where: { id: patientId }, select: { id: true }, }); @@ -49,10 +49,10 @@ export class CreatePatientRequirementUseCase { this.logger.log( { id: patientRequirement.id, - patientId: patient_id, + patientId, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Requirement created successfully', ); diff --git a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts index 8a23cc9..02cc621 100644 --- a/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts +++ b/src/app/http/patient-requirements/use-cases/decline-patient-requirement.use-case.ts @@ -52,7 +52,7 @@ export class DeclinePatientRequirementUseCase { }); this.logger.log( - { id, userId: user.id, userEmail: user.email, role: user.role }, + { id, userId: user.id, userEmail: user.email, userRole: user.role }, 'Patient requirement declined successfully', ); } diff --git a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts index edf60f3..828d1cb 100644 --- a/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/create-patient-support.use-case.ts @@ -37,7 +37,7 @@ export class CreatePatientSupportUseCase { }: CreatePatientSupportUseCaseInput): Promise { if (user.id !== patientId) { this.logger.log( - { patientId, userId: user.id, role: user.role }, + { patientId, userId: user.id, userRole: user.role }, 'Create patient support failed: User does not have permission to create patient support for this patient', ); throw new ForbiddenException( @@ -67,7 +67,7 @@ export class CreatePatientSupportUseCase { patientId, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Patient support created successfully', ); diff --git a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts index 5d22072..fc91d5a 100644 --- a/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/delete-patient-support.use-case.ts @@ -44,7 +44,7 @@ export class DeletePatientSupportUseCase { patientId, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Remove patient support failed: User does not have permission to remove this patient support', ); @@ -61,7 +61,7 @@ export class DeletePatientSupportUseCase { patientId, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Patient support removed successfully', ); diff --git a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts index 39c5dd8..3322b38 100644 --- a/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts +++ b/src/app/http/patient-supports/use-cases/update-patient-support.use-case.ts @@ -50,7 +50,7 @@ export class UpdatePatientSupportUseCase { patientId, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Update patient support failed: User does not have permission to update this patient support', ); @@ -70,7 +70,7 @@ export class UpdatePatientSupportUseCase { patientId, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Patient support updated successfully', ); diff --git a/src/app/http/patients/use-cases/create-patient.use-case.ts b/src/app/http/patients/use-cases/create-patient.use-case.ts index 94d15df..6e18759 100644 --- a/src/app/http/patients/use-cases/create-patient.use-case.ts +++ b/src/app/http/patients/use-cases/create-patient.use-case.ts @@ -38,7 +38,7 @@ export class CreatePatientUseCase { if (patientWithSameEmail) { this.logger.error( - { email, userId: user.id, userEmail: user.email, role: user.role }, + { email, userId: user.id, userEmail: user.email, userRole: user.role }, 'Create patient failed: Email already registered', ); throw new ConflictException('O e-mail informado já está registrado.'); @@ -51,7 +51,7 @@ export class CreatePatientUseCase { if (patientWithSameCpf) { this.logger.error( - { cpf, userId: user.id, userEmail: user.email, role: user.role }, + { cpf, userId: user.id, userEmail: user.email, userRole: user.role }, 'Create patient failed: CPF already registered', ); throw new ConflictException('O CPF informado já está registrado.'); @@ -89,7 +89,7 @@ export class CreatePatientUseCase { email, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Patient created successfully', ); diff --git a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts index 3c75595..41f1621 100644 --- a/src/app/http/patients/use-cases/deactivate-patient.use-case.ts +++ b/src/app/http/patients/use-cases/deactivate-patient.use-case.ts @@ -39,14 +39,14 @@ export class DeactivatePatientUseCase { throw new ConflictException('Este paciente já está inativo.'); } - await this.patientsRepository.save({ id, status: 'inactive' }); + await this.patientsRepository.update({ id }, { status: 'inactive' }); this.logger.log( { patientId: id, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Patient deactivated successfully', ); diff --git a/src/app/http/patients/use-cases/update-patient.use-case.ts b/src/app/http/patients/use-cases/update-patient.use-case.ts index 0b1a924..bc5bb0a 100644 --- a/src/app/http/patients/use-cases/update-patient.use-case.ts +++ b/src/app/http/patients/use-cases/update-patient.use-case.ts @@ -35,7 +35,7 @@ export class UpdatePatientUseCase { }: UpdatePatientUseCaseInput): Promise { if (user.role === 'patient' && user.id !== id) { this.logger.log( - { id, userId: user.id, userEmail: user.email, role: user.role }, + { id, userId: user.id, userEmail: user.email, userRole: user.role }, 'Update patient failed: User does not have permission to update this patient', ); throw new ForbiddenException( @@ -65,7 +65,7 @@ export class UpdatePatientUseCase { cpf: updatePatientDto.cpf, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Update patient failed: CPF already registered', ); @@ -82,11 +82,11 @@ export class UpdatePatientUseCase { if (patientWithSameEmail && patientWithSameEmail.id !== id) { this.logger.error( { - patientId: id, + id, email: updatePatientDto.email, userId: user.id, userEmail: user.email, - role: user.role, + userRole: user.role, }, 'Update patient failed: Email already registered', ); @@ -97,12 +97,7 @@ export class UpdatePatientUseCase { await this.patientsRepository.save({ id, ...updatePatientDto }); this.logger.log( - { - patientId: id, - userId: user.id, - userEmail: user.email, - role: user.role, - }, + { id, userId: user.id, userEmail: user.email, userRole: user.role }, 'Patient updated successfully', ); } diff --git a/src/app/http/referrals/referrals.controller.ts b/src/app/http/referrals/referrals.controller.ts index 519d68a..696be16 100644 --- a/src/app/http/referrals/referrals.controller.ts +++ b/src/app/http/referrals/referrals.controller.ts @@ -57,8 +57,8 @@ export class ReferralsController { @Body() createReferralDto: CreateReferralDto, ): Promise { await this.createReferralUseCase.execute({ - userId: user.id, createReferralDto, + user, }); return { success: true, message: 'Encaminhamento cadastrado com sucesso.' }; @@ -72,7 +72,7 @@ export class ReferralsController { @Param('id') id: string, @AuthUser() user: AuthUserDto, ): Promise { - await this.cancelReferralUseCase.execute({ id, userId: user.id }); + await this.cancelReferralUseCase.execute({ id, user }); return { success: true, diff --git a/src/app/http/referrals/use-cases/cancel-referral.use-case.ts b/src/app/http/referrals/use-cases/cancel-referral.use-case.ts index 853d7ac..9b2625a 100644 --- a/src/app/http/referrals/use-cases/cancel-referral.use-case.ts +++ b/src/app/http/referrals/use-cases/cancel-referral.use-case.ts @@ -9,9 +9,11 @@ import type { Repository } from 'typeorm'; import { Referral } from '@/domain/entities/referral'; +import type { AuthUserDto } from '../../auth/auth.dtos'; + interface CancelReferralUseCaseInput { id: string; - userId: string; + user: AuthUserDto; } @Injectable() @@ -23,7 +25,7 @@ export class CancelReferralUseCase { private readonly referralsRepository: Repository, ) {} - async execute({ id, userId }: CancelReferralUseCaseInput): Promise { + async execute({ id, user }: CancelReferralUseCaseInput): Promise { const referral = await this.referralsRepository.findOne({ select: { id: true, status: true }, where: { id }, @@ -37,8 +39,11 @@ export class CancelReferralUseCase { throw new BadRequestException('Este encaminhamento já está cancelado.'); } - await this.referralsRepository.save({ id, status: 'canceled' }); + await this.referralsRepository.update({ id }, { status: 'canceled' }); - this.logger.log({ id, userId }, 'Referral canceled successfully.'); + this.logger.log( + { id, userId: user.id, userEmail: user.email, userRole: user.role }, + 'Referral canceled successfully.', + ); } } diff --git a/src/app/http/referrals/use-cases/create-referrals.use-case.ts b/src/app/http/referrals/use-cases/create-referrals.use-case.ts index cc71abe..71edd11 100644 --- a/src/app/http/referrals/use-cases/create-referrals.use-case.ts +++ b/src/app/http/referrals/use-cases/create-referrals.use-case.ts @@ -5,10 +5,11 @@ import { type Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; import { Referral } from '@/domain/entities/referral'; +import type { AuthUserDto } from '../../auth/auth.dtos'; import { CreateReferralDto } from '../referrals.dtos'; interface CreateReferralUseCaseInput { - userId: string; + user: AuthUserDto; createReferralDto: CreateReferralDto; } @@ -23,13 +24,13 @@ export class CreateReferralUseCase { private readonly referralsRepository: Repository, ) {} async execute({ + user, createReferralDto, - userId, }: CreateReferralUseCaseInput): Promise { - const { patient_id } = createReferralDto; + const { patient_id: patientId } = createReferralDto; const patient = await this.patientsRepository.findOne({ - where: { id: patient_id }, + where: { id: patientId }, select: { id: true }, }); @@ -37,14 +38,20 @@ export class CreateReferralUseCase { throw new NotFoundException('Paciente não encontrado.'); } - await this.referralsRepository.save({ + const referral = await this.referralsRepository.save({ ...createReferralDto, status: 'scheduled', - referred_by: userId, + created_by: user.id, }); this.logger.log( - { patientId: patient_id, referredBy: userId }, + { + id: referral.id, + patientId, + userId: user.id, + userEmail: user.email, + userRole: user.role, + }, 'Referral created successfully', ); } diff --git a/src/app/http/users/use-cases/create-user-invite.use-case.ts b/src/app/http/users/use-cases/create-user-invite.use-case.ts index f165d05..c803e5f 100644 --- a/src/app/http/users/use-cases/create-user-invite.use-case.ts +++ b/src/app/http/users/use-cases/create-user-invite.use-case.ts @@ -71,7 +71,14 @@ export class CreateUserInviteUseCase { // TODO: send email with register user URL including invite token this.logger.log( - { id: newInviteUserToken.id, email, role, createdBy: user.id }, + { + id: newInviteUserToken.id, + email, + role, + userId: user.id, + userEmail: user.email, + userRole: user.role, + }, 'Invite user token created successfully', ); } diff --git a/src/app/http/users/use-cases/update-user.use-case.ts b/src/app/http/users/use-cases/update-user.use-case.ts index 885ba89..1ec1637 100644 --- a/src/app/http/users/use-cases/update-user.use-case.ts +++ b/src/app/http/users/use-cases/update-user.use-case.ts @@ -34,7 +34,7 @@ export class UpdateUserUseCase { }: UpdateUserUseCaseInput): Promise { if (user.role !== 'admin' && user.id !== id) { this.logger.log( - { id, userId: user.id, role: user.role }, + { id, userId: user.id, userEmail: user.email, userRole: user.role }, 'Update user failed: User does not have permission to update this user', ); throw new ForbiddenException( @@ -53,7 +53,13 @@ export class UpdateUserUseCase { await this.usersRepository.save(userToUpdate); this.logger.log( - { id, email: updateUserDto.email }, + { + id, + email: updateUserDto.email, + userId: user.id, + userEmail: user.email, + userRole: user.role, + }, 'User updated successfully', ); } From 1bbbab4a5577ce146e2a52cc989d3d743ccf32de Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Mon, 19 Jan 2026 23:09:35 -0300 Subject: [PATCH 21/21] chore(docs): fix prettier errors --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 544f6e5..ed336c8 100644 --- a/README.md +++ b/README.md @@ -37,26 +37,31 @@ npm install ### Executando pela primeira vez 1. Copie o arquivo `.env.example` e renomeie para `.env` ou execute o comando: + ```bash cp .env.example .env ``` 2. Com o Docker em execução, inicie a instância do banco de dados: + ```bash npm run services:up ``` 3. Execute as migrações do banco de dados: + ```bash npm run db:migrate ``` 4. Popule o banco de dados com dados de exemplo: + ```bash npm run db:seed-dev ``` 5. Inicie a aplicação em modo de desenvolvimento: + ```bash npm run dev ``` @@ -64,6 +69,7 @@ npm run dev ### Executando a aplicação Para iniciar a aplicação novamente, execute o comando abaixo com o Docker em funcionamento: + ```bash npm run dev ```