diff --git a/src/app.module.ts b/src/app.module.ts index adcc223..a61526c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -19,7 +19,7 @@ import { PassportModule } from '@nestjs/passport'; import { UserRepository } from './modules/user/repository/user.repository'; import { UsersEntity } from './database/entities/users.entity'; import { AlertsModule } from './modules/alert/alerts.module'; - +import { CandidacyModule } from './modules/candidacy/candidacy.module'; @Module({ imports: [ @@ -44,11 +44,9 @@ import { AlertsModule } from './modules/alert/alerts.module'; ApplicationsModule, TypeOrmModule.forFeature([UsersEntity]), AlertsModule, + CandidacyModule, ], controllers: [AppController], - providers: [ - AppService, - UserRepository - ], + providers: [AppService, UserRepository], }) export class AppModule {} diff --git a/src/database/entities/candidacy.entity.ts b/src/database/entities/candidacy.entity.ts new file mode 100644 index 0000000..4ebcaaf --- /dev/null +++ b/src/database/entities/candidacy.entity.ts @@ -0,0 +1,43 @@ +import { + Column, + Entity, + PrimaryGeneratedColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { UsersEntity } from './users.entity'; +import { JobsEntity } from './jobs.entity'; +import { CandidacyStatus } from './candidancy-status.enum'; + +@Entity('tb_candidacies') +export class CandidacyEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column('uuid', { name: 'job_id' }) + jobId: string; + + @Column('uuid', { name: 'user_id' }) + userId: string; + + @Column({ type: 'enum', enum: CandidacyStatus }) + status: CandidacyStatus; + + @Column({ + type: 'date', + name: 'date_candidacy', + default: () => 'CURRENT_TIMESTAMP', + }) + dateCandidacy: Date; + + @Column({ name: 'date_closing', type: 'timestamp', nullable: true }) + dateClosing: Date; + + @ManyToOne(() => UsersEntity) + @JoinColumn({ name: 'user_id' }) + user: UsersEntity; + + @ManyToOne(() => JobsEntity) + @JoinColumn({ name: 'job_id' }) + job: JobsEntity; +} diff --git a/src/database/entities/candidancy-status.enum.ts b/src/database/entities/candidancy-status.enum.ts new file mode 100644 index 0000000..46e4ce2 --- /dev/null +++ b/src/database/entities/candidancy-status.enum.ts @@ -0,0 +1,5 @@ +export enum CandidacyStatus { + InProgress = 'em andamento', + Closed = 'encerrada', + NoInterest = 'sem interesse', +} diff --git a/src/database/entities/users.entity.ts b/src/database/entities/users.entity.ts index 2f20bf3..62c6934 100644 --- a/src/database/entities/users.entity.ts +++ b/src/database/entities/users.entity.ts @@ -11,6 +11,7 @@ import { import { ApplicationEntity } from './applications.entity'; import { CurriculumEntity } from './curriculum.entity'; import { PersonalDataEntity } from './personal-data.entity'; +import { CandidacyEntity } from './candidacy.entity'; enum RolesEnum { ADMIN = 'ADMIN', @@ -72,6 +73,9 @@ export class UsersEntity { @OneToMany(() => ApplicationEntity, (application) => application.user) applications: ApplicationEntity[]; + @OneToMany(() => CandidacyEntity, (candidacy) => candidacy.user) + candidacies: CandidacyEntity[]; + @CreateDateColumn() created_at: Date; diff --git a/src/database/migrations/1731094752487-Candidacy.ts b/src/database/migrations/1731094752487-Candidacy.ts new file mode 100644 index 0000000..9e0c8be --- /dev/null +++ b/src/database/migrations/1731094752487-Candidacy.ts @@ -0,0 +1,78 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, +} from 'typeorm'; + +export class Candidacy1731094752487 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'tb_candidacies', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'uuid_generate_v4()', + }, + { + name: 'job_id', + type: 'uuid', + }, + { + name: 'user_id', + type: 'uuid', + }, + { + name: 'status', + type: 'enum', + enum: ['em andamento', 'encerrada', 'sem interesse'], + }, + { + name: 'date_candidacy', + type: 'timestamp', + default: 'CURRENT_TIMESTAMP', + }, + { + name: 'date_closing', + type: 'timestamp', + isNullable: true, + }, + ], + }), + true, + ); + + await queryRunner.createForeignKey( + 'tb_candidacies', + new TableForeignKey({ + columnNames: ['user_id'], + referencedTableName: 'tb_users', + referencedColumnNames: ['id'], + }), + ); + + await queryRunner.createForeignKey( + 'tb_candidacies', + new TableForeignKey({ + columnNames: ['job_id'], + referencedTableName: 'tb_jobs', + referencedColumnNames: ['id'], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + const table = await queryRunner.getTable('tb_candidacies'); + const foreignKeys = table.foreignKeys.filter( + (fk) => + fk.columnNames.indexOf('user_id') !== -1 || + fk.columnNames.indexOf('job_id') !== -1, + ); + await queryRunner.dropForeignKeys('tb_candidacies', foreignKeys); + await queryRunner.dropTable('tb_candidacies'); + } +} diff --git a/src/main.ts b/src/main.ts index e2d46fd..dbcbf36 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,4 +41,4 @@ async function bootstrap() { console.info(`🚀🚀 App listening on port ${process.env.PORT || 3000} 🚀🚀`); }); } -bootstrap(); +bootstrap(); \ No newline at end of file diff --git a/src/modules/candidacy/candidacy.module.ts b/src/modules/candidacy/candidacy.module.ts new file mode 100644 index 0000000..f28c96c --- /dev/null +++ b/src/modules/candidacy/candidacy.module.ts @@ -0,0 +1,14 @@ +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Module } from '@nestjs/common'; +import { CandidacyEntity } from 'src/database/entities/candidacy.entity'; +import { CandidacyRepository } from './repository/candidacy.repository'; +import { CandidacyService } from './service/candidacy.service'; +import { CandidacyController } from './controller/candidacy.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([CandidacyEntity])], + controllers: [CandidacyController], + providers: [CandidacyService, CandidacyRepository], + exports: [CandidacyService], +}) +export class CandidacyModule {} diff --git a/src/modules/candidacy/controller/candidacy.controller.ts b/src/modules/candidacy/controller/candidacy.controller.ts new file mode 100644 index 0000000..79f5831 --- /dev/null +++ b/src/modules/candidacy/controller/candidacy.controller.ts @@ -0,0 +1,53 @@ +import { + BadRequestException, + Body, + Controller, + Get, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { CandidacyService } from '../service/candidacy.service'; +import { CreateCandidacyDto } from '../dto/create-candidacy.dto'; +import { LoggedUser } from 'src/modules/auth/decorator/logged-user.decorator'; +import { UsersEntity } from 'src/database/entities/users.entity'; +import { AuthGuard } from '@nestjs/passport'; +import { UpdateCandidacyDto } from '../dto/update-candidacy.dto'; +import { CandidacyStatus } from 'src/database/entities/candidancy-status.enum'; +import { ApiTags } from '@nestjs/swagger'; +import { CreateCandidacySwagger } from 'src/shared/Swagger/decorators/candidacy/create-candidacy.swagger'; +import { GetCandidaciesSwagger } from 'src/shared/Swagger/decorators/candidacy/get-candidacies.swagger'; +import { UpdateCandidacySwagger } from 'src/shared/Swagger/decorators/candidacy/update-candidacy.swagger'; + +@ApiTags('Candidacy') +@Controller('candidacy') +@UseGuards(AuthGuard('jwt')) +export class CandidacyController { + constructor(private readonly candidacyService: CandidacyService) {} + + @Post() + @CreateCandidacySwagger() + async createCandidacy(@Body() createCandidacyDTO: CreateCandidacyDto) { + return await this.candidacyService.create(createCandidacyDTO); + } + + @Get() + @GetCandidaciesSwagger() + async getCandidacies(@LoggedUser() user: UsersEntity) { + return await this.candidacyService.getCandidacyByUserId(user.id); + } + + @Patch() + @UpdateCandidacySwagger() + async updateCandidacy(@Body() updateCandidacyDto: UpdateCandidacyDto) { + if (updateCandidacyDto.status === CandidacyStatus.InProgress) { + throw new BadRequestException( + 'Não é possível atualizar para o status "em andamento"', + ); + } + return await this.candidacyService.closeCandidacy( + updateCandidacyDto.id, + updateCandidacyDto.status, + ); + } +} diff --git a/src/modules/candidacy/dto/create-candidacy.dto.ts b/src/modules/candidacy/dto/create-candidacy.dto.ts new file mode 100644 index 0000000..d467867 --- /dev/null +++ b/src/modules/candidacy/dto/create-candidacy.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID, IsNotEmpty } from 'class-validator'; + +export class CreateCandidacyDto { + @IsUUID() + @IsNotEmpty() + @ApiProperty({ type: 'string', format: 'uuid' }) + userId: string; + + @IsUUID() + @IsNotEmpty() + @ApiProperty({ type: 'string', format: 'uuid' }) + jobId: string; +} diff --git a/src/modules/candidacy/dto/update-candidacy.dto.ts b/src/modules/candidacy/dto/update-candidacy.dto.ts new file mode 100644 index 0000000..91ef0c9 --- /dev/null +++ b/src/modules/candidacy/dto/update-candidacy.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; +import { CandidacyStatus } from 'src/database/entities/candidancy-status.enum'; + +export class UpdateCandidacyDto { + @ApiProperty({ type: 'string', format: 'uuid' }) + @IsUUID() + @IsNotEmpty() + id: string; + + @ApiProperty({ enum: CandidacyStatus }) + @IsNotEmpty() + @IsEnum(CandidacyStatus) + status: CandidacyStatus; +} diff --git a/src/modules/candidacy/repository/candidacy.repository.ts b/src/modules/candidacy/repository/candidacy.repository.ts new file mode 100644 index 0000000..57ae3b6 --- /dev/null +++ b/src/modules/candidacy/repository/candidacy.repository.ts @@ -0,0 +1,69 @@ +import { + BadRequestException, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { CandidacyEntity } from '../../../database/entities/candidacy.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CandidacyStatus } from 'src/database/entities/candidancy-status.enum'; + +@Injectable() +export class CandidacyRepository { + constructor( + @InjectRepository(CandidacyEntity) + private candidacyRepository: Repository, + ) {} + + async createCandidacy(candidacy: CandidacyEntity): Promise { + return this.candidacyRepository.save(candidacy); + } + + async findAllByUserId(userId: string): Promise { + if (!userId) { + throw new BadRequestException('userId é obrigatório'); + } + try { + const candidacy = await this.candidacyRepository.find({ + where: { userId: userId }, + }); + if (!candidacy.length) { + throw new NotFoundException( + 'Nenhuma candidatura encontrada para este usuário', + ); + } + return candidacy; + } catch (error) { + throw new BadRequestException( + 'Erro ao buscar candidaturas: ' + error.message, + ); + } + } + + async updateStatus( + id: string, + status: CandidacyStatus, + ): Promise { + try { + const candidacy = await this.candidacyRepository.findOne({ + where: { id }, + }); + if (!candidacy) { + throw new NotFoundException('Candidatura não encontrada'); + } + candidacy.status = status; + await this.candidacyRepository.save(candidacy); + + return candidacy; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } else { + throw new InternalServerErrorException( + 'Erro ao atualizar o status da candidatura: ' + error.message, + ); + } + } + } +} diff --git a/src/modules/candidacy/service/candidacy.service.spec.ts b/src/modules/candidacy/service/candidacy.service.spec.ts new file mode 100644 index 0000000..9f9c1e0 --- /dev/null +++ b/src/modules/candidacy/service/candidacy.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CandidacyService } from '../candidacy.service'; + +describe('CandidacyService', () => { + let service: CandidacyService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CandidacyService], + }).compile(); + + service = module.get(CandidacyService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/candidacy/service/candidacy.service.ts b/src/modules/candidacy/service/candidacy.service.ts new file mode 100644 index 0000000..99bd96b --- /dev/null +++ b/src/modules/candidacy/service/candidacy.service.ts @@ -0,0 +1,76 @@ +import { + Injectable, + BadRequestException, + NotFoundException, +} from '@nestjs/common'; +import { CandidacyEntity } from 'src/database/entities/candidacy.entity'; +import { CandidacyStatus } from 'src/database/entities/candidancy-status.enum'; +import { CandidacyRepository } from 'src/modules/candidacy/repository/candidacy.repository'; +import { CreateCandidacyDto } from '../dto/create-candidacy.dto'; + +@Injectable() +export class CandidacyService { + constructor(private readonly candidacyRepository: CandidacyRepository) {} + + async create( + createCandidacyDto: CreateCandidacyDto, + ): Promise { + const candidacy = new CandidacyEntity(); + candidacy.userId = createCandidacyDto.userId; + candidacy.jobId = createCandidacyDto.jobId; + candidacy.status = CandidacyStatus.InProgress; + + try { + return await this.candidacyRepository.createCandidacy(candidacy); + } catch (error) { + throw new BadRequestException( + 'Erro ao criar a candidatura: ' + error.message, + ); + } + } + + async getCandidacyByUserId(userId: string): Promise { + if (!userId) { + throw new BadRequestException('userId é obrigatório'); + } + + try { + const candidacy = await this.candidacyRepository.findAllByUserId(userId); + if (!candidacy.length) { + throw new NotFoundException( + 'Nenhuma candidatura encontrada para este usuário', + ); + } + return candidacy; + } catch (error) { + throw new BadRequestException( + 'Erro ao buscar candidaturas: ' + error.message, + ); + } + } + + async closeCandidacy( + id: string, + status: CandidacyStatus.Closed | CandidacyStatus.NoInterest, + ): Promise { + if (!id) { + throw new BadRequestException('ID é obrigatório'); + } + + if (!Object.values(CandidacyStatus).includes(status)) { + throw new BadRequestException('Status inválido'); + } + + try { + const candidacy = await this.candidacyRepository.updateStatus(id, status); + if (!candidacy) { + throw new NotFoundException('Candidatura não encontrada'); + } + return candidacy; + } catch (error) { + throw new BadRequestException( + 'Erro ao encerrar a candidatura: ' + error.message, + ); + } + } +} diff --git a/src/shared/Swagger/decorators/candidacy/create-candidacy.swagger.ts b/src/shared/Swagger/decorators/candidacy/create-candidacy.swagger.ts new file mode 100644 index 0000000..f12a887 --- /dev/null +++ b/src/shared/Swagger/decorators/candidacy/create-candidacy.swagger.ts @@ -0,0 +1,22 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { BadRequestSwagger } from '../../bad-request.swagger'; +import { CandidacyEntity } from 'src/database/entities/candidacy.entity'; + +export function CreateCandidacySwagger() { + return applyDecorators( + ApiResponse({ + status: HttpStatus.CREATED, + description: 'Exemplo do retorno de sucesso da rota', + type: CandidacyEntity, + }), + ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Modelo de erro', + type: BadRequestSwagger, + }), + ApiOperation({ + summary: 'Rota para registrar uma candidatura', + }), + ); +} diff --git a/src/shared/Swagger/decorators/candidacy/get-candidacies.swagger.ts b/src/shared/Swagger/decorators/candidacy/get-candidacies.swagger.ts new file mode 100644 index 0000000..7c00d31 --- /dev/null +++ b/src/shared/Swagger/decorators/candidacy/get-candidacies.swagger.ts @@ -0,0 +1,22 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { BadRequestSwagger } from '../../bad-request.swagger'; +import { CandidacyEntity } from 'src/database/entities/candidacy.entity'; + +export function GetCandidaciesSwagger() { + return applyDecorators( + ApiResponse({ + status: HttpStatus.OK, + description: 'Exemplo do retorno de sucesso da rota', + type: [CandidacyEntity], + }), + ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Modelo de erro', + type: BadRequestSwagger, + }), + ApiOperation({ + summary: 'Rota para obter todas as candidaturas de um usuário', + }), + ); +} diff --git a/src/shared/Swagger/decorators/candidacy/update-candidacy.swagger.ts b/src/shared/Swagger/decorators/candidacy/update-candidacy.swagger.ts new file mode 100644 index 0000000..09c56fd --- /dev/null +++ b/src/shared/Swagger/decorators/candidacy/update-candidacy.swagger.ts @@ -0,0 +1,22 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { BadRequestSwagger } from '../../bad-request.swagger'; +import { CandidacyEntity } from 'src/database/entities/candidacy.entity'; + +export function UpdateCandidacySwagger() { + return applyDecorators( + ApiResponse({ + status: HttpStatus.CREATED, + description: 'Exemplo do retorno de sucesso da rota', + type: CandidacyEntity, + }), + ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Modelo de erro', + type: BadRequestSwagger, + }), + ApiOperation({ + summary: 'Rota para atualizar uma candidatura', + }), + ); +}