Skip to content

Commit

Permalink
feat: [Contracts] Add custom errors for Create Contract Usecase
Browse files Browse the repository at this point in the history
  • Loading branch information
radulescuandrew committed Sep 20, 2024
1 parent 76dd232 commit d2cb41c
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 50 deletions.
1 change: 1 addition & 0 deletions backend/src/common/exceptions/exceptions.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface IError {
message: string;
code_error?: string;
details?: unknown;
}

export interface IException {
Expand Down
3 changes: 3 additions & 0 deletions backend/src/common/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export function jsonToExcelBuffer<T>(

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');
}
Expand Down
32 changes: 31 additions & 1 deletion backend/src/modules/documents/exceptions/contract.exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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',
},
};
13 changes: 12 additions & 1 deletion backend/src/modules/documents/models/document-contract.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<boolean> {
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<boolean> {
return this.documentContractRepository.exists({ where: options });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
IDocumentContractModel,
FindOneDocumentContractOptions,
UpdateDocumentContractOptions,
FindExistingContractForVolunteerInInterval,
} from '../models/document-contract.model';
import { DocumentContractListViewRepository } from '../repositories/document-contract-list-view.repository';
import {
Expand Down Expand Up @@ -61,6 +62,12 @@ export class DocumentContractFacade {
return this.documentContractRepository.exists(options);
}

async existsInSamePeriod(
options: FindExistingContractForVolunteerInInterval,
): Promise<boolean> {
return this.documentContractRepository.existsInSamePeriod(options);
}

async findMany(
options: FindManyDocumentContractListViewOptions,
): Promise<Pagination<IDocumentContractListViewModel>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<string> {
constructor(
Expand All @@ -25,60 +38,68 @@ export class CreateDocumentContractUsecase implements IUseCaseService<string> {
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<
CreateDocumentContractOptions,
'volunteerData' | 'volunteerTutorData' | 'status'
>,
): Promise<string> {
// 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,
Expand All @@ -94,8 +115,8 @@ export class CreateDocumentContractUsecase implements IUseCaseService<string> {
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,
});
}

Expand All @@ -111,6 +132,12 @@ export class CreateDocumentContractUsecase implements IUseCaseService<string> {
private async validateVolunteerPersonalData(
volunteerPersonalData: IUserPersonalDataModel,
): Promise<void> {
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'),
Expand Down Expand Up @@ -138,12 +165,25 @@ export class CreateDocumentContractUsecase implements IUseCaseService<string> {
}));

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);
}
}

Expand Down Expand Up @@ -174,11 +214,14 @@ export class CreateDocumentContractUsecase implements IUseCaseService<string> {
}));

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,
});
}
}
}
Expand Down

0 comments on commit d2cb41c

Please sign in to comment.