diff --git a/api/src/modules/import/dtos/excel-projects.dto.ts b/api/src/modules/import/dtos/excel-projects.dto.ts index bf6a2848..093e7cc0 100644 --- a/api/src/modules/import/dtos/excel-projects.dto.ts +++ b/api/src/modules/import/dtos/excel-projects.dto.ts @@ -5,7 +5,7 @@ import { import { ECOSYSTEM } from '@shared/entities/ecosystem.enum'; import { PROJECT_PRICE_TYPE } from '@shared/entities/projects.entity'; -export type ExcelProjects = { +export type ExcelProject = { project_name: string; continent: string; country: string; diff --git a/api/src/modules/import/import.controller.ts b/api/src/modules/import/import.controller.ts index 495d15ca..bb8c647b 100644 --- a/api/src/modules/import/import.controller.ts +++ b/api/src/modules/import/import.controller.ts @@ -36,10 +36,10 @@ export class ImportController { @GetUser() user: User, ): Promise { return tsRestHandler(adminContract.uploadFile, async () => { - const importedData = await this.service.import(file.buffer, user.id); + await this.service.import(file.buffer, user.id); return { status: 201, - body: importedData, + body: null, }; }); } diff --git a/api/src/modules/import/import.module.ts b/api/src/modules/import/import.module.ts index d5355111..a24d161a 100644 --- a/api/src/modules/import/import.module.ts +++ b/api/src/modules/import/import.module.ts @@ -7,15 +7,22 @@ import { EntityPreprocessor } from '@api/modules/import/services/entity.preproce import { ExcelParserToken } from '@api/modules/import/services/excel-parser.interface'; import { ImportRepository } from '@api/modules/import/import.repostiory'; import { ImportEventHandler } from '@api/modules/import/events/handlers/import-event.handler'; +import { DataIngestionExcelParser } from '@api/modules/import/parser/data-ingestion.xlsx-parser'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ProjectScorecard } from '@shared/entities/project-scorecard.entity'; @Module({ - imports: [MulterModule.register({})], + imports: [ + TypeOrmModule.forFeature([ProjectScorecard]), + MulterModule.register({}), + ], controllers: [ImportController], providers: [ ImportService, EntityPreprocessor, ImportRepository, ImportEventHandler, + DataIngestionExcelParser, { provide: ExcelParserToken, useClass: XlsxParser }, ], }) diff --git a/api/src/modules/import/import.service.ts b/api/src/modules/import/import.service.ts index 5fe1cdf2..8a529622 100644 --- a/api/src/modules/import/import.service.ts +++ b/api/src/modules/import/import.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ConflictException, Inject, Injectable, Logger } from '@nestjs/common'; import { EntityPreprocessor } from '@api/modules/import/services/entity.preprocessor'; import { ExcelParserInterface, @@ -17,6 +17,7 @@ import { import { UserUploadCostInputs } from '@shared/entities/users/user-upload-cost-inputs.entity'; import { UserUploadRestorationInputs } from '@shared/entities/users/user-upload-restoration-inputs.entity'; import { UserUploadConservationInputs } from '@shared/entities/users/user-upload-conservation-inputs.entity'; +import { DataIngestionExcelParser } from '@api/modules/import/parser/data-ingestion.xlsx-parser'; @Injectable() export class ImportService { @@ -28,6 +29,7 @@ export class ImportService { }; constructor( + private readonly dataIngestionParser: DataIngestionExcelParser, @Inject(ExcelParserToken) private readonly excelParser: ExcelParserInterface, private readonly importRepo: ImportRepository, @@ -54,23 +56,32 @@ export class ImportService { } } - async import(fileBuffer: Buffer, userId: string) { + async import(fileBuffer: Buffer, userId: string): Promise { this.logger.warn('Excel file import started...'); this.registerImportEvent(userId, this.eventMap.STARTED); try { - const parsedSheets = await this.excelParser.parseExcel(fileBuffer); - const parsedDBEntities = this.preprocessor.toDbEntities(parsedSheets); + const parsedSheets = + await this.dataIngestionParser.parseBuffer(fileBuffer); + const parsedDBEntities = + await this.preprocessor.toDbEntities(parsedSheets); await this.importRepo.ingest(parsedDBEntities); this.logger.warn('Excel file import completed successfully'); this.registerImportEvent(userId, this.eventMap.SUCCESS); } catch (e) { this.logger.error('Excel file import failed', e); - this.registerImportEvent(userId, this.eventMap.FAILED); + this.registerImportEvent(userId, this.eventMap.FAILED, { + error: { type: e.constructor.name, message: e.message }, + }); + throw new ConflictException(e); } } - registerImportEvent(userId: string, eventType: typeof this.eventMap) { - this.eventBus.publish(new ImportEvent(eventType, userId, {})); + registerImportEvent( + userId: string, + eventType: typeof this.eventMap, + payload = {}, + ) { + this.eventBus.publish(new ImportEvent(eventType, userId, payload)); } async importDataProvidedByPartner(fileBuffers: Buffer[], userId: string) { diff --git a/api/src/modules/import/parser/data-ingestion.xlsx-parser.ts b/api/src/modules/import/parser/data-ingestion.xlsx-parser.ts new file mode 100644 index 00000000..ff190a09 --- /dev/null +++ b/api/src/modules/import/parser/data-ingestion.xlsx-parser.ts @@ -0,0 +1,89 @@ +import { RawDataIngestionData } from '@api/modules/import/parser/raw-data-ingestion.type'; +import { + ExcelTabNotFoundError, + IExcelParser, +} from '@api/modules/import/parser/excel-parser.interface'; +import { read, utils, WorkBook, WorkSheet } from 'xlsx'; + +export const EXPECTED_SHEETS = [ + 'Index', + 'Sheet20', + 'Pivot Table 1', + 'Model assumptions and constrain', + 'Backoffice', + 'Input', + 'Model assumptions', + 'Data pull', + 'Data ingestion >>>', + 'master_table', + 'base_size_table', + 'base_increase', + 'Projects', + 'Base inputs>>>', + 'Countries', + 'Ecosystem', + 'Activity', + 'Restoration_activity', + 'Cost inputs>>', + 'Project size', + 'Feasibility analysis', + 'Conservation planning and admin', + 'Data collection and field costs', + 'Community representation', + 'Blue carbon project planning', + 'Establishing carbon rights', + 'Financing cost', + 'Validation', + 'Implementation labor', + 'Monitoring', + 'Maintenance', + 'Community benefit sharing fund', + 'Baseline reassessment', + 'MRV', + 'Long-term project operating', + 'Carbon standard fees', + 'Community cash flow', + 'Carbon inputs>>', + 'Ecosystem extent', + 'Ecosystem loss', + 'Restorable land', + 'Sequestration rate', + 'Emission factors', + 'Mapping references>>', + 'Continent', + 'HDI', + 'Abbreviation', + 'Sources>>', + 'Sequestration rates', + 'Emissions sources', + 'MangroveEmissionsValues', + 'Loss rates & restorable land', + 'Datasets>>', + 'Mangrove extent', + 'Mangrove protected area', + 'Seagrass extent', + 'Salt marsh extent', + 'Mangrove restorable land', +] as const; + +export class DataIngestionExcelParser implements IExcelParser { + public async parseBuffer(buffer: Buffer): Promise { + const workbook: WorkBook = read(buffer); + const parsedData: any = {}; + + for (const sheetName of EXPECTED_SHEETS) { + const sheet: WorkSheet = workbook.Sheets[sheetName]; + if (sheet === undefined) { + throw new ExcelTabNotFoundError(sheetName); + } + + const parsedSheet = utils.sheet_to_json(sheet, { + raw: true, + }); + // We can validate the sheet tab headers and column values when we have more information from the science team. + parsedData[sheetName] = parsedSheet; + } + + return parsedData; + } +} diff --git a/api/src/modules/import/parser/excel-parser.interface.ts b/api/src/modules/import/parser/excel-parser.interface.ts new file mode 100644 index 00000000..33308358 --- /dev/null +++ b/api/src/modules/import/parser/excel-parser.interface.ts @@ -0,0 +1,21 @@ +export class ExcelTabNotFoundError extends Error { + constructor(tabName: string) { + super(`Tab ${tabName} not found in the excel file`); + } +} + +export class ExcelTabHeaderNotFoundError extends Error { + constructor(header: string, tabName: string) { + super(`Header (${header}) not found in the tab ${tabName}`); + } +} + +export class RowColumnInvalidError extends Error { + constructor(row: number, column: string) { + super(`Invalid value in row ${row} and column ${column}`); + } +} + +export interface IExcelParser { + parseBuffer(buffer: Buffer): Promise; +} diff --git a/api/src/modules/import/parser/raw-data-ingestion.type.ts b/api/src/modules/import/parser/raw-data-ingestion.type.ts new file mode 100644 index 00000000..74681c26 --- /dev/null +++ b/api/src/modules/import/parser/raw-data-ingestion.type.ts @@ -0,0 +1,57 @@ +import { ExcelBaseIncrease } from '@api/modules/import/dtos/excel-base-increase.dto'; +import { ExcelBaseSize } from '@api/modules/import/dtos/excel-base-size.dto'; +import { ExcelBaselineReassessment } from '@api/modules/import/dtos/excel-baseline-reassessment.dto'; +import { ExcelBlueCarbonProjectPlanning } from '@api/modules/import/dtos/excel-blue-carbon-project-planning.dto'; +import { ExcelCarbonStandardFees } from '@api/modules/import/dtos/excel-carbon-standard-fees.dto'; +import { ExcelEcosystemLoss } from '@api/modules/import/dtos/excel-ccosystem-loss.dto'; +import { ExcelCommunityBenefitSharingFund } from '@api/modules/import/dtos/excel-community-benefit-sharing-fund.dto'; +import { ExcelCommunityCashFlow } from '@api/modules/import/dtos/excel-community-cash-flow.dto'; +import { ExcelCommunityRepresentation } from '@api/modules/import/dtos/excel-community-representation.dto'; +import { ExcelConservationPlanningAndAdmin } from '@api/modules/import/dtos/excel-conservation-planning-and-admin.dto'; +import { ExcelDataCollectionAndFieldCosts } from '@api/modules/import/dtos/excel-data-collection-field-cost.dto'; +import { ExcelEcosystemExtent } from '@api/modules/import/dtos/excel-ecosystem-extent.dto'; +import { ExcelEmissionFactors } from '@api/modules/import/dtos/excel-emission-factors.dto'; +import { ExcelEstablishingCarbonRights } from '@api/modules/import/dtos/excel-establishing-carbon-rights.dto'; +import { ExcelFeasibilityAnalysis } from '@api/modules/import/dtos/excel-feasibility-analysis.dto'; +import { ExcelFinancingCost } from '@api/modules/import/dtos/excel-financing-cost.dto'; +import { ExcelImplementationLaborCost } from '@api/modules/import/dtos/excel-implementation-labor.dto'; +import { ExcelLongTermProjectOperating } from '@api/modules/import/dtos/excel-long-term-project-operating.dto'; +import { ExcelMaintenance } from '@api/modules/import/dtos/excel-maintenance.dto'; +import { ExcelModelAssumptions } from '@api/modules/import/dtos/excel-model-assumptions.dto'; +import { ExcelMonitoring } from '@api/modules/import/dtos/excel-monitoring.dto'; +import { ExcelMRV } from '@api/modules/import/dtos/excel-mrv.dto'; +import { ExcelProjectSize } from '@api/modules/import/dtos/excel-project-size.dto'; +import { ExcelProject } from '@api/modules/import/dtos/excel-projects.dto'; +import { ExcelRestorableLand } from '@api/modules/import/dtos/excel-restorable-land.dto'; +import { ExcelSequestrationRate } from '@api/modules/import/dtos/excel-sequestration-rate.dto'; +import { ExcelValidation } from '@api/modules/import/dtos/excel-validation.dto'; + +export type RawDataIngestionData = { + Projects: ExcelProject[]; + 'Project size': ExcelProjectSize[]; + 'Feasibility analysis': ExcelFeasibilityAnalysis[]; + 'Conservation planning and admin': ExcelConservationPlanningAndAdmin[]; + 'Data collection and field costs': ExcelDataCollectionAndFieldCosts[]; + 'Community representation': ExcelCommunityRepresentation[]; + 'Blue carbon project planning': ExcelBlueCarbonProjectPlanning[]; + 'Establishing carbon rights': ExcelEstablishingCarbonRights[]; + 'Financing cost': ExcelFinancingCost[]; + Validation: ExcelValidation[]; + Monitoring: ExcelMonitoring[]; + Maintenance: ExcelMaintenance[]; + 'Community benefit sharing fund': ExcelCommunityBenefitSharingFund[]; + 'Baseline reassessment': ExcelBaselineReassessment[]; + MRV: ExcelMRV[]; + 'Long-term project operating': ExcelLongTermProjectOperating[]; + 'Carbon standard fees': ExcelCarbonStandardFees[]; + 'Community cash flow': ExcelCommunityCashFlow[]; + 'Ecosystem extent': ExcelEcosystemExtent[]; + 'Ecosystem loss': ExcelEcosystemLoss[]; + 'Restorable land': ExcelRestorableLand[]; + 'Sequestration rate': ExcelSequestrationRate[]; + 'Emission factors': ExcelEmissionFactors[]; + 'Implementation labor': ExcelImplementationLaborCost[]; + base_size_table: ExcelBaseSize[]; + base_increase: ExcelBaseIncrease[]; + 'Model assumptions': ExcelModelAssumptions[]; +}; diff --git a/api/src/modules/import/services/entity.preprocessor.ts b/api/src/modules/import/services/entity.preprocessor.ts index 74ec57de..e0b2aa72 100644 --- a/api/src/modules/import/services/entity.preprocessor.ts +++ b/api/src/modules/import/services/entity.preprocessor.ts @@ -1,9 +1,8 @@ import { ExcelEstablishingCarbonRights } from './../dtos/excel-establishing-carbon-rights.dto'; import { Injectable } from '@nestjs/common'; - import { Country } from '@shared/entities/country.entity'; import { Project } from '@shared/entities/projects.entity'; -import { ExcelProjects } from '@api/modules/import/dtos/excel-projects.dto'; +import { ExcelProject } from '@api/modules/import/dtos/excel-projects.dto'; import { ExcelProjectSize } from '@api/modules/import/dtos/excel-project-size.dto'; import { ACTIVITY } from '@shared/entities/activity.enum'; import { ECOSYSTEM } from '@shared/entities/ecosystem.enum'; @@ -72,6 +71,10 @@ import { ModelAssumptions } from '@shared/entities/model-assumptions.entity'; import { ProjectScorecard } from '@shared/entities/project-scorecard.entity'; import { ExcelProjectScorecard } from '../dtos/excel-projects-scorecard.dto '; import { PROJECT_SCORE } from '@shared/entities/project-score.enum'; +import { ProjectScoreUtils } from '@shared/entities/project-score.utils'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { RawDataIngestionData } from '@api/modules/import/parser/raw-data-ingestion.type'; export type ParsedDBEntities = { projects: Project[]; @@ -103,42 +106,25 @@ export type ParsedDBEntities = { modelAssumptions: ModelAssumptions[]; }; +export class ProjectScoreCardNotFoundError extends Error { + constructor(projectName: string) { + super(`Project scorecard for project ${projectName} not found.`); + } +} + @Injectable() export class EntityPreprocessor { - toProjectScorecardDbEntries(raw: {}): ProjectScorecard[] { + constructor( + @InjectRepository(ProjectScorecard) + private readonly projectScoreCardRepository: Repository, + ) {} + + toProjectScorecardDbEntries(raw: unknown): ProjectScorecard[] { return this.processProjectScorecard(raw['Data_ingestion']); } - toDbEntities(raw: { - Projects: ExcelProjects[]; - 'Project size': ExcelProjectSize[]; - 'Feasibility analysis': ExcelFeasibilityAnalysis[]; - 'Conservation planning and admin': ExcelConservationPlanningAndAdmin[]; - 'Data collection and field costs': ExcelDataCollectionAndFieldCosts[]; - 'Community representation': ExcelCommunityRepresentation[]; - 'Blue carbon project planning': ExcelBlueCarbonProjectPlanning[]; - 'Establishing carbon rights': ExcelEstablishingCarbonRights[]; - 'Financing cost': ExcelFinancingCost[]; - Validation: ExcelValidation[]; - Monitoring: ExcelMonitoring[]; - Maintenance: ExcelMaintenance[]; - 'Community benefit sharing fund': ExcelCommunityBenefitSharingFund[]; - 'Baseline reassessment': ExcelBaselineReassessment[]; - MRV: ExcelMRV[]; - 'Long-term project operating': ExcelLongTermProjectOperating[]; - 'Carbon standard fees': ExcelCarbonStandardFees[]; - 'Community cash flow': ExcelCommunityCashFlow[]; - 'Ecosystem extent': ExcelEcosystemExtent[]; - 'Ecosystem loss': ExcelEcosystemLoss[]; - 'Restorable land': ExcelRestorableLand[]; - 'Sequestration rate': ExcelSequestrationRate[]; - 'Emission factors': ExcelEmissionFactors[]; - 'Implementation labor': ExcelImplementationLaborCost[]; - base_size_table: ExcelBaseSize[]; - base_increase: ExcelBaseIncrease[]; - 'Model assumptions': ExcelModelAssumptions[]; - }): ParsedDBEntities { - const processedProjects = this.processProjects(raw.Projects); + async toDbEntities(raw: RawDataIngestionData): Promise { + const processedProjects = await this.processProjects(raw.Projects); // process cost inputs const projectSize = this.processProjectSize(raw['Project size']); @@ -1106,69 +1092,87 @@ export class EntityPreprocessor { return parsedArray; } - private processProjects(raw: ExcelProjects[]) { - const parsedArray: Project[] = []; - raw.forEach((row: ExcelProjects) => { - const project = new Project(); - project.projectName = row.project_name; - project.countryCode = row.country_code; - project.ecosystem = row.ecosystem; - project.activity = row.activity; - project.restorationActivity = row.activity_type; - project.projectSize = row.project_size_ha; - project.projectSizeFilter = row.project_size_filter; - project.abatementPotential = row.abatement_potential; - project.opexNPV = row.opex_npv; - project.opex = row.opex; - project.capexNPV = row.capex_npv; - project.capex = row.capex; - project.totalCostNPV = row.total_cost_npv; - project.totalCost = row.total_cost; - project.costPerTCO2eNPV = row.cost_per_tco2e_npv; - project.costPerTCO2e = row.cost_per_tco2e; - project.initialPriceAssumption = row.initial_price_assumption; - project.priceType = row.price_type; - project.feasibilityAnalysisNPV = row.feasibility_analysis_npv; - project.feasibilityAnalysis = row.feasibility_analysis; - project.conservationPlanningNPV = row.conservation_planning_npv; - project.conservationPlanning = row.conservation_planning; - project.dataCollectionNPV = row.data_collection_npv; - project.dataCollection = row.data_collection; - project.communityRepresentationNPV = row.community_representation_npv; - project.communityRepresentation = row.community_representation; - project.blueCarbonProjectPlanningNPV = - row.blue_carbon_project_planning_npv; - project.blueCarbonProjectPlanning = row.blue_carbon_project_planning; - project.establishingCarbonRightsNPV = row.establishing_carbon_rights_npv; - project.establishingCarbonRights = row.establishing_carbon_rights; - project.validationNPV = row.validation_npv; - project.validation = row.validation; - project.implementationLaborNPV = row.implementation_labor_npv; - project.implementationLabor = row.implementation_labor; - project.monitoringNPV = row.monitoring_npv; - project.maintenanceNPV = row.maintenance_npv; - project.monitoring = row.monitoring; - project.maintenance = row.maintenance; - project.monitoringMaintenanceNPV = row.monitoring_maintenance_npv; - project.monitoringMaintenance = row.monitoring_maintenance; - project.communityBenefitNPV = row.community_benefit_npv; - project.communityBenefit = row.community_benefit; - project.carbonStandardFeesNPV = row.carbon_standard_fees_npv; - project.carbonStandardFees = row.carbon_standard_fees; - project.baselineReassessmentNPV = row.baseline_reassessment_npv; - project.baselineReassessment = row.baseline_reassessment; - project.mrvNPV = row.mrv_npv; - project.mrv = row.mrv; - project.longTermProjectOperatingNPV = row.long_term_project_operating_npv; - project.longTermProjectOperating = row.long_term_project_operating; - project.leftoverAfterOpex = row.leftover_after_opex; - project.leftoverAfterOpexNPV = row.leftover_after_opex_NPV; - project.totalRevenue = row.total_revenue; - project.totalRevenueNPV = row.total_revenu_npv; - project.creditsIssued = row.credits_issued; - parsedArray.push(project); - }); - return parsedArray; + private async processProjects(raw: ExcelProject[]) { + return await Promise.all( + raw.map(async (row: ExcelProject) => { + const projectName = row.project_name; + const ecosystem = row.ecosystem; + const country_code = row.country_code; + + const projectScoreCard = + await this.projectScoreCardRepository.findOneBy({ + countryCode: country_code, + ecosystem, + }); + if (projectScoreCard === null) { + throw new ProjectScoreCardNotFoundError(projectName); + } + const scoreCardRating = + ProjectScoreUtils.computeProjectScoreCardRating(projectScoreCard); + + const project = new Project(); + project.projectName = projectName; + project.countryCode = country_code; + project.ecosystem = ecosystem; + project.activity = row.activity; + project.restorationActivity = row.activity_type; + project.projectSize = row.project_size_ha; + project.projectSizeFilter = row.project_size_filter; + project.abatementPotential = row.abatement_potential; + project.opexNPV = row.opex_npv; + project.opex = row.opex; + project.capexNPV = row.capex_npv; + project.capex = row.capex; + project.totalCostNPV = row.total_cost_npv; + project.totalCost = row.total_cost; + project.costPerTCO2eNPV = row.cost_per_tco2e_npv; + project.costPerTCO2e = row.cost_per_tco2e; + project.initialPriceAssumption = row.initial_price_assumption; + project.priceType = row.price_type; + project.feasibilityAnalysisNPV = row.feasibility_analysis_npv; + project.feasibilityAnalysis = row.feasibility_analysis; + project.conservationPlanningNPV = row.conservation_planning_npv; + project.conservationPlanning = row.conservation_planning; + project.dataCollectionNPV = row.data_collection_npv; + project.dataCollection = row.data_collection; + project.communityRepresentationNPV = row.community_representation_npv; + project.communityRepresentation = row.community_representation; + project.blueCarbonProjectPlanningNPV = + row.blue_carbon_project_planning_npv; + project.blueCarbonProjectPlanning = row.blue_carbon_project_planning; + project.establishingCarbonRightsNPV = + row.establishing_carbon_rights_npv; + project.establishingCarbonRights = row.establishing_carbon_rights; + project.validationNPV = row.validation_npv; + project.validation = row.validation; + project.implementationLaborNPV = row.implementation_labor_npv; + project.implementationLabor = row.implementation_labor; + project.monitoringNPV = row.monitoring_npv; + project.maintenanceNPV = row.maintenance_npv; + project.monitoring = row.monitoring; + project.maintenance = row.maintenance; + project.monitoringMaintenanceNPV = row.monitoring_maintenance_npv; + project.monitoringMaintenance = row.monitoring_maintenance; + project.communityBenefitNPV = row.community_benefit_npv; + project.communityBenefit = row.community_benefit; + project.carbonStandardFeesNPV = row.carbon_standard_fees_npv; + project.carbonStandardFees = row.carbon_standard_fees; + project.baselineReassessmentNPV = row.baseline_reassessment_npv; + project.baselineReassessment = row.baseline_reassessment; + project.mrvNPV = row.mrv_npv; + project.mrv = row.mrv; + project.longTermProjectOperatingNPV = + row.long_term_project_operating_npv; + project.longTermProjectOperating = row.long_term_project_operating; + project.leftoverAfterOpex = row.leftover_after_opex; + project.leftoverAfterOpexNPV = row.leftover_after_opex_NPV; + project.totalRevenue = row.total_revenue; + project.totalRevenueNPV = row.total_revenu_npv; + project.creditsIssued = row.credits_issued; + project.scoreCardRating = scoreCardRating; + return project; + }), + ); } private processProjectScorecard(raw: ExcelProjectScorecard[]) { diff --git a/shared/entities/project-score.utils.ts b/shared/entities/project-score.utils.ts new file mode 100644 index 00000000..9e62ef39 --- /dev/null +++ b/shared/entities/project-score.utils.ts @@ -0,0 +1,67 @@ +import { PROJECT_SCORE } from "@shared/entities/project-score.enum"; +import { ProjectScorecard } from "@shared/entities/project-scorecard.entity"; + +export const ProjectScoreUtils = { + toNumber: (score: PROJECT_SCORE): number => { + switch (score) { + case PROJECT_SCORE.LOW: + return 1; + case PROJECT_SCORE.MEDIUM: + return 2; + case PROJECT_SCORE.HIGH: + return 3; + default: + return 0; + } + }, + computeProjectScoreCardRating( + projectScoreCard: ProjectScorecard, + ): PROJECT_SCORE | null { + const legalFeasibility = ProjectScoreUtils.toNumber( + projectScoreCard.legalFeasibility, + ); + const implementationFeasibility = ProjectScoreUtils.toNumber( + projectScoreCard.implementationFeasibility, + ); + const socialFeasibility = ProjectScoreUtils.toNumber( + projectScoreCard.socialFeasibility, + ); + const securityRating = ProjectScoreUtils.toNumber( + projectScoreCard.securityRating, + ); + const availabilityOfExperiencedLabor = ProjectScoreUtils.toNumber( + projectScoreCard.availabilityOfExperiencedLabor, + ); + const availabilityOfAlternatingFunding = ProjectScoreUtils.toNumber( + projectScoreCard.availabilityOfAlternatingFunding, + ); + const coastalProtectionBenefits = ProjectScoreUtils.toNumber( + projectScoreCard.coastalProtectionBenefits, + ); + const biodiversityBenefit = ProjectScoreUtils.toNumber( + projectScoreCard.biodiversityBenefit, + ); + + const scoreCardRating: number = + (legalFeasibility * 0.12 + + implementationFeasibility * 0.12 + + socialFeasibility * 0.12 + + securityRating * 0.05 + + availabilityOfExperiencedLabor * 0.1 + + availabilityOfAlternatingFunding * 0.05 + + coastalProtectionBenefits * 0.03 + + biodiversityBenefit * 0.03) / + (0.12 + 0.12 + 0.12 + 0.05 + 0.1 + 0.05 + 0.03 + 0.03); + + if (scoreCardRating > 1 && scoreCardRating <= 1.666) { + return PROJECT_SCORE.HIGH; + } + if (scoreCardRating > 1.666 && scoreCardRating <= 2.333) { + return PROJECT_SCORE.MEDIUM; + } + if (scoreCardRating > 2.333 && scoreCardRating <= 3) { + return PROJECT_SCORE.LOW; + } + return null; + }, +} as const; diff --git a/shared/entities/project-scorecard.entity.ts b/shared/entities/project-scorecard.entity.ts index f4ba29ce..b030992e 100644 --- a/shared/entities/project-scorecard.entity.ts +++ b/shared/entities/project-scorecard.entity.ts @@ -6,6 +6,7 @@ import { ManyToOne, Unique, JoinColumn, + BeforeInsert, } from "typeorm"; import { Country } from "@shared/entities/country.entity"; import { ECOSYSTEM } from "@shared/entities/ecosystem.enum"; diff --git a/shared/entities/projects.entity.ts b/shared/entities/projects.entity.ts index 452af0d2..db348a86 100644 --- a/shared/entities/projects.entity.ts +++ b/shared/entities/projects.entity.ts @@ -387,6 +387,7 @@ export class Project extends BaseEntity { enum: PROJECT_SCORE, default: PROJECT_SCORE.MEDIUM, name: "score_card_rating", + nullable: true, }) scoreCardRating: PROJECT_SCORE; }