Skip to content

Commit

Permalink
feat(api): Refactor, add validations and generate project score card …
Browse files Browse the repository at this point in the history
…rating in the data ingestion excel file process
  • Loading branch information
alepefe committed Jan 9, 2025
1 parent 4b3fb93 commit 201288f
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 107 deletions.
2 changes: 1 addition & 1 deletion api/src/modules/import/dtos/excel-projects.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions api/src/modules/import/import.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ export class ImportController {
@GetUser() user: User,
): Promise<ControllerResponse> {
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,
};
});
}
Expand Down
9 changes: 8 additions & 1 deletion api/src/modules/import/import.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
],
})
Expand Down
25 changes: 18 additions & 7 deletions api/src/modules/import/import.service.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand All @@ -28,6 +29,7 @@ export class ImportService {
};

constructor(
private readonly dataIngestionParser: DataIngestionExcelParser,
@Inject(ExcelParserToken)
private readonly excelParser: ExcelParserInterface,
private readonly importRepo: ImportRepository,
Expand All @@ -54,23 +56,32 @@ export class ImportService {
}
}

async import(fileBuffer: Buffer, userId: string) {
async import(fileBuffer: Buffer, userId: string): Promise<void> {
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) {
Expand Down
89 changes: 89 additions & 0 deletions api/src/modules/import/parser/data-ingestion.xlsx-parser.ts
Original file line number Diff line number Diff line change
@@ -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<RawDataIngestionData> {
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;
}
}
21 changes: 21 additions & 0 deletions api/src/modules/import/parser/excel-parser.interface.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>;
}
57 changes: 57 additions & 0 deletions api/src/modules/import/parser/raw-data-ingestion.type.ts
Original file line number Diff line number Diff line change
@@ -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[];
};
Loading

0 comments on commit 201288f

Please sign in to comment.