Skip to content

Commit

Permalink
feat(CostSurface): Link Cost Surface To Scenario PR Tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
KevSanchez authored and hotzevzl committed Oct 20, 2023
1 parent 694f639 commit 5497a69
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Command } from '@nestjs-architects/typed-cqrs';
import { Either } from 'fp-ts/lib/Either';
import { LinkCostSurfaceToScenarioMode } from '@marxan/artifact-cache/surface-cost-job-input';

export const linkCostSurfaceToScenarioFailed = Symbol(
'link surface cost to scenario failed',
);

export type LinkCostSurfaceToScenarioError = typeof linkCostSurfaceToScenarioFailed;
export type LinkCostSurfaceToScenarioError =
typeof linkCostSurfaceToScenarioFailed;

export type LinkCostSurfaceToScenarioResponse = Either<
LinkCostSurfaceToScenarioError,
Expand All @@ -20,7 +22,7 @@ export class LinkCostSurfaceToScenarioCommand extends Command<LinkCostSurfaceToS
constructor(
public readonly scenarioId: string,
public readonly costSurfaceId: string,
public readonly mode: 'creation' | 'update',
public readonly mode: LinkCostSurfaceToScenarioMode,
) {
super();
}
Expand Down
29 changes: 29 additions & 0 deletions api/apps/api/src/modules/cost-surface/cost-surface.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,35 @@ export class CostSurfaceService {
);
}

async unlinkCostSurfaceFromScenario(
userId: string,
scenarioId: string,
): Promise<
Either<
| typeof scenarioNotEditable
| typeof costSurfaceNotFound
| typeof scenarioNotFound
| LinkCostSurfaceToScenarioError,
true
>
> {
const scenario = await this.scenarioRepository.findOne({
where: { id: scenarioId },
});
if (!scenario) {
return left(scenarioNotFound);
}

const costSurface = await this.costSurfaceRepository.findOne({
where: { projectId: scenario.projectId, isDefault: true },
});
if (!costSurface) {
return left(costSurfaceNotFound);
}

return this.linkCostSurfaceToScenario(userId, scenarioId, costSurface.id);
}

async update(
userId: string,
projectId: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,4 @@ const eventToJobStatusMapping: Record<
ApiEventJobStatus.done,
[API_EVENT_KINDS.scenario__protectedAreas__failed__v1__alpha]:
ApiEventJobStatus.failure,

// RENAME Y AÑADIR MAPPING
};
13 changes: 5 additions & 8 deletions api/apps/api/src/modules/scenarios/scenarios.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ export class ScenariosController {
required: true,
})
@ApiTags(asyncJobTag)
@Post(`:scenarioId/link-cost-surface/:costSurfaceId`)
@Post(`:scenarioId/cost-surface/:costSurfaceId`)
async linkCostSurfaceToScenario(
@Param('scenarioId') scenarioId: string,
@Param('costSurfaceId') costSurfaceId: string,
Expand All @@ -469,25 +469,22 @@ export class ScenariosController {
}

@ApiOperation({
description:
'To be removed soon to POST /projects/:projectId/cost-surface/shapefile',
description: `Unlinks the currently applied CostSurface from the given Scenario, and links back the default Cost Surface of the Scenario's Project`,
})
@ApiParam({
name: 'scenarioId',
description: 'Id of the Scenario that the Cost Surface will be applied',
description: 'Id of the Scenario that will have its Cost Surface unlinked',
required: true,
})
@ApiTags(asyncJobTag)
@Post(`:scenarioId/unlink-cost-surface/`)
@Delete(`:scenarioId/cost-surface/`)
async unlinkCostSurfaceToScenario(
@Param('scenarioId') scenarioId: string,
@Param('costSurfaceId') costSurfaceId: string,
@Req() req: RequestWithAuthenticatedUser,
): Promise<JsonApiAsyncJobMeta> {
const result = await this.costSurfaceService.linkCostSurfaceToScenario(
const result = await this.costSurfaceService.unlinkCostSurfaceFromScenario(
req.user.id,
scenarioId,
costSurfaceId,
);

if (isLeft(result)) {
Expand Down
4 changes: 2 additions & 2 deletions api/apps/api/test/project-scenarios.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ let fixtures: FixtureType<typeof getFixtures>;

beforeEach(async () => {
fixtures = await getFixtures();
}, 1000000);
}, 12_000);

describe('ScenariosModule (e2e)', () => {
it('Creating a scenario with incomplete data should fail', async () => {
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('ScenariosModule (e2e)', () => {
const response =
await fixtures.WhenCreatingAScenarioWithMinimumRequiredDataAsOwner(false);
fixtures.ThenCostSurfaceNotFoundMessageIsReturned(response);
}, 1000000);
});

it('Creating a scenario with complete data should succeed', async () => {
const response =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,40 @@ describe('Cost Surface', () => {
scenario.id,
);
});
it(`should link back to the scenario's project default cost surface when unlinkind`, async () => {
// ARRANGE
const projectId = await fixtures.GivenProject('someProject');
const defaultCostSurface = await fixtures.GivenDefaultCostSurfaceForProject(
projectId,
);
const costSurface = await fixtures.GivenCostSurfaceMetadataForProject(
projectId,
'someCostSurface',
);
const scenario = await fixtures.GivenScenario(
projectId,
costSurface.id,
'someName',
);
fixtures.GivenNoJobsOnScenarioCostSurfaceQueue();

// ACT
await fixtures.WhenUnlinkingCostSurfaceToScenario(scenario.id);

// ASSERT
await fixtures.ThenCostSurfaceIsLinkedToScenario(
scenario.id,
defaultCostSurface.id,
);
await fixtures.ThenLinkCostSurfaceToScenarioJobWasSent(
scenario.id,
defaultCostSurface.id,
costSurface.id,
);
await fixtures.ThenLinkCostSurfaceToScenarioSubmittedApiEventWasSaved(
scenario.id,
);
});

it(`should return error when the Scenario was not found`, async () => {
// ARRANGE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,17 @@ export const getProjectCostSurfaceFixtures = async () => {
) => {
return request(app.getHttpServer())
.post(
`/api/v1/scenarios/${scenarioId}/link-cost-surface/${costSurfaceId}`,
`/api/v1/scenarios/${scenarioId}/cost-surface/${costSurfaceId}`,
)
.set('Authorization', `Bearer ${token}`)
.send();
},
WhenUnlinkingCostSurfaceToScenario: async (
scenarioId: string,
) => {
return request(app.getHttpServer())
.delete(
`/api/v1/scenarios/${scenarioId}/cost-surface/`,
)
.set('Authorization', `Bearer ${token}`)
.send();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { CHUNK_SIZE_FOR_BATCH_GEODB_OPERATIONS } from '@marxan-geoprocessing/utils/chunk-size-for-batch-geodb-operations';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { chunk } from 'lodash';
import { EntityManager } from 'typeorm';
import { CostSurfacePuDataEntity } from '@marxan/cost-surfaces';
import { geoprocessingConnections } from '@marxan-geoprocessing/ormconfig';
import { ScenarioCostSurfacePersistencePort } from '@marxan-geoprocessing/modules/cost-surface/ports/persistence/scenario-cost-surface-persistence.port';
import {
ScenariosPuCostDataGeo,
ScenariosPuPaDataGeo,
} from '@marxan/scenarios-planning-unit';
import { LinkCostSurfaceToScenarioMode } from '@marxan/artifact-cache/surface-cost-job-input';

@Injectable()
export class TypeormScenarioCostSurface
implements ScenarioCostSurfacePersistencePort {
implements ScenarioCostSurfacePersistencePort
{
constructor(
@InjectEntityManager(geoprocessingConnections.default)
private readonly geoprocessingEntityManager: EntityManager,
Expand All @@ -22,45 +17,33 @@ export class TypeormScenarioCostSurface
async linkScenarioToCostSurface(
scenarioId: string,
costSurfaceId: string,
mode: LinkCostSurfaceToScenarioMode,
): Promise<void> {
await this.geoprocessingEntityManager.transaction(async (em) => {
const costsForScenarioPus: {
scenariosPuId: string;
cost: number;
}[] = await em
.createQueryBuilder()
.select('spd.id', 'scenariosPuId')
.addSelect('csp.cost', 'cost')
.from(ScenariosPuPaDataGeo, 'spd')
.leftJoin(
CostSurfacePuDataEntity,
'csp',
'csp.projects_pu_id = spd.project_pu_id',
)
.where('spd.scenario_id = :scenarioId', { scenarioId })
.andWhere('csp.cost_surface_id = :costSurfaceId', { costSurfaceId })
.execute();

await em.query(
` DELETE FROM scenarios_pu_cost_data spcd
USING scenarios_pu_data spd
WHERE spcd.scenarios_pu_data_id = spd.id and spd.scenario_id = $1`,
[scenarioId],
);

await Promise.all(
chunk(costsForScenarioPus, CHUNK_SIZE_FOR_BATCH_GEODB_OPERATIONS).map(
async (rows) => {
await em.insert(
ScenariosPuCostDataGeo,
rows.map((row) => ({
cost: row.cost,
scenariosPuDataId: row.scenariosPuId,
})),
);
},
),
);
if (mode === 'update') {
await em.query(
` UPDATE scenarios_pu_cost_data
SET cost = cost_surface."cost_value"
FROM
(
SELECT spd.id, cspd."cost" as cost_value
FROM scenarios_pu_data spd
LEFT JOIN cost_surface_pu_data cspd ON cspd.projects_pu_id = spd.project_pu_id
WHERE spd.scenario_id = $1 AND cspd.cost_surface_id = $2
) cost_surface
WHERE scenarios_pu_cost_data.scenarios_pu_data_id = cost_surface.id`,
[scenarioId, costSurfaceId],
);
} else if (mode === 'creation') {
await em.query(
` INSERT INTO scenarios_pu_cost_data (scenarios_pu_data_id, cost)
SELECT spd.id as scenarios_pu_data_id, cspd."cost" as cost
FROM scenarios_pu_data spd
LEFT JOIN cost_surface_pu_data cspd ON cspd.projects_pu_id = spd.project_pu_id
WHERE spd.scenario_id = $1 AND cspd.cost_surface_id = $2`,
[scenarioId, costSurfaceId],
);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { ScenarioCostSurfacePersistencePort } from '@marxan-geoprocessing/module

@Injectable()
export class ScenarioCostSurfaceProcessor
implements WorkerProcessor<ScenarioCostSurfaceJobInput, true> {
implements WorkerProcessor<ScenarioCostSurfaceJobInput, true>
{
constructor(private readonly repo: ScenarioCostSurfacePersistencePort) {}

private async linkCostSurfaceToScenario({
Expand All @@ -18,6 +19,7 @@ export class ScenarioCostSurfaceProcessor
await this.repo.linkScenarioToCostSurface(
data.scenarioId,
data.costSurfaceId,
data.mode,
);

return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { LinkCostSurfaceToScenarioMode } from '@marxan/artifact-cache/surface-cost-job-input';

export abstract class ScenarioCostSurfacePersistencePort {
abstract linkScenarioToCostSurface(
scenarioId: string,
costSurface: string,
mode: LinkCostSurfaceToScenarioMode,
): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('should process cost surface', () => {
const app = await bootstrapApplication();
world = await createWorld(app);
});
it('should link the cost surface data to the scenario data', async () => {
it('should update the cost surface cost data when linking in update mode', async () => {
const scenarioId = v4();
const projectId = v4();
await world.GivenScenarioPuDataExists(projectId, scenarioId);
Expand All @@ -19,6 +19,23 @@ describe('should process cost surface', () => {
const linkCostSurfaceJob = world.getLinkCostSurfaceToScenarioJob(
scenarioId,
costSurfaceId,
'update',
);
await world.WhenTheCostSurfaceLinkingJobIsProcessed(linkCostSurfaceJob);
await world.ThenTheScenarioPuCostDataIsUpdated(costSurfaceId, 42);
});

it('should insert cost surface cost data when linking in creation mode', async () => {
const scenarioId = v4();
const projectId = v4();
await world.GivenScenarioPuDataExists(projectId, scenarioId);
const costSurfaceId = v4();
await world.GivenCostSurfacePuDataExists(costSurfaceId);

const linkCostSurfaceJob = world.getLinkCostSurfaceToScenarioJob(
scenarioId,
costSurfaceId,
'creation',
);
await world.WhenTheCostSurfaceLinkingJobIsProcessed(linkCostSurfaceJob);
await world.ThenTheScenarioPuCostDataIsUpdated(costSurfaceId, 42);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getFixtures } from '../planning-unit-fixtures';
import { CostSurfaceShapefileRecord } from '@marxan-geoprocessing/modules/cost-surface/ports/cost-surface-shapefile-record';
import {
FromProjectShapefileJobInput,
LinkCostSurfaceToScenarioJobInput,
LinkCostSurfaceToScenarioJobInput, LinkCostSurfaceToScenarioMode,
ProjectCostSurfaceJobInput,
} from '@marxan/artifact-cache/surface-cost-job-input';
import { CostSurfacePuDataEntity } from '@marxan/cost-surfaces';
Expand Down Expand Up @@ -64,6 +64,7 @@ export const createWorld = async (app: INestApplication) => {
getLinkCostSurfaceToScenarioJob: (
scenarioId: string,
costSurfaceId: string,
mode: LinkCostSurfaceToScenarioMode
) =>
(({
data: {
Expand All @@ -72,7 +73,7 @@ export const createWorld = async (app: INestApplication) => {
scenarioId,
costSurfaceId,
originalCostSurfaceId: v4(),
mode: 'creation',
mode: mode,
},
id: 'test-job',
} as unknown) as Job<LinkCostSurfaceToScenarioJobInput>),
Expand Down
5 changes: 3 additions & 2 deletions api/libs/artifact-cache/src/surface-cost-job-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ export type LinkCostSurfaceToScenarioJobInput = {
scenarioId: string;
costSurfaceId: string;
originalCostSurfaceId: string;

mode: 'creation' | 'update';
mode: LinkCostSurfaceToScenarioMode;
};

export type LinkCostSurfaceToScenarioMode = 'creation' | 'update';

export type ScenarioCostSurfaceJobInput = LinkCostSurfaceToScenarioJobInput;

/**
Expand Down

1 comment on commit 5497a69

@vercel
Copy link

@vercel vercel bot commented on 5497a69 Oct 20, 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 – ./

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

Please sign in to comment.