Skip to content

Commit

Permalink
fix(CostSurface): Fixes missing min/max update for default CostSurfac…
Browse files Browse the repository at this point in the history
…e metadata for non shapefile based Projects
  • Loading branch information
KevSanchez committed Nov 27, 2023
1 parent 8c0d69e commit f580e91
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 48 deletions.
3 changes: 2 additions & 1 deletion api/apps/api/src/modules/projects/projects-crud.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,14 @@ export class ProjectsCrudService extends AppBaseService<
this.logger.debug(
'creating planning unit job and assigning project to area',
);
const defaultCostSurfaceId = getDefaultCostSurfaceIdFromProject(model);
await Promise.all([
this.planningUnitsService.create({
...createModel,
planningUnitAreakm2: createModel.planningUnitAreakm2,
planningUnitGridShape: createModel.planningUnitGridShape,
projectId: model.id,
costSurfaceId: getDefaultCostSurfaceIdFromProject(model),
costSurfaceId: defaultCostSurfaceId,
}),
this.planningAreasService.assignProject({
projectId: model.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ import { ProjectCostSurfacePersistencePort } from '@marxan-geoprocessing/modules
useClass: ShapefileConverter,
},
],
exports: [ProjectCostSurfacePersistencePort],
})
export class ProjectCostSurfaceModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { validate } from 'class-validator';
import { chunk } from 'lodash';
import { EntityManager } from 'typeorm';
import { CHUNK_SIZE_FOR_BATCH_GEODB_OPERATIONS } from '@marxan-geoprocessing/utils/chunk-size-for-batch-geodb-operations';
import { ProjectCostSurfacePersistencePort } from '@marxan-geoprocessing/modules/cost-surface/ports/persistence/project-cost-surface-persistence.port';

type CustomPlanningAreaJob = Required<
Omit<
Expand Down Expand Up @@ -56,6 +57,7 @@ export class PlanningUnitsJobProcessor {
private logger = new Logger('planning-units-job-processor');

constructor(
private readonly repo: ProjectCostSurfacePersistencePort,
@InjectEntityManager()
private readonly entityManager: EntityManager,
) {}
Expand Down Expand Up @@ -262,6 +264,8 @@ grid.geom

return geometries;
});

await this.repo.updateCostSurfaceRange(job.data.costSurfaceId!);
this.logger.debug(`Finished planning-units processing for ${job.id}`);
} catch (err) {
this.logger.error(err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { ShapefileService, FileService } from '@marxan/shapefile-converter';
import { PlanningUnitsService } from './planning-units.service';
import { WorkerModule } from '../worker';
import { PlanningUnitsJobProcessor } from './planning-units.job';
import { ProjectCostSurfaceModule } from '@marxan-geoprocessing/modules/cost-surface/project/project-cost-surface.module';

@Module({
imports: [TileModule, WorkerModule],
imports: [TileModule, WorkerModule, ProjectCostSurfaceModule],
providers: [
PlanningUnitsProcessor,
ShapefileService,
Expand Down
29 changes: 22 additions & 7 deletions api/apps/geoprocessing/test/e2e.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ interface OptionsWithCountryCode {
countryCode: string;
adminAreaLevel1Id?: string;
adminAreaLevel2Id?: string;
planningUnitAreakm2?: number;
projectId?: string;
costSurfaceId?: string;
}

export const E2E_CONFIG: {
Expand All @@ -32,16 +35,22 @@ export const E2E_CONFIG: {
adminAreaLevel2Id:
options.adminAreaLevel2Id ?? faker.random.alphaNumeric(12),
planningUnitGridShape: PlanningUnitGridShape.Hexagon,
planningUnitAreakm2: 100,
projectId: 'a9d965a2-35ce-44b2-8112-50bcdfe98447',
planningUnitAreakm2: options.planningUnitAreakm2 ?? 100,
projectId:
options.projectId ?? 'a9d965a2-35ce-44b2-8112-50bcdfe98447',
costSurfaceId:
options.costSurfaceId ?? '700d7cf9-011b-4adf-bbe8-4c35a100abc0',
}),
adminRegion: (options: OptionsWithCountryCode): PlanningUnitsJob => ({
countryId: options.countryCode,
adminAreaLevel1Id: faker.random.alphaNumeric(7),
adminAreaLevel2Id: faker.random.alphaNumeric(12),
planningUnitGridShape: PlanningUnitGridShape.Square,
planningUnitAreakm2: 100,
projectId: 'a9d965a2-35ce-44b2-8112-50bcdfe98447',
planningUnitAreakm2: options.planningUnitAreakm2 ?? 100,
projectId:
options.projectId ?? 'a9d965a2-35ce-44b2-8112-50bcdfe98447',
costSurfaceId:
options.costSurfaceId ?? '700d7cf9-011b-4adf-bbe8-4c35a100abc0',
}),
},
invalid: {
Expand All @@ -51,15 +60,21 @@ export const E2E_CONFIG: {
adminAreaLevel2Id: faker.random.alphaNumeric(12),
planningUnitGridShape: PlanningUnitGridShape.Hexagon,
planningUnitAreakm2: -100,
projectId: 'a9d965a2-35ce-44b2-8112-50bcdfe98447',
projectId:
options.projectId ?? 'a9d965a2-35ce-44b2-8112-50bcdfe98447',
costSurfaceId:
options.costSurfaceId ?? '700d7cf9-011b-4adf-bbe8-4c35a100abc0',
}),
adminRegion: (options: OptionsWithCountryCode): PlanningUnitsJob => ({
countryId: options.countryCode,
adminAreaLevel1Id: faker.random.alphaNumeric(7),
adminAreaLevel2Id: faker.random.alphaNumeric(12),
planningUnitGridShape: PlanningUnitGridShape.Square,
planningUnitAreakm2: 100,
projectId: 'a9d965a2-35ce-44b2-8112-50bcdfe98447',
planningUnitAreakm2: options.planningUnitAreakm2 ?? 100,
projectId:
options.projectId ?? 'a9d965a2-35ce-44b2-8112-50bcdfe98447',
costSurfaceId:
options.costSurfaceId ?? '700d7cf9-011b-4adf-bbe8-4c35a100abc0',
}),
},
},
Expand Down
222 changes: 183 additions & 39 deletions api/apps/geoprocessing/test/planning-units-processor.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,45 @@ import {
ProjectsPuEntity,
} from '@marxan-jobs/planning-unit-geometry';
import { PlanningUnitsJob } from '@marxan-jobs/planning-unit-geometry/create.regular.planning-units.dto';
import { Test } from '@nestjs/testing';
import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm';
import { Job } from 'bullmq';
import { In, Repository } from 'typeorm';
import { Test, TestingModule } from '@nestjs/testing';
import {
PlanningUnitsJobProcessor,
RegularPlanningAreaJob,
} from '../src/modules/planning-units/planning-units.job';
getEntityManagerToken,
getRepositoryToken,
TypeOrmModule,
} from '@nestjs/typeorm';
import { Job } from 'bullmq';
import { EntityManager, In, Repository } from 'typeorm';
import { E2E_CONFIG } from './e2e.config';
import { seedAdminRegions } from './utils/seeds/seed-admin-regions';
import { CostSurfacePuDataEntity } from '@marxan/cost-surfaces';
import { v4 } from 'uuid';
import { ProjectCostSurfaceModule } from '@marxan-geoprocessing/modules/cost-surface/project/project-cost-surface.module';
import {
PlanningUnitsJobProcessor,
RegularPlanningAreaJob,
} from '@marxan-geoprocessing/modules/planning-units/planning-units.job';

/**
* @TODO
* we need to add a couple of test that cath errors on invalid user input.
*/
describe('planning units jobs (e2e)', () => {
let sut: PlanningUnitsJobProcessor;
let data: PlanningUnitsJob;
let projectsPuRepo: Repository<ProjectsPuEntity>;
let planningUnitsRepo: Repository<PlanningUnitsGeom>;
let costPuDataRepo: Repository<CostSurfacePuDataEntity>;
let fixtures: any;

beforeEach(async () => {
const sandbox = await Test.createTestingModule({
imports: [
ProjectCostSurfaceModule,
TypeOrmModule.forRoot({
...geoprocessingConnections.default,
keepConnectionAlive: true,
logging: false,
}),
TypeOrmModule.forRoot({
...geoprocessingConnections.apiDB,
keepConnectionAlive: true,
logging: false,
}),
TypeOrmModule.forFeature(
[ProjectsPuEntity, PlanningUnitsGeom, CostSurfacePuDataEntity],
geoprocessingConnections.default,
Expand All @@ -44,46 +51,169 @@ describe('planning units jobs (e2e)', () => {
providers: [PlanningUnitsJobProcessor],
}).compile();

projectsPuRepo = sandbox.get(getRepositoryToken(ProjectsPuEntity));
costPuDataRepo = sandbox.get(getRepositoryToken(CostSurfacePuDataEntity));
planningUnitsRepo = sandbox.get(getRepositoryToken(PlanningUnitsGeom));
sut = sandbox.get(PlanningUnitsJobProcessor);

await seedAdminRegions(sandbox);

fixtures = await getPlanningAreaFixtures(sandbox);
});

afterEach(async () => {
const projectId = data.projectId;
await fixtures.cleanup();
});

const projectPus = await projectsPuRepo.find({ where: { projectId } });
const geometriesIds = projectPus.map((projectPu) => projectPu.geomId);
it('executes the child job processor with mock data', async () => {
const data = E2E_CONFIG.planningUnits.creationJob.valid.customArea({
countryCode: 'NAM',
adminAreaLevel1Id: 'NAM.13_1',
adminAreaLevel2Id: 'NAM.13.5_1',
});
const job = fixtures.GivenCreatePlanningUnitJob(data);

await planningUnitsRepo.delete({ id: In(geometriesIds) });
await fixtures.WhenProcessingJob(job);

await fixtures.ThenPusWereSaved(data.projectId);
});

it(
'executes the child job processor with mock data',
async () => {
data = E2E_CONFIG.planningUnits.creationJob.valid.customArea({
countryCode: 'NAM',
adminAreaLevel1Id: 'NAM.13_1',
adminAreaLevel2Id: 'NAM.13.5_1',
});
it('updates min/max on associated cost surface metadata', async () => {
const projectId1 = await fixtures.GivenProjectMetadata();
const projectId2 = await fixtures.GivenProjectMetadata();
const costSurfaceId1 = await fixtures.GivenCostSurfaceMetadata(
projectId1,
true,
);
const costSurfaceId2 = await fixtures.GivenCostSurfaceMetadata(
projectId2,
true,
);
const data1 = E2E_CONFIG.planningUnits.creationJob.valid.customArea({
countryCode: 'NAM',
adminAreaLevel1Id: 'NAM.13_1',
adminAreaLevel2Id: 'NAM.13.5_1',
planningUnitAreakm2: 23,
projectId: projectId1,
costSurfaceId: costSurfaceId1,
});

const createPlanningUnitsDTO = {
const data2 = E2E_CONFIG.planningUnits.creationJob.valid.customArea({
countryCode: 'NAM',
adminAreaLevel1Id: 'NAM.13_1',
adminAreaLevel2Id: 'NAM.13.5_1',
planningUnitAreakm2: 76,
projectId: projectId2,
costSurfaceId: costSurfaceId2,
});

const job1 = fixtures.GivenCreatePlanningUnitJob(data1);
const job2 = fixtures.GivenCreatePlanningUnitJob(data2);

await fixtures.WhenProcessingJob(job1);
await fixtures.WhenProcessingJob(job2);

await fixtures.ThenPusWereSaved(data1.projectId);
await fixtures.ThenPusWereSaved(data2.projectId);
await fixtures.ThenMinMaxForCostSurfaceMetadataWasUpdated(
data1.costSurfaceId!,
{ min: 23, max: 23 },
);
await fixtures.ThenMinMaxForCostSurfaceMetadataWasUpdated(
data2.costSurfaceId!,
{ min: 76, max: 76 },
);
});
});

const getPlanningAreaFixtures = async (sandbox: TestingModule) => {
const sut = sandbox.get(PlanningUnitsJobProcessor);

const projectsPuRepo: Repository<ProjectsPuEntity> = sandbox.get(
getRepositoryToken(ProjectsPuEntity),
);
const costPuDataRepo: Repository<CostSurfacePuDataEntity> = sandbox.get(
getRepositoryToken(CostSurfacePuDataEntity),
);
const planningUnitsRepo: Repository<PlanningUnitsGeom> = sandbox.get(
getRepositoryToken(PlanningUnitsGeom),
);

const apiEntityManager: EntityManager = sandbox.get(
getEntityManagerToken(geoprocessingConnections.apiDB.name),
);

return {
cleanup: async () => {
await apiEntityManager
.createQueryBuilder()
.delete()
.from('cost_surfaces')
.execute();
await apiEntityManager
.createQueryBuilder()
.delete()
.from('projects')
.execute();
await apiEntityManager
.createQueryBuilder()
.delete()
.from('organizations')
.execute();

await projectsPuRepo.delete({});
await planningUnitsRepo.delete({});
},

GivenProjectMetadata: async () => {
const organizationId = v4();
await apiEntityManager
.createQueryBuilder()
.insert()
.into('organizations')
.values({ id: organizationId, name: organizationId })
.execute();
const projectId = v4();
await apiEntityManager
.createQueryBuilder()
.insert()
.into('projects')
.values({
id: projectId,
name: projectId,
organization_id: organizationId,
})
.execute();
return projectId;
},
GivenCostSurfaceMetadata: async (projectId: string, isDefault: boolean) => {
const id = v4();
await apiEntityManager
.createQueryBuilder()
.insert()
.into('cost_surfaces')
.values({
project_id: projectId,
name: id,
id,
is_default: isDefault,
min: 1,
max: 1,
})
.execute();
return id;
},
GivenCreatePlanningUnitJob: (data: PlanningUnitsJob) => {
return {
id: '1',
name: 'create-regular-pu',
data: {
...data,
costSurfaceId: v4(),
},
data,
} as Job<RegularPlanningAreaJob>;
},

await expect(sut.process(createPlanningUnitsDTO)).resolves.not.toThrow();
WhenProcessingJob: async (createPlanningUnitJob: Job) => {
await expect(sut.process(createPlanningUnitJob)).resolves.not.toThrow();
},

ThenPusWereSaved: async (projectId: string) => {
const projectPus = await projectsPuRepo.find({
where: {
projectId: data.projectId,
projectId,
},
});
const costPus = await costPuDataRepo.find({
Expand All @@ -93,6 +223,20 @@ describe('planning units jobs (e2e)', () => {
expect(projectPus.length).toBeGreaterThan(0);
expect(costPus.length).toEqual(projectPus.length);
},
50 * 1000,
);
});
ThenMinMaxForCostSurfaceMetadataWasUpdated: async (
costSurfaceId: string,
range: { min: number; max: number },
) => {
const [result] = await apiEntityManager
.createQueryBuilder()
.select('min')
.addSelect('max')
.from('cost_surfaces', 'cs')
.where('id = :costSurfaceId and is_default = true', { costSurfaceId })
.execute();

expect(result.min).toBe(range.min);
expect(result.max).toBe(range.max);
},
};
};

1 comment on commit f580e91

@vercel
Copy link

@vercel vercel bot commented on f580e91 Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

marxan – ./

marxan-vizzuality1.vercel.app
marxan-git-develop-vizzuality1.vercel.app
marxan23.vercel.app

Please sign in to comment.