Skip to content

Commit

Permalink
add general custom project validation
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Nov 8, 2024
1 parent 1ff554b commit e989e65
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 205 deletions.
9 changes: 5 additions & 4 deletions api/src/modules/custom-projects/custom-projects.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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';
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 {
Expand Down Expand Up @@ -44,13 +44,14 @@ export class CustomProjectsController {

@TsRestHandler(customProjectContract.createCustomProject)
async create(
@Body()
@Body(new ValidationPipe({ enableDebugMessages: true }))
dto: CreateCustomProjectDto,
): Promise<ControllerResponse> {
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 },
Expand Down
7 changes: 1 addition & 6 deletions api/src/modules/custom-projects/custom-projects.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,14 @@ 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: [
TypeOrmModule.forFeature([CustomProject]),
CountriesModule,
CalculationsModule,
],
providers: [
CustomProjectsService,
CustomProjectFactory,
CreateCustomProjectValidator,
],
providers: [CustomProjectsService, CustomProjectFactory],
controllers: [CustomProjectsController],
})
export class CustomProjectsModule {}
2 changes: 1 addition & 1 deletion api/src/modules/custom-projects/custom-projects.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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<any, any>;
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<any, any>;
// 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;
// }
// }
// }
Original file line number Diff line number Diff line change
@@ -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<any, any>;
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<any, any>;
// 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();
// }
// }
// }
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
];
Loading

0 comments on commit e989e65

Please sign in to comment.