Skip to content

Commit

Permalink
feat(api): cost-surface: application logic
Browse files Browse the repository at this point in the history
  • Loading branch information
kgajowy committed May 18, 2021
1 parent df30341 commit cf20031
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import { AdjustCostSurface } from '../../../analysis/entry-points/adjust-cost-surface';
import { CostSurfaceInputDto } from '../../../analysis/entry-points/adjust-cost-surface-input';

@Injectable()
export class AdjustCostServiceFake implements AdjustCostSurface {
mock = jest.fn();

async update(
scenarioId: string,
constraints: CostSurfaceInputDto,
): Promise<true> {
return this.mock(scenarioId, constraints);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable, Scope } from '@nestjs/common';
import {
CostSurfaceEventsPort,
CostSurfaceState,
} from '../cost-surface-events.port';

@Injectable()
export class CostSurfaceEventsFake implements CostSurfaceEventsPort {
mock = jest.fn();
events: [string, CostSurfaceState][] = [];

async event(
scenarioId: string,
state: CostSurfaceState,
context?: Record<string, unknown> | Error,
): Promise<void> {
this.events.push([scenarioId, state]);
return this.mock(scenarioId, state, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
import { ResolvePuWithCost } from '../resolve-pu-with-cost';
import { CostSurfaceInputDto } from '../../../analysis/entry-points/adjust-cost-surface-input';

@Injectable()
export class ShapefileConverterFake implements ResolvePuWithCost {
mock = jest.fn();

async fromShapefile(file: Express.Multer.File): Promise<CostSurfaceInputDto> {
return this.mock(file);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CostSurfaceInputDto } from '../../../analysis/entry-points/adjust-cost-surface-input';

export const getValidSurfaceCost = (): CostSurfaceInputDto => ({
planningUnits: [
{
id: 'pu-id-1',
cost: 300,
},
{
id: 'pu-id-2',
cost: 2000,
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Injectable } from '@nestjs/common';
import {
CostSurfaceEventsPort,
CostSurfaceState,
} from '../cost-surface-events.port';

@Injectable()
export class CostSurfaceApiEvents implements CostSurfaceEventsPort {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async event(scenarioId: string, state: CostSurfaceState): Promise<void> {
//
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Injectable } from '@nestjs/common';
import { ResolvePuWithCost } from '../resolve-pu-with-cost';
import { CostSurfaceInputDto } from '../../../analysis/entry-points/adjust-cost-surface-input';

@Injectable()
export class GeoprocessingCostFromShapefile implements ResolvePuWithCost {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async fromShapefile(file: Express.Multer.File): Promise<CostSurfaceInputDto> {
return {
planningUnits: [],
};
}
}
15 changes: 15 additions & 0 deletions api/src/modules/scenarios/cost-surface/cost-surface-events.port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum CostSurfaceState {
Submitted = 'submitted',
ShapefileConverted = 'shapefile-converted',
ShapefileConversionFailed = 'shapefile-conversion-failed',
CostUpdateFailed = 'cost-update-failed',
Finished = 'finished',
}

export abstract class CostSurfaceEventsPort {
abstract event(
scenarioId: string,
state: CostSurfaceState,
context?: Record<string, unknown> | Error,
): Promise<void>;
}
141 changes: 141 additions & 0 deletions api/src/modules/scenarios/cost-surface/cost-surface.facade.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { CostSurfaceFacade } from './cost-surface.facade';
import { Test } from '@nestjs/testing';
import { Request } from 'express';
import { AdjustCostSurface } from '../../analysis/entry-points/adjust-cost-surface';
import { AdjustCostServiceFake } from './__mocks__/adjust-cost-service-fake';
import { ResolvePuWithCost } from './resolve-pu-with-cost';
import { ShapefileConverterFake } from './__mocks__/shapefile-converter-fake';
import { CostSurfaceEventsPort } from './cost-surface-events.port';
import { CostSurfaceEventsFake } from './__mocks__/cost-surface-events-fake';
import { getValidSurfaceCost } from './__mocks__/surface-cost.data';

let sut: CostSurfaceFacade;

let costService: AdjustCostServiceFake;
let fileConverter: ShapefileConverterFake;
let events: CostSurfaceEventsFake;

beforeEach(async () => {
const sandbox = await Test.createTestingModule({
providers: [
{
provide: AdjustCostSurface,
useClass: AdjustCostServiceFake,
},
{
provide: ResolvePuWithCost,
useClass: ShapefileConverterFake,
},
{
provide: CostSurfaceEventsPort,
useClass: CostSurfaceEventsFake,
},
CostSurfaceFacade,
],
}).compile();

sut = sandbox.get(CostSurfaceFacade);
costService = sandbox.get(AdjustCostSurface);
fileConverter = sandbox.get(ResolvePuWithCost);
events = sandbox.get(CostSurfaceEventsPort);
});

const scenarioId = 'scenarioId';
const request: Request = Object.freeze({
file: ({ fakeFile: 1 } as unknown) as Express.Multer.File,
} as unknown) as Request;

describe(`when file couldn't be converted`, () => {
beforeEach(async () => {
fileConverter.mock.mockRejectedValue(new Error('Not a shapefile.'));

// Act
await sut.convert(scenarioId, request);
});

it(`should emit relevant events`, () => {
expect(events.events).toMatchInlineSnapshot(`
Array [
Array [
"scenarioId",
"submitted",
],
Array [
"scenarioId",
"shapefile-conversion-failed",
],
]
`);
});
});

describe(`when file can be converted`, () => {
beforeEach(() => {
fileConverter.mock.mockResolvedValue(getValidSurfaceCost());
});

describe(`when cost couldn't be adjusted`, () => {
beforeEach(async () => {
costService.mock.mockRejectedValue(new Error('SQL Error'));

// Act
await sut.convert(scenarioId, request);
});

it(`should emit relevant events`, () => {
expect(events.events).toMatchInlineSnapshot(`
Array [
Array [
"scenarioId",
"submitted",
],
Array [
"scenarioId",
"shapefile-converted",
],
Array [
"scenarioId",
"cost-update-failed",
],
]
`);
});
});

describe(`when cost can be adjusted`, () => {
beforeEach(async () => {
costService.mock.mockResolvedValue(undefined);

// Act
await sut.convert(scenarioId, request);
});

it(`proxies file to port`, () => {
expect(fileConverter.mock.mock.calls[0][0]).toEqual(request.file);
});

it(`proxies port output to update service`, () => {
expect(costService.mock.mock.calls[0][0]).toEqual(scenarioId);
expect(costService.mock.mock.calls[0][1]).toEqual(getValidSurfaceCost());
});

it(`emits valid events chain`, () => {
expect(events.events).toMatchInlineSnapshot(`
Array [
Array [
"scenarioId",
"submitted",
],
Array [
"scenarioId",
"shapefile-converted",
],
Array [
"scenarioId",
"finished",
],
]
`);
});
});
});
50 changes: 26 additions & 24 deletions api/src/modules/scenarios/cost-surface/cost-surface.facade.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
import { Injectable } from '@nestjs/common';
import { Request, Response } from 'express';
import { Request } from 'express';
import { AdjustCostSurface } from '../../analysis/entry-points/adjust-cost-surface';
import { ProxyService } from '../../proxy/proxy.service';
import { ResolvePuWithCost } from './resolve-pu-with-cost';
import {
CostSurfaceEventsPort,
CostSurfaceState,
} from './cost-surface-events.port';
import { CostSurfaceInputDto } from '../../analysis/entry-points/adjust-cost-surface-input';

@Injectable()
export class CostSurfaceFacade {
constructor(
private readonly adjustCostSurfaceService: AdjustCostSurface,
private readonly proxyService: ProxyService,
private readonly shapefileConverter: ResolvePuWithCost,
private readonly events: CostSurfaceEventsPort,
) {}

/**
* non blocking - will do job in "background"
*
* @param scenarioId
* @param request
*/
convert(scenarioId: string, request: Request) {
// TODO #0 Generate & Dispatch Api Event (some wrapping service for /dummy/"terminating" if already running)
async convert(scenarioId: string, request: Request) {
await this.events.event(scenarioId, CostSurfaceState.Submitted);
let costSurface: CostSurfaceInputDto;

// TODO #1 Call Proxy Service to get Planning Units and their surface cost
// this.proxyService.... - modify this to send back data, not act on Response
try {
costSurface = await this.shapefileConverter.fromShapefile(request.file);
} catch (error) {
await this.events.event(
scenarioId,
CostSurfaceState.ShapefileConversionFailed,
);
return;
}

// TODO #2 Call Analysis-module with scenario id & output from the above
await this.events.event(scenarioId, CostSurfaceState.ShapefileConverted);
this.adjustCostSurfaceService
.update(scenarioId, {
planningUnits: [
{
id: '0',
cost: 100,
},
],
})
.then(() => {
// dispatch ApiEvent - Done
})
.catch(() => {
// dispatch ApiEvent - Failed
.update(scenarioId, costSurface)
.then(() => this.events.event(scenarioId, CostSurfaceState.Finished))
.catch(async () => {
await this.events.event(scenarioId, CostSurfaceState.CostUpdateFailed);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CostSurfaceInputDto } from '../../analysis/entry-points/adjust-cost-surface-input';

export abstract class ResolvePuWithCost {
abstract fromShapefile(
file: Express.Multer.File,
): Promise<CostSurfaceInputDto>;
}
4 changes: 3 additions & 1 deletion api/src/modules/scenarios/scenarios.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ export class ScenariosController {
* or just ...BaseService
*/

this.costSurface.convert(scenarioId, request);
this.costSurface.convert(scenarioId, request).then(() => {
//
});
return;
}

Expand Down
13 changes: 13 additions & 0 deletions api/src/modules/scenarios/scenarios.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { ProxyService } from 'modules/proxy/proxy.service';
import { WdpaAreaCalculationService } from './wdpa-area-calculation.service';
import { AnalysisModule } from '../analysis/analysis.module';
import { CostSurfaceFacade } from './cost-surface/cost-surface.facade';
import { ResolvePuWithCost } from './cost-surface/resolve-pu-with-cost';
import { CostSurfaceEventsPort } from './cost-surface/cost-surface-events.port';
import { GeoprocessingCostFromShapefile } from './cost-surface/adapters/geoprocessing-cost-from-shapefile';
import { CostSurfaceApiEvents } from './cost-surface/adapters/cost-surface-api-events';

@Module({
imports: [
Expand All @@ -30,6 +34,15 @@ import { CostSurfaceFacade } from './cost-surface/cost-surface.facade';
ProxyService,
WdpaAreaCalculationService,
CostSurfaceFacade,
// internals for cost-surface
{
provide: ResolvePuWithCost,
useClass: GeoprocessingCostFromShapefile,
},
{
provide: CostSurfaceEventsPort,
useClass: CostSurfaceApiEvents,
},
],
controllers: [ScenariosController],
exports: [ScenariosService],
Expand Down

0 comments on commit cf20031

Please sign in to comment.