diff --git a/api/apps/api/src/modules/geo-features/geo-features.service.ts b/api/apps/api/src/modules/geo-features/geo-features.service.ts index 1a43e0f43e..0f7600ca6c 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.service.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.service.ts @@ -111,6 +111,8 @@ export class GeoFeaturesService extends AppBaseService< private readonly geoDataSource: DataSource, @InjectRepository(GeoFeatureGeometry, DbConnections.geoprocessingDB) private readonly geoFeaturesGeometriesRepository: Repository, + @InjectEntityManager() + private readonly apiEntityManager: EntityManager, @InjectEntityManager(DbConnections.geoprocessingDB) private readonly geoEntityManager: EntityManager, @InjectRepository(GeoFeature) @@ -432,9 +434,9 @@ export class GeoFeaturesService extends AppBaseService< ); await this.saveAmountRangeForFeatures( + [geoFeature.id], apiQueryRunner.manager, geoQueryRunner.manager, - [geoFeature.id], ); await apiQueryRunner.commitTransaction(); @@ -887,10 +889,17 @@ export class GeoFeaturesService extends AppBaseService< } async saveAmountRangeForFeatures( - apiEntityManager: EntityManager, - geoEntityManager: EntityManager, featureIds: string[], + apiEntityManager?: EntityManager, + geoEntityManager?: EntityManager, ) { + apiEntityManager = apiEntityManager + ? apiEntityManager + : this.apiEntityManager; + geoEntityManager = geoEntityManager + ? geoEntityManager + : this.geoEntityManager; + this.logger.log(`Saving min and max amounts for new features...`); const minAndMaxAmountsForFeatures = await geoEntityManager diff --git a/api/apps/api/src/modules/geo-features/import/features-amounts-upload.service.ts b/api/apps/api/src/modules/geo-features/import/features-amounts-upload.service.ts index a5610f9fca..14c4d83556 100644 --- a/api/apps/api/src/modules/geo-features/import/features-amounts-upload.service.ts +++ b/api/apps/api/src/modules/geo-features/import/features-amounts-upload.service.ts @@ -110,9 +110,9 @@ export class FeatureAmountUploadService { this.logger.log(`Saving min and max amounts for new features...`); await this.geoFeaturesService.saveAmountRangeForFeatures( + newFeaturesFromCsvUpload.map((feature) => feature.id), apiQueryRunner.manager, geoQueryRunner.manager, - newFeaturesFromCsvUpload.map((feature) => feature.id), ); this.logger.log(`Csv file upload process finished successfully`); diff --git a/api/apps/api/src/modules/scenarios-features/compute-area.service.spec.ts b/api/apps/api/src/modules/scenarios-features/compute-area.service.spec.ts index f38f0396a2..1a816ec9ed 100644 --- a/api/apps/api/src/modules/scenarios-features/compute-area.service.spec.ts +++ b/api/apps/api/src/modules/scenarios-features/compute-area.service.spec.ts @@ -10,6 +10,8 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { v4 } from 'uuid'; import { Project } from '../projects/project.api.entity'; import { ComputeArea } from './compute-area.service'; +import { GeoFeature } from '@marxan-api/modules/geo-features/geo-feature.api.entity'; +import { GeoFeaturesService } from '@marxan-api/modules/geo-features'; describe(ComputeArea, () => { let fixtures: FixtureType; @@ -17,24 +19,59 @@ describe(ComputeArea, () => { fixtures = await getFixtures(); }); - it('saves amounts per planning unit when computation has not been made', async () => { + it('saves amounts per planning unit when computation has not been made and save min/max amount for the feature', async () => { const { projectId, scenarioId } = fixtures.GivenProject(); const featureId = fixtures.GivenNoComputationHasBeenSaved(); + fixtures.GivenMinMaxAmount(featureId, undefined, undefined); + await fixtures.WhenComputing(projectId, scenarioId, featureId); + await fixtures.ThenComputationsHasBeenSaved(projectId, featureId); + await fixtures.ThenMinMaxAmountWasSaved(); + }); + + it('does not save saves amounts per planning unit when computation has already been made but saves min/max amount for the feature', async () => { + const { projectId, scenarioId } = fixtures.GivenProject(); + const featureId = fixtures.GivenNoComputationHasBeenSaved(); + fixtures.GivenMinMaxAmount(featureId, 1, undefined); + fixtures.GivenComputationAlreadySaved(projectId, featureId); + + await fixtures.WhenComputing(projectId, scenarioId, featureId); + + await fixtures.ThenComputationsWasNotDone(); + await fixtures.ThenMinMaxAmountWasSaved(); + }); + it('does not save saves amounts per planning unit when computation has already been made and does not save min/max amount for the feature if already present', async () => { + const { projectId, scenarioId } = fixtures.GivenProject(); + const featureId = fixtures.GivenNoComputationHasBeenSaved(); + fixtures.GivenMinMaxAmount(featureId, 1, 10); + fixtures.GivenComputationAlreadySaved(projectId, featureId); + + await fixtures.WhenComputing(projectId, scenarioId, featureId); + + await fixtures.ThenComputationsWasNotDone(); + await fixtures.ThenMinMaxAmountWasNotSaved(); }); it('does not save amount per planning unit when is a legacy project', async () => { const { projectId, scenarioId } = await fixtures.GivenLegacyProject(); const featureId = fixtures.GivenNoComputationHasBeenSaved(); + await fixtures.WhenComputing(projectId, scenarioId, featureId); - await fixtures.ThenComputationsHasNotBeenSaved(projectId, featureId); + + await fixtures.ThenComputationsHasNotBeenSavedForLegacyProject( + projectId, + featureId, + ); + await fixtures.ThenMinMaxAmountWasNotSaved(); }); }); const getFixtures = async () => { const computeMarxanAmountPerPlanningUnitMock = jest.fn(); const findProjectMock = jest.fn(); + const findGeoFeatureMock = jest.fn(); + const saveAmountRangeForFeaturesMock = jest.fn(); const sandbox = await Test.createTestingModule({ imports: [], providers: [ @@ -42,6 +79,16 @@ const getFixtures = async () => { provide: getRepositoryToken(Project), useValue: { find: findProjectMock }, }, + { + provide: getRepositoryToken(GeoFeature), + useValue: { findOneOrFail: findGeoFeatureMock }, + }, + { + provide: GeoFeaturesService, + useValue: { + saveAmountRangeForFeatures: saveAmountRangeForFeaturesMock, + }, + }, { provide: FeatureAmountsPerPlanningUnitRepository, useClass: MemoryFeatureAmountsPerPlanningUnitRepository, @@ -99,6 +146,18 @@ const getFixtures = async () => { return featureId; }, + GivenComputationAlreadySaved: (projectId: string, featureId: string) => { + featureAmountsPerPlanningUnitRepo.memory[projectId] = [ + { featureId, amount: 42, projectPuId: v4() }, + ]; + }, + GivenMinMaxAmount: (featureId: string, min?: number, max?: number) => { + findGeoFeatureMock.mockResolvedValueOnce({ + id: featureId, + amountMin: min, + amountMax: max, + }); + }, WhenComputing: (projectId: string, scenarioId: string, featureId: string) => sut.computeAreaPerPlanningUnitOfFeature(projectId, scenarioId, featureId), ThenComputationsHasBeenSaved: async ( @@ -118,7 +177,10 @@ const getFixtures = async () => { featureId, }); }, - ThenComputationsHasNotBeenSaved: async ( + ThenComputationsWasNotDone: async () => { + expect(computeMarxanAmountPerPlanningUnitMock).not.toHaveBeenCalled(); + }, + ThenComputationsHasNotBeenSavedForLegacyProject: async ( projectId: string, featureId: string, ) => { @@ -131,5 +193,11 @@ const getFixtures = async () => { expect(hasBeenSaved).toEqual(false); }, + ThenMinMaxAmountWasSaved: async () => { + expect(saveAmountRangeForFeaturesMock).toBeCalledTimes(1); + }, + ThenMinMaxAmountWasNotSaved: async () => { + expect(saveAmountRangeForFeaturesMock).toBeCalledTimes(0); + }, }; }; diff --git a/api/apps/api/src/modules/scenarios-features/compute-area.service.ts b/api/apps/api/src/modules/scenarios-features/compute-area.service.ts index a09123cc21..bce86ce26e 100644 --- a/api/apps/api/src/modules/scenarios-features/compute-area.service.ts +++ b/api/apps/api/src/modules/scenarios-features/compute-area.service.ts @@ -7,6 +7,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Project } from '../projects/project.api.entity'; +import { GeoFeaturesService } from '@marxan-api/modules/geo-features'; +import { GeoFeature } from '@marxan-api/modules/geo-features/geo-feature.api.entity'; @Injectable() export class ComputeArea { @@ -15,7 +17,11 @@ export class ComputeArea { private readonly featureAmountsPerPlanningUnit: FeatureAmountsPerPlanningUnitService, @InjectRepository(Project) private readonly projectsRepo: Repository, + @InjectRepository(GeoFeature) + private readonly geoFeatureRepo: Repository, + private readonly geoFeatureService: GeoFeaturesService, ) {} + public async computeAreaPerPlanningUnitOfFeature( projectId: string, scenarioId: string, @@ -31,24 +37,37 @@ export class ComputeArea { featureId, ); - if (alreadyComputed) return; + if (!alreadyComputed) { + const amountPerPlanningUnitOfFeature = + await this.featureAmountsPerPlanningUnit.computeMarxanAmountPerPlanningUnit( + featureId, + projectId, + ); - const amountPerPlanningUnitOfFeature = - await this.featureAmountsPerPlanningUnit.computeMarxanAmountPerPlanningUnit( - featureId, + await this.featureAmountsPerPlanningUnitRepo.saveAmountPerPlanningUnitAndFeature( projectId, + amountPerPlanningUnitOfFeature.map( + ({ featureId, projectPuId, amount }) => ({ + featureId, + projectPuId, + amount, + }), + ), ); + } - return this.featureAmountsPerPlanningUnitRepo.saveAmountPerPlanningUnitAndFeature( - projectId, - amountPerPlanningUnitOfFeature.map( - ({ featureId, projectPuId, amount }) => ({ - featureId, - projectPuId, - amount, - }), - ), - ); + const minMaxAlreadyComputed = await this.areFeatureMinMaxSaved(featureId); + + if (!minMaxAlreadyComputed) { + await this.geoFeatureService.saveAmountRangeForFeatures([featureId]); + } + } + + private async areFeatureMinMaxSaved(featureId: string): Promise { + const feature = await this.geoFeatureRepo.findOneOrFail({ + where: { id: featureId }, + }); + return !!(feature.amountMin && feature.amountMax); } private async isLegacyProject(projectId: string) { diff --git a/api/apps/api/src/modules/scenarios-features/scenario-features.module.ts b/api/apps/api/src/modules/scenarios-features/scenario-features.module.ts index 6009b99d96..374cd41cca 100644 --- a/api/apps/api/src/modules/scenarios-features/scenario-features.module.ts +++ b/api/apps/api/src/modules/scenarios-features/scenario-features.module.ts @@ -10,7 +10,7 @@ import { import { DbConnections } from '@marxan-api/ormconfig.connections'; import { ProjectsModule } from '@marxan-api/modules/projects/projects.module'; import { CqrsModule } from '@nestjs/cqrs'; -import { ApiEventsModule } from '../api-events/api-events.module'; +import { ApiEventsModule } from '@marxan-api/modules/api-events'; import { CreateFeaturesSaga } from './create-features.saga'; import { CreateFeaturesHandler } from './create-features.handler'; import { CopyDataProvider, CopyOperation, CopyQuery } from './copy'; @@ -27,12 +27,12 @@ import { } from './stratification'; import { AccessControlModule } from '@marxan-api/modules/access-control'; import { ComputeArea } from './compute-area.service'; -import { LegacyProjectImportRepositoryModule } from '../legacy-project-import/infra/legacy-project-import.repository.module'; import { FeatureAmountsPerPlanningUnitModule } from '@marxan/feature-amounts-per-planning-unit'; import { SplitFeatureConfigMapper } from '../scenarios/specification/split-feature-config.mapper'; import { FeatureHashModule } from '../features-hash/features-hash.module'; import { SplitCreateFeatures } from './split/split-create-features.service'; import { Project } from '../projects/project.api.entity'; +import { GeoFeaturesModule } from '@marxan-api/modules/geo-features/geo-features.module'; @Module({ imports: [ @@ -52,6 +52,7 @@ import { Project } from '../projects/project.api.entity'; ApiEventsModule, IntersectWithPuModule, AccessControlModule, + GeoFeaturesModule, ], providers: [ ScenarioFeaturesService,