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

Feat/user data importer #111

Merged
merged 5 commits into from
Nov 20, 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
6 changes: 6 additions & 0 deletions admin/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ import { ImplementationLaborCost } from "@shared/entities/cost-inputs/implementa
import { BaseSize } from "@shared/entities/base-size.entity.js";
import { BaseIncrease } from "@shared/entities/base-increase.entity.js";
import { ModelAssumptions } from "@shared/entities/model-assumptions.entity.js";
import { UserUploadCostInputs } from "@shared/entities/users/user-upload-cost-inputs.entity.js";
import { UserUploadRestorationInputs } from "@shared/entities/users/user-upload-restoration-inputs.entity.js";
import { UserUploadConservationInputs } from "@shared/entities/users/user-upload-conservation-inputs.entity.js";

// TODO: If we import the COMMON_DATABASE_ENTITIES from shared, we get an error where DataSouce is not set for a given entity
export const ADMINJS_ENTITIES = [
User,
UserUploadCostInputs,
UserUploadRestorationInputs,
UserUploadConservationInputs,
ApiEventsEntity,
Country,
ProjectSize,
Expand Down
35 changes: 34 additions & 1 deletion admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import { ImplementationLaborCostResource } from "./resources/implementation-labo
import { BaseSizeResource } from "./resources/base-size/base-size.resource.js";
import { BaseIncreaseResource } from "./resources/base-increase/base-increase.resource.js";
import { ModelAssumptionResource } from "./resources/model-assumptions/model-assumptions.resource.js";
import { UserUploadCostInputs } from "@shared/entities/users/user-upload-cost-inputs.entity.js";
import { UserUploadConservationInputs } from "@shared/entities/users/user-upload-conservation-inputs.entity.js";
import { UserUploadRestorationInputs } from "@shared/entities/users/user-upload-restoration-inputs.entity.js";

AdminJS.registerAdapter({
Database: AdminJSTypeorm.Database,
Expand All @@ -59,6 +62,36 @@ const start = async () => {
componentLoader,
resources: [
UserResource,
{
resource: UserUploadCostInputs,
name: "UserUploadCostInputs",
options: {
navigation: {
name: "User Data",
icon: "File",
},
},
},
{
resource: UserUploadConservationInputs,
name: "UserUploadConservationInputs",
options: {
navigation: {
name: "User Data",
icon: "File",
},
},
},
{
resource: UserUploadRestorationInputs,
name: "UserUploadRestorationInputs",
options: {
navigation: {
name: "User Data",
icon: "File",
},
},
},
ProjectSizeResource,
FeasibilityAnalysisResource,
ConservationAndPlanningAdminResource,
Expand Down Expand Up @@ -119,7 +152,7 @@ const start = async () => {

app.listen(PORT, () => {
console.log(
`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`
`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`,
);
});
};
Expand Down
29 changes: 27 additions & 2 deletions api/src/modules/import/import.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Controller, UseGuards, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
Controller,
HttpStatus,
UploadedFiles,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from '@api/modules/auth/guards/jwt-auth.guard';
import { RolesGuard } from '@api/modules/auth/guards/roles.guard';
import { RequiredRoles } from '@api/modules/auth/decorators/roles.decorator';
Expand All @@ -13,6 +19,7 @@ import { ControllerResponse } from '@api/types/controller-response.type';
import { Multer } from 'multer';
import { GetUser } from '@api/modules/auth/decorators/get-user.decorator';
import { User } from '@shared/entities/users/user.entity';
import { usersContract } from '@shared/contracts/users.contract';

@Controller()
@UseGuards(JwtAuthGuard, RolesGuard)
Expand All @@ -36,4 +43,22 @@ export class ImportController {
};
});
}

@UseInterceptors(FilesInterceptor('files', 2))
@RequiredRoles(ROLES.PARTNER, ROLES.ADMIN)
@TsRestHandler(usersContract.uploadData)
async uploadData(
@GetUser() user: User,
@UploadedFiles() files: Array<Express.Multer.File>,
): Promise<any> {
return tsRestHandler(usersContract.uploadData, async () => {
const [file1, file2] = files;
const [file1Buffer, file2Buffer] = [file1.buffer, file2.buffer];
const data = await this.service.importDataProvidedByPartner(
[file1Buffer, file2Buffer],
user.id,
);
return { body: data, status: HttpStatus.OK };
});
}
}
42 changes: 42 additions & 0 deletions api/src/modules/import/import.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import { ImportRepository } from '@api/modules/import/import.repostiory';
import { EventBus } from '@nestjs/cqrs';
import { API_EVENT_TYPES } from '@api/modules/api-events/events.enum';
import { ImportEvent } from '@api/modules/import/events/import.event';
import { DataSource } from 'typeorm';
import {
userDataConservationInputMapJsonToEntity,
userDataCostInputsMapJsonToEntity,
userDataRestorationInputMapJsonToEntity,
} from '@api/modules/import/services/user-data-parser';
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';

@Injectable()
export class ImportService {
Expand All @@ -24,6 +33,7 @@ export class ImportService {
private readonly importRepo: ImportRepository,
private readonly preprocessor: EntityPreprocessor,
private readonly eventBus: EventBus,
private readonly dataSource: DataSource,
) {}

async import(fileBuffer: Buffer, userId: string) {
Expand All @@ -44,4 +54,36 @@ export class ImportService {
registerImportEvent(userId: string, eventType: typeof this.eventMap) {
this.eventBus.publish(new ImportEvent(eventType, userId, {}));
}

async importDataProvidedByPartner(fileBuffers: Buffer[], userId: string) {
// TODO: Debt, add event handling
const { costInputs, carbonInputs } =
await this.excelParser.parseUserExcels(fileBuffers);
const mappedCostInputs = userDataCostInputsMapJsonToEntity(
costInputs,
userId,
);
const mappedRestorationInputs = userDataRestorationInputMapJsonToEntity(
carbonInputs.restoration,
userId,
);
const mappedConservationInputs = userDataConservationInputMapJsonToEntity(
carbonInputs.conservation,
userId,
);
await this.dataSource.transaction(async (manager) => {
const userCostInputsRepo = manager.getRepository(UserUploadCostInputs);
const userRestorationInputsRepo = manager.getRepository(
UserUploadRestorationInputs,
);
const userConservationInputsRepo = manager.getRepository(
UserUploadConservationInputs,
);
await userCostInputsRepo.save(mappedCostInputs);
await userRestorationInputsRepo.save(mappedRestorationInputs);
await userConservationInputsRepo.save(mappedConservationInputs);
});
//
return carbonInputs;
}
}
2 changes: 2 additions & 0 deletions api/src/modules/import/services/excel-parser.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ export const SHEETS_TO_PARSE = [

export interface ExcelParserInterface {
parseExcel(data: Buffer): Promise<any>;

parseUserExcels(data: Buffer[]): Promise<any>;
}
150 changes: 150 additions & 0 deletions api/src/modules/import/services/user-data-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// I feel dirty doing this...

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';

export function userDataCostInputsMapJsonToEntity(
inputJson: Record<string, any>,
userId: string,
): Partial<UserUploadCostInputs> {
return {
programName: inputJson['Program name (if willing to share)'],
intendedLengthOfProject: inputJson['Intended length of project'],
country: inputJson['Country'],
currency: inputJson['Currency'],
projectStartYear: inputJson['Project start year'],
projectActivity: inputJson['Project activity'],
ecosystem: inputJson['Ecosystem'],
projectSize: inputJson['Project size'],
validationStandard:
inputJson['Validation standard / accrediting organization'],
numberOfLocalIndividuals:
inputJson[
'Number of local individuals who own, work, and/or live in the project site (e.g., land tenure)'
],
cityOrRegion: inputJson['City / province / region / state of project'],
intendedAlternativeUseOfLand: inputJson['Intended alternative use of land'],
landOwnershipBeforeProject: inputJson['Land ownership before project'],
sdgsBenefitted: inputJson['SDGs benefitted / co-benefitted'],
projectEligibleForCarbonCredits:
inputJson['Project eligible for voluntary carbon credits?'] === 'yes',
willingToSpeakAboutPricing:
inputJson[
'Are you willing to speak with us about your carbon credit pricing?'
] === 'yes',
ableToProvideDetailedCostDocumentation:
inputJson[
'Are you able to provide additional detailed cost documentation for the project?'
] === 'yes',
costCategories: inputJson['Cost categories '],
establishingCommunityEngagement:
inputJson['Establishing community engagement / buy-in'],
conservationProjectPlanning:
inputJson['Conservation project planning & administration '],
carbonProjectPlanning:
inputJson['Carbon project planning & administration'],
landCost: inputJson['Land cost'],
financingCost: inputJson['Financing cost'],
materialsSeedsFertilizer:
inputJson['Materials (e.g., seeds, fertilizer, seedlings)'],
materialsMachineryEquipment:
inputJson['Materials (e.g., machinery, equipment, etc.)'],
projectLaborActivity: inputJson['Project labor / activity'],
engineeringIntervention:
inputJson['Engineering / construction intervention'],
ongoingCommunityEngagement: inputJson['Ongoing community engagement'],
otherProjectRunningCost: inputJson['Other project running cost'],
projectMonitoring: inputJson['Project monitoring'],
otherCost1: inputJson['1) Other cost (please specify activities)'],
otherCost2: inputJson['2) Other cost (please specify activities)'],
otherCost3: inputJson['3) Other cost (please specify activities)'],
projectCumulativeSequestration:
inputJson['Project site cumulative sequestration / carbon stock'],
detailedProjectActivity:
inputJson[
'Please describe in detail the project activity (e.g., planted mangrove seedlings, set up perimeter around conservation area)'
],
communityEngagementSpending:
inputJson[
'When you kicked off the project, how did you spend to engage the community...'
],
landRightsAndEasements:
inputJson[
'How did you acquire the rights to establish the project on the land?...'
],
hourlyWageRate:
inputJson[
'What was the hourly wage rate paid for labor? How many hours worked for each activity?'
],
ongoingCommunityCompensation:
inputJson[
'Please describe the ongoing community engagement for your project.'
],
engineeringDetails:
inputJson[
'Did you undertake any engineering / construction interventions for your project?'
],
user: { id: userId } as any,
};
}

export function userDataRestorationInputMapJsonToEntity(
data: Record<string, any>,
userId: string,
): Partial<UserUploadRestorationInputs> {
return {
projectName: data['Project name'],
country: data['Country'],
cityOrRegion: data['City / province / region / state of project'],
projectStartYear: data['Project start year'],
mostRecentYearOfData: data['Most recent year of data collection'],
ecosystem: data['Ecosystem'],
projectActivity: data['Project activity']?.toString(), // Convertir a string si es numérico
projectSizeAreaStudied: data['Project size / area studied'],
categories: data['Categories'],
projectArea: data['Project area '],
abovegroundBiomassStock: data['Aboveground biomass stock'],
belowgroundBiomassStock: data['Belowground biomass stock '],
soilOrganicCarbonStock: data['Soil organic carbon stock '],
methaneEmissions: data['Methane emissions '],
nitrousOxideEmissions: data['Nitrous oxide emissions '],
abovegroundBiomassEmissionsFactor:
data['Aboveground biomass emissions factor'],
belowgroundBiomassEmissionsFactor:
data['Belowground biomass emissions factor'],
soilOrganicCarbonEmissionsFactor:
data['Soil organic carbon emissions factor '],
user: { id: userId } as any,
};
}

export function userDataConservationInputMapJsonToEntity(
data: Record<string, any>,
userId: string,
): Partial<UserUploadConservationInputs> {
return {
projectName: data['Project name'],
country: data['Country'],
cityOrRegion: data['City / province / region / state of project'],
projectStartYear: data['Project start year'],
mostRecentYearOfData: data['Most recent year of data collection'],
ecosystem: data['Ecosystem'],
projectActivity: data['Project activity']?.toString(), // Convertir a string si es numérico
projectSizeAreaStudied: data['Project size / area studied'],
categories: data['Categories'],
projectArea: data['Project area '],
abovegroundBiomassStock: data['Aboveground biomass stock'],
belowgroundBiomassStock: data['Belowground biomass stock '],
soilOrganicCarbonStock: data['Soil organic carbon stock '],
methaneEmissions: data['Methane emissions '],
nitrousOxideEmissions: data['Nitrous oxide emissions '],
abovegroundBiomassEmissionsFactor:
data['Aboveground biomass emissions factor'],
belowgroundBiomassEmissionsFactor:
data['Belowground biomass emissions factor'],
soilOrganicCarbonEmissionsFactor:
data['Soil organic carbon emissions factor '],
user: { id: userId } as any,
};
}
Loading
Loading