diff --git a/api/src/modules/calculations/cost.calculator.ts b/api/src/modules/calculations/cost.calculator.ts index a62498b2..d880d0c9 100644 --- a/api/src/modules/calculations/cost.calculator.ts +++ b/api/src/modules/calculations/cost.calculator.ts @@ -1,14 +1,14 @@ /** * @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 { Injectable } from '@nestjs/common'; import { ConservationProjectInput } from '@api/modules/custom-projects/input-factory/conservation-project.input'; -import { SequestrationRatesCalculator } from '@api/modules/calculations/sequestration-rate.calculator'; -import { RevenueProfitCalculator } from '@api/modules/calculations/revenue-profit.calculators'; 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 } from '@api/modules/custom-projects/dto/project-cost-inputs.dto'; +import { + CostInputs, + PROJECT_DEVELOPMENT_TYPE, +} from '@api/modules/custom-projects/dto/project-cost-inputs.dto'; type CostPlanMap = { [year: number]: number; @@ -44,6 +44,8 @@ export enum COST_KEYS { BASELINE_REASSESSMENT = 'baselineReassessment', MRV = 'mrv', LONG_TERM_PROJECT_OPERATING_COST = 'longTermProjectOperatingCost', + IMPLEMENTATION_LABOR = 'implementationLabor', + MAINTENANCE = 'maintenance', } type ProjectInput = ConservationProjectInput | RestorationProjectInput; @@ -78,6 +80,7 @@ export class CostCalculator { this.opexTotalCostPlan = this.initializeTotalCostPlan( this.defaultProjectLength, ); + return this; } /** @@ -110,21 +113,128 @@ export class CostCalculator { 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 calculateFeasibilityAnalysisCosts() { + 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}`); } } @@ -132,7 +242,16 @@ export class CostCalculator { calculateCosts() { // @ts-ignore this.costPlans = { - feasibilityAnalysis: this.calculateFeasibilityAnalysisCosts(), + // 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(), }; } } diff --git a/api/src/modules/custom-projects/custom-projects.service.ts b/api/src/modules/custom-projects/custom-projects.service.ts index a5de41e6..f527b48b 100644 --- a/api/src/modules/custom-projects/custom-projects.service.ts +++ b/api/src/modules/custom-projects/custom-projects.service.ts @@ -45,7 +45,6 @@ export class CustomProjectsService extends AppBaseService< ); // TODO: Don't know where this values should come from. i.e default project length comes from the assumptions based on activity? In the python calcs, the same // value is used regardless of the activity. - const DEFAULT_PROJECT_LENGTH = 40; const CONSERVATION_STARTING_POINT_SCALING = 500; const RESTORATION_STARTING_POINT_SCALING = 20000; @@ -58,8 +57,7 @@ export class CustomProjectsService extends AppBaseService< baseIncrease, ); - calculator.initializeCostPlans(); - calculator.calculateCosts(); + calculator.initializeCostPlans().calculateCosts(); return calculator.costPlans; } diff --git a/shared/entities/base-data.view.ts b/shared/entities/base-data.view.ts index c3da8859..91f894e9 100644 --- a/shared/entities/base-data.view.ts +++ b/shared/entities/base-data.view.ts @@ -1,4 +1,5 @@ import { ValueTransformer, ViewColumn, ViewEntity } from "typeorm"; +import { PROJECT_DEVELOPMENT_TYPE } from "@api/modules/custom-projects/dto/project-cost-inputs.dto"; export const decimalTransformer: ValueTransformer = { to: (value: number | null) => value, @@ -278,7 +279,6 @@ export class BaseDataView { @ViewColumn({ name: "other_community_cash_flow", - transformer: decimalTransformer, }) - otherCommunityCashFlow: string; + otherCommunityCashFlow: string | PROJECT_DEVELOPMENT_TYPE; }