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

Custom project snapshotting #106

Merged
merged 8 commits into from
Nov 23, 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
18 changes: 18 additions & 0 deletions api/src/modules/custom-projects/custom-projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 '@api/modules/custom-projects/dto/create-custom-project-dto';
import { CustomProjectSnapshotDto } from './dto/custom-project-snapshot.dto';

@Controller()
export class CustomProjectsController {
Expand Down Expand Up @@ -65,4 +66,21 @@ export class CustomProjectsController {
},
);
}

@TsRestHandler(customProjectContract.snapshotCustomProject)
async snapshot(
@Body(new ValidationPipe({ enableDebugMessages: true, transform: true }))
dto: CustomProjectSnapshotDto,
): Promise<ControllerResponse> {
return tsRestHandler(
customProjectContract.snapshotCustomProject,
async ({ body }) => {
await this.customProjects.saveCustomProject(dto);
return {
status: 201,
body: null,
};
},
);
}
}
5 changes: 5 additions & 0 deletions api/src/modules/custom-projects/custom-projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { GetOverridableCostInputs } from '@shared/dtos/custom-projects/get-overr
import { DataRepository } from '@api/modules/calculations/data.repository';
import { OverridableCostInputs } from '@api/modules/custom-projects/dto/project-cost-inputs.dto';
import { CostCalculator } from '@api/modules/calculations/cost.calculator';
import { CustomProjectSnapshotDto } from './dto/custom-project-snapshot.dto';
import { GetOverridableAssumptionsDTO } from '@shared/dtos/custom-projects/get-overridable-assumptions.dto';
import { AssumptionsRepository } from '@api/modules/calculations/assumptions.repository';

Expand Down Expand Up @@ -62,6 +63,10 @@ export class CustomProjectsService extends AppBaseService<
return calculator.costPlans;
}

async saveCustomProject(dto: CustomProjectSnapshotDto): Promise<void> {
await this.repo.save(CustomProject.fromCustomProjectSnapshotDTO(dto));
}

async getDefaultCostInputs(
dto: GetOverridableCostInputs,
): Promise<OverridableCostInputs> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class CreateCustomProjectDto {
parameters: ConservationProjectParamDto | RestorationProjectParamsDto;
}

// @ts-ignore: TS7031
function injectEcosystemToParams({ obj, value }) {
// Helper to inject the ecosystem into the parameters object so we can perform further validations that are specific to
// the activity type
Expand Down
174 changes: 174 additions & 0 deletions api/src/modules/custom-projects/dto/custom-project-snapshot.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
IsNotEmpty,
IsNumber,
IsArray,
IsString,
IsOptional,
} from 'class-validator';
import { CreateCustomProjectDto } from './create-custom-project-dto';

export class CustomPrpjectAnnualProjectCashFlowDto {
@IsArray()
feasiabilityAnalysis: number[];

@IsArray()
conservationPlanningAndAdmin: number[];

@IsArray()
dataCollectionAndFieldCost: number[];

@IsArray()
communityRepresentation: number[];

@IsArray()
blueCarbonProjectPlanning: number[];

@IsArray()
establishingCarbonRights: number[];

@IsArray()
validation: number[];

@IsArray()
implementationLabor: number[];

@IsArray()
totalCapex: number[];

// Opex costs
@IsArray()
monitoring: number[];

@IsArray()
maintenance: number[];

@IsArray()
communityBenefitSharingFund: number[];

@IsArray()
carbonStandardFees: number[];

@IsArray()
baselineReassessment: number[];

@IsArray()
mrv: number[];

@IsArray()
longTermProjectOperatingCost: number[];

@IsArray()
totalOpex: number[];

// Total costs
@IsArray()
totalCost: number[];

@IsArray()
estCreditsIssued: number[];

@IsArray()
estRevenue: number[];

@IsArray()
annualNetIncomeRevLessOpex: number[];

@IsArray()
cummulativeNetIncomeRevLessOpex: number[];

@IsArray()
fundingGap: number[];

@IsArray()
irrOpex: number[];

@IsArray()
irrTotalCost: number[];

@IsArray()
irrAnnualNetIncome: number[];

@IsArray()
annualNetCashFlow: number[];
}

export class CustomProjectSummaryDto {
@IsNumber()
costPerTCO2e: number;

@IsNumber()
costPerHa: number;

@IsNumber()
leftoverAfterOpexTotalCost: number;

@IsNumber()
irrCoveringOpex: number;

@IsNumber()
irrCoveringTotalCost: number;

@IsNumber()
totalCost: number;

@IsNumber()
capitalExpenditure: number;

@IsNumber()
operatingExpenditure: number;

@IsNumber()
creditsIssued: number;

@IsNumber()
totalRevenue: number;

@IsNumber()
nonDiscountedTotalRevenue: number;

@IsNumber()
financingCost: number;

@IsNumber()
foundingGap: number;

@IsNumber()
foundingGapPerTCO2e: number;

@IsNumber()
communityBenefitSharingFundRevenuePc: number;
}

export class CustomProjectCostDetailEntry {
@IsString()
costName: string;

@IsNumber()
costValue: number;

@IsOptional()
@IsNumber()
sensitiveAnalysis: number;
}

export class CustomProjectOutputSnapshot {
@IsNumber()
projectLength: number;

@IsNotEmpty()
annualProjectCashFlow: CustomPrpjectAnnualProjectCashFlowDto;

@IsNotEmpty()
projectSummary: CustomProjectSummaryDto;

@IsArray()
costDetails: CustomProjectCostDetailEntry[];
}

export class CustomProjectSnapshotDto {
@IsNotEmpty()
inputSnapshot: CreateCustomProjectDto;

@IsNotEmpty()
outputSnapshot: CustomProjectOutputSnapshot;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export class ProjectParamsValidator implements ValidatorConstraintInterface {
return validationErrors.length === 0;
}

private formatErrors(errors: ValidationError[]): any[] {
const formattedErrors = [];
private formatErrors(errors: ValidationError[]): string[] {
const formattedErrors: string[] = [];
errors.forEach((error) => {
Object.values(error.constraints).forEach((constraint) => {
Object.values(error.constraints || {}).forEach((constraint) => {
formattedErrors.push(constraint);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,51 +62,7 @@ describe('Create Custom Projects - Setup', () => {
},
});

expect(response.body).toMatchObject({
data: {
carbonInputs: {
lossRate: -0.0016,
emissionFactor: null,
emissionFactorAgb: 67.7,
emissionFactorSoc: 85.5,
},
costInputs: {
feasibilityAnalysis: 50000,
conservationPlanningAndAdmin: 166766.66666666666,
dataCollectionAndFieldCost: 26666.666666666668,
communityRepresentation: 71183.33333333333,
blueCarbonProjectPlanning: 100000,
establishingCarbonRights: 46666.666666666664,
financingCost: 0.05,
validation: 50000,
implementationLaborHybrid: null,
monitoring: 15000,
maintenance: 0.0833,
carbonStandardFees: 0.2,
communityBenefitSharingFund: 0.5,
baselineReassessment: 40000,
mrv: 75000,
longTermProjectOperatingCost: 26400,
implementationLabor: 0,
},
modelAssumptions: {
verificationFrequency: 5,
baselineReassessmentFrequency: 10,
discountRate: 0.04,
restorationRate: 250,
carbonPriceIncrease: 0.015,
buffer: 0.2,
projectLength: 20,
},
projectName: 'My custom project',
countryCode: 'IND',
activity: 'Conservation',
ecosystem: 'Mangrove',
projectSizeHa: 1000,
initialCarbonPriceAssumption: 1000,
carbonRevenuesToCover: 'Opex',
},
});
// TODO: Write tests for cost calculations
});
});
});
Loading
Loading