Skip to content

Commit

Permalink
feat(CostSurface): Adds endpoint to link a CostSurface to a Scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
KevSanchez authored and hotzevzl committed Oct 20, 2023
1 parent f22f385 commit 694f639
Show file tree
Hide file tree
Showing 33 changed files with 1,233 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddCostSurfaceLinkingNewEvents1696603545456
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
INSERT INTO api_event_kinds (id) values
('scenario.costSurface.link.submitted/v1/alpha1'),
('scenario.costSurface.link.finished/v1/alpha1'),
('scenario.costSurface.link.failed/v1/alpha1');
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM api_event_kinds WHERE id = 'scenario.costSurface.link.submitted/v1/alpha1';`,
);
await queryRunner.query(
`DELETE FROM api_event_kinds WHERE id = 'scenario.costSurface.link.finished/v1/alpha1';`,
);
await queryRunner.query(
`DELETE FROM api_event_kinds WHERE id = 'scenario.costSurface.link.failed/v1/alpha1';`,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class CostSurfaceAsyncJob extends AsyncJob {
getFailedAsyncJobState(): CostSurfaceApiEvents {
/*
At the moment in the codebase, we are not using this two api events:
API_EVENT_KINDS.scenario__costSurface__shapeConverted__v1_alpha1
API_EVENT_KINDS.scenario__costSurface__shapeConverted__v1_alpha1
API_EVENT_KINDS.scenario__costSurface__shapeConversionFailed__v1_alpha1
In case we start using them, we migh have to do add new state to getEndAsynJobStates()
and check the latestApitEvent when executing getFailedAsyncJobState()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { ApiEventsModule } from '../../api-events';
import { CqrsModule } from '@nestjs/cqrs';
import { ScenarioCostSurfaceEventsPort } from '@marxan-api/modules/cost-surface/ports/scenario/scenario-cost-surface-events.port';
import { ScenarioCostSurfaceApiEvents } from '@marxan-api/modules/cost-surface/adapters/scenario/scenario-cost-surface-api-events';

@Module({
imports: [ApiEventsModule, CqrsModule],
providers: [
{
provide: ScenarioCostSurfaceEventsPort,
useClass: ScenarioCostSurfaceApiEvents,
},
],
exports: [ScenarioCostSurfaceEventsPort],
})
export class ScenarioCostSurfaceAdaptersModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { API_EVENT_KINDS } from '@marxan/api-events';
import { ApiEventsService } from '@marxan-api/modules/api-events';
import {
ScenarioCostSurfaceEventsPort,
ScenarioCostSurfaceState,
} from '@marxan-api/modules/cost-surface/ports/scenario/scenario-cost-surface-events.port';

@Injectable()
export class ScenarioCostSurfaceApiEvents
extends ApiEventsService
implements ScenarioCostSurfaceEventsPort {
private readonly eventsMap: Record<
ScenarioCostSurfaceState,
API_EVENT_KINDS
> = {
[ScenarioCostSurfaceState.LinkToScenarioFailed]:
API_EVENT_KINDS.scenario__costSurface__link__failed__v1_alpha1,
[ScenarioCostSurfaceState.LinkToScenarioFinished]:
API_EVENT_KINDS.scenario__costSurface__link__finished__v1_alpha1,
[ScenarioCostSurfaceState.LinkToScenarioSubmitted]:
API_EVENT_KINDS.scenario__costSurface__link__submitted__v1_alpha1,
};

async event(
scenarioId: string,
state: ScenarioCostSurfaceState,
context?: Record<string, unknown>,
): Promise<void> {
await this.create({
data: context ?? {},
topic: scenarioId,
kind: this.eventsMap[state],
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Command } from '@nestjs-architects/typed-cqrs';
import { Either } from 'fp-ts/lib/Either';

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

export type LinkCostSurfaceToScenarioError = typeof linkCostSurfaceToScenarioFailed;

export type LinkCostSurfaceToScenarioResponse = Either<
LinkCostSurfaceToScenarioError,
true
>;

/**
* @todo: Temporal substitute for UpdateCostSurface command, which works at scenario level. It should be
* removed and use there once the implementation is fully validated
*/
export class LinkCostSurfaceToScenarioCommand extends Command<LinkCostSurfaceToScenarioResponse> {
constructor(
public readonly scenarioId: string,
public readonly costSurfaceId: string,
public readonly mode: 'creation' | 'update',
) {
super();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Inject, Logger } from '@nestjs/common';
import { CommandHandler, IInferredCommandHandler } from '@nestjs/cqrs';
import { Queue } from 'bullmq';
import { left, right } from 'fp-ts/lib/Either';
import { LinkCostSurfaceToScenarioJobInput } from '@marxan/artifact-cache/surface-cost-job-input';
import {
LinkCostSurfaceToScenarioCommand,
linkCostSurfaceToScenarioFailed,
LinkCostSurfaceToScenarioResponse,
} from '@marxan-api/modules/cost-surface/application/scenario/link-cost-surface-to-scenario.command';
import { InjectRepository } from '@nestjs/typeorm';
import { Scenario } from '@marxan-api/modules/scenarios/scenario.api.entity';
import { Repository } from 'typeorm';
import {
ScenarioCostSurfaceEventsPort,
ScenarioCostSurfaceState,
} from '@marxan-api/modules/cost-surface/ports/scenario/scenario-cost-surface-events.port';
import { scenarioCostSurfaceQueueToken } from '@marxan-api/modules/cost-surface/infra/scenario/scenario-cost-surface-queue.provider';

@CommandHandler(LinkCostSurfaceToScenarioCommand)
export class LinkCostSurfaceToScenarioHandler
implements IInferredCommandHandler<LinkCostSurfaceToScenarioCommand> {
private readonly logger: Logger = new Logger(
LinkCostSurfaceToScenarioHandler.name,
);

constructor(
@Inject(scenarioCostSurfaceQueueToken)
private readonly queue: Queue<LinkCostSurfaceToScenarioJobInput>,
@InjectRepository(Scenario)
private readonly scenarioRepo: Repository<Scenario>,
private readonly events: ScenarioCostSurfaceEventsPort,
) {}

async execute({
scenarioId,
costSurfaceId,
mode,
}: LinkCostSurfaceToScenarioCommand): Promise<LinkCostSurfaceToScenarioResponse> {
try {
const scenario = await this.scenarioRepo.findOneOrFail({
where: { id: scenarioId },
});
const originalCostSurfaceId = scenario.costSurfaceId;

await this.queue.add(`link-cost-surface-for-scenario-${scenarioId}`, {
type: 'LinkCostSurfaceToScenarioJobInput',
scenarioId,
costSurfaceId,
originalCostSurfaceId,
mode,
});

await this.scenarioRepo.update(scenarioId, { costSurfaceId });

await this.events.event(
scenarioId,
ScenarioCostSurfaceState.LinkToScenarioSubmitted,
);
} catch (error) {
await this.markAsFailed(scenarioId, error);
return left(linkCostSurfaceToScenarioFailed);
}

return right(true);
}

private markAsFailed = async (scenarioId: string, error: unknown) => {
this.logger.error(
`Failed executing link-cost-surface-to-scenario command for scenario ${scenarioId}`,
String(error),
);
await this.events.event(
scenarioId,
ScenarioCostSurfaceState.LinkToScenarioFailed,
{
error,
},
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { LinkCostSurfaceToScenarioHandler } from '@marxan-api/modules/cost-surface/application/scenario/link-cost-surface-to-scenario.handler';
import { ScenarioCostSurfaceInfraModule } from '@marxan-api/modules/cost-surface/infra/scenario/scenario-cost-surface-infra.module';
import { ScenarioCostSurfaceAdaptersModule } from '@marxan-api/modules/cost-surface/adapters/scenario-cost-surface-adapters.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Scenario } from '@marxan-api/modules/scenarios/scenario.api.entity';

@Module({
imports: [
ScenarioCostSurfaceInfraModule,
ScenarioCostSurfaceAdaptersModule,
CqrsModule,
TypeOrmModule.forFeature([Scenario]),
],
providers: [LinkCostSurfaceToScenarioHandler],
})
export class ScenarioCostSurfaceApplicationModule {}
9 changes: 8 additions & 1 deletion api/apps/api/src/modules/cost-surface/cost-surface.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@ import { DeleteCostSurfaceModule } from '@marxan-api/modules/cost-surface/delete
import { ProjectCostSurfaceAdaptersModule } from '@marxan-api/modules/cost-surface/adapters/cost-surface-adapters.module';
import { CostRangeService } from '@marxan-api/modules/scenarios/cost-range-service';
import { CqrsModule } from '@nestjs/cqrs';
import { ScenarioAclModule } from '@marxan-api/modules/access-control/scenarios-acl/scenario-acl.module';
import { Scenario } from '@marxan-api/modules/scenarios/scenario.api.entity';
import { ScenarioCostSurfaceApplicationModule } from '@marxan-api/modules/cost-surface/application/scenario/scenario-cost-surface-application.module';
import { ScenarioCostSurfaceAdaptersModule } from '@marxan-api/modules/cost-surface/adapters/scenario-cost-surface-adapters.module';

@Module({
imports: [
ProjectCostSurfaceApplicationModule,
ProjectCostSurfaceAdaptersModule,
ScenarioCostSurfaceApplicationModule,
ScenarioCostSurfaceAdaptersModule,
CostSurfaceApplicationModule,
DeleteProjectModule,
TypeOrmModule.forFeature([CostSurface]),
TypeOrmModule.forFeature([CostSurface, Scenario]),
ProjectAclModule,
ScenarioAclModule,
DeleteCostSurfaceModule,
CqrsModule,
],
Expand Down
54 changes: 54 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 @@ -18,6 +18,13 @@ import {
} from '@marxan-api/modules/cost-surface/delete-cost-surface/delete-cost-surface.command';
import { CostSurfaceCalculationPort } from '@marxan-api/modules/cost-surface/ports/project/cost-surface-calculation.port';
import { CommandBus } from '@nestjs/cqrs';
import { scenarioNotFound } from '@marxan-api/modules/blm/values/blm-repos';
import {
LinkCostSurfaceToScenarioCommand,
LinkCostSurfaceToScenarioError,
} from '@marxan-api/modules/cost-surface/application/scenario/link-cost-surface-to-scenario.command';
import { scenarioNotEditable } from '@marxan-api/modules/scenarios/scenarios.service';
import { ScenarioAclService } from '@marxan-api/modules/access-control/scenarios-acl/scenario-acl.service';

export const costSurfaceNotEditableWithinProject = Symbol(
`cost surface not editable within project`,
Expand Down Expand Up @@ -45,7 +52,10 @@ export class CostSurfaceService {
constructor(
@InjectRepository(CostSurface)
private readonly costSurfaceRepository: Repository<CostSurface>,
@InjectRepository(Scenario)
private readonly scenarioRepository: Repository<Scenario>,
private readonly projectAclService: ProjectAclService,
private readonly scenarioAclService: ScenarioAclService,
private readonly calculateCost: CostSurfaceCalculationPort,
private readonly commandBus: CommandBus,
) {}
Expand Down Expand Up @@ -155,6 +165,49 @@ export class CostSurfaceService {
);
}

async linkCostSurfaceToScenario(
userId: string,
scenarioId: string,
costSurfaceId: 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);
}

if (!(await this.scenarioAclService.canEditScenario(userId, scenarioId))) {
return left(scenarioNotEditable);
}

const costSurface = await this.costSurfaceRepository.findOne({
where: { id: costSurfaceId },
});
if (!costSurface) {
return left(costSurfaceNotFound);
}

if (scenario.projectId !== costSurface.projectId) {
return left(costSurfaceNotFound);
}
if (scenario.costSurfaceId === costSurface.id) {
return right(true);
}

return this.commandBus.execute(
new LinkCostSurfaceToScenarioCommand(scenarioId, costSurfaceId, 'update'),
);
}

async update(
userId: string,
projectId: string,
Expand Down Expand Up @@ -244,6 +297,7 @@ export class CostSurfaceService {

return right(costSurface);
}

async getCostSurface(
userId: string,
projectId: string,
Expand Down
Loading

0 comments on commit 694f639

Please sign in to comment.