From 7d8af8ad53b6954501aa86829c7984fe0d02fb20 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 28 Nov 2024 07:25:56 +0100 Subject: [PATCH] add types and refactor entity --- .../DEPRECATED-revenue-profit.calculators.ts | 116 ------ ...EPRECATED-sequestration-rate.calculator.ts | 393 ------------------ .../modules/calculations/cost.calculator.ts | 29 +- .../calculations/revenue-profit.calculator.ts | 6 +- .../sequestration-rate.calculator.ts | 6 +- .../modules/calculations/summary.generator.ts | 190 --------- .../custom-projects.controller.ts | 19 +- .../custom-projects.service.ts | 65 +-- .../dto/custom-project-snapshot.dto.ts | 3 +- .../custom-project-output.dto.ts | 93 +++++ shared/entities/custom-project.entity.ts | 18 +- 11 files changed, 181 insertions(+), 757 deletions(-) delete mode 100644 api/src/modules/calculations/DEPRECATED-revenue-profit.calculators.ts delete mode 100644 api/src/modules/calculations/DEPRECATED-sequestration-rate.calculator.ts delete mode 100644 api/src/modules/calculations/summary.generator.ts create mode 100644 shared/dtos/custom-projects/custom-project-output.dto.ts diff --git a/api/src/modules/calculations/DEPRECATED-revenue-profit.calculators.ts b/api/src/modules/calculations/DEPRECATED-revenue-profit.calculators.ts deleted file mode 100644 index 59637758..00000000 --- a/api/src/modules/calculations/DEPRECATED-revenue-profit.calculators.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { ConservationProject } from '@api/modules/custom-projects/conservation.project'; -import { SequestrationRatesCalculatorDEPRECATED } from '@api/modules/calculations/DEPRECATED-sequestration-rate.calculator'; -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class RevenueProfitCalculatorDEPRECATED { - private project: ConservationProject; - private sequestrationCreditsCalculator: SequestrationRatesCalculatorDEPRECATED; - private projectLength: number; - private defaultProjectLength: number; - - constructor( - project: ConservationProject, - projectLength: number, - defaultProjectLength: number, - sequestrationCreditsCalculator: SequestrationRatesCalculatorDEPRECATED, - ) { - this.project = project; - this.sequestrationCreditsCalculator = sequestrationCreditsCalculator; - this.projectLength = projectLength; - this.defaultProjectLength = defaultProjectLength; - } - - public calculateEstimatedRevenue(): { [year: number]: number } { - const estimatedRevenuePlan: { [year: number]: number } = {}; - - for (let year = -4; year <= this.defaultProjectLength; year++) { - if (year !== 0) { - estimatedRevenuePlan[year] = 0; - } - } - - const estimatedCreditsIssued = - this.sequestrationCreditsCalculator.calculateEstimatedCreditsIssued(); - - for (const year in estimatedRevenuePlan) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - if (yearNum < -1) { - estimatedRevenuePlan[yearNum] = 0; - } else { - estimatedRevenuePlan[yearNum] = - estimatedCreditsIssued[yearNum] * - this.project.carbonPrice * - Math.pow(1 + this.project.carbonPriceIncrease, yearNum); - } - } else { - estimatedRevenuePlan[yearNum] = 0; - } - } - - return estimatedRevenuePlan; - } - - public calculateAnnualNetCashFlow( - capexTotalCostPlan: { [year: number]: number }, - opexTotalCostPlan: { [year: number]: number }, - ): { [year: number]: number } { - const estimatedRevenue = this.calculateEstimatedRevenue(); - const costPlans = { - capexTotal: { ...capexTotalCostPlan }, - opexTotal: { ...opexTotalCostPlan }, - }; - - for (const key in costPlans) { - for (const year in costPlans[key]) { - costPlans[key][year] = -costPlans[key][year]; - } - } - - const totalCostPlan: { [year: number]: number } = {}; - for (const year of new Set([ - ...Object.keys(costPlans.capexTotal), - ...Object.keys(costPlans.opexTotal), - ])) { - const yearNum = Number(year); - totalCostPlan[yearNum] = - (costPlans.capexTotal[yearNum] || 0) + - (costPlans.opexTotal[yearNum] || 0); - } - - const annualNetCashFlow: { [year: number]: number } = {}; - for (let year = -4; year <= this.projectLength; year++) { - if (year !== 0) { - annualNetCashFlow[year] = - estimatedRevenue[year] + (totalCostPlan[year] || 0); - } - } - - return annualNetCashFlow; - } - - public calculateAnnualNetIncome(opexTotalCostPlan: { - [year: number]: number; - }): { [year: number]: number } { - const costPlans = { - opexTotal: { ...opexTotalCostPlan }, - }; - - for (const year in costPlans.opexTotal) { - costPlans.opexTotal[year] = -costPlans.opexTotal[year]; - } - - const estimatedRevenue = this.calculateEstimatedRevenue(); - - const annualNetIncome: { [year: number]: number } = {}; - for (let year = -4; year <= this.projectLength; year++) { - if (year !== 0) { - annualNetIncome[year] = - estimatedRevenue[year] + (costPlans.opexTotal[year] || 0); - } - } - - return annualNetIncome; - } -} diff --git a/api/src/modules/calculations/DEPRECATED-sequestration-rate.calculator.ts b/api/src/modules/calculations/DEPRECATED-sequestration-rate.calculator.ts deleted file mode 100644 index 15e3c92a..00000000 --- a/api/src/modules/calculations/DEPRECATED-sequestration-rate.calculator.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { ConservationProject } from '@api/modules/custom-projects/conservation.project'; -import { - ACTIVITY, - RESTORATION_ACTIVITY_SUBTYPE, -} from '@shared/entities/activity.enum'; - -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class SequestrationRatesCalculatorDEPRECATED { - // TODO: This should accept both Conservation and Restoration - private project: ConservationProject; - private projectLength: number; - private defaultProjectLength: number; - private activity: ACTIVITY; - private activitySubType: RESTORATION_ACTIVITY_SUBTYPE; - // TODO: !!! These only apply for Restoration projects, so we need to somehow pass the value from the project or calculator, not sure yet - private restorationRate: number = 250; - private sequestrationRate: number = 0.5; - - constructor( - project: ConservationProject, - projectLength: number, - defaultProjectLength: number, - activity: ACTIVITY, - activitySubType: RESTORATION_ACTIVITY_SUBTYPE, - ) { - this.project = project; - // TODO: Project Length comes from constant and is set based on the activity - this.projectLength = projectLength; - this.defaultProjectLength = defaultProjectLength; - this.activity = activity; - this.activitySubType = activitySubType; - } - - public calculateProjectedLoss(): { [year: number]: number } { - if (this.project.activity !== ACTIVITY.CONSERVATION) { - throw new Error( - 'Cumulative loss rate can only be calculated for conservation projects.', - ); - } - const lossRate = this.project.lossRate; - const projectSizeHa = this.project.projectSizeHa; - const annualProjectedLoss: { [year: number]: number } = {}; - - for (let year = -1; year <= this.defaultProjectLength; year++) { - if (year !== 0) { - annualProjectedLoss[year] = 0; - } - } - - for (const year in annualProjectedLoss) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - if (yearNum === -1) { - annualProjectedLoss[yearNum] = projectSizeHa; - } else { - annualProjectedLoss[yearNum] = - projectSizeHa * Math.pow(1 + lossRate, yearNum); - } - } else { - annualProjectedLoss[yearNum] = 0; - } - } - - return annualProjectedLoss; - } - - public calculateAnnualAvoidedLoss(): { [year: number]: number } { - if (this.project.activity !== ACTIVITY.CONSERVATION) { - throw new Error( - 'Cumulative loss rate can only be calculated for conservation projects.', - ); - } - - const projectedLoss = this.calculateProjectedLoss(); - const annualAvoidedLoss: { [year: number]: number } = {}; - - for (let year = 1; year <= this.defaultProjectLength; year++) { - annualAvoidedLoss[year] = 0; - } - - for (const year in annualAvoidedLoss) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - if (yearNum === 1) { - annualAvoidedLoss[yearNum] = - (projectedLoss[yearNum] - projectedLoss[-1]) * -1; - } else { - annualAvoidedLoss[yearNum] = - (projectedLoss[yearNum] - projectedLoss[yearNum - 1]) * -1; - } - } else { - annualAvoidedLoss[yearNum] = 0; - } - } - - return annualAvoidedLoss; - } - - public calculateCumulativeLossRate(): { [year: number]: number } { - if (this.project.activity !== ACTIVITY.CONSERVATION) { - throw new Error( - 'Cumulative loss rate can only be calculated for conservation projects.', - ); - } - - const cumulativeLossRate: { [year: number]: number } = {}; - const annualAvoidedLoss = this.calculateAnnualAvoidedLoss(); - - for (let year = 1; year <= this.defaultProjectLength; year++) { - cumulativeLossRate[year] = 0; - } - - for (const year in cumulativeLossRate) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - if (yearNum === 1) { - cumulativeLossRate[yearNum] = annualAvoidedLoss[yearNum]; - } else { - cumulativeLossRate[yearNum] = - annualAvoidedLoss[yearNum] + cumulativeLossRate[yearNum - 1]; - } - } else { - cumulativeLossRate[yearNum] = 0; - } - } - - return cumulativeLossRate; - } - - public calculateCumulativeLossRateIncorporatingSOCReleaseTime(): { - [year: number]: number; - } { - if (this.project.activity !== ACTIVITY.CONSERVATION) { - throw new Error( - 'Cumulative loss rate can only be calculated for conservation projects.', - ); - } - - const cumulativeLossRateIncorporatingSOC: { [year: number]: number } = {}; - const cumulativeLoss = this.calculateCumulativeLossRate(); - - // Inicializamos el plan con años de 1 a defaultProjectLength - for (let year = 1; year <= this.defaultProjectLength; year++) { - cumulativeLossRateIncorporatingSOC[year] = 0; - } - - // Calculamos la tasa de pérdida acumulativa incorporando el tiempo de liberación de SOC - for (const year in cumulativeLossRateIncorporatingSOC) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - if (yearNum > this.project.soilOrganicCarbonReleaseLength) { - const offsetValue = - cumulativeLoss[ - yearNum - this.project.soilOrganicCarbonReleaseLength - ]; - cumulativeLossRateIncorporatingSOC[yearNum] = - cumulativeLoss[yearNum] - offsetValue; - } else { - cumulativeLossRateIncorporatingSOC[yearNum] = cumulativeLoss[yearNum]; - } - } else { - cumulativeLossRateIncorporatingSOC[yearNum] = 0; - } - } - - return cumulativeLossRateIncorporatingSOC; - } - - public calculateBaselineEmissions(): { [year: number]: number } { - if (this.project.activity !== ACTIVITY.CONSERVATION) { - throw new Error( - 'Baseline emissions can only be calculated for conservation projects.', - ); - } - - const sequestrationRateTier1 = - this.project.costInputs.tier1SequestrationRate; - let emissionFactor: number | undefined; - let emissionFactorAGB: number | undefined; - let emissionFactorSOC: number | undefined; - - if (this.project.emissionFactorUsed === 'Tier 1 - Global emission factor') { - emissionFactor = this.project.emissionFactor; - } else if ( - this.project.emissionFactorUsed === - 'Tier 2 - Country-specific emission factor' - ) { - emissionFactorAGB = this.project.emissionFactorAGB; - emissionFactorSOC = this.project.emissionFactorSOC; - } else { - emissionFactorAGB = this.project.emissionFactorAGB; - emissionFactorSOC = this.project.emissionFactorSOC; - } - - const baselineEmissionPlan: { [year: number]: number } = {}; - const cumulativeLoss = this.calculateCumulativeLossRate(); - const cumulativeLossRateIncorporatingSOC = - this.calculateCumulativeLossRateIncorporatingSOCReleaseTime(); - const annualAvoidedLoss = this.calculateAnnualAvoidedLoss(); - - for (let year = 1; year <= this.defaultProjectLength; year++) { - baselineEmissionPlan[year] = 0; - } - - for (const year in baselineEmissionPlan) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - if ( - this.project.emissionFactorUsed !== 'Tier 1 - Global emission factor' - ) { - baselineEmissionPlan[yearNum] = - emissionFactorAGB! * annualAvoidedLoss[yearNum] + - cumulativeLossRateIncorporatingSOC[yearNum] * emissionFactorSOC! + - sequestrationRateTier1 * cumulativeLoss[yearNum]; - } else { - baselineEmissionPlan[yearNum] = - cumulativeLoss[yearNum] * emissionFactor! + - sequestrationRateTier1 * cumulativeLoss[yearNum]; - } - } else { - baselineEmissionPlan[yearNum] = 0; - } - } - - return baselineEmissionPlan; - } - - public calculateNetEmissionsReductions(): { [year: number]: number } { - const netEmissionReductionsPlan: { [year: number]: number } = {}; - - for (let year = -1; year <= this.defaultProjectLength; year++) { - if (year !== 0) { - netEmissionReductionsPlan[year] = 0; - } - } - - if (this.project.activity === ACTIVITY.CONSERVATION) { - return this.calculateConservationEmissions(netEmissionReductionsPlan); - } else if (this.project.activity === ACTIVITY.RESTORATION) { - return this.calculateRestorationEmissions(netEmissionReductionsPlan); - } - - return netEmissionReductionsPlan; - } - - private calculateRestorationEmissions(netEmissionReductionsPlan: { - [year: number]: number; - }): { [year: number]: number } { - const areaRestoredOrConservedPlan = this.calculateAreaRestoredOrConserved(); - const sequestrationRate = this.sequestrationRate; - - for (const year in netEmissionReductionsPlan) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - if (yearNum === -1) { - netEmissionReductionsPlan[yearNum] = 0; - } else if (this.activitySubType === 'Planting') { - netEmissionReductionsPlan[yearNum] = this.calculatePlantingEmissions( - areaRestoredOrConservedPlan, - sequestrationRate, - yearNum, - ); - } else { - netEmissionReductionsPlan[yearNum] = - areaRestoredOrConservedPlan[yearNum - 1] * sequestrationRate; - } - } else { - netEmissionReductionsPlan[yearNum] = 0; - } - } - - return netEmissionReductionsPlan; - } - - private calculatePlantingEmissions( - areaRestoredOrConservedPlan: { [year: number]: number }, - sequestrationRate: number, - year: number, - ): number { - const plantingSuccessRate = this.project.plantingSuccessRate; - - if (year === 1) { - return ( - areaRestoredOrConservedPlan[year - 2] * - sequestrationRate * - plantingSuccessRate - ); - } - - return ( - areaRestoredOrConservedPlan[year - 1] * - sequestrationRate * - plantingSuccessRate - ); - } - - public calculateAreaRestoredOrConserved(): { [year: number]: number } { - const cumulativeHaRestoredInYear: { [year: number]: number } = {}; - - for (let year = -1; year <= this.defaultProjectLength; year++) { - if (year !== 0) { - cumulativeHaRestoredInYear[year] = 0; - } - } - - for (const year in cumulativeHaRestoredInYear) { - const yearNum = Number(year); - if (yearNum > this.projectLength) { - cumulativeHaRestoredInYear[yearNum] = 0; - } else if (this.activity === ACTIVITY.RESTORATION) { - cumulativeHaRestoredInYear[yearNum] = Math.min( - this.project.restorationRate, - this.project.projectSizeHa, - ); - } else { - cumulativeHaRestoredInYear[yearNum] = this.project.projectSizeHa; - } - } - - return cumulativeHaRestoredInYear; - } - - public calculateImplementationLabor(): { [year: number]: number } { - const baseCost = - this.activity === ACTIVITY.RESTORATION - ? this.project.costInputs.implementationLabor - : 0; - const areaRestoredOrConservedPlan = this.calculateAreaRestoredOrConserved(); - const implementationLaborCostPlan: { [year: number]: number } = {}; - - for (let year = -4; year <= this.defaultProjectLength; year++) { - if (year !== 0) { - implementationLaborCostPlan[year] = 0; - } - } - - for (let year = 1; year <= this.projectLength; year++) { - const laborCost = - baseCost * - (areaRestoredOrConservedPlan[year] - - (areaRestoredOrConservedPlan[year - 1] || 0)); - implementationLaborCostPlan[year] = laborCost; - } - - return implementationLaborCostPlan; - } - private calculateConservationEmissions(netEmissionReductionsPlan: { - [year: number]: number; - }): { [year: number]: number } { - const baselineEmissions = this.calculateBaselineEmissions(); - - for (const year in netEmissionReductionsPlan) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - if (yearNum === -1) { - netEmissionReductionsPlan[yearNum] = 0; - } else { - netEmissionReductionsPlan[yearNum] = baselineEmissions[yearNum]; - } - } else { - netEmissionReductionsPlan[yearNum] = 0; - } - } - - return netEmissionReductionsPlan; - } - - public calculateEstimatedCreditsIssued(): { [year: number]: number } { - const estCreditsIssuedPlan: { [year: number]: number } = {}; - - for (let year = -1; year <= this.defaultProjectLength; year++) { - if (year !== 0) { - estCreditsIssuedPlan[year] = 0; - } - } - - const netEmissionsReductions = this.calculateNetEmissionsReductions(); - - for (const year in estCreditsIssuedPlan) { - const yearNum = Number(year); - if (yearNum <= this.projectLength) { - estCreditsIssuedPlan[yearNum] = - netEmissionsReductions[yearNum] * (1 - this.project.buffer); - } else { - estCreditsIssuedPlan[yearNum] = 0; - } - } - - return estCreditsIssuedPlan; - } -} diff --git a/api/src/modules/calculations/cost.calculator.ts b/api/src/modules/calculations/cost.calculator.ts index 665f507c..fb00dbe7 100644 --- a/api/src/modules/calculations/cost.calculator.ts +++ b/api/src/modules/calculations/cost.calculator.ts @@ -13,23 +13,18 @@ import { RevenueProfitCalculator } from '@api/modules/calculations/revenue-profi import { SequestrationRateCalculator } from '@api/modules/calculations/sequestration-rate.calculator'; import { parseInt, sum } from 'lodash'; import { irr } from 'financial'; - -export type CostPlanMap = { - [year: number]: number; -}; +import { + CostPlanMap, + CustomProjectCostDetails, + CustomProjectSummary, + YearlyBreakdown, +} from '@shared/dtos/custom-projects/custom-project-output.dto'; export type CostPlans = Record< keyof OverridableCostInputs | string, CostPlanMap >; -export type YearlyBreakdown = { - costName: keyof OverridableCostInputs; - totalCost: number; - totalNPV: number; - costValues: CostPlanMap; -}; - // TODO: Strongly type this to bound it to existing types export enum COST_KEYS { FEASIBILITY_ANALYSIS = 'feasibilityAnalysis', @@ -59,8 +54,6 @@ export class CostCalculator { capexTotalCostPlan: CostPlanMap; opexTotalCostPlan: CostPlanMap; costPlans: CostPlans; - restOfStuff: Record; - totalCapexNPV: number; totalOpexNPV: number; revenueProfitCalculator: RevenueProfitCalculator; sequestrationRateCalculator: SequestrationRateCalculator; @@ -181,7 +174,7 @@ export class CostCalculator { }; } - getSummary(stuff: any): any { + getSummary(stuff: any): CustomProjectSummary { const { costPerTCO2e, costPerHa, @@ -201,7 +194,7 @@ export class CostCalculator { totalCommunityBenefitSharingFund, } = stuff; return { - '$/tCO2e (total cost, NPV': costPerTCO2e, + '$/tCO2e (total cost, NPV)': costPerTCO2e, '$/ha': costPerHa, 'NPV covering cost': npvCoveringCosts, 'Leftover after OpEx / total cost': null, @@ -221,13 +214,17 @@ export class CostCalculator { }; } - getCostDetails(stuff: any): any { + getCostDetails(stuff: any): { + total: CustomProjectCostDetails; + npv: CustomProjectCostDetails; + } { const discountRate = this.projectInput.assumptions.discountRate; const { totalOpex, totalCapex, totalCapexNPV, totalOpexNPV, totalNPV } = stuff; return { total: { capitalExpenditure: totalCapex, + operationalExpenditure: totalOpex, totalCost: totalCapex + totalCapex, operationExpenditure: totalOpex, feasibilityAnalysis: sum( diff --git a/api/src/modules/calculations/revenue-profit.calculator.ts b/api/src/modules/calculations/revenue-profit.calculator.ts index ca6d744f..03bb6497 100644 --- a/api/src/modules/calculations/revenue-profit.calculator.ts +++ b/api/src/modules/calculations/revenue-profit.calculator.ts @@ -1,9 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { - CostPlanMap, - ProjectInput, -} from '@api/modules/calculations/cost.calculator'; +import { ProjectInput } from '@api/modules/calculations/cost.calculator'; import { SequestrationRateCalculator } from '@api/modules/calculations/sequestration-rate.calculator'; +import { CostPlanMap } from '@shared/dtos/custom-projects/custom-project-output.dto'; @Injectable() export class RevenueProfitCalculator { diff --git a/api/src/modules/calculations/sequestration-rate.calculator.ts b/api/src/modules/calculations/sequestration-rate.calculator.ts index b57284fa..a8445d6a 100644 --- a/api/src/modules/calculations/sequestration-rate.calculator.ts +++ b/api/src/modules/calculations/sequestration-rate.calculator.ts @@ -1,12 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { - CostPlanMap, - ProjectInput, -} from '@api/modules/calculations/cost.calculator'; +import { ProjectInput } from '@api/modules/calculations/cost.calculator'; import { ACTIVITY } from '@shared/entities/activity.enum'; import { OverridableAssumptions } from '@api/modules/custom-projects/dto/project-assumptions.dto'; import { NonOverridableModelAssumptions } from '@api/modules/calculations/assumptions.repository'; import { AdditionalBaseData } from '@api/modules/calculations/data.repository'; +import { CostPlanMap } from '@shared/dtos/custom-projects/custom-project-output.dto'; @Injectable() export class SequestrationRateCalculator { diff --git a/api/src/modules/calculations/summary.generator.ts b/api/src/modules/calculations/summary.generator.ts deleted file mode 100644 index 93876252..00000000 --- a/api/src/modules/calculations/summary.generator.ts +++ /dev/null @@ -1,190 +0,0 @@ -// TODO: First approach to get the summary and yearly cost breakdown, the cost calculator is already way too big and complex -import { - CostPlanMap, - CostPlans, -} from '@api/modules/calculations/cost.calculator'; -import { sum } from 'lodash'; -import { SequestrationRateCalculator } from '@api/modules/calculations/sequestration-rate.calculator'; - -export class SummaryGenerator { - costs: CostPlans; - capexCostPlan: CostPlanMap; - opexCostPlan: CostPlanMap; - discountRate: number; - totalCapexNPV: number; - totalOpexNPV: number; - totalNPV: number; - sequestrationRateCalculator: SequestrationRateCalculator; - constructor( - costs: CostPlans, - capexCostPlan: CostPlanMap, - opexCostPlan: CostPlanMap, - discountRate: number, - sequestrationRateCalculator: SequestrationRateCalculator, - ) { - this.costs = costs; - this.capexCostPlan = capexCostPlan; - this.opexCostPlan = opexCostPlan; - this.discountRate = discountRate; - this.totalCapexNPV = this.calculateNpv(capexCostPlan, discountRate); - this.totalOpexNPV = sum(Object.values(opexCostPlan)); - this.totalNPV = this.totalCapexNPV + this.totalOpexNPV; - this.sequestrationRateCalculator = sequestrationRateCalculator; - } - calculateNpv( - costPlan: CostPlanMap, - discountRate: number, - actualYear: number = -4, - ): number { - let npv = 0; - - for (const yearStr in costPlan) { - const year = Number(yearStr); - const cost = costPlan[year]; - - if (year === actualYear) { - npv += cost; - } else if (year > 0) { - npv += cost / Math.pow(1 + discountRate, year + (-actualYear - 1)); - } else { - npv += cost / Math.pow(1 + discountRate, -actualYear + year); - } - } - - return npv; - } - - // TODO: strongly type this and share it - getCostEstimates(): any { - return { - costEstimatesUds: { - total: { - capitalExpenditure: sum(Object.values(this.capexCostPlan)), - feasibilityAnalysis: sum( - Object.values(this.costs.feasibilityAnalysis), - ), - conservationPlanningAndAdmin: sum( - Object.values(this.costs.conservationPlanningAndAdmin), - ), - dataCollectionAndFieldCost: sum( - Object.values(this.costs.dataCollectionAndFieldCost), - ), - communityRepresentation: sum( - Object.values(this.costs.communityRepresentation), - ), - blueCarbonProjectPlanning: sum( - Object.values(this.costs.blueCarbonProjectPlanning), - ), - establishingCarbonRights: sum( - Object.values(this.costs.establishingCarbonRights), - ), - validation: sum(Object.values(this.costs.validation)), - implementationLabor: sum( - Object.values(this.costs.implementationLabor), - ), - operationExpenditure: sum(Object.values(this.opexCostPlan)), - monitoring: sum(Object.values(this.costs.monitoring)), - maintenance: sum(Object.values(this.costs.maintenance)), - communityBenefitSharingFund: sum( - Object.values(this.costs.communityBenefitSharingFund), - ), - carbonStandardFees: sum(Object.values(this.costs.carbonStandardFees)), - baselineReassessment: sum( - Object.values(this.costs.baselineReassessment), - ), - mrv: sum(Object.values(this.costs.mrv)), - longTermProjectOperatingCost: sum( - Object.values(this.costs.longTermProjectOperatingCost), - ), - totalCost: - sum(Object.values(this.capexCostPlan)) + - sum(Object.values(this.opexCostPlan)), - }, - npv: { - capitalExpenditure: this.totalCapexNPV, - feasibilityAnalysis: this.calculateNpv( - this.costs.feasibilityAnalysis, - this.discountRate, - ), - conservationPlanningAndAdmin: this.calculateNpv( - this.costs.conservationPlanningAndAdmin, - this.discountRate, - ), - dataCollectionAndFieldCost: this.calculateNpv( - this.costs.dataCollectionAndFieldCost, - this.discountRate, - ), - communityRepresentation: this.calculateNpv( - this.costs.communityRepresentation, - this.discountRate, - ), - blueCarbonProjectPlanning: this.calculateNpv( - this.costs.blueCarbonProjectPlanning, - this.discountRate, - ), - establishingCarbonRights: this.calculateNpv( - this.costs.establishingCarbonRights, - this.discountRate, - ), - validation: this.calculateNpv( - this.costs.validation, - this.discountRate, - ), - implementationLabor: this.calculateNpv( - this.costs.implementationLabor, - this.discountRate, - ), - operationExpenditure: this.totalOpexNPV, - monitoring: this.calculateNpv( - this.costs.monitoring, - this.discountRate, - ), - maintenance: this.calculateNpv( - this.costs.maintenance, - this.discountRate, - ), - communityBenefitSharingFund: this.calculateNpv( - this.costs.communityBenefitSharingFund, - this.discountRate, - ), - carbonStandardFees: this.calculateNpv( - this.costs.carbonStandardFees, - this.discountRate, - ), - baselineReassessment: this.calculateNpv( - this.costs.baselineReassessment, - this.discountRate, - ), - mrv: this.calculateNpv(this.costs.mrv, this.discountRate), - longTermProjectOperatingCost: this.calculateNpv( - this.costs.longTermProjectOperatingCost, - this.discountRate, - ), - totalCost: this.totalOpexNPV + this.totalCapexNPV, - }, - }, - }; - } - - getSummary(): any { - return { - '$/tCO2e (total cost, NPV': null, - '$/ha': null, - 'Leftover after OpEx / total cost': null, - 'NPV covering cost': null, - 'IRR when priced to cover OpEx': null, - 'IRR when priced to cover total cost': null, - 'Total cost (NPV)': this.totalNPV, - 'Capital expenditure (NPV)': this.totalCapexNPV, - 'Operating expenditure (NPV)': this.totalOpexNPV, - 'Credits issued': null, - 'Total revenue (NPV)': null, - 'Total revenue (non-discounted)': null, - 'Financing cost': null, - 'Funding gap': null, - 'Funding gap (NPV)': null, - 'Funding gap per tCO2e (NPV)': null, - 'Community benefit sharing fund': null, - }; - } -} diff --git a/api/src/modules/custom-projects/custom-projects.controller.ts b/api/src/modules/custom-projects/custom-projects.controller.ts index 057348f9..6aedd316 100644 --- a/api/src/modules/custom-projects/custom-projects.controller.ts +++ b/api/src/modules/custom-projects/custom-projects.controller.ts @@ -1,4 +1,10 @@ -import { Body, Controller, HttpStatus, ValidationPipe } from '@nestjs/common'; +import { + Body, + Controller, + HttpStatus, + UseGuards, + ValidationPipe, +} from '@nestjs/common'; import { CountriesService } from '@api/modules/countries/countries.service'; import { tsRestHandler, TsRestHandler } from '@ts-rest/nest'; import { ControllerResponse } from '@api/types/controller-response.type'; @@ -6,6 +12,12 @@ import { customProjectContract } from '@shared/contracts/custom-projects.contrac import { CustomProjectsService } from '@api/modules/custom-projects/custom-projects.service'; import { CreateCustomProjectDto } from '@api/modules/custom-projects/dto/create-custom-project-dto'; import { CustomProjectSnapshotDto } from './dto/custom-project-snapshot.dto'; +import { GetUser } from '@api/decorators/get-user.decorator'; +import { User } from '@shared/entities/users/user.entity'; +import { AuthGuard } from '@nestjs/passport'; +import { RolesGuard } from '@api/modules/auth/guards/roles.guard'; +import { RequiredRoles } from '@api/modules/auth/decorators/roles.decorator'; +import { ROLES } from '@shared/entities/users/roles.enum'; @Controller() export class CustomProjectsController { @@ -67,15 +79,18 @@ export class CustomProjectsController { ); } + @UseGuards(AuthGuard('jwt'), RolesGuard) + @RequiredRoles(ROLES.PARTNER, ROLES.ADMIN) @TsRestHandler(customProjectContract.snapshotCustomProject) async snapshot( @Body(new ValidationPipe({ enableDebugMessages: true, transform: true })) dto: CustomProjectSnapshotDto, + @GetUser() user: User, ): Promise { return tsRestHandler( customProjectContract.snapshotCustomProject, async ({ body }) => { - await this.customProjects.saveCustomProject(dto); + await this.customProjects.saveCustomProject(dto, user); return { status: 201, body: null, diff --git a/api/src/modules/custom-projects/custom-projects.service.ts b/api/src/modules/custom-projects/custom-projects.service.ts index d48117b0..7b0b51d3 100644 --- a/api/src/modules/custom-projects/custom-projects.service.ts +++ b/api/src/modules/custom-projects/custom-projects.service.ts @@ -15,6 +15,8 @@ import { GetOverridableAssumptionsDTO } from '@shared/dtos/custom-projects/get-o import { AssumptionsRepository } from '@api/modules/calculations/assumptions.repository'; import { SequestrationRateCalculator } from '@api/modules/calculations/sequestration-rate.calculator'; import { CountriesService } from '@api/modules/countries/countries.service'; +import { YearlyBreakdown } from '@shared/dtos/custom-projects/custom-project-output.dto'; +import { User } from '@shared/entities/users/user.entity'; @Injectable() export class CustomProjectsService extends AppBaseService< @@ -70,40 +72,55 @@ export class CustomProjectsService extends AppBaseService< const projectOutput = { projectName: dto.projectName, + abatementPotential: null, // We still dont know how to calculate this country, + totalNPV: costPlans.totalCapexNPV + costPlans.totalOpexNPV, + totalCost: costPlans.totalCapex + costPlans.totalOpex, projectSize: dto.projectSizeHa, + projectLength: dto.assumptions.projectLength, ecosystem: dto.ecosystem, activity: dto.activity, - lossRate: projectInput.lossRate, - carbonRevenuesToCover: projectInput.carbonRevenuesToCover, - initialCarbonPrice: projectInput.initialCarbonPriceAssumption, - emissionFactors: { - emissionFactor: projectInput.emissionFactor, - emissionFactorAgb: projectInput.emissionFactorAgb, - emissionFactorSoc: projectInput.emissionFactorSoc, - }, - totalProjectCost: { - total: { - total: costPlans.totalCapex + costPlans.totalOpex, - capex: costPlans.totalCapex, - opex: costPlans.totalOpex, + output: { + lossRate: projectInput.lossRate, + carbonRevenuesToCover: projectInput.carbonRevenuesToCover, + initialCarbonPrice: projectInput.initialCarbonPriceAssumption, + emissionFactors: { + emissionFactor: projectInput.emissionFactor, + emissionFactorAgb: projectInput.emissionFactorAgb, + emissionFactorSoc: projectInput.emissionFactorSoc, }, - npv: { - total: costPlans.totalCapexNPV + costPlans.totalOpexNPV, - capex: costPlans.totalCapexNPV, - opex: costPlans.totalOpexNPV, + totalProjectCost: { + total: { + total: costPlans.totalCapex + costPlans.totalOpex, + capex: costPlans.totalCapex, + opex: costPlans.totalOpex, + }, + npv: { + total: costPlans.totalCapexNPV + costPlans.totalOpexNPV, + capex: costPlans.totalCapexNPV, + opex: costPlans.totalOpexNPV, + }, }, - }, - summary: calculator.getSummary(costPlans), - costDetails: calculator.getCostDetails(costPlans), - breakdown: calculator.getYearlyBreakdown(), + summary: calculator.getSummary(costPlans), + costDetails: calculator.getCostDetails(costPlans), + yearlyBreakdown: calculator.getYearlyBreakdown(), + }, + input: dto, }; - return projectOutput.breakdown; + console.log( + projectOutput.output.yearlyBreakdown.map( + (x: YearlyBreakdown) => x.costName, + ), + ); + return projectOutput; } - async saveCustomProject(dto: CustomProjectSnapshotDto): Promise { - await this.repo.save(CustomProject.fromCustomProjectSnapshotDTO(dto)); + async saveCustomProject( + dto: CustomProjectSnapshotDto, + user: User, + ): Promise { + await this.repo.save(CustomProject.fromCustomProjectSnapshotDTO(dto, user)); } async getDefaultCostInputs( diff --git a/api/src/modules/custom-projects/dto/custom-project-snapshot.dto.ts b/api/src/modules/custom-projects/dto/custom-project-snapshot.dto.ts index 94fd00ea..efb468dc 100644 --- a/api/src/modules/custom-projects/dto/custom-project-snapshot.dto.ts +++ b/api/src/modules/custom-projects/dto/custom-project-snapshot.dto.ts @@ -6,6 +6,7 @@ import { IsOptional, } from 'class-validator'; import { CreateCustomProjectDto } from './create-custom-project-dto'; +import { CustomProjectOutput } from '@shared/dtos/custom-projects/custom-project-output.dto'; export class CustomPrpjectAnnualProjectCashFlowDto { @IsArray() @@ -170,5 +171,5 @@ export class CustomProjectSnapshotDto { inputSnapshot: CreateCustomProjectDto; @IsNotEmpty() - outputSnapshot: CustomProjectOutputSnapshot; + outputSnapshot: CustomProjectOutput; } diff --git a/shared/dtos/custom-projects/custom-project-output.dto.ts b/shared/dtos/custom-projects/custom-project-output.dto.ts new file mode 100644 index 00000000..5f46461f --- /dev/null +++ b/shared/dtos/custom-projects/custom-project-output.dto.ts @@ -0,0 +1,93 @@ +import { ConservationProjectInput } from "@api/modules/custom-projects/input-factory/conservation-project.input"; + +import { OverridableCostInputs } from "@api/modules/custom-projects/dto/project-cost-inputs.dto"; + +export type CustomProjectSummary = { + "$/tCO2e (total cost, NPV)": number; + "$/ha": number; + "NPV covering cost": number; + "Leftover after OpEx / total cost": number | null; + "IRR when priced to cover OpEx": number; + "IRR when priced to cover total cost": number; + "Total cost (NPV)": number; + "Capital expenditure (NPV)": number; + "Operating expenditure (NPV)": number; + "Credits issued": number; + "Total revenue (NPV)": number; + "Total revenue (non-discounted)": number; + "Financing cost": number; + "Funding gap": number; + "Funding gap (NPV)": number; + "Funding gap per tCO2e (NPV)": number; + "Community benefit sharing fund": number; +}; + +export type CustomProjectCostDetails = { + capitalExpenditure: number; + operationalExpenditure: number; + totalCost: number; + feasibilityAnalysis: number; + conservationPlanningAndAdmin: number; + dataCollectionAndFieldCost: number; + communityRepresentation: number; + blueCarbonProjectPlanning: number; + establishingCarbonRights: number; + validation: number; + implementationLabor: number; + operationExpenditure: number; + monitoring: number; + maintenance: number; + communityBenefitSharingFund: number; + carbonStandardFees: number; + baselineReassessment: number; + mrv: number; + longTermProjectOperatingCost: number; +}; + +export type YearlyBreakdown = { + costName: keyof OverridableCostInputs; + totalCost: number; + totalNPV: number; + costValues: CostPlanMap; +}; + +export type CostPlanMap = { + [year: number]: number; +}; + +export type CustomProjectOutput = + | ConservationProjectOutput + | RestorationProjectOutput; + +export class RestorationProjectOutput { + // to be defined. it will share most of the props, but probably carbon input related fields will be different + // i.e conservation does not account for sequestration rate, but restoration does + // Restoration does not care about emission factors, but conservation does +} + +export class ConservationProjectOutput { + lossRate: ConservationProjectInput["lossRate"]; + emissionFactors: { + emissionFactor: ConservationProjectInput["emissionFactor"]; + emissionFactorAgb: ConservationProjectInput["emissionFactorAgb"]; + emissionFactorSoc: ConservationProjectInput["emissionFactorSoc"]; + }; + totalProjectCost: { + total: { + total: number; + capex: number; + opex: number; + }; + npv: { + total: number; + capex: number; + opex: number; + }; + }; + summary: CustomProjectSummary; + costDetails: { + total: CustomProjectCostDetails; + npv: CustomProjectCostDetails; + }; + yearlyBreakdown: YearlyBreakdown[]; +} diff --git a/shared/entities/custom-project.entity.ts b/shared/entities/custom-project.entity.ts index e531c5ae..d6344ae1 100644 --- a/shared/entities/custom-project.entity.ts +++ b/shared/entities/custom-project.entity.ts @@ -10,6 +10,8 @@ import { ECOSYSTEM } from "@shared/entities/ecosystem.enum"; import { ACTIVITY } from "@shared/entities/activity.enum"; import { Country } from "@shared/entities/country.entity"; import { User } from "@shared/entities/users/user.entity"; +import { CreateCustomProjectDto } from "@api/modules/custom-projects/dto/create-custom-project-dto"; +import { CustomProjectOutput } from "@shared/dtos/custom-projects/custom-project-output.dto"; /** * @description: This entity is to save Custom Projects (that are calculated, and can be saved only by registered users. Most likely, we don't need to add these as a resource @@ -28,11 +30,11 @@ export class CustomProject { @ManyToOne(() => User, (user) => user.customProjects, { onDelete: "CASCADE" }) @JoinColumn({ name: "user_id" }) - user: User; + user?: User; @ManyToOne(() => Country, (country) => country.code, { onDelete: "CASCADE" }) @JoinColumn({ name: "country_code" }) - countryCode: Country; + country: Country; @Column({ name: "ecosystem", enum: ECOSYSTEM, type: "enum" }) ecosystem: ECOSYSTEM; @@ -41,22 +43,24 @@ export class CustomProject { activity: ACTIVITY; @Column({ name: "input_snapshot", type: "jsonb" }) - input_snapshot: any; + input: CreateCustomProjectDto; @Column({ name: "output_snapshot", type: "jsonb" }) - output_snapshot: any; + output: CustomProjectOutput; static fromCustomProjectSnapshotDTO( dto: CustomProjectSnapshotDto, + user: User, ): CustomProject { const customProject = new CustomProject(); - customProject.countryCode = { + customProject.country = { code: dto.inputSnapshot.countryCode, } as Country; customProject.ecosystem = dto.inputSnapshot.ecosystem; customProject.activity = dto.inputSnapshot.activity; - customProject.input_snapshot = dto.inputSnapshot; - customProject.output_snapshot = dto.outputSnapshot; + customProject.input = dto.inputSnapshot; + customProject.output = dto.outputSnapshot; + customProject.user = user; return customProject; } }