Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/conservation project calculations #103

Merged
merged 5 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 1 addition & 68 deletions api/src/modules/calculations/calculation.engine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable } 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,8 +7,6 @@ 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 { GetDefaultCostInputsDto } from '@shared/dtos/custom-projects/get-default-cost-inputs.dto';
import { CostInputs } from '@api/modules/custom-projects/dto/project-cost-inputs.dto';

export type GetBaseData = {
countryCode: Country['code'];
Expand All @@ -26,69 +24,4 @@ export type BaseDataForCalculation = {
@Injectable()
export class CalculationEngine {
constructor(private readonly dataSource: DataSource) {}

async getBaseData(filter: GetBaseData): Promise<BaseDataForCalculation> {
return this.dataSource.transaction(async (manager) => {
const defaultAssumptions = await manager
.getRepository(ModelAssumptions)
.find();
const baseData = await manager.getRepository(BaseDataView).findOne({
where: {
countryCode: filter.countryCode,
ecosystem: filter.ecosystem,
activity: filter.activity,
},
});
const baseSize = await manager.getRepository(BaseSize).findOne({
where: { activity: filter.activity, ecosystem: filter.ecosystem },
});
const baseIncrease = await manager
.getRepository(BaseIncrease)
.findOne({ where: { ecosystem: filter.ecosystem } });
return {
defaultAssumptions,
baseData,
baseSize,
baseIncrease,
};
});
}

async getDefaultCostInputs(
dto: GetDefaultCostInputsDto,
): Promise<CostInputs> {
const { countryCode, activity, ecosystem } = dto;
// The coming CostInput has a implementation labor property which does not exist in the BaseDataView entity, so we use a partial type to avoid the error
const costInputs: Partial<CostInputs> = 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',
'implementationLaborPlanting',
'implementationLaborHydrology',
],
});
if (!costInputs) {
throw new NotFoundException(
`Could not find default Cost Inputs for country ${countryCode}, activity ${activity} and ecosystem ${ecosystem}`,
);
}
return costInputs as CostInputs;
}
}
12 changes: 11 additions & 1 deletion api/src/modules/calculations/calculations.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ import { CalculationEngine } from '@api/modules/calculations/calculation.engine'
import { TypeOrmModule } from '@nestjs/typeorm';
import { BaseDataView } from '@shared/entities/base-data.view';
import { DataRepository } from '@api/modules/calculations/data.repository';
import { ModelAssumptions } from '@shared/entities/model-assumptions.entity';
import { BaseIncrease } from '@shared/entities/base-increase.entity';
import { BaseSize } from '@shared/entities/base-size.entity';

@Module({
imports: [TypeOrmModule.forFeature([BaseDataView])],
imports: [
TypeOrmModule.forFeature([
BaseDataView,
ModelAssumptions,
BaseIncrease,
BaseSize,
]),
],
providers: [CalculationEngine, DataRepository],
exports: [CalculationEngine, DataRepository],
})
Expand Down
254 changes: 253 additions & 1 deletion api/src/modules/calculations/cost.calculator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,257 @@
/**
* @description: Once we understand how the cost is calculated, we might move the common logic to this class, and extend it for each specific project type
*/
import { ConservationProjectInput } from '@api/modules/custom-projects/input-factory/conservation-project.input';
import { RestorationProjectInput } from '@api/modules/custom-projects/input-factory/restoration-project.input';
import { BaseSize } from '@shared/entities/base-size.entity';
import { BaseIncrease } from '@shared/entities/base-increase.entity';
import {
CostInputs,
PROJECT_DEVELOPMENT_TYPE,
} from '@api/modules/custom-projects/dto/project-cost-inputs.dto';

export class CostCalculator {}
type CostPlanMap = {
[year: number]: number;
};

// @Injectable()
// export class CostCalculatorToImplement {
// constructor(
// private readonly sequestrationRateCalculator: SequestrationRatesCalculator,
// private readonly revenueProfitCalculator: RevenueProfitCalculator,
// ) {}
//
// private createCostPlan(defaultProjectLength: number): CostPlan {}
//
// calculateConservationProjectCosts(
// projectInput: ConservationProjectInput,
// defaultProjectLength: number,
// ) {}
// }

type CostPlans = Record<keyof CostInputs, CostPlanMap>;

export enum COST_KEYS {
FEASIBILITY_ANALYSIS = 'feasibilityAnalysis',
CONSERVATION_PLANNING_AND_ADMIN = 'conservationPlanningAndAdmin',
DATA_COLLECTION_AND_FIELD_COST = 'dataCollectionAndFieldCost',
COMMUNITY_REPRESENTATION = 'communityRepresentation',
BLUE_CARBON_PROJECT_PLANNING = 'blueCarbonProjectPlanning',
ESTABLISHING_CARBON_RIGHTS = 'establishingCarbonRights',
FINANCING_COST = 'financingCost',
VALIDATION = 'validation',
MONITORING = 'monitoring',
BASELINE_REASSESSMENT = 'baselineReassessment',
MRV = 'mrv',
LONG_TERM_PROJECT_OPERATING_COST = 'longTermProjectOperatingCost',
IMPLEMENTATION_LABOR = 'implementationLabor',
MAINTENANCE = 'maintenance',
}

type ProjectInput = ConservationProjectInput | RestorationProjectInput;

export class CostCalculator {
projectInput: ProjectInput;
defaultProjectLength: number;
startingPointScaling: number;
baseSize: BaseSize;
baseIncrease: BaseIncrease;
capexTotalCostPlan: CostPlanMap;
opexTotalCostPlan: CostPlanMap;
costPlans: CostPlans;
constructor(
projectInput: ProjectInput,
defaultProjectLength: number,
startingPointScaling: number,
baseSize: BaseSize,
baseIncrease: BaseIncrease,
) {
this.projectInput = projectInput;
this.defaultProjectLength = defaultProjectLength;
this.startingPointScaling = startingPointScaling;
this.baseIncrease = baseIncrease;
this.baseSize = baseSize;
}

initializeCostPlans() {
this.capexTotalCostPlan = this.initializeTotalCostPlan(
this.defaultProjectLength,
);
this.opexTotalCostPlan = this.initializeTotalCostPlan(
this.defaultProjectLength,
);
return this;
}

/**
* @description: Initialize the cost plan with the default project length, with 0 costs for each year
* @param defaultProjectLength
*/
private initializeTotalCostPlan(defaultProjectLength: number): CostPlanMap {
const costPlan: CostPlanMap = {};
for (let i = 1; i <= defaultProjectLength; i++) {
costPlan[i] = 0;
}
return costPlan;
}

private createSimpleCostPlan(
totalBaseCost: number,
years = [-4, -3, -2, -1],
) {
const costPlan: CostPlanMap = {};
years.forEach((year) => {
costPlan[year] = totalBaseCost;
});
return costPlan;
}

private getTotalBaseCost(costType: COST_KEYS): number {
const baseCost = this.projectInput.costInputs[costType];
const increasedBy: number = this.baseIncrease[costType];
const sizeDifference =
this.projectInput.projectSizeHa - this.startingPointScaling;
const scalingFactor = Math.max(Math.round(sizeDifference / baseCost), 0);
const totalBaseCost = baseCost + increasedBy * scalingFactor * baseCost;

this.throwIfValueIsNotValid(totalBaseCost, costType);
return totalBaseCost;
}

private feasibilityAnalysisCosts() {
const totalBaseCost = this.getTotalBaseCost(COST_KEYS.FEASIBILITY_ANALYSIS);
const feasibilityAnalysisCostPlan = this.createSimpleCostPlan(
totalBaseCost,

[-4],
);
return feasibilityAnalysisCostPlan;
}

private conservationPlanningAndAdminCosts() {
const totalBaseCost = this.getTotalBaseCost(
COST_KEYS.CONSERVATION_PLANNING_AND_ADMIN,
);
const conservationPlanningAndAdminCostPlan = this.createSimpleCostPlan(
totalBaseCost,
[-4, -3, -2, -1],
);
return conservationPlanningAndAdminCostPlan;
}

private dataCollectionAndFieldCosts() {
const totalBaseCost = this.getTotalBaseCost(
COST_KEYS.DATA_COLLECTION_AND_FIELD_COST,
);
const dataCollectionAndFieldCostPlan = this.createSimpleCostPlan(
totalBaseCost,
[-4, -3, -2],
);
return dataCollectionAndFieldCostPlan;
}

private blueCarbonProjectPlanningCosts() {
const totalBaseCost = this.getTotalBaseCost(
COST_KEYS.BLUE_CARBON_PROJECT_PLANNING,
);
const blueCarbonProjectPlanningCostPlan = this.createSimpleCostPlan(
totalBaseCost,
[-4, -3, -2],
);
return blueCarbonProjectPlanningCostPlan;
}

private communityRepresentationCosts() {
const totalBaseCost = this.getTotalBaseCost(
COST_KEYS.COMMUNITY_REPRESENTATION,
);
const projectDevelopmentType =
this.projectInput.costInputs.otherCommunityCashFlow;
const initialCost =
projectDevelopmentType === PROJECT_DEVELOPMENT_TYPE.DEVELOPMENT
? 0
: totalBaseCost;
const communityRepresentationCostPlan = this.createSimpleCostPlan(
totalBaseCost,
[-4, -3, -2, -1],
);
communityRepresentationCostPlan[-4] = initialCost;
return communityRepresentationCostPlan;
}

private establishingCarbonRightsCosts() {
const totalBaseCost = this.getTotalBaseCost(
COST_KEYS.ESTABLISHING_CARBON_RIGHTS,
);
const establishingCarbonRightsCostPlan = this.createSimpleCostPlan(
totalBaseCost,
[-3, -2, -1],
);
return establishingCarbonRightsCostPlan;
}

private validationCosts() {
const totalBaseCost = this.getTotalBaseCost(COST_KEYS.VALIDATION);
const validationCostPlan = this.createSimpleCostPlan(totalBaseCost, [-1]);
return validationCostPlan;
}

private implementationLaborCosts() {
// TODO: This needs sequestration credits calculator
// const totalBaseCost = this.getTotalBaseCost(COST_KEYS.IMPLEMENTATION_LABOR);
// const implementationLaborCostPlan = this.createSimpleCostPlan(
// totalBaseCost,
// [-1],
// );
// return implementationLaborCostPlan;
console.warn('Implementation labor costs not implemented');
return this.createSimpleCostPlan(0, [-1]);
}

private calculateMonitoringCosts() {
const totalBaseCost = this.getTotalBaseCost(COST_KEYS.MONITORING);
const monitoringCostPlan: CostPlanMap = {};
// TODO: How is this plan different from the others?
for (let year = -4; year <= this.defaultProjectLength; year++) {
if (year !== 0) {
monitoringCostPlan[year] =
year >= 1 && year <= this.projectInput.modelAssumptions.projectLength
? totalBaseCost
: 0;
}
}
return monitoringCostPlan;
}

private calculateMaintenanceCosts() {
const totalBaseCost = this.getTotalBaseCost(COST_KEYS.MAINTENANCE);
console.log('totalBaseCost', totalBaseCost);
// TODO: We need Maintenance and MaintenanceDuration values, which are present in BaseDataView but not in CostInputs.
// Are these actually CostInputs? Can be overriden? If not, we need to change the approach, and have CostInputs and BaseData values as well
const maintenanceCostPlan: CostPlanMap = {};
return this.implementationLaborCosts();
}

private throwIfValueIsNotValid(value: number, costKey: COST_KEYS): void {
if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
console.error(`Invalid number: ${value} produced for ${costKey}`);
throw new Error(`Invalid number: ${value} produced for ${costKey}`);
}
}

calculateCosts() {
// @ts-ignore
this.costPlans = {
// feasibilityAnalysis: this.feasibilityAnalysisCosts(),
// conservationPlanningAndAdmin: this.conservationPlanningAndAdminCosts(),
// dataCollectionAndFieldCost: this.dataCollectionAndFieldCosts(),
// blueCarbonProjectPlanning: this.blueCarbonProjectPlanningCosts(),
// communityRepresentation: this.communityRepresentationCosts(),
// establishingCarbonRights: this.establishingCarbonRightsCosts(),
// validation: this.validationCosts(),
// implementationLabor: this.implementationLaborCosts(),
// monitoring: this.calculateMonitoringCosts(),
maintenance: this.calculateMaintenanceCosts(),
};
}
}
Loading
Loading