From e989e65588a5d8b78312044f269e6004e0d96402 Mon Sep 17 00:00:00 2001 From: alexeh Date: Fri, 8 Nov 2024 06:08:27 +0100 Subject: [PATCH] add general custom project validation --- .../custom-projects.controller.ts | 9 +- .../custom-projects/custom-projects.module.ts | 7 +- .../custom-projects.service.ts | 2 +- .../conservation-project.validator.ts | 174 +++++++++--------- .../create-custom-project.validator.ts | 47 +++-- .../custom-projects-validations.spec.ts | 104 ++++++----- shared/contracts/custom-projects.contract.ts | 2 +- .../conservation-project-params.dto.ts | 53 ++++++ ...> create-custom-project-dto.deprecated.ts} | 53 ++---- .../project-params.validator.ts | 35 ++++ .../restoration-project-params.dto.ts | 1 + .../carbon-inputs/emission-factors.entity.ts | 2 + 12 files changed, 284 insertions(+), 205 deletions(-) create mode 100644 shared/dtos/custom-projects/conservation-project-params.dto.ts rename shared/dtos/custom-projects/{create-custom-project.dto.ts => create-custom-project-dto.deprecated.ts} (57%) create mode 100644 shared/dtos/custom-projects/project-params.validator.ts create mode 100644 shared/dtos/custom-projects/restoration-project-params.dto.ts diff --git a/api/src/modules/custom-projects/custom-projects.controller.ts b/api/src/modules/custom-projects/custom-projects.controller.ts index 623cffe5..62f341be 100644 --- a/api/src/modules/custom-projects/custom-projects.controller.ts +++ b/api/src/modules/custom-projects/custom-projects.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, HttpStatus } from '@nestjs/common'; +import { Body, Controller, HttpStatus, ValidationPipe } from '@nestjs/common'; import { CountriesService } from '@api/modules/countries/countries.service'; import { DataSource } from 'typeorm'; import { ModelAssumptions } from '@shared/entities/model-assumptions.entity'; @@ -6,7 +6,7 @@ import { tsRestHandler, TsRestHandler } from '@ts-rest/nest'; import { ControllerResponse } from '@api/types/controller-response.type'; import { customProjectContract } from '@shared/contracts/custom-projects.contract'; import { CustomProjectsService } from '@api/modules/custom-projects/custom-projects.service'; -import { CreateCustomProjectDto } from '@shared/dtos/custom-projects/create-custom-project.dto'; +import { CreateCustomProjectDto } from '@shared/dtos/custom-projects/create-custom-project-dto.deprecated'; @Controller() export class CustomProjectsController { @@ -44,13 +44,14 @@ export class CustomProjectsController { @TsRestHandler(customProjectContract.createCustomProject) async create( - @Body() + @Body(new ValidationPipe({ enableDebugMessages: true })) dto: CreateCustomProjectDto, ): Promise { return tsRestHandler( customProjectContract.createCustomProject, async ({ body }) => { - const customProject = await this.customProjects.create(dto); + console.log('dto', dto); + const customProject = await this.customProjects.create(dto as any); return { status: 201, body: { data: customProject }, diff --git a/api/src/modules/custom-projects/custom-projects.module.ts b/api/src/modules/custom-projects/custom-projects.module.ts index 62ebc419..4dd587f9 100644 --- a/api/src/modules/custom-projects/custom-projects.module.ts +++ b/api/src/modules/custom-projects/custom-projects.module.ts @@ -6,7 +6,6 @@ import { CustomProject } from '@shared/entities/custom-project.entity'; import { CustomProjectsController } from './custom-projects.controller'; import { CalculationsModule } from '@api/modules/calculations/calculations.module'; import { CustomProjectFactory } from '@api/modules/custom-projects/custom-project.factory'; -import { CreateCustomProjectValidator } from '@api/modules/custom-projects/validation/create-custom-project.validator'; @Module({ imports: [ @@ -14,11 +13,7 @@ import { CreateCustomProjectValidator } from '@api/modules/custom-projects/valid CountriesModule, CalculationsModule, ], - providers: [ - CustomProjectsService, - CustomProjectFactory, - CreateCustomProjectValidator, - ], + providers: [CustomProjectsService, CustomProjectFactory], controllers: [CustomProjectsController], }) export class CustomProjectsModule {} diff --git a/api/src/modules/custom-projects/custom-projects.service.ts b/api/src/modules/custom-projects/custom-projects.service.ts index 43e00abd..f0c1e20e 100644 --- a/api/src/modules/custom-projects/custom-projects.service.ts +++ b/api/src/modules/custom-projects/custom-projects.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { AppBaseService } from '@api/utils/app-base.service'; -import { CreateCustomProjectDto } from '@shared/dtos/custom-projects/create-custom-project.dto'; +import { CreateCustomProjectDto } from '@shared/dtos/custom-projects/create-custom-project-dto.deprecated'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { CustomProject } from '@shared/entities/custom-project.entity'; diff --git a/api/src/modules/custom-projects/validation/conservation-project.validator.ts b/api/src/modules/custom-projects/validation/conservation-project.validator.ts index c549dc8c..49377f69 100644 --- a/api/src/modules/custom-projects/validation/conservation-project.validator.ts +++ b/api/src/modules/custom-projects/validation/conservation-project.validator.ts @@ -1,86 +1,88 @@ -import { CreateCustomProjectDto } from '@shared/dtos/custom-projects/create-custom-project.dto'; -import { LOSS_RATE_USED } from '@shared/schemas/custom-projects/create-custom-project.schema'; -import { BadRequestException } from '@nestjs/common'; -import { BaseDataView } from '@shared/entities/base-data.view'; -import { EMISSION_FACTORS_TIER_TYPES } from '@shared/entities/carbon-inputs/emission-factors.entity'; -import { ECOSYSTEM } from '@shared/entities/ecosystem.enum'; - -export class ConservationProjectValidator { - validatedProject: Record; - data: BaseDataView; - createDto: CreateCustomProjectDto & { projectSpecificLossRate: number }; - constructor(dto, data: BaseDataView) { - this.createDto = dto; - this.data = data; - } - // Probably SET PROJECT PARAMETERS WOULD BE A BETTER NAME, and just return the validated project parameters - validate() { - this.setLossRate(); - return this.createDto; - } - private setLossRate() { - if ( - this.createDto.lossRateUsed === LOSS_RATE_USED.PROJECT_SPECIFIC && - !this.createDto.projectSpecificLossRate - ) { - throw new BadRequestException( - 'projectSpecificLossRate is required when lossRateUsed is projectSpecific', - ); - } - if ( - this.createDto.lossRateUsed === LOSS_RATE_USED.PROJECT_SPECIFIC && - this.createDto.projectSpecificLossRate >= 0 - ) { - throw new BadRequestException( - 'projectSpecificLossRate should be negative when lossRateUsed is projectSpecific', - ); - } - if ( - this.createDto.lossRateUsed === LOSS_RATE_USED.NATIONAL_AVERAGE && - this.createDto.projectSpecificLossRate - ) { - throw new BadRequestException( - 'projectSpecificLossRate should not be provided when lossRateUsed is not projectSpecific', - ); - } - if (this.createDto.lossRateUsed === LOSS_RATE_USED.NATIONAL_AVERAGE) { - this.validatedProject.lossRate = this.data.ecosystemLossRate; - } - if (this.createDto.lossRateUsed === LOSS_RATE_USED.PROJECT_SPECIFIC) { - this.validatedProject.lossRate = this.createDto.projectSpecificLossRate; - } - } - - private setEmissionFactor() { - if ( - this.createDto.emissionFactorUsed === EMISSION_FACTORS_TIER_TYPES.TIER_1 - ) { - this.validatedProject.emissionFactor = this.data.tier1EmissionFactor; - return; - } - if ( - this.createDto.emissionFactorUsed === EMISSION_FACTORS_TIER_TYPES.TIER_2 - ) { - if (this.createDto.ecosystem !== ECOSYSTEM.MANGROVE) { - throw new BadRequestException( - `No Tier 2 emission factors for ${this.createDto.ecosystem}`, - ); - } - this.validatedProject.emissionFactorAGB = this.data.emissionFactorAgb; - this.validatedProject.emissionFactorSOC = this.data.emissionFactorSoc; - return; - } - if (this.createDto.projectSpecificEmission === 'One emission factor') { - this.validatedProject.emissionFactor = - this.createDto.projectSpecificEmissionFactor; - this.validatedProject.emissionFactorAGB = 0; - this.validatedProject.emissionFactorSOC = 0; - } else { - this.validatedProject.emissionFactor = null; - this.validatedProject.emissionFactorAGB = - this.createDto.emissionFactorAGB; - this.validatedProject.emissionFactorSOC = - this.createDto.emissionFactorSOC; - } - } -} +// import { LOSS_RATE_USED } from '@shared/schemas/custom-projects/create-custom-project.schema'; +// import { BadRequestException } from '@nestjs/common'; +// import { BaseDataView } from '@shared/entities/base-data.view'; +// import { EMISSION_FACTORS_TIER_TYPES } from '@shared/entities/carbon-inputs/emission-factors.entity'; +// import { ECOSYSTEM } from '@shared/entities/ecosystem.enum'; +// import { CreateCustomProjectDto } from '@shared/dtos/custom-projects/create-custom-project-dto.deprecated'; +// +// export class ConservationProjectValidator { +// validatedProject: Record; +// data: BaseDataView; +// createDto: CreateCustomProjectDto & { +// projectSpecificLossRate: number; +// }; +// constructor(dto, data: BaseDataView) { +// this.createDto = dto; +// this.data = data; +// } +// // Probably SET PROJECT PARAMETERS WOULD BE A BETTER NAME, and just return the validated project parameters +// validate() { +// this.setLossRate(); +// return this.createDto; +// } +// private setLossRate() { +// if ( +// this.createDto.lossRateUsed === LOSS_RATE_USED.PROJECT_SPECIFIC && +// !this.createDto.projectSpecificLossRate +// ) { +// throw new BadRequestException( +// 'projectSpecificLossRate is required when lossRateUsed is projectSpecific', +// ); +// } +// if ( +// this.createDto.lossRateUsed === LOSS_RATE_USED.PROJECT_SPECIFIC && +// this.createDto.projectSpecificLossRate >= 0 +// ) { +// throw new BadRequestException( +// 'projectSpecificLossRate should be negative when lossRateUsed is projectSpecific', +// ); +// } +// if ( +// this.createDto.lossRateUsed === LOSS_RATE_USED.NATIONAL_AVERAGE && +// this.createDto.projectSpecificLossRate +// ) { +// throw new BadRequestException( +// 'projectSpecificLossRate should not be provided when lossRateUsed is not projectSpecific', +// ); +// } +// if (this.createDto.lossRateUsed === LOSS_RATE_USED.NATIONAL_AVERAGE) { +// this.validatedProject.lossRate = this.data.ecosystemLossRate; +// } +// if (this.createDto.lossRateUsed === LOSS_RATE_USED.PROJECT_SPECIFIC) { +// this.validatedProject.lossRate = this.createDto.projectSpecificLossRate; +// } +// } +// +// private setEmissionFactor() { +// if ( +// this.createDto.emissionFactorUsed === EMISSION_FACTORS_TIER_TYPES.TIER_1 +// ) { +// this.validatedProject.emissionFactor = this.data.tier1EmissionFactor; +// return; +// } +// if ( +// this.createDto.emissionFactorUsed === EMISSION_FACTORS_TIER_TYPES.TIER_2 +// ) { +// if (this.createDto.ecosystem !== ECOSYSTEM.MANGROVE) { +// throw new BadRequestException( +// `No Tier 2 emission factors for ${this.createDto.ecosystem}`, +// ); +// } +// this.validatedProject.emissionFactorAGB = this.data.emissionFactorAgb; +// this.validatedProject.emissionFactorSOC = this.data.emissionFactorSoc; +// return; +// } +// if (this.createDto.projectSpecificEmission === 'One emission factor') { +// this.validatedProject.emissionFactor = +// this.createDto.projectSpecificEmissionFactor; +// this.validatedProject.emissionFactorAGB = 0; +// this.validatedProject.emissionFactorSOC = 0; +// } else { +// this.validatedProject.emissionFactor = null; +// this.validatedProject.emissionFactorAGB = +// this.createDto.emissionFactorAGB; +// this.validatedProject.emissionFactorSOC = +// this.createDto.emissionFactorSOC; +// } +// } +// } diff --git a/api/src/modules/custom-projects/validation/create-custom-project.validator.ts b/api/src/modules/custom-projects/validation/create-custom-project.validator.ts index bc7c349e..56e20bde 100644 --- a/api/src/modules/custom-projects/validation/create-custom-project.validator.ts +++ b/api/src/modules/custom-projects/validation/create-custom-project.validator.ts @@ -1,24 +1,23 @@ -import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { CreateCustomProjectDto } from '@shared/dtos/custom-projects/create-custom-project.dto'; -import { ACTIVITY } from '@shared/entities/activity.enum'; -import { ConservationProjectValidator } from '@api/modules/custom-projects/validation/conservation-project.validator'; -import { CalculationEngine } from '@api/modules/calculations/calculation.engine'; - -@Injectable() -export class CreateCustomProjectValidator { - project: Record; - constructor(private readonly calculationEngine: CalculationEngine) {} - - async validate(dto: CreateCustomProjectDto) { - const { countryCode, ecosystem, activity, projectName } = dto; - const { baseData } = await this.calculationEngine.getBaseData({ - countryCode, - ecosystem, - activity, - }); - if (dto.activity === ACTIVITY.CONSERVATION) { - return new ConservationProjectValidator(dto, baseData).validate(); - } - } -} +// import { Injectable } from '@nestjs/common'; +// import { ACTIVITY } from '@shared/entities/activity.enum'; +// import { ConservationProjectValidator } from '@api/modules/custom-projects/validation/conservation-project.validator'; +// import { CalculationEngine } from '@api/modules/calculations/calculation.engine'; +// import { CreateCustomProjectDto } from '@shared/dtos/custom-projects/create-custom-project-dto.deprecated'; +// +// @Injectable() +// export class CreateCustomProjectValidator { +// project: Record; +// constructor(private readonly calculationEngine: CalculationEngine) {} +// +// async validate(dto: CreateCustomProjectDto) { +// const { countryCode, ecosystem, activity, projectName } = dto; +// const { baseData } = await this.calculationEngine.getBaseData({ +// countryCode, +// ecosystem, +// activity, +// }); +// if (dto.activity === ACTIVITY.CONSERVATION) { +// return new ConservationProjectValidator(dto, baseData).validate(); +// } +// } +// } diff --git a/api/test/integration/custom-projects/custom-projects-validations.spec.ts b/api/test/integration/custom-projects/custom-projects-validations.spec.ts index ff0c57ca..a0893aaf 100644 --- a/api/test/integration/custom-projects/custom-projects-validations.spec.ts +++ b/api/test/integration/custom-projects/custom-projects-validations.spec.ts @@ -15,53 +15,63 @@ describe('Create Custom Projects - Request Validations', () => { await testManager.close(); }); - test(`if lossRateUsed is ${LOSS_RATE_USED.NATIONAL_AVERAGE} then project specific loss rate should not be provided`, async () => { - const response = await testManager - .request() - .post(customProjectContract.createConservationCustomProject.path) - .send({ - activity: ACTIVITY.CONSERVATION, - countryCode: 'USA', - ecosystem: ECOSYSTEM.MANGROVE, - lossRateUsed: LOSS_RATE_USED.NATIONAL_AVERAGE, - projectSpecificLossRate: 10, - }); + describe('General Custom Project Validations', () => { + test('Should fail if common project parameters are not provided', async () => { + const response = await testManager + .request() + .post(customProjectContract.createCustomProject.path) + .send({}); - expect(response.status).toBe(400); - expect(response.body.errors[0].title).toBe( - `Project Specific Loss Rate should not be provided when lossRateUsed is ${LOSS_RATE_USED.NATIONAL_AVERAGE}`, - ); - }); - test('If loss rate used is project specific, then project specific loss rate should be provided, and it should be a negative number', async () => { - const noProjectSpecificLossRateProvided = await testManager - .request() - .post(customProjectContract.createConservationCustomProject.path) - .send({ - activity: ACTIVITY.CONSERVATION, - countryCode: 'USA', - ecosystem: ECOSYSTEM.MANGROVE, - lossRateUsed: LOSS_RATE_USED.PROJECT_SPECIFIC, - }); - - expect(noProjectSpecificLossRateProvided.status).toBe(400); - expect(noProjectSpecificLossRateProvided.body.title).toBe( - `Project Specific Loss Rate is required when lossRateUsed is ${LOSS_RATE_USED.PROJECT_SPECIFIC}`, - ); - - const positiveProjectSpecificLossRate = await testManager - .request() - .post(customProjectContract.createConservationCustomProject.path) - .send({ - activity: ACTIVITY.CONSERVATION, - countryCode: 'USA', - ecosystem: ECOSYSTEM.MANGROVE, - lossRateUsed: LOSS_RATE_USED.PROJECT_SPECIFIC, - projectSpecificLossRate: 10, - }); - - expect(positiveProjectSpecificLossRate.status).toBe(400); - expect(positiveProjectSpecificLossRate.body.title).toBe( - 'Project Specific Loss Rate should be negative', - ); + expect(response.body.errors).toHaveLength(10); + expect(response.body.errors).toMatchObject(GENERAL_VALIDATION_ERRORS); + }); }); }); + +const GENERAL_VALIDATION_ERRORS = [ + { + status: '400', + title: 'countryCode must be longer than or equal to 3 characters', + }, + { + status: '400', + title: 'countryCode must be a string', + }, + { + status: '400', + title: 'projectName must be a string', + }, + { + status: '400', + title: + 'activity must be one of the following values: Restoration, Conservation', + }, + { + status: '400', + title: + 'ecosystem must be one of the following values: Mangrove, Seagrass, Salt marsh', + }, + { + status: '400', + title: + 'projectSizeHa must be a number conforming to the specified constraints', + }, + { + status: '400', + title: + 'initialCarbonPriceAssumption must be a number conforming to the specified constraints', + }, + { + status: '400', + title: + 'carbonRevenuesToCover must be one of the following values: Opex, Capex and Opex', + }, + { + status: '400', + title: 'Invalid project parameters for the selected activity type.', + }, + { + status: '400', + title: 'parameters should not be empty', + }, +]; diff --git a/shared/contracts/custom-projects.contract.ts b/shared/contracts/custom-projects.contract.ts index af581c4c..79440389 100644 --- a/shared/contracts/custom-projects.contract.ts +++ b/shared/contracts/custom-projects.contract.ts @@ -3,7 +3,7 @@ import { ApiResponse } from "@shared/dtos/global/api-response.dto"; import { Country } from "@shared/entities/country.entity"; import { ModelAssumptions } from "@shared/entities/model-assumptions.entity"; import { CustomProject } from "@shared/entities/custom-project.entity"; -import { CreateCustomProjectDto } from "@shared/dtos/custom-projects/create-custom-project.dto"; +import { CreateCustomProjectDto } from "@shared/dtos/custom-projects/create-custom-project-dto.deprecated"; // TODO: This is a scaffold. We need to define types for responses, zod schemas for body and query param validation etc. diff --git a/shared/dtos/custom-projects/conservation-project-params.dto.ts b/shared/dtos/custom-projects/conservation-project-params.dto.ts new file mode 100644 index 00000000..dbb6ea84 --- /dev/null +++ b/shared/dtos/custom-projects/conservation-project-params.dto.ts @@ -0,0 +1,53 @@ +import { LOSS_RATE_USED } from "@shared/schemas/custom-projects/create-custom-project.schema"; +import { + IsEnum, + IsNegative, + IsNotEmpty, + IsNumber, + ValidateIf, +} from "class-validator"; +import { EMISSION_FACTORS_TIER_TYPES } from "@shared/entities/carbon-inputs/emission-factors.entity"; +import { CreateCustomProjectDto } from "@shared/dtos/custom-projects/create-custom-project-dto.deprecated"; +import { ACTIVITY } from "@shared/entities/activity.enum"; + +export enum PROJECT_SPECIFIC_EMISSION { + ONE_EMISSION_FACTOR = "One emission factor", + TWO_EMISSION_FACTORS = "Two emission factors", +} +export enum TIER_3_EMISSION_FACTORS { + TIER_3 = "Tier 3 - Project specific emission factor", +} + +export class ConservationProjectParamDto { + @ValidateIf((o) => o.acitvity === ACTIVITY.CONSERVATION) + @IsEnum(LOSS_RATE_USED) + lossRateUsed: LOSS_RATE_USED; + + @ValidateIf((o) => o.lossRateUsed === LOSS_RATE_USED.PROJECT_SPECIFIC) + @IsNotEmpty({ + message: + "Project Specific Loss Rate is required when lossRateUsed is projectSpecific", + }) + @IsNumber() + @IsNegative({ message: "Project Specific Loss Rate must be negative" }) + projectSpecificLossRate?: number; + + @IsEnum(EMISSION_FACTORS_TIER_TYPES || TIER_3_EMISSION_FACTORS) + emissionFactorUsed: EMISSION_FACTORS_TIER_TYPES | TIER_3_EMISSION_FACTORS; + + // This should be set at later stages for the calculations, but it is not required for the consumer + //emissionFactor: number; + + @ValidateIf( + (o) => o.emissionFactorUsed === EMISSION_FACTORS_TIER_TYPES.TIER_2, + ) + emissionFactorAGB: number; + + emissionFactorSOC: number; + + @ValidateIf((o) => o.emissionFactorUsed === TIER_3_EMISSION_FACTORS.TIER_3) + @IsEnum(PROJECT_SPECIFIC_EMISSION) + projectSpecificEmission: PROJECT_SPECIFIC_EMISSION; + + projectSpecificEmissionFactor: number; +} diff --git a/shared/dtos/custom-projects/create-custom-project.dto.ts b/shared/dtos/custom-projects/create-custom-project-dto.deprecated.ts similarity index 57% rename from shared/dtos/custom-projects/create-custom-project.dto.ts rename to shared/dtos/custom-projects/create-custom-project-dto.deprecated.ts index ba08e89d..8d8c8cd5 100644 --- a/shared/dtos/custom-projects/create-custom-project.dto.ts +++ b/shared/dtos/custom-projects/create-custom-project-dto.deprecated.ts @@ -1,58 +1,33 @@ import { IsEnum, IsNegative, + IsNotEmpty, IsNumber, IsOptional, IsString, + Length, Max, Min, + Validate, + ValidateIf, } from "class-validator"; import { ACTIVITY } from "@shared/entities/activity.enum"; import { ECOSYSTEM } from "@shared/entities/ecosystem.enum"; import { LOSS_RATE_USED } from "@shared/schemas/custom-projects/create-custom-project.schema"; import { EMISSION_FACTORS_TIER_TYPES } from "@shared/entities/carbon-inputs/emission-factors.entity"; - -export class CreateCustomProjectDto { - @IsString() - @Min(3) - @Max(3) - countryCode: string; - - @IsString() - projectName: string; - - @IsEnum(ACTIVITY) - activity: ACTIVITY; - - @IsEnum(ECOSYSTEM) - ecosystem: ECOSYSTEM; - - @IsEnum(LOSS_RATE_USED) - lossRateUsed: LOSS_RATE_USED; - - projectSpecificLossRate?: number; - - emissionFactorUsed: EMISSION_FACTORS_TIER_TYPES; - - emissionFactor: number; - - emissionFactorAGB: number; - - emissionFactorSOC: number; - - projectSpecificEmission: "One emission factor" | "Two emission factors"; - projectSpecificEmissionFactor: number; -} +import { ConservationProjectParamDto } from "@shared/dtos/custom-projects/conservation-project-params.dto"; +import { RestorationProjectParamsDto } from "@shared/dtos/custom-projects/restoration-project-params.dto"; +import { ProjectParamsValidator } from "@shared/dtos/custom-projects/project-params.validator"; +import { Transform, Type } from "class-transformer"; export enum CARBON_REVENUES_TO_COVER { OPEX = "Opex", CAPEX_AND_OPEX = "Capex and Opex", } -export class GeneralCustomProjectParam { +export class CreateCustomProjectDto { @IsString() - @Min(3) - @Max(3) + @Length(3, 3) countryCode: string; @IsString() @@ -65,11 +40,17 @@ export class GeneralCustomProjectParam { ecosystem: ECOSYSTEM; @IsNumber() - projectsSizeHa: number; + projectSizeHa: number; @IsNumber() initialCarbonPriceAssumption: number; @IsEnum(CARBON_REVENUES_TO_COVER) carbonRevenuesToCover: CARBON_REVENUES_TO_COVER; + + @IsNotEmpty() + @Validate(ProjectParamsValidator, { + message: "Invalid project parameters for the selected activity type.", + }) + parameters: ConservationProjectParamDto | RestorationProjectParamsDto; } diff --git a/shared/dtos/custom-projects/project-params.validator.ts b/shared/dtos/custom-projects/project-params.validator.ts new file mode 100644 index 00000000..35beab09 --- /dev/null +++ b/shared/dtos/custom-projects/project-params.validator.ts @@ -0,0 +1,35 @@ +import { + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, + validateSync, +} from "class-validator"; + +import { ACTIVITY } from "@shared/entities/activity.enum"; +import { ConservationProjectParamDto } from "@shared/dtos/custom-projects/conservation-project-params.dto"; +import { RestorationProjectParamsDto } from "@shared/dtos/custom-projects/restoration-project-params.dto"; +import { CreateCustomProjectDto } from "@shared/dtos/custom-projects/create-custom-project-dto.deprecated"; + +@ValidatorConstraint({ name: "ProjectParameterValidator", async: false }) +export class ProjectParamsValidator implements ValidatorConstraintInterface { + validate(value: CreateCustomProjectDto, args: ValidationArguments): boolean { + const object = args.object as any; + const { activity } = object; + + let dto; + if (activity === ACTIVITY.CONSERVATION) { + dto = new ConservationProjectParamDto(); + } else if (activity === ACTIVITY.RESTORATION) { + dto = new RestorationProjectParamsDto(); + } else { + return false; + } + + const validationErrors = validateSync(Object.assign(dto, value)); + return validationErrors.length === 0; + } + + defaultMessage(args: ValidationArguments): string { + return `Invalid parameters for the selected activity type.`; + } +} diff --git a/shared/dtos/custom-projects/restoration-project-params.dto.ts b/shared/dtos/custom-projects/restoration-project-params.dto.ts new file mode 100644 index 00000000..6b7ba31c --- /dev/null +++ b/shared/dtos/custom-projects/restoration-project-params.dto.ts @@ -0,0 +1 @@ +export class RestorationProjectParamsDto {} diff --git a/shared/entities/carbon-inputs/emission-factors.entity.ts b/shared/entities/carbon-inputs/emission-factors.entity.ts index 6c7d0c28..a0c92443 100644 --- a/shared/entities/carbon-inputs/emission-factors.entity.ts +++ b/shared/entities/carbon-inputs/emission-factors.entity.ts @@ -12,6 +12,8 @@ import { import { Country } from "@shared/entities/country.entity"; import { ECOSYSTEM } from "@shared/entities/ecosystem.enum"; +// TODO: The calculations provide a third option Tier 3 which is provided by the user. Do we need to support this in the DB for this entity, +// or would be enough to save the user provided value as metadata for the custom project export enum EMISSION_FACTORS_TIER_TYPES { TIER_2 = "Tier 2 - Country-specific emission factor", TIER_1 = "Tier 1 - Global emission factor",