Skip to content

Commit

Permalink
basic endpoint to get default cost inputs for custom project creation
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Nov 11, 2024
1 parent 954df2b commit 155f5a4
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 44 deletions.
75 changes: 39 additions & 36 deletions api/src/modules/calculations/calculation.engine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { ModelAssumptions } from '@shared/entities/model-assumptions.entity';
import { Country } from '@shared/entities/country.entity';
Expand All @@ -7,7 +7,8 @@ import { ACTIVITY } from '@shared/entities/activity.enum';
import { BaseDataView } from '@shared/entities/base-data.view';
import { BaseSize } from '@shared/entities/base-size.entity';
import { BaseIncrease } from '@shared/entities/base-increase.entity';
import { EMISSION_FACTORS_TIER_TYPES } from '@shared/entities/carbon-inputs/emission-factors.entity';
import { DefaultCostInputsDto } from '@shared/dtos/custom-projects/default-cost-inputs.dto';
import { GetDefaultCostInputsDto } from '@shared/dtos/custom-projects/get-default-cost-inputs.dto';

export type GetBaseData = {
countryCode: Country['code'];
Expand Down Expand Up @@ -53,38 +54,40 @@ export class CalculationEngine {
});
}

// buildProject(data: any) {
// const {
// countryCode,
// ecosystem,
// activity,
// activitySubType,
// baseData,
// assumptions,
// plantingSuccessRate,
// sequestrationRateUsed,
// projectSpecificSequestrationRate,
// } = data;
// const carbonPrice = 20;
// const carbonRevenuesToCover = 'Opex';
// const lossRateUsed = 'project-specific';
// const projectSpecificLossRate = 0.001;
// const emissionFactorUsed = EMISSION_FACTORS_TIER_TYPES.TIER_2;
// return new ProjectCalculationBuilder({
// countryCode,
// ecosystem,
// activity,
// activitySubType,
// carbonPrice,
// carbonRevenuesToCover,
// baseData,
// assumptions,
// plantingSuccessRate,
// sequestrationRateUsed,
// projectSpecificSequestrationRate,
// projectSpecificLossRate,
// lossRateUsed,
// emissionFactorUsed,
// });
// }
async getDefaultCostInputs(
dto: GetDefaultCostInputsDto,
): Promise<DefaultCostInputsDto> {
const { countryCode, activity, ecosystem } = dto;
// TODO: In the UI we have "implementation labor", which in the calculations we actually set it as value, but in the base data view we have
// this property as implementation_labor_activity_subtype (hydrology etc). Check with science!
const costInputs: DefaultCostInputsDto = await this.dataSource
.getRepository(BaseDataView)
.findOne({
where: { countryCode, activity, ecosystem },
select: [
'feasibilityAnalysis',
'conservationPlanningAndAdmin',
'dataCollectionAndFieldCost',
'communityRepresentation',
'blueCarbonProjectPlanning',
'establishingCarbonRights',
'validation',
'implementationLaborHybrid',
'monitoring',
'maintenance',
'communityBenefitSharingFund',
'carbonStandardFees',
'baselineReassessment',
'mrv',
'longTermProjectOperatingCost',
'financingCost',
],
});
if (!costInputs) {
throw new NotFoundException(
`Could not find default Cost Inputs for country ${countryCode}, activity ${activity} and ecosystem ${ecosystem}`,
);
}
return costInputs;
}
}
11 changes: 11 additions & 0 deletions api/src/modules/custom-projects/custom-projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export class CustomProjectsController {
);
}

@TsRestHandler(customProjectContract.getDefaultCostInputs)
async getCostInputs(): Promise<ControllerResponse> {
return tsRestHandler(
customProjectContract.getDefaultCostInputs,
async ({ query }) => {
const data = await this.customProjects.getDefaultCostInputs(query);
return { body: { data }, status: HttpStatus.OK };
},
);
}

@TsRestHandler(customProjectContract.createCustomProject)
async create(
@Body(new ValidationPipe({ enableDebugMessages: true, transform: true }))
Expand Down
8 changes: 8 additions & 0 deletions api/src/modules/custom-projects/custom-projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { CalculationEngine } from '@api/modules/calculations/calculation.engine'
import { CustomProjectFactory } from '@api/modules/custom-projects/custom-project.factory';
import { EMISSION_FACTORS_TIER_TYPES } from '@shared/entities/carbon-inputs/emission-factors.entity';
import { ConservationCostCalculator } from '@api/modules/calculations/conservation-cost.calculator';
import { DefaultCostInputsDto } from '@shared/dtos/custom-projects/default-cost-inputs.dto';
import { GetDefaultCostInputsDto } from '@shared/dtos/custom-projects/get-default-cost-inputs.dto';

@Injectable()
export class CustomProjectsService extends AppBaseService<
Expand Down Expand Up @@ -62,4 +64,10 @@ export class CustomProjectsService extends AppBaseService<
yearBreakdown: calculator.getYearlyCostBreakdown(),
};
}

async getDefaultCostInputs(
dto: GetDefaultCostInputsDto,
): Promise<DefaultCostInputsDto> {
return this.calculationEngine.getDefaultCostInputs(dto);
}
}
32 changes: 32 additions & 0 deletions api/test/integration/custom-projects/custom-projects-setup.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { TestManager } from '../../utils/test-manager';
import { customProjectContract } from '@shared/contracts/custom-projects.contract';
import { ECOSYSTEM } from '@shared/entities/ecosystem.enum';
import { ACTIVITY } from '@shared/entities/activity.enum';

describe('Create Custom Projects - Setup', () => {
let testManager: TestManager;
Expand Down Expand Up @@ -32,4 +34,34 @@ describe('Create Custom Projects - Setup', () => {

expect(response.body.data).toHaveLength(18);
});

test('Should return default cost inputs given required filters', async () => {
const response = await testManager
.request()
.get(customProjectContract.getDefaultCostInputs.path)
.query({
countryCode: 'IND',
ecosystem: ECOSYSTEM.MANGROVE,
activity: ACTIVITY.CONSERVATION,
});

expect(response.body.data).toMatchObject({
feasibilityAnalysis: '50000',
conservationPlanningAndAdmin: '166766.66666666666',
dataCollectionAndFieldCost: '26666.666666666668',
communityRepresentation: '67633.33333333333',
blueCarbonProjectPlanning: '100000',
establishingCarbonRights: '46666.666666666664',
financingCost: '0.05',
validation: '50000',
implementationLaborHybrid: 'NaN',
monitoring: '8400',
maintenance: '0.0833',
carbonStandardFees: '0.2',
communityBenefitSharingFund: '0.5',
baselineReassessment: '40000',
mrv: '75000',
longTermProjectOperatingCost: '22200',
});
});
});
18 changes: 10 additions & 8 deletions shared/contracts/custom-projects.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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 "@api/modules/custom-projects/dto/create-custom-project-dto";
import { DefaultCostInputsDto } from "@shared/dtos/custom-projects/default-cost-inputs.dto";
import { GetDefaultCostInputsSchema } from "@shared/schemas/custom-projects/get-cost-inputs.schema";

// TODO: This is a scaffold. We need to define types for responses, zod schemas for body and query param validation etc.

Expand All @@ -26,6 +28,14 @@ export const customProjectContract = contract.router({
},
summary: "Get default model assumptions",
},
getDefaultCostInputs: {
method: "GET",
path: "/custom-projects/cost-inputs",
responses: {
200: contract.type<ApiResponse<DefaultCostInputsDto>>(),
},
query: GetDefaultCostInputsSchema,
},
createCustomProject: {
method: "POST",
path: "/custom-projects",
Expand All @@ -34,14 +44,6 @@ export const customProjectContract = contract.router({
},
body: contract.type<CreateCustomProjectDto>(),
},
// createConservationCustomProject: {
// method: "POST",
// path: "/custom-projects/conservation",
// responses: {
// 201: contract.type<ApiResponse<CustomProject>>(),
// },
// body: contract.type<CreateCustomProjectDto>(),
// },
});

// TODO: Due to dificulties crafting a deeply nested conditional schema, I will go forward with nestjs custom validation pipe for now
21 changes: 21 additions & 0 deletions shared/dtos/custom-projects/default-cost-inputs.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { BaseDataView } from "@shared/entities/base-data.view";

export type DefaultCostInputsDto = Pick<
BaseDataView,
| "feasibilityAnalysis"
| "conservationPlanningAndAdmin"
| "dataCollectionAndFieldCost"
| "communityRepresentation"
| "blueCarbonProjectPlanning"
| "establishingCarbonRights"
| "validation"
| "implementationLaborHybrid"
| "monitoring"
| "maintenance"
| "communityBenefitSharingFund"
| "carbonStandardFees"
| "baselineReassessment"
| "mrv"
| "longTermProjectOperatingCost"
| "financingCost"
>;
6 changes: 6 additions & 0 deletions shared/dtos/custom-projects/get-default-cost-inputs.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from "zod";
import { GetDefaultCostInputsSchema } from "@shared/schemas/custom-projects/get-cost-inputs.schema";

export type GetDefaultCostInputsDto = z.infer<
typeof GetDefaultCostInputsSchema
>;
9 changes: 9 additions & 0 deletions shared/schemas/custom-projects/get-cost-inputs.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";
import { ECOSYSTEM } from "@shared/entities/ecosystem.enum";
import { ACTIVITY } from "@shared/entities/activity.enum";

export const GetDefaultCostInputsSchema = z.object({
countryCode: z.string().min(3).max(3),
ecosystem: z.nativeEnum(ECOSYSTEM),
activity: z.nativeEnum(ACTIVITY),
});

0 comments on commit 155f5a4

Please sign in to comment.