diff --git a/backend/src/common/exceptions/exceptions.interface.ts b/backend/src/common/exceptions/exceptions.interface.ts index 12b697fc8..f35856c25 100644 --- a/backend/src/common/exceptions/exceptions.interface.ts +++ b/backend/src/common/exceptions/exceptions.interface.ts @@ -1,6 +1,7 @@ export interface IError { message: string; code_error?: string; + details?: unknown; } export interface IException { diff --git a/backend/src/common/helpers/utils.ts b/backend/src/common/helpers/utils.ts index ebdc6fc93..ae3608547 100644 --- a/backend/src/common/helpers/utils.ts +++ b/backend/src/common/helpers/utils.ts @@ -29,6 +29,9 @@ export function jsonToExcelBuffer( export const isOver16FromCNP = (cnp: string): boolean => { // we don't need to perform the calculation before the user has entered all the necessary digits to calculate + if (!cnp) { + throw new Error('CNP is required'); + } if (cnp.length !== 13) { throw new Error('CNP must be 13 digits long'); } diff --git a/backend/src/modules/documents/exceptions/contract.exceptions.ts b/backend/src/modules/documents/exceptions/contract.exceptions.ts index 14651a91d..a341ae617 100644 --- a/backend/src/modules/documents/exceptions/contract.exceptions.ts +++ b/backend/src/modules/documents/exceptions/contract.exceptions.ts @@ -8,6 +8,12 @@ export enum ContractExceptionCodes { CONTRACT_005 = 'CONTRACT_005', CONTRACT_006 = 'CONTRACT_006', CONTRACT_007 = 'CONTRACT_007', + CONTRACT_008 = 'CONTRACT_008', + CONTRACT_009 = 'CONTRACT_009', + CONTRACT_010 = 'CONTRACT_010', + CONTRACT_011 = 'CONTRACT_011', + CONTRACT_012 = 'CONTRACT_012', + CONTRACT_013 = 'CONTRACT_013', } type ContractExceptionCodeType = keyof typeof ContractExceptionCodes; @@ -31,7 +37,7 @@ export const ContractExceptionMessages: Record< [ContractExceptionCodes.CONTRACT_004]: { code_error: ContractExceptionCodes.CONTRACT_004, message: - 'There is already a conract with this number for your organization', + 'There is already a contract with this number for your organization', }, [ContractExceptionCodes.CONTRACT_005]: { code_error: ContractExceptionCodes.CONTRACT_005, @@ -45,4 +51,28 @@ export const ContractExceptionMessages: Record< code_error: ContractExceptionCodes.CONTRACT_007, message: 'Can only cancel a contract pending to the ADMIN', }, + [ContractExceptionCodes.CONTRACT_008]: { + code_error: ContractExceptionCodes.CONTRACT_008, + message: 'Legal guardian data is required for under 16 volunteers', + }, + [ContractExceptionCodes.CONTRACT_009]: { + code_error: ContractExceptionCodes.CONTRACT_009, + message: 'FATAL: Error while creating the contract in DB', + }, + [ContractExceptionCodes.CONTRACT_010]: { + code_error: ContractExceptionCodes.CONTRACT_010, + message: '[Create Contract] Invalid input data', + }, + [ContractExceptionCodes.CONTRACT_011]: { + code_error: ContractExceptionCodes.CONTRACT_011, + message: '[Create Contract] Invalid legal guardian data', + }, + [ContractExceptionCodes.CONTRACT_012]: { + code_error: ContractExceptionCodes.CONTRACT_012, + message: '[Create Contract] Invalid personal data', + }, + [ContractExceptionCodes.CONTRACT_013]: { + code_error: ContractExceptionCodes.CONTRACT_013, + message: '[Create Contract] Missing volunteer personal data', + }, }; diff --git a/backend/src/modules/documents/models/document-contract.model.ts b/backend/src/modules/documents/models/document-contract.model.ts index 64e810b8d..f5abdf9ce 100644 --- a/backend/src/modules/documents/models/document-contract.model.ts +++ b/backend/src/modules/documents/models/document-contract.model.ts @@ -78,10 +78,21 @@ export type UpdateDocumentContractOptions = { export type FindOneDocumentContractOptions = Partial< Pick< IDocumentContractModel, - 'id' | 'volunteerId' | 'organizationId' | 'status' | 'documentTemplateId' + | 'id' + | 'volunteerId' + | 'organizationId' + | 'status' + | 'documentTemplateId' + | 'documentNumber' > >; +export type FindExistingContractForVolunteerInInterval = { + volunteerId: string; + documentStartDate: Date; + documentEndDate: Date; +}; + export class DocumentContractTransformer { static fromEntity(entity: DocumentContractEntity): IDocumentContractModel { if (!entity) { diff --git a/backend/src/modules/documents/repositories/document-contract.repository.ts b/backend/src/modules/documents/repositories/document-contract.repository.ts index 2202a189b..968794524 100644 --- a/backend/src/modules/documents/repositories/document-contract.repository.ts +++ b/backend/src/modules/documents/repositories/document-contract.repository.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; import { DocumentContractEntity } from '../entities/document-contract.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm'; import { RepositoryWithPagination } from 'src/infrastructure/base/repository-with-pagination.class'; import { CreateDocumentContractOptions, DocumentContractTransformer, + FindExistingContractForVolunteerInInterval, FindOneDocumentContractOptions, IDocumentContractModel, UpdateDocumentContractOptions, @@ -43,6 +44,26 @@ export class DocumentContractRepositoryService extends RepositoryWithPagination< return DocumentContractTransformer.fromEntity(documentContract); } + /** + * Checks if a contract exists for a volunteer in the given period + * + * The method checks if there's any overlap between the new contract period + * and any existing contract for the volunteer. + */ + async existsInSamePeriod( + options: FindExistingContractForVolunteerInInterval, + ): Promise { + const existingContract = await this.documentContractRepository.findOne({ + where: { + volunteerId: options.volunteerId, + documentStartDate: LessThanOrEqual(options.documentEndDate), + documentEndDate: MoreThanOrEqual(options.documentStartDate), + }, + }); + + return !!existingContract; + } + async exists(options: FindOneDocumentContractOptions): Promise { return this.documentContractRepository.exists({ where: options }); } diff --git a/backend/src/modules/documents/services/document-contract.facade.ts b/backend/src/modules/documents/services/document-contract.facade.ts index e20bebcd6..416c18439 100644 --- a/backend/src/modules/documents/services/document-contract.facade.ts +++ b/backend/src/modules/documents/services/document-contract.facade.ts @@ -5,6 +5,7 @@ import { IDocumentContractModel, FindOneDocumentContractOptions, UpdateDocumentContractOptions, + FindExistingContractForVolunteerInInterval, } from '../models/document-contract.model'; import { DocumentContractListViewRepository } from '../repositories/document-contract-list-view.repository'; import { @@ -61,6 +62,12 @@ export class DocumentContractFacade { return this.documentContractRepository.exists(options); } + async existsInSamePeriod( + options: FindExistingContractForVolunteerInInterval, + ): Promise { + return this.documentContractRepository.existsInSamePeriod(options); + } + async findMany( options: FindManyDocumentContractListViewOptions, ): Promise> { diff --git a/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts b/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts index 4a5128156..83ec552e5 100644 --- a/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts +++ b/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts @@ -3,6 +3,7 @@ import { isOver16FromCNP } from 'src/common/helpers/utils'; import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service'; import { DocumentContractStatus } from 'src/modules/documents/enums/contract-status.enum'; +import { ContractExceptionMessages } from 'src/modules/documents/exceptions/contract.exceptions'; import { CreateDocumentContractOptions } from 'src/modules/documents/models/document-contract.model'; import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade'; import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade'; @@ -17,6 +18,18 @@ import { VolunteerFacade } from 'src/modules/volunteer/services/volunteer.facade import { GetOrganizationUseCaseService } from 'src/usecases/organization/get-organization.usecase'; import * as z from 'zod'; +/* +Possible errors thrown: +1. CONTRACT_010: Invalid input data +2. CONTRACT_004: Contract number already exists +3. CONTRACT_005: Volunteer already has a contract for that period +4. CONTRACT_013: Missing volunteer personal data +5. CONTRACT_012: Invalid personal data +6. CONTRACT_008: Legal guardian data is required for under 16 volunteers +7. CONTRACT_011: Invalid legal guardian data +8. CONTRACT_009: Error while creating the contract in DB +*/ + @Injectable() export class CreateDocumentContractUsecase implements IUseCaseService { constructor( @@ -25,18 +38,7 @@ export class CreateDocumentContractUsecase implements IUseCaseService { private readonly getOrganizationUsecase: GetOrganizationUseCaseService, private readonly volunteerFacade: VolunteerFacade, private readonly exceptionsService: ExceptionsService, - ) { - // this.execute({ - // documentDate: new Date(), - // documentStartDate: new Date(), - // documentEndDate: new Date(), - // volunteerId: '1a53406f-263b-41bc-b60c-cb30a1805f1e', - // organizationId: '7f005461-07c3-4693-a85d-40d31db43a4c', - // documentTemplateId: 'bc3b7d74-686e-47b4-850a-b1de69574e28', - // createdByAdminId: '4db075bd-4095-432e-98bd-dc68b4599337', - // status: DocumentContractStatus.CREATED, - // }); - } + ) {} public async execute( newContract: Omit< @@ -44,41 +46,60 @@ export class CreateDocumentContractUsecase implements IUseCaseService { 'volunteerData' | 'volunteerTutorData' | 'status' >, ): Promise { - // 1. check if the organization exists - await this.getOrganizationUsecase.execute(newContract.organizationId); - - // 2. check if the volunteer exists - const volunteer = await this.checkVolunteerExists( - newContract.volunteerId, - newContract.organizationId, - ); - - //3. check if template exists - await this.checkTemplateExists( - newContract.documentTemplateId, - newContract.organizationId, - ); + let volunteer: IVolunteerModel; + try { + // 1. Check if the organization exists + await this.getOrganizationUsecase.execute(newContract.organizationId); - //TODO: 4. check if the contract number already exists + // 2. Check if the volunteer exists + volunteer = await this.checkVolunteerExists( + newContract.volunteerId, + newContract.organizationId, + ); - //TODO: 5. check if the volunteer has already a contract in that period + //3. Check if template exists + await this.checkTemplateExists( + newContract.documentTemplateId, + newContract.organizationId, + ); + } catch (error) { + this.exceptionsService.badRequestException({ + ...ContractExceptionMessages.CONTRACT_010, + details: error, + }); + } - // 6. Extract volunteerData and volunteerTutorData from the user - const volunteerPersonalData = volunteer.user.userPersonalData; + // 4. Check if the contract number already exists + const existingContract = await this.documentContractFacade.findOne({ + documentNumber: newContract.documentNumber, + organizationId: newContract.organizationId, + }); - await this.validateVolunteerPersonalData(volunteerPersonalData); + if (existingContract) { + this.exceptionsService.badRequestException( + ContractExceptionMessages.CONTRACT_004, + ); + } - if (!isOver16FromCNP(volunteerPersonalData.cnp)) { - if (!volunteerPersonalData.legalGuardian) { - this.exceptionsService.badRequestException({ - message: 'Legal guardian data is required for under 16 volunteers', - code_error: 'LEGAL_GUARDIAN_DATA_REQUIRED', - }); - } + // 5. Check if the volunteer has already a contract in that period + const existingContractInSamePeriod = + await this.documentContractFacade.existsInSamePeriod({ + volunteerId: newContract.volunteerId, + documentStartDate: newContract.documentStartDate, + documentEndDate: newContract.documentEndDate, + }); - await this.validateLegalGuardianData(volunteerPersonalData.legalGuardian); + if (existingContractInSamePeriod) { + this.exceptionsService.badRequestException( + ContractExceptionMessages.CONTRACT_005, + ); } + // 6. Extract volunteerData and volunteerTutorData from the user + const volunteerPersonalData = volunteer?.user?.userPersonalData; + await this.validateVolunteerPersonalData(volunteerPersonalData); + + // 7. Create the contract input const newContractOptions: CreateDocumentContractOptions = { ...newContract, status: DocumentContractStatus.CREATED, @@ -94,8 +115,8 @@ export class CreateDocumentContractUsecase implements IUseCaseService { contractId = await this.documentContractFacade.create(newContractOptions); } catch (error) { this.exceptionsService.internalServerErrorException({ - message: 'Error creating contract', - code_error: 'ERROR_CREATING_CONTRACT', // TODO: create a new error code for this + ...ContractExceptionMessages.CONTRACT_009, + details: error, }); } @@ -111,6 +132,12 @@ export class CreateDocumentContractUsecase implements IUseCaseService { private async validateVolunteerPersonalData( volunteerPersonalData: IUserPersonalDataModel, ): Promise { + if (!volunteerPersonalData) { + this.exceptionsService.badRequestException( + ContractExceptionMessages.CONTRACT_013, + ); + } + const personalDataSchema = z.object({ cnp: z.string().length(13, 'CNP must be 13 digits'), address: z.string().min(1, 'Address is required'), @@ -138,12 +165,25 @@ export class CreateDocumentContractUsecase implements IUseCaseService { })); this.exceptionsService.badRequestException({ - message: `Invalid personal data ${JSON.stringify(invalidFields)}`, - code_error: 'INVALID_PERSONAL_DATA', // TODO: create a new error code for this + ...ContractExceptionMessages.CONTRACT_012, + details: invalidFields, }); } else { - throw error; // Re-throw unexpected errors + this.exceptionsService.badRequestException({ + ...ContractExceptionMessages.CONTRACT_012, + details: error, + }); + } + } + + if (!isOver16FromCNP(volunteerPersonalData.cnp)) { + if (!volunteerPersonalData.legalGuardian) { + this.exceptionsService.badRequestException( + ContractExceptionMessages.CONTRACT_008, + ); } + + await this.validateLegalGuardianData(volunteerPersonalData.legalGuardian); } } @@ -174,11 +214,14 @@ export class CreateDocumentContractUsecase implements IUseCaseService { })); this.exceptionsService.badRequestException({ - message: `Invalid legal guardian data ${JSON.stringify(invalidFields)}`, - code_error: 'INVALID_LEGAL_GUARDIAN_DATA', + ...ContractExceptionMessages.CONTRACT_011, + details: invalidFields, }); } else { - throw error; // Re-throw unexpected errors + this.exceptionsService.badRequestException({ + ...ContractExceptionMessages.CONTRACT_011, + details: error, + }); } } }