diff --git a/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-as-finished.handler.ts b/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-as-finished.handler.ts index 89ee489889..edf8862d8a 100644 --- a/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-as-finished.handler.ts +++ b/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-as-finished.handler.ts @@ -1,3 +1,5 @@ +import { ProjectRoles } from '@marxan-api/modules/access-control/projects-acl/dto/user-role-project.dto'; +import { UsersProjectsApiEntity } from '@marxan-api/modules/access-control/projects-acl/entity/users-projects.api.entity'; import { ApiEventsService } from '@marxan-api/modules/api-events'; import { API_EVENT_KINDS } from '@marxan/api-events'; import { ResourceId } from '@marxan/cloning/domain'; @@ -7,7 +9,9 @@ import { CommandHandler, IInferredCommandHandler, } from '@nestjs/cqrs'; +import { InjectRepository } from '@nestjs/typeorm'; import { isLeft } from 'fp-ts/lib/Either'; +import { Repository } from 'typeorm'; import { LegacyProjectImportRepository } from '../domain/legacy-project-import/legacy-project-import.repository'; import { MarkLegacyProjectImportAsFailed } from './mark-legacy-project-import-as-failed.command'; import { MarkLegacyProjectImportAsFinished } from './mark-legacy-project-import-as-finished.command'; @@ -19,6 +23,8 @@ export class MarkLegacyProjectImportAsFinishedHandler private readonly apiEvents: ApiEventsService, private readonly legacyProjectImportRepository: LegacyProjectImportRepository, private readonly commandBus: CommandBus, + @InjectRepository(UsersProjectsApiEntity) + private readonly usersRepo: Repository, private readonly logger: Logger, ) { this.logger.setContext(MarkLegacyProjectImportAsFinishedHandler.name); @@ -61,5 +67,11 @@ export class MarkLegacyProjectImportAsFinishedHandler ownerId, }, }); + + await this.usersRepo.save({ + userId: ownerId, + projectId: projectId.value, + roleName: ProjectRoles.project_owner, + }); } } diff --git a/api/apps/api/src/modules/legacy-project-import/application/run-legacy-project-import.handler.ts b/api/apps/api/src/modules/legacy-project-import/application/run-legacy-project-import.handler.ts index fb520a74e5..70609de666 100644 --- a/api/apps/api/src/modules/legacy-project-import/application/run-legacy-project-import.handler.ts +++ b/api/apps/api/src/modules/legacy-project-import/application/run-legacy-project-import.handler.ts @@ -1,5 +1,4 @@ import { forbiddenError } from '@marxan-api/modules/access-control'; -import { ProjectRoles } from '@marxan-api/modules/access-control/projects-acl/dto/user-role-project.dto'; import { UsersProjectsApiEntity } from '@marxan-api/modules/access-control/projects-acl/entity/users-projects.api.entity'; import { CommandHandler, @@ -54,12 +53,6 @@ export class RunLegacyProjectImportHandler if (isLeft(legacyProjectImportSaveError)) return legacyProjectImportSaveError; - await this.usersRepo.save({ - userId: ownerId, - projectId: projectId.value, - roleName: ProjectRoles.project_owner, - }); - legacyProjectImport.commit(); return right(true); diff --git a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.module.ts b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.module.ts new file mode 100644 index 0000000000..7f8186819c --- /dev/null +++ b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { LegacyProjectImportRepositoryModule } from '../../infra/legacy-project-import.repository.module'; +import { LegacyProjectImportChecker } from './legacy-project-import-checker.service'; +import { MarxanLegacyProjectImportChecker } from './marxan-legacy-project-import-checker.service'; + +@Module({ + imports: [LegacyProjectImportRepositoryModule], + providers: [ + { + provide: LegacyProjectImportChecker, + useClass: MarxanLegacyProjectImportChecker, + }, + ], + exports: [LegacyProjectImportChecker], +}) +export class LegacyProjectImportCheckerModule {} diff --git a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service-fake.ts b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service-fake.ts new file mode 100644 index 0000000000..22db660bbb --- /dev/null +++ b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service-fake.ts @@ -0,0 +1,40 @@ +import { + LegacyProjectImportChecker, + LegacyProjectImportDoesntExist, + legacyProjectImportDoesntExist, +} from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service'; +import { LegacyProjectImportRepository } from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.repository'; +import { ResourceId } from '@marxan/cloning/domain'; +import { Injectable } from '@nestjs/common'; +import { Either, left, right } from 'fp-ts/lib/Either'; + +@Injectable() +export class LegacyProjectImportCheckerFake + implements LegacyProjectImportChecker { + private legacyProjectImportWithPendingImports: string[] = []; + + constructor( + private readonly legacyProjectImportRepo: LegacyProjectImportRepository, + ) {} + + async isLegacyProjectImportCompletedFor( + projectId: string, + ): Promise> { + const legacyProjectImport = await this.legacyProjectImportRepo.find( + new ResourceId(projectId), + ); + if (!legacyProjectImport) return left(legacyProjectImportDoesntExist); + + return right( + !this.legacyProjectImportWithPendingImports.includes(projectId), + ); + } + + addPendingLegacyProjecImport(projectId: string) { + this.legacyProjectImportWithPendingImports.push(projectId); + } + + clear() { + this.legacyProjectImportWithPendingImports = []; + } +} diff --git a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service.spec.ts b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service.spec.ts new file mode 100644 index 0000000000..9eb8682f48 --- /dev/null +++ b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service.spec.ts @@ -0,0 +1,121 @@ +import { FixtureType } from '@marxan/utils/tests/fixture-type'; +import { Test } from '@nestjs/testing'; +import { Either } from 'fp-ts/lib/Either'; +import { v4 } from 'uuid'; +import { LegacyProjectImportMemoryRepository } from '../../infra/legacy-project-import-memory.repository'; +import { LegacyProjectImport } from '../legacy-project-import/legacy-project-import'; +import { LegacyProjectImportStatuses } from '../legacy-project-import/legacy-project-import-status'; +import { LegacyProjectImportRepository } from '../legacy-project-import/legacy-project-import.repository'; +import { + LegacyProjectImportChecker, + legacyProjectImportDoesntExist, + LegacyProjectImportDoesntExist, +} from './legacy-project-import-checker.service'; +import { MarxanLegacyProjectImportChecker } from './marxan-legacy-project-import-checker.service'; + +let fixtures: FixtureType; + +beforeEach(async () => { + fixtures = await getFixtures(); +}); + +it(`isLegacyProjectImportCompletedFor() should return legacyProjectImportdoesntExist if the project does not have a legacy project import`, async () => { + const res = fixtures.GivenProjectIsNotALegacyProject(); + + const result = await fixtures.WhenHasImportedLegacyProjectMethodIsCalled(res); + + fixtures.ThenLegacyProjectImportDoesntExistIsReturned(result); +}); + +it.each( + Object.values(LegacyProjectImportStatuses).filter( + (kind) => kind !== LegacyProjectImportStatuses.Completed, + ), +)( + `isLegacyProjectImportCompletedFor() should return false if given project has a legacy project import with status %s`, + async (kind) => { + const id = await fixtures.GivenLegacyProjectImport(kind); + + const result = await fixtures.WhenHasImportedLegacyProjectMethodIsCalled( + id, + ); + + fixtures.ThenFalseIsReturned(result); + }, +); + +it(`isLegacyProjectImportCompletedFor() should return true if given project has an already completed legacy project import`, async () => { + const id = await fixtures.GivenLegacyProjectImport( + LegacyProjectImportStatuses.Completed, + ); + + const result = await fixtures.WhenHasImportedLegacyProjectMethodIsCalled(id); + + fixtures.ThenTrueIsReturned(result); +}); + +async function getFixtures() { + const testingModule = await Test.createTestingModule({ + providers: [ + { + provide: LegacyProjectImportChecker, + useClass: MarxanLegacyProjectImportChecker, + }, + { + provide: LegacyProjectImportRepository, + useClass: LegacyProjectImportMemoryRepository, + }, + ], + }).compile(); + const sut = testingModule.get(LegacyProjectImportChecker); + const repo = testingModule.get(LegacyProjectImportRepository); + const projectId = v4(); + + return { + GivenProjectIsNotALegacyProject: () => { + return projectId; + }, + GivenLegacyProjectImport: async (status: LegacyProjectImportStatuses) => { + const legacyProjectImport = LegacyProjectImport.fromSnapshot({ + files: [], + pieces: [], + id: v4(), + ownerId: v4(), + projectId, + scenarioId: v4(), + status, + toBeRemoved: false, + }); + await repo.save(legacyProjectImport); + + return projectId; + }, + WhenHasImportedLegacyProjectMethodIsCalled: (projectId: string) => { + return sut.isLegacyProjectImportCompletedFor(projectId); + }, + ThenLegacyProjectImportDoesntExistIsReturned: ( + result: Either, + ) => { + expect(result).toEqual({ + _tag: 'Left', + left: legacyProjectImportDoesntExist, + }); + }, + ThenFalseIsReturned: ( + result: Either, + ) => { + expect(result).toEqual({ + _tag: 'Right', + right: false, + }); + }, + ThenTrueIsReturned: ( + result: Either, + ) => { + expect(result).toEqual({ + _tag: 'Right', + right: true, + }); + }, + }; +} diff --git a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service.ts b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service.ts new file mode 100644 index 0000000000..1b85e84b10 --- /dev/null +++ b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service.ts @@ -0,0 +1,12 @@ +import { Either } from 'fp-ts/Either'; + +export const legacyProjectImportDoesntExist = Symbol( + `doesn't exist legacy project import`, +); +export type LegacyProjectImportDoesntExist = typeof legacyProjectImportDoesntExist; + +export abstract class LegacyProjectImportChecker { + abstract isLegacyProjectImportCompletedFor( + projectId: string, + ): Promise>; +} diff --git a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/marxan-legacy-project-import-checker.service.ts b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/marxan-legacy-project-import-checker.service.ts new file mode 100644 index 0000000000..061eb65ccd --- /dev/null +++ b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import-checker/marxan-legacy-project-import-checker.service.ts @@ -0,0 +1,29 @@ +import { ResourceId } from '@marxan/cloning/domain'; +import { Injectable } from '@nestjs/common'; +import { Either, isLeft, left, right } from 'fp-ts/Either'; +import { LegacyProjectImportRepository } from '../legacy-project-import/legacy-project-import.repository'; +import { + legacyProjectImportDoesntExist, + LegacyProjectImportChecker, + LegacyProjectImportDoesntExist, +} from './legacy-project-import-checker.service'; + +@Injectable() +export class MarxanLegacyProjectImportChecker + implements LegacyProjectImportChecker { + constructor( + private readonly legacyProjectImportRepo: LegacyProjectImportRepository, + ) {} + async isLegacyProjectImportCompletedFor( + projectId: string, + ): Promise> { + const legacyProjectImport = await this.legacyProjectImportRepo.find( + new ResourceId(projectId), + ); + + if (isLeft(legacyProjectImport)) + return left(legacyProjectImportDoesntExist); + + return right(legacyProjectImport.right.hasImportedLegacyProject()); + } +} diff --git a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.ts b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.ts index 9c55fd7855..5c65ace6e5 100644 --- a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.ts +++ b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.ts @@ -134,6 +134,10 @@ export class LegacyProjectImport extends AggregateRoot { return !this.status.isAcceptingFiles(); } + public hasImportedLegacyProject() { + return this.status.hasCompleted(); + } + public areRequiredFilesUploaded(): boolean { const requiredFilesTypes = [ LegacyProjectImportFileType.PlanningGridShapefile, diff --git a/api/apps/api/src/modules/projects/block-guard/block-guard.module.ts b/api/apps/api/src/modules/projects/block-guard/block-guard.module.ts index 216c673db2..fb7435c237 100644 --- a/api/apps/api/src/modules/projects/block-guard/block-guard.module.ts +++ b/api/apps/api/src/modules/projects/block-guard/block-guard.module.ts @@ -8,11 +8,13 @@ import { PlanningAreasModule } from '@marxan-api/modules/planning-areas'; import { MarxanBlockGuard } from '@marxan-api/modules/projects/block-guard/marxan-block-guard.service'; import { ScenarioCheckerModule } from '@marxan-api/modules/scenarios/scenario-checker/scenario-checker.module'; import { Scenario } from '@marxan-api/modules/scenarios/scenario.api.entity'; +import { LegacyProjectImportCheckerModule } from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.module'; @Module({ imports: [ ProjectCheckerModule, ScenarioCheckerModule, + LegacyProjectImportCheckerModule, PlanningAreasModule, ApiEventsModule, TypeOrmModule.forFeature([Project, Scenario]), diff --git a/api/apps/api/src/modules/projects/block-guard/marxan-block-guard.service.spec.ts b/api/apps/api/src/modules/projects/block-guard/marxan-block-guard.service.spec.ts index 596921709a..29c9865c2e 100644 --- a/api/apps/api/src/modules/projects/block-guard/marxan-block-guard.service.spec.ts +++ b/api/apps/api/src/modules/projects/block-guard/marxan-block-guard.service.spec.ts @@ -12,6 +12,12 @@ import { v4 } from 'uuid'; import { ProjectCheckerFake } from '../../../../test/utils/project-checker.service-fake'; import { Project } from '../project.api.entity'; import { NotFoundException } from '@nestjs/common'; +import { LegacyProjectImportChecker } from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service'; +import { LegacyProjectImportCheckerFake } from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service-fake'; +import { LegacyProjectImportRepository } from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.repository'; +import { LegacyProjectImportMemoryRepository } from '@marxan-api/modules/legacy-project-import/infra/legacy-project-import-memory.repository'; +import { LegacyProjectImport } from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import'; +import { LegacyProjectImportStatuses } from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import-status'; describe('MarxanBlockGuard - ensureThatProjectIsNotBlocked', () => { let fixtures: FixtureType; @@ -33,7 +39,7 @@ describe('MarxanBlockGuard - ensureThatProjectIsNotBlocked', () => { }); it(`throws an exception if the given project has an ongoing export`, async () => { - const [projectId] = fixtures.GivenProjectWasCreated(); + const [projectId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAnOngoingExport(projectId); @@ -43,7 +49,7 @@ describe('MarxanBlockGuard - ensureThatProjectIsNotBlocked', () => { }); it(`throws an exception if the given project has a scenario with an ongoing export`, async () => { - const [projectId, scenarioId] = fixtures.GivenProjectWasCreated(); + const [projectId, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAScenarioWithAnOngoingExport(scenarioId); @@ -53,7 +59,7 @@ describe('MarxanBlockGuard - ensureThatProjectIsNotBlocked', () => { }); it(`throws an exception if the given project has an ongoing import`, async () => { - const [projectId] = fixtures.GivenProjectWasCreated(); + const [projectId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAnOngoingImport(projectId); @@ -63,7 +69,7 @@ describe('MarxanBlockGuard - ensureThatProjectIsNotBlocked', () => { }); it(`throws an exception if the given project has a scenario with an ongoing import`, async () => { - const [projectId, scenarioId] = fixtures.GivenProjectWasCreated(); + const [projectId, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAScenarioWithAnOngoingImport(scenarioId); @@ -73,7 +79,7 @@ describe('MarxanBlockGuard - ensureThatProjectIsNotBlocked', () => { }); it(`throws an exception if the given project has a scenario with an ongoing blm calibration`, async () => { - const [projectId, scenarioId] = fixtures.GivenProjectWasCreated(); + const [projectId, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAScenarioWithAnOngoingBlmCalibration(scenarioId); @@ -83,7 +89,7 @@ describe('MarxanBlockGuard - ensureThatProjectIsNotBlocked', () => { }); it(`throws an exception if the given project has a scenario with an ongoing marxan run`, async () => { - const [projectId, scenarioId] = fixtures.GivenProjectWasCreated(); + const [projectId, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAScenarioWithAnOngoingMarxanRun(scenarioId); @@ -92,8 +98,18 @@ describe('MarxanBlockGuard - ensureThatProjectIsNotBlocked', () => { .ThenAPendingMarxanRunErrorIsThrown(); }); + it(`while a legacy project import process is ongoing, the project should be marked as non-editable`, async () => { + const [projectId] = await fixtures.GivenProjectWasCreated(); + + fixtures.WhenProjectHasAnOngoingLegacyProjectImport(projectId); + + await fixtures + .WhenCheckingWhetherTheProjectCanBeEdited(projectId) + .ThenAPendingLegacyProjectImportErrorIsThrown(); + }); + it(`does nothing if the given project is not blocked`, async () => { - const [projectId] = fixtures.GivenProjectWasCreated(); + const [projectId] = await fixtures.GivenProjectWasCreated(); await fixtures .WhenCheckingWhetherTheProjectCanBeEdited(projectId) @@ -121,7 +137,7 @@ describe('MarxanBlockGuard - ensureThatScenarioIsNotBlocked', () => { }); it(`throws an exception if the given scenario has an ongoing export`, async () => { - const [_, scenarioId] = fixtures.GivenProjectWasCreated(); + const [_, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAScenarioWithAnOngoingExport(scenarioId); @@ -131,7 +147,7 @@ describe('MarxanBlockGuard - ensureThatScenarioIsNotBlocked', () => { }); it(`throws an exception if the given scenario's parent project has an ongoing export`, async () => { - const [projectId, scenarioId] = fixtures.GivenProjectWasCreated(); + const [projectId, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAnOngoingExport(projectId); @@ -141,7 +157,7 @@ describe('MarxanBlockGuard - ensureThatScenarioIsNotBlocked', () => { }); it(`throws an exception if the given scenario has an ongoing import`, async () => { - const [_, scenarioId] = fixtures.GivenProjectWasCreated(); + const [_, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAScenarioWithAnOngoingImport(scenarioId); @@ -151,7 +167,7 @@ describe('MarxanBlockGuard - ensureThatScenarioIsNotBlocked', () => { }); it(`throws an exception if the given scenario's parent project has an ongoing import`, async () => { - const [projectId, scenarioId] = fixtures.GivenProjectWasCreated(); + const [projectId, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAnOngoingImport(projectId); @@ -161,7 +177,7 @@ describe('MarxanBlockGuard - ensureThatScenarioIsNotBlocked', () => { }); it(`throws an exception if the given scenario has an ongoing blm calibration`, async () => { - const [_, scenarioId] = fixtures.GivenProjectWasCreated(); + const [_, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAScenarioWithAnOngoingBlmCalibration(scenarioId); @@ -171,7 +187,7 @@ describe('MarxanBlockGuard - ensureThatScenarioIsNotBlocked', () => { }); it(`throws an exception if the given scenario has an ongoing marxan run`, async () => { - const [_, scenarioId] = fixtures.GivenProjectWasCreated(); + const [_, scenarioId] = await fixtures.GivenProjectWasCreated(); fixtures.WhenProjectHasAScenarioWithAnOngoingMarxanRun(scenarioId); @@ -180,8 +196,18 @@ describe('MarxanBlockGuard - ensureThatScenarioIsNotBlocked', () => { .ThenAPendingMarxanRunErrorIsThrown(); }); + it(`while a legacy project import process is ongoing, scenarios within the project should be marked as non-editable`, async () => { + const [projectId, scenarioId] = await fixtures.GivenProjectWasCreated(); + + fixtures.WhenProjectHasAnOngoingLegacyProjectImport(projectId); + + await fixtures + .WhenCheckingWhetherTheScenarioCanBeEdited(scenarioId) + .ThenAPendingLegacyProjectImportErrorIsThrown(); + }); + it(`does nothing if the given scenario is not blocked`, async () => { - const [_, scenarioId] = fixtures.GivenProjectWasCreated(); + const [_, scenarioId] = await fixtures.GivenProjectWasCreated(); await fixtures .WhenCheckingWhetherTheScenarioCanBeEdited(scenarioId) @@ -210,6 +236,10 @@ const getFixtures = async () => { provide: ScenarioChecker, useClass: ScenarioCheckerFake, }, + { + provide: LegacyProjectImportChecker, + useClass: LegacyProjectImportCheckerFake, + }, { provide: BlockGuard, useClass: MarxanBlockGuard, @@ -222,19 +252,28 @@ const getFixtures = async () => { provide: getRepositoryToken(Scenario), useValue: fakeScenariosRepo, }, + { + provide: LegacyProjectImportRepository, + useClass: LegacyProjectImportMemoryRepository, + }, ], }).compile(); await sandbox.init(); const projectChecker = sandbox.get(ProjectChecker) as ProjectCheckerFake; const scenarioChecker = sandbox.get(ScenarioChecker) as ScenarioCheckerFake; + const repo = sandbox.get(LegacyProjectImportRepository); + const legacyProjectImportChecker: LegacyProjectImportCheckerFake = sandbox.get( + LegacyProjectImportChecker, + ); const blockGuard = sandbox.get(BlockGuard); return { cleanup: () => { projectChecker.clear(); scenarioChecker.clear(); + legacyProjectImportChecker.clear(); }, - GivenProjectWasCreated: () => { + GivenProjectWasCreated: async () => { const projectId = v4(); const scenarioId = v4(); fakeProjectsRepo.findOne.mockResolvedValue({ @@ -245,6 +284,18 @@ const getFixtures = async () => { id: scenarioId, projectId, } as Scenario); + await repo.save( + LegacyProjectImport.fromSnapshot({ + id: v4(), + files: [], + ownerId: v4(), + pieces: [], + projectId, + scenarioId, + status: LegacyProjectImportStatuses.AcceptingFiles, + toBeRemoved: false, + }), + ); return [projectId, scenarioId]; }, @@ -268,6 +319,9 @@ const getFixtures = async () => { WhenProjectHasAnOngoingImport: (projectId: string) => { projectChecker.addPendingImportForProject(projectId); }, + WhenProjectHasAnOngoingLegacyProjectImport: (projectId: string) => { + legacyProjectImportChecker.addPendingLegacyProjecImport(projectId); + }, WhenCheckingWhetherTheProjectCanBeEdited: (projectId: string) => { return { ThenAPendingExportErrorIsThrown: async () => { @@ -290,6 +344,11 @@ const getFixtures = async () => { blockGuard.ensureThatProjectIsNotBlocked(projectId), ).rejects.toThrow(/pending marxan run/gi); }, + ThenAPendingLegacyProjectImportErrorIsThrown: async () => { + await expect( + blockGuard.ensureThatProjectIsNotBlocked(projectId), + ).rejects.toThrow(/pending legacy project import/gi); + }, ThenANotFoundExceptionIsThrown: async () => { await expect( blockGuard.ensureThatProjectIsNotBlocked(projectId), @@ -344,6 +403,11 @@ const getFixtures = async () => { blockGuard.ensureThatScenarioIsNotBlocked(scenarioId), ).rejects.toThrow(/scenario.+pending marxan run/gi); }, + ThenAPendingLegacyProjectImportErrorIsThrown: async () => { + await expect( + blockGuard.ensureThatScenarioIsNotBlocked(scenarioId), + ).rejects.toThrow(/scenario.+pending legacy project import/gi); + }, }; }, }; diff --git a/api/apps/api/src/modules/projects/block-guard/marxan-block-guard.service.ts b/api/apps/api/src/modules/projects/block-guard/marxan-block-guard.service.ts index c6a0211243..2ccf18be41 100644 --- a/api/apps/api/src/modules/projects/block-guard/marxan-block-guard.service.ts +++ b/api/apps/api/src/modules/projects/block-guard/marxan-block-guard.service.ts @@ -1,3 +1,4 @@ +import { LegacyProjectImportChecker } from '@marxan-api/modules/legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service'; import { BlockGuard } from '@marxan-api/modules/projects/block-guard/block-guard.service'; import { ProjectChecker } from '@marxan-api/modules/projects/project-checker/project-checker.service'; import { ScenarioChecker } from '@marxan-api/modules/scenarios/scenario-checker/scenario-checker.service'; @@ -17,6 +18,7 @@ export class MarxanBlockGuard implements BlockGuard { constructor( private readonly projectChecker: ProjectChecker, private readonly scenarioChecker: ScenarioChecker, + private readonly legacyProjectImportChecker: LegacyProjectImportChecker, @InjectRepository(Scenario) private readonly scenarioRepo: Repository, @InjectRepository(Project) @@ -37,11 +39,15 @@ export class MarxanBlockGuard implements BlockGuard { hasPendingImports, hasPendingBlmCalibration, hasPendingMarxanRun, + hasImportedLegacyProject, ] = await Promise.all([ this.projectChecker.hasPendingExports(projectId), this.projectChecker.hasPendingImports(projectId), this.projectChecker.hasPendingBlmCalibration(projectId), this.projectChecker.hasPendingMarxanRun(projectId), + this.legacyProjectImportChecker.isLegacyProjectImportCompletedFor( + projectId, + ), ]); if (isRight(hasPendingExports) && hasPendingExports.right) @@ -63,6 +69,10 @@ export class MarxanBlockGuard implements BlockGuard { throw new BadRequestException( `Project ${projectId} editing is blocked because of pending marxan run`, ); + if (isRight(hasImportedLegacyProject) && !hasImportedLegacyProject.right) + throw new BadRequestException( + `Project ${projectId} editing is blocked because of pending legacy project import`, + ); } async ensureThatScenarioIsNotBlocked(scenarioId: string): Promise { @@ -81,6 +91,7 @@ export class MarxanBlockGuard implements BlockGuard { hasPendingImports, projectHasPendingExports, projectHasPendingImports, + hasImportedLegacyProject, ] = await Promise.all([ this.scenarioChecker.hasPendingBlmCalibration(scenarioId), this.scenarioChecker.hasPendingMarxanRun(scenarioId), @@ -88,6 +99,9 @@ export class MarxanBlockGuard implements BlockGuard { this.scenarioChecker.hasPendingImport(scenarioId), this.projectChecker.hasPendingExports(scenario.projectId), this.projectChecker.hasPendingImports(scenario.projectId), + this.legacyProjectImportChecker.isLegacyProjectImportCompletedFor( + scenario.projectId, + ), ]); if (isRight(hasPendingExports) && hasPendingExports.right) @@ -119,5 +133,9 @@ export class MarxanBlockGuard implements BlockGuard { throw new BadRequestException( `Scenario ${scenarioId} editing is blocked because of project pending import`, ); + if (isRight(hasImportedLegacyProject) && !hasImportedLegacyProject.right) + throw new BadRequestException( + `Scenario ${scenarioId} editing is blocked because of pending legacy project import`, + ); } } diff --git a/api/apps/api/src/modules/scenarios/scenarios.module.ts b/api/apps/api/src/modules/scenarios/scenarios.module.ts index 42038a973f..5617f12a44 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.module.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.module.ts @@ -1,7 +1,6 @@ import { forwardRef, HttpModule, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CqrsModule } from '@nestjs/cqrs'; - import { MarxanInput } from '@marxan/marxan-input/marxan-input'; import { ScenariosController } from './scenarios.controller'; import { Scenario } from './scenario.api.entity'; @@ -33,7 +32,6 @@ import { ScenarioPlanningUnitSerializer } from './dto/scenario-planning-unit.ser import { ScenarioPlanningUnitsService } from './planning-units/scenario-planning-units.service'; import { ScenarioPlanningUnitsLinkerService } from './planning-units/scenario-planning-units-linker-service'; import { AdminAreasModule } from '../admin-areas/admin-areas.module'; - import { SpecificationModule } from './specification'; import { ScenarioFeaturesGapDataSerializer } from './dto/scenario-feature-gap-data.serializer'; import { ScenarioFeaturesOutputGapDataSerializer } from './dto/scenario-feature-output-gap-data.serializer'; @@ -52,6 +50,7 @@ import { LockService } from '../access-control/scenarios-acl/locks/lock.service' import { IssuedAuthnToken } from '../authentication/issued-authn-token.api.entity'; import { WebshotModule } from '@marxan/webshot'; import { DeleteScenarioModule } from './delete-scenario/delete-scenario.module'; +import { LegacyProjectImportCheckerModule } from '../legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.module'; @Module({ imports: [ @@ -73,6 +72,7 @@ import { DeleteScenarioModule } from './delete-scenario/delete-scenario.module'; ), BlockGuardModule, ProjectCheckerModule, + LegacyProjectImportCheckerModule, PlanningAreasModule, UsersModule, ScenarioFeaturesModule, diff --git a/api/apps/api/src/modules/scenarios/scenarios.service.ts b/api/apps/api/src/modules/scenarios/scenarios.service.ts index fc5faca6de..3c2dd8f1e4 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.service.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.service.ts @@ -8,7 +8,7 @@ import { import { FetchSpecification } from 'nestjs-base-service'; import { classToClass } from 'class-transformer'; import * as stream from 'stream'; -import { Either, isLeft, left, right } from 'fp-ts/Either'; +import { Either, isLeft, isRight, left, right } from 'fp-ts/Either'; import { pick } from 'lodash'; import { MarxanInput, MarxanParameters } from '@marxan/marxan-input'; import { AppInfoDTO } from '@marxan-api/dto/info.dto'; @@ -116,6 +116,7 @@ import { DeleteScenario as DeleteScenarioUnusedResources, deleteScenarioFailed, } from './delete-scenario/delete-scenario.command'; +import { LegacyProjectImportChecker } from '../legacy-project-import/domain/legacy-project-import-checker/legacy-project-import-checker.service'; /** @debt move to own module */ const EmptyGeoFeaturesSpecification: GeoFeatureSetSpecification = { @@ -174,6 +175,7 @@ export class ScenariosService { private readonly costService: CostRangeService, private readonly blockGuard: BlockGuard, private readonly projectChecker: ProjectChecker, + private readonly legacyProjectChecker: LegacyProjectImportChecker, private readonly protectedArea: ProtectedAreaService, private readonly queryBus: QueryBus, private readonly commandBus: CommandBus, @@ -281,6 +283,13 @@ export class ScenariosService { return left(projectNotReady); } } + const isLegacyProjectCompleted = await this.legacyProjectChecker.isLegacyProjectImportCompletedFor( + input.projectId, + ); + + if (isRight(isLegacyProjectCompleted) && !isLegacyProjectCompleted.right) + return left(projectNotReady); + const scenario = await this.crudService.create(validatedMetadata, info); const blmCreationResult = await this.commandBus.execute( new CreateInitialScenarioBlm(scenario.id, scenario.projectId),