From ac8e459e90b8aed4e65b357eb9ae2e9a7b89f594 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 22 Aug 2023 16:14:36 +0200 Subject: [PATCH] move route compute to create service as entity creation is not async --- src/modules/ad/ad.di-tokens.ts | 3 + src/modules/ad/ad.mapper.ts | 27 +++----- .../commands/create-ad/create-ad.service.ts | 21 +++++- .../application/ports/route-provider.port.ts | 2 +- src/modules/ad/core/domain/ad.types.ts | 13 +++- .../value-objects/point.value-object.ts | 31 +++++++++ .../ad/infrastructure/route-provider.ts | 21 ++++++ src/modules/ad/tests/unit/ad.mapper.spec.ts | 56 ++++++--------- .../ad/tests/unit/core/ad.entity.spec.ts | 9 ++- .../tests/unit/core/create-ad.service.spec.ts | 40 ++++++++++- .../unit/core/point.value-object.spec.ts | 49 +++++++++++++ .../unit/core/waypoint.value-object.spec.ts | 2 +- .../infrastructure/route-provider.spec.ts | 69 +++++++++++++++++++ .../ports/get-basic-route-controller.port.ts | 6 ++ .../core/application/types/route.type.ts | 3 +- .../geography/core/domain/route.entity.ts | 4 +- .../geography/core/domain/route.types.ts | 6 +- .../value-objects/coordinates.value-object.ts | 31 +++++++++ src/modules/geography/geography.module.ts | 6 +- .../controllers/dtos/get-route.request.dto.ts | 6 +- ...oller.ts => get-basic-route.controller.ts} | 13 ++-- .../interface/dtos/route.response.dto.ts | 3 +- src/modules/geography/route.mapper.ts | 2 +- .../core/coordinates.value-object.spec.ts | 49 +++++++++++++ .../unit/core/waypoint.value-object.spec.ts | 2 +- ....ts => get-basic-route.controller.spec.ts} | 21 +++--- .../geography/tests/unit/route.mapper.spec.ts | 2 +- 27 files changed, 403 insertions(+), 94 deletions(-) create mode 100644 src/modules/ad/core/domain/value-objects/point.value-object.ts create mode 100644 src/modules/ad/infrastructure/route-provider.ts create mode 100644 src/modules/ad/tests/unit/core/point.value-object.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts create mode 100644 src/modules/geography/core/application/ports/get-basic-route-controller.port.ts create mode 100644 src/modules/geography/core/domain/value-objects/coordinates.value-object.ts rename src/modules/geography/interface/controllers/{get-route.controller.ts => get-basic-route.controller.ts} (63%) create mode 100644 src/modules/geography/tests/unit/core/coordinates.value-object.spec.ts rename src/modules/geography/tests/unit/interface/{get-route.controller.spec.ts => get-basic-route.controller.spec.ts} (70%) diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 3e2571d..8690c8d 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -1,4 +1,7 @@ export const AD_REPOSITORY = Symbol('AD_REPOSITORY'); export const AD_DIRECTION_ENCODER = Symbol('AD_DIRECTION_ENCODER'); export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER'); +export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol( + 'AD_GET_BASIC_ROUTE_CONTROLLER', +); export const AD_ROUTE_PROVIDER = Symbol('AD_ROUTE_PROVIDER'); diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index c9df8b1..bb5d25c 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -6,13 +6,11 @@ import { AdReadModel, ScheduleItemModel, } from './infrastructure/ad.repository'; -import { Frequency, Role } from './core/domain/ad.types'; +import { Frequency } from './core/domain/ad.types'; import { v4 } from 'uuid'; import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; -import { AD_DIRECTION_ENCODER, AD_ROUTE_PROVIDER } from './ad.di-tokens'; -import { RouteProviderPort } from './core/application/ports/route-provider.port'; -import { Route } from './core/application/types/route.type'; +import { AD_DIRECTION_ENCODER } from './ad.di-tokens'; /** * Mapper constructs objects that are used in different layers: @@ -28,17 +26,11 @@ export class AdMapper constructor( @Inject(AD_DIRECTION_ENCODER) private readonly directionEncoder: DirectionEncoderPort, - @Inject(AD_ROUTE_PROVIDER) - private readonly routeProvider: RouteProviderPort, ) {} toPersistence = (entity: AdEntity): AdWriteModel => { const copy = entity.getProps(); const now = new Date(); - const roles: Role[] = []; - if (copy.driver) roles.push(Role.DRIVER); - if (copy.passenger) roles.push(Role.PASSENGER); - const route: Route = this.routeProvider.getBasic(roles, copy.waypoints); const record: AdWriteModel = { uuid: copy.id, driver: copy.driver, @@ -65,14 +57,14 @@ export class AdMapper seatsProposed: copy.seatsProposed, seatsRequested: copy.seatsRequested, strict: copy.strict, - driverDuration: route.driverDuration, - driverDistance: route.driverDistance, - passengerDuration: route.passengerDuration, - passengerDistance: route.passengerDistance, + driverDuration: copy.driverDuration, + driverDistance: copy.driverDistance, + passengerDuration: copy.passengerDuration, + passengerDistance: copy.passengerDistance, waypoints: this.directionEncoder.encode(copy.waypoints), - direction: this.directionEncoder.encode(route.points), - fwdAzimuth: route.fwdAzimuth, - backAzimuth: route.backAzimuth, + direction: this.directionEncoder.encode(copy.points), + fwdAzimuth: copy.fwdAzimuth, + backAzimuth: copy.backAzimuth, createdAt: copy.createdAt, updatedAt: copy.updatedAt, }; @@ -116,6 +108,7 @@ export class AdMapper })), fwdAzimuth: record.fwdAzimuth, backAzimuth: record.backAzimuth, + points: [], }, }); return entity; diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 776e43d..47d367f 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -1,20 +1,32 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { CreateAdCommand } from './create-ad.command'; import { Inject } from '@nestjs/common'; -import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; +import { RouteProviderPort } from '../../ports/route-provider.port'; +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Route } from '../../types/route.type'; @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { constructor( @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, + @Inject(AD_ROUTE_PROVIDER) + private readonly routeProvider: RouteProviderPort, ) {} async execute(command: CreateAdCommand): Promise { + const roles: Role[] = []; + if (command.driver) roles.push(Role.DRIVER); + if (command.passenger) roles.push(Role.PASSENGER); + const route: Route = await this.routeProvider.getBasic( + roles, + command.waypoints, + ); const ad = AdEntity.create({ id: command.id, driver: command.driver, @@ -27,6 +39,13 @@ export class CreateAdService implements ICommandHandler { seatsRequested: command.seatsRequested, strict: command.strict, waypoints: command.waypoints, + points: route.points, + driverDistance: route.driverDistance, + driverDuration: route.driverDuration, + passengerDistance: route.passengerDistance, + passengerDuration: route.passengerDuration, + fwdAzimuth: route.fwdAzimuth, + backAzimuth: route.backAzimuth, }); try { diff --git a/src/modules/ad/core/application/ports/route-provider.port.ts b/src/modules/ad/core/application/ports/route-provider.port.ts index f8fd19e..4ce43b0 100644 --- a/src/modules/ad/core/application/ports/route-provider.port.ts +++ b/src/modules/ad/core/application/ports/route-provider.port.ts @@ -6,5 +6,5 @@ export interface RouteProviderPort { /** * Get a basic route with points and overall duration / distance */ - getBasic(roles: Role[], waypoints: Waypoint[]): Route; + getBasic(roles: Role[], waypoints: Waypoint[]): Promise; } diff --git a/src/modules/ad/core/domain/ad.types.ts b/src/modules/ad/core/domain/ad.types.ts index ad2c7ef..ba60073 100644 --- a/src/modules/ad/core/domain/ad.types.ts +++ b/src/modules/ad/core/domain/ad.types.ts @@ -1,3 +1,4 @@ +import { PointProps } from './value-objects/point.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { WaypointProps } from './value-objects/waypoint.value-object'; @@ -17,8 +18,9 @@ export interface AdProps { passengerDuration?: number; passengerDistance?: number; waypoints: WaypointProps[]; - fwdAzimuth?: number; - backAzimuth?: number; + points: PointProps[]; + fwdAzimuth: number; + backAzimuth: number; } // Properties that are needed for an Ad creation @@ -34,6 +36,13 @@ export interface CreateAdProps { seatsRequested: number; strict: boolean; waypoints: WaypointProps[]; + driverDuration?: number; + driverDistance?: number; + passengerDuration?: number; + passengerDistance?: number; + fwdAzimuth: number; + backAzimuth: number; + points: PointProps[]; } export enum Frequency { diff --git a/src/modules/ad/core/domain/value-objects/point.value-object.ts b/src/modules/ad/core/domain/value-objects/point.value-object.ts new file mode 100644 index 0000000..2047ead --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/point.value-object.ts @@ -0,0 +1,31 @@ +import { + ArgumentOutOfRangeException, + ValueObject, +} from '@mobicoop/ddd-library'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface PointProps { + lon: number; + lat: number; +} + +export class Point extends ValueObject { + get lon(): number { + return this.props.lon; + } + + get lat(): number { + return this.props.lat; + } + + protected validate(props: PointProps): void { + if (props.lon > 180 || props.lon < -180) + throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); + if (props.lat > 90 || props.lat < -90) + throw new ArgumentOutOfRangeException('lat must be between -90 and 90'); + } +} diff --git a/src/modules/ad/infrastructure/route-provider.ts b/src/modules/ad/infrastructure/route-provider.ts new file mode 100644 index 0000000..cd57ec2 --- /dev/null +++ b/src/modules/ad/infrastructure/route-provider.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { RouteProviderPort } from '../core/application/ports/route-provider.port'; +import { Route } from '../core/application/types/route.type'; +import { Waypoint } from '../core/application/types/waypoint.type'; +import { Role } from '../core/domain/ad.types'; +import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; +import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; + +@Injectable() +export class RouteProvider implements RouteProviderPort { + constructor( + @Inject(AD_GET_BASIC_ROUTE_CONTROLLER) + private readonly getBasicRouteController: GetBasicRouteControllerPort, + ) {} + + getBasic = async (roles: Role[], waypoints: Waypoint[]): Promise => + await this.getBasicRouteController.get({ + roles, + waypoints, + }); +} diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index 14ae563..0099653 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -1,9 +1,5 @@ -import { - AD_DIRECTION_ENCODER, - AD_ROUTE_PROVIDER, -} from '@modules/ad/ad.di-tokens'; +import { AD_DIRECTION_ENCODER } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; -import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { @@ -44,6 +40,26 @@ const adEntity: AdEntity = new AdEntity({ strict: false, seatsProposed: 3, seatsRequested: 1, + driverDistance: 350101, + driverDuration: 14422, + passengerDistance: 350101, + passengerDuration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], }, createdAt: now, updatedAt: now, @@ -104,32 +120,6 @@ const mockDirectionEncoder: DirectionEncoderPort = { ]), }; -const mockRouteProvider: RouteProviderPort = { - getBasic: jest.fn().mockImplementation(() => ({ - driverDistance: 350101, - driverDuration: 14422, - passengerDistance: 350101, - passengerDuration: 14422, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336544, - points: [ - { - lon: 6.1765102, - lat: 48.689445, - }, - { - lon: 4.984578, - lat: 48.725687, - }, - { - lon: 2.3522, - lat: 48.8566, - }, - ], - })), -}; - describe('Ad Mapper', () => { let adMapper: AdMapper; @@ -141,10 +131,6 @@ describe('Ad Mapper', () => { provide: AD_DIRECTION_ENCODER, useValue: mockDirectionEncoder, }, - { - provide: AD_ROUTE_PROVIDER, - useValue: mockRouteProvider, - }, ], }).compile(); adMapper = module.get(AdMapper); diff --git a/src/modules/ad/tests/unit/core/ad.entity.spec.ts b/src/modules/ad/tests/unit/core/ad.entity.spec.ts index 899e3f0..90077ac 100644 --- a/src/modules/ad/tests/unit/core/ad.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/ad.entity.spec.ts @@ -31,6 +31,13 @@ const createAdProps: CreateAdProps = { seatsRequested: 1, strict: false, waypoints: [originWaypointProps, destinationWaypointProps], + driverDistance: 23000, + driverDuration: 900, + passengerDistance: 23000, + passengerDuration: 900, + fwdAzimuth: 283, + backAzimuth: 93, + points: [], }; describe('Ad entity create', () => { @@ -42,6 +49,6 @@ describe('Ad entity create', () => { expect(ad.getProps().schedule[0].time).toBe('08:30'); expect(ad.getProps().driver).toBeTruthy(); expect(ad.getProps().passenger).toBeTruthy(); - expect(ad.getProps().driverDistance).toBeUndefined(); + expect(ad.getProps().driverDistance).toBe(23000); }); }); diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 74f26e6..d78729c 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { AggregateID } from '@mobicoop/ddd-library'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { ConflictException } from '@mobicoop/ddd-library'; @@ -8,6 +8,7 @@ import { CreateAdService } from '@modules/ad/core/application/commands/create-ad import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; const originWaypoint: WaypointProps = { position: 0, @@ -37,6 +38,13 @@ const createAdProps: CreateAdProps = { strict: false, frequency: Frequency.PUNCTUAL, waypoints: [originWaypoint, destinationWaypoint], + driverDistance: 23000, + driverDuration: 900, + passengerDistance: 23000, + passengerDuration: 900, + fwdAzimuth: 283, + backAzimuth: 93, + points: [], }; const mockAdRepository = { @@ -51,6 +59,32 @@ const mockAdRepository = { }), }; +const mockRouteProvider: RouteProviderPort = { + getBasic: jest.fn().mockImplementation(() => ({ + driverDistance: 350101, + driverDuration: 14422, + passengerDistance: 350101, + passengerDuration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + })), +}; + describe('create-ad.service', () => { let createAdService: CreateAdService; @@ -61,6 +95,10 @@ describe('create-ad.service', () => { provide: AD_REPOSITORY, useValue: mockAdRepository, }, + { + provide: AD_ROUTE_PROVIDER, + useValue: mockRouteProvider, + }, CreateAdService, ], }).compile(); diff --git a/src/modules/ad/tests/unit/core/point.value-object.spec.ts b/src/modules/ad/tests/unit/core/point.value-object.spec.ts new file mode 100644 index 0000000..eb09cd8 --- /dev/null +++ b/src/modules/ad/tests/unit/core/point.value-object.spec.ts @@ -0,0 +1,49 @@ +import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library'; +import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; + +describe('Point value object', () => { + it('should create a point value object', () => { + const pointVO = new Point({ + lon: 48.689445, + lat: 6.17651, + }); + expect(pointVO.lon).toBe(48.689445); + expect(pointVO.lat).toBe(6.17651); + }); + it('should throw an exception if longitude is invalid', () => { + try { + new Point({ + lon: 348.689445, + lat: 6.17651, + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + try { + new Point({ + lon: -348.689445, + lat: 6.17651, + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + }); + it('should throw an exception if latitude is invalid', () => { + try { + new Point({ + lon: 48.689445, + lat: 96.17651, + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + try { + new Point({ + lon: 48.689445, + lat: -96.17651, + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + }); +}); diff --git a/src/modules/ad/tests/unit/core/waypoint.value-object.spec.ts b/src/modules/ad/tests/unit/core/waypoint.value-object.spec.ts index a2f363f..4191313 100644 --- a/src/modules/ad/tests/unit/core/waypoint.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/waypoint.value-object.spec.ts @@ -46,7 +46,7 @@ describe('Waypoint value object', () => { expect(e).toBeInstanceOf(ArgumentOutOfRangeException); } }); - it('should throw an exception if longitude is invalid', () => { + it('should throw an exception if latitude is invalid', () => { try { new Waypoint({ position: 0, diff --git a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts new file mode 100644 index 0000000..7a41ed8 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts @@ -0,0 +1,69 @@ +import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens'; +import { Route } from '@modules/ad/core/application/types/route.type'; +import { Role } from '@modules/ad/core/domain/ad.types'; +import { RouteProvider } from '@modules/ad/infrastructure/route-provider'; +import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockGetBasicRouteController: GetBasicRouteControllerPort = { + get: jest.fn().mockImplementation(() => ({ + driverDistance: 23000, + driverDuration: 900, + passengerDistance: 23000, + passengerDuration: 900, + fwdAzimuth: 283, + backAzimuth: 93, + distanceAzimuth: 19840, + points: [ + { + lon: 6.1765103, + lat: 48.689446, + }, + { + lon: 2.3523, + lat: 48.8567, + }, + ], + })), +}; + +describe('Route provider', () => { + let routeProvider: RouteProvider; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RouteProvider, + { + provide: AD_GET_BASIC_ROUTE_CONTROLLER, + useValue: mockGetBasicRouteController, + }, + ], + }).compile(); + + routeProvider = module.get(RouteProvider); + }); + + it('should be defined', () => { + expect(routeProvider).toBeDefined(); + }); + + it('should provide a route', async () => { + const route: Route = await routeProvider.getBasic( + [Role.DRIVER], + [ + { + position: 0, + lat: 48.689445, + lon: 6.1765102, + }, + { + position: 1, + lat: 48.8566, + lon: 2.3522, + }, + ], + ); + expect(route.driverDistance).toBe(23000); + }); +}); diff --git a/src/modules/geography/core/application/ports/get-basic-route-controller.port.ts b/src/modules/geography/core/application/ports/get-basic-route-controller.port.ts new file mode 100644 index 0000000..a1f0bd4 --- /dev/null +++ b/src/modules/geography/core/application/ports/get-basic-route-controller.port.ts @@ -0,0 +1,6 @@ +import { GetRouteRequestDto } from '@modules/geography/interface/controllers/dtos/get-route.request.dto'; +import { RouteResponseDto } from '@modules/geography/interface/dtos/route.response.dto'; + +export interface GetBasicRouteControllerPort { + get(data: GetRouteRequestDto): Promise; +} diff --git a/src/modules/geography/core/application/types/route.type.ts b/src/modules/geography/core/application/types/route.type.ts index 3913deb..791a00e 100644 --- a/src/modules/geography/core/application/types/route.type.ts +++ b/src/modules/geography/core/application/types/route.type.ts @@ -9,6 +9,5 @@ export type Route = { fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - points: Coordinates[]; - spacetimePoints: SpacetimePoint[]; + points: Coordinates[] | SpacetimePoint[]; }; diff --git a/src/modules/geography/core/domain/route.entity.ts b/src/modules/geography/core/domain/route.entity.ts index 705a4c2..dc81161 100644 --- a/src/modules/geography/core/domain/route.entity.ts +++ b/src/modules/geography/core/domain/route.entity.ts @@ -60,9 +60,7 @@ export class RouteEntity extends AggregateRoot { ? driverRoute.distanceAzimuth : passengerRoute.distanceAzimuth, waypoints: create.waypoints, - spacetimePoints: driverRoute - ? driverRoute.spacetimePoints - : passengerRoute.spacetimePoints, + points: driverRoute ? driverRoute.points : passengerRoute.points, }; return new RouteEntity({ id: v4(), diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index 172d207..7fe7776 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -1,5 +1,6 @@ import { GeorouterPort } from '../application/ports/georouter.port'; import { GeorouterSettings } from '../application/types/georouter-settings.type'; +import { CoordinatesProps } from './value-objects/coordinates.value-object'; import { SpacetimePointProps } from './value-objects/spacetime-point.value-object'; import { WaypointProps } from './value-objects/waypoint.value-object'; @@ -13,7 +14,7 @@ export interface RouteProps { backAzimuth: number; distanceAzimuth: number; waypoints: WaypointProps[]; - spacetimePoints: SpacetimePointProps[]; + points: SpacetimePointProps[] | CoordinatesProps[]; } // Properties that are needed for a Route creation @@ -31,8 +32,7 @@ export type Direction = { fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - points: Point[]; - spacetimePoints: SpacetimePoint[]; + points: SpacetimePoint[] | Point[]; }; export type Path = { diff --git a/src/modules/geography/core/domain/value-objects/coordinates.value-object.ts b/src/modules/geography/core/domain/value-objects/coordinates.value-object.ts new file mode 100644 index 0000000..9bd81e7 --- /dev/null +++ b/src/modules/geography/core/domain/value-objects/coordinates.value-object.ts @@ -0,0 +1,31 @@ +import { + ArgumentOutOfRangeException, + ValueObject, +} from '@mobicoop/ddd-library'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface CoordinatesProps { + lon: number; + lat: number; +} + +export class Coordinates extends ValueObject { + get lon(): number { + return this.props.lon; + } + + get lat(): number { + return this.props.lat; + } + + protected validate(props: CoordinatesProps): void { + if (props.lon > 180 || props.lon < -180) + throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); + if (props.lat > 90 || props.lat < -90) + throw new ArgumentOutOfRangeException('lat must be between -90 and 90'); + } +} diff --git a/src/modules/geography/geography.module.ts b/src/modules/geography/geography.module.ts index d974d9b..192f6df 100644 --- a/src/modules/geography/geography.module.ts +++ b/src/modules/geography/geography.module.ts @@ -3,7 +3,7 @@ import { CqrsModule } from '@nestjs/cqrs'; import { DIRECTION_ENCODER, PARAMS_PROVIDER } from './geography.di-tokens'; import { DefaultParamsProvider } from './infrastructure/default-params-provider'; import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder'; -import { GetRouteController } from './interface/controllers/get-route.controller'; +import { GetBasicRouteController } from './interface/controllers/get-basic-route.controller'; const adapters: Provider[] = [ { @@ -14,12 +14,12 @@ const adapters: Provider[] = [ provide: DIRECTION_ENCODER, useClass: PostgresDirectionEncoder, }, - GetRouteController, + GetBasicRouteController, ]; @Module({ imports: [CqrsModule], providers: [...adapters], - exports: [DIRECTION_ENCODER, GetRouteController], + exports: [DIRECTION_ENCODER, GetBasicRouteController], }) export class GeographyModule {} diff --git a/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts b/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts index b57502e..c6948df 100644 --- a/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts +++ b/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts @@ -1,9 +1,7 @@ -import { GeorouterSettings } from '@modules/geography/core/application/types/georouter-settings.type'; import { Waypoint } from '@modules/geography/core/application/types/waypoint.type'; import { Role } from '@modules/geography/core/domain/route.types'; -export class GetRouteRequestDto { +export type GetRouteRequestDto = { roles: Role[]; waypoints: Waypoint[]; - georouterSettings: GeorouterSettings; -} +}; diff --git a/src/modules/geography/interface/controllers/get-route.controller.ts b/src/modules/geography/interface/controllers/get-basic-route.controller.ts similarity index 63% rename from src/modules/geography/interface/controllers/get-route.controller.ts rename to src/modules/geography/interface/controllers/get-basic-route.controller.ts index 0f16766..3bdc42e 100644 --- a/src/modules/geography/interface/controllers/get-route.controller.ts +++ b/src/modules/geography/interface/controllers/get-basic-route.controller.ts @@ -4,10 +4,11 @@ import { GetRouteRequestDto } from './dtos/get-route.request.dto'; import { RouteEntity } from '@modules/geography/core/domain/route.entity'; import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query'; import { RouteMapper } from '@modules/geography/route.mapper'; -import { Injectable } from '@nestjs/common'; +import { Controller } from '@nestjs/common'; +import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; -@Injectable() -export class GetRouteController { +@Controller() +export class GetBasicRouteController implements GetBasicRouteControllerPort { constructor( private readonly queryBus: QueryBus, private readonly mapper: RouteMapper, @@ -15,7 +16,11 @@ export class GetRouteController { async get(data: GetRouteRequestDto): Promise { const route: RouteEntity = await this.queryBus.execute( - new GetRouteQuery(data.roles, data.waypoints, data.georouterSettings), + new GetRouteQuery(data.roles, data.waypoints, { + detailedDistance: false, + detailedDuration: false, + points: true, + }), ); return this.mapper.toResponse(route); } diff --git a/src/modules/geography/interface/dtos/route.response.dto.ts b/src/modules/geography/interface/dtos/route.response.dto.ts index 6829714..58d3c52 100644 --- a/src/modules/geography/interface/dtos/route.response.dto.ts +++ b/src/modules/geography/interface/dtos/route.response.dto.ts @@ -1,3 +1,4 @@ +import { Coordinates } from '@modules/geography/core/application/types/coordinates.type'; import { SpacetimePoint } from '@modules/geography/core/application/types/spacetime-point.type'; export class RouteResponseDto { @@ -8,5 +9,5 @@ export class RouteResponseDto { fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - spacetimePoints: SpacetimePoint[]; + points: SpacetimePoint[] | Coordinates[]; } diff --git a/src/modules/geography/route.mapper.ts b/src/modules/geography/route.mapper.ts index 0c8aaca..98fc10c 100644 --- a/src/modules/geography/route.mapper.ts +++ b/src/modules/geography/route.mapper.ts @@ -33,7 +33,7 @@ export class RouteMapper response.fwdAzimuth = entity.getProps().fwdAzimuth; response.backAzimuth = entity.getProps().backAzimuth; response.distanceAzimuth = entity.getProps().distanceAzimuth; - response.spacetimePoints = entity.getProps().spacetimePoints; + response.points = entity.getProps().points; return response; }; } diff --git a/src/modules/geography/tests/unit/core/coordinates.value-object.spec.ts b/src/modules/geography/tests/unit/core/coordinates.value-object.spec.ts new file mode 100644 index 0000000..da2cf7e --- /dev/null +++ b/src/modules/geography/tests/unit/core/coordinates.value-object.spec.ts @@ -0,0 +1,49 @@ +import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library'; +import { Coordinates } from '@modules/geography/core/domain/value-objects/coordinates.value-object'; + +describe('Waypoint value object', () => { + it('should create a waypoint value object', () => { + const coordinatesVO = new Coordinates({ + lon: 48.689445, + lat: 6.17651, + }); + expect(coordinatesVO.lon).toBe(48.689445); + expect(coordinatesVO.lat).toBe(6.17651); + }); + it('should throw an exception if longitude is invalid', () => { + try { + new Coordinates({ + lon: 348.689445, + lat: 6.17651, + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + try { + new Coordinates({ + lon: -348.689445, + lat: 6.17651, + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + }); + it('should throw an exception if latitude is invalid', () => { + try { + new Coordinates({ + lon: 48.689445, + lat: 96.17651, + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + try { + new Coordinates({ + lon: 48.689445, + lat: -96.17651, + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + }); +}); diff --git a/src/modules/geography/tests/unit/core/waypoint.value-object.spec.ts b/src/modules/geography/tests/unit/core/waypoint.value-object.spec.ts index e69c1d2..8c44dd5 100644 --- a/src/modules/geography/tests/unit/core/waypoint.value-object.spec.ts +++ b/src/modules/geography/tests/unit/core/waypoint.value-object.spec.ts @@ -46,7 +46,7 @@ describe('Waypoint value object', () => { expect(e).toBeInstanceOf(ArgumentOutOfRangeException); } }); - it('should throw an exception if longitude is invalid', () => { + it('should throw an exception if latitude is invalid', () => { try { new Waypoint({ position: 0, diff --git a/src/modules/geography/tests/unit/interface/get-route.controller.spec.ts b/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts similarity index 70% rename from src/modules/geography/tests/unit/interface/get-route.controller.spec.ts rename to src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts index 6b0ed30..2aeaf75 100644 --- a/src/modules/geography/tests/unit/interface/get-route.controller.spec.ts +++ b/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts @@ -1,5 +1,5 @@ import { Role } from '@modules/geography/core/domain/route.types'; -import { GetRouteController } from '@modules/geography/interface/controllers/get-route.controller'; +import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller'; import { RouteMapper } from '@modules/geography/route.mapper'; import { QueryBus } from '@nestjs/cqrs'; import { Test, TestingModule } from '@nestjs/testing'; @@ -14,8 +14,8 @@ const mockRouteMapper = { toResponse: jest.fn(), }; -describe('Get Route Controller', () => { - let getRouteController: GetRouteController; +describe('Get Basic Route Controller', () => { + let getBasicRouteController: GetBasicRouteController; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -28,11 +28,13 @@ describe('Get Route Controller', () => { provide: RouteMapper, useValue: mockRouteMapper, }, - GetRouteController, + GetBasicRouteController, ], }).compile(); - getRouteController = module.get(GetRouteController); + getBasicRouteController = module.get( + GetBasicRouteController, + ); }); afterEach(async () => { @@ -40,12 +42,12 @@ describe('Get Route Controller', () => { }); it('should be defined', () => { - expect(getRouteController).toBeDefined(); + expect(getBasicRouteController).toBeDefined(); }); it('should get a route', async () => { jest.spyOn(mockQueryBus, 'execute'); - await getRouteController.get({ + await getBasicRouteController.get({ roles: [Role.DRIVER], waypoints: [ { @@ -59,11 +61,6 @@ describe('Get Route Controller', () => { lat: 2.3522, }, ], - georouterSettings: { - points: true, - detailedDistance: false, - detailedDuration: false, - }, }); expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); }); diff --git a/src/modules/geography/tests/unit/route.mapper.spec.ts b/src/modules/geography/tests/unit/route.mapper.spec.ts index 8c27c2d..0846a7b 100644 --- a/src/modules/geography/tests/unit/route.mapper.spec.ts +++ b/src/modules/geography/tests/unit/route.mapper.spec.ts @@ -38,7 +38,7 @@ describe('Route Mapper', () => { fwdAzimuth: 283, backAzimuth: 93, distanceAzimuth: 19840, - spacetimePoints: [], + points: [], waypoints: [ { position: 0,