From 34ad357f47360868feb135c6017461ae9adc31e9 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Fri, 3 May 2024 14:45:27 +0200 Subject: [PATCH] Refactor AdEntity creation in a factory in the domain layer --- .../commands/create-ad/create-ad.command.ts | 6 +- .../commands/create-ad/create-ad.service.ts | 131 ++---------------- .../match/completer/route.completer.ts | 8 +- .../application/queries/match/match.query.ts | 6 +- src/modules/ad/core/domain/ad.factory.ts | 94 +++++++++++++ src/modules/ad/core/domain/ad.types.ts | 17 +++ .../georouter.service.ts} | 7 +- src/modules/ad/infrastructure/georouter.ts | 16 +-- .../grpc-controllers/match.grpc-controller.ts | 4 +- .../tests/unit/core/create-ad.service.spec.ts | 15 +- .../ad/tests/unit/core/match.query.spec.ts | 4 +- .../tests/unit/core/route.completer.spec.ts | 8 +- src/modules/ad/tests/unit/georouter.mock.ts | 6 +- 13 files changed, 164 insertions(+), 158 deletions(-) create mode 100644 src/modules/ad/core/domain/ad.factory.ts rename src/modules/ad/core/{application/ports/georouter.port.ts => domain/georouter.service.ts} (72%) diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts index a3a6d9a..a2f4922 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts @@ -1,9 +1,9 @@ -import { Frequency } from '@modules/ad/core/domain/ad.types'; import { Command, CommandProps } from '@mobicoop/ddd-library'; -import { ScheduleItem } from '../../types/schedule-item.type'; +import { Frequency, UserAd } from '@modules/ad/core/domain/ad.types'; import { Address } from '../../types/address.type'; +import { ScheduleItem } from '../../types/schedule-item.type'; -export class CreateAdCommand extends Command { +export class CreateAdCommand extends Command implements UserAd { readonly id: string; readonly driver: boolean; readonly passenger: boolean; 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 4158100..1f5b78f 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,32 +1,22 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { CreateAdCommand } from './create-ad.command'; -import { Inject } from '@nestjs/common'; -import { - AD_MESSAGE_PUBLISHER, - 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, MessagePublisherPort, } from '@mobicoop/ddd-library'; -import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; -import { Role } from '@modules/ad/core/domain/ad.types'; import { - Path, - PathCreator, - PathType, - TypedRoute, -} from '@modules/ad/core/domain/path-creator.service'; -import { Waypoint } from '../../types/waypoint.type'; -import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects/point.value-object'; -import { Point } from '@modules/geography/core/domain/route.types'; -import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event'; + AD_MESSAGE_PUBLISHER, + AD_REPOSITORY, + AD_ROUTE_PROVIDER, +} from '@modules/ad/ad.di-tokens'; +import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; +import { AdFactory } from '@modules/ad/core/domain/ad.factory'; +import { Inject } from '@nestjs/common'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants'; -import { GeorouterPort } from '../../ports/georouter.port'; +import { GeorouterService } from '../../../domain/georouter.service'; +import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; +import { CreateAdCommand } from './create-ad.command'; @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { @@ -36,104 +26,13 @@ export class CreateAdService implements ICommandHandler { @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, @Inject(AD_ROUTE_PROVIDER) - private readonly routeProvider: GeorouterPort, + private readonly routeProvider: GeorouterService, ) {} async execute(command: CreateAdCommand): Promise { - const roles: Role[] = []; - if (command.driver) roles.push(Role.DRIVER); - if (command.passenger) roles.push(Role.PASSENGER); - - const pathCreator: PathCreator = new PathCreator( - roles, - command.waypoints.map( - (waypoint: Waypoint) => - new PointValueObject({ - lon: waypoint.lon, - lat: waypoint.lat, - }), - ), - ); - - let typedRoutes: TypedRoute[]; - let driverDistance: number | undefined; - let driverDuration: number | undefined; - let passengerDistance: number | undefined; - let passengerDuration: number | undefined; - let points: PointValueObject[] | undefined; - let fwdAzimuth: number | undefined; - let backAzimuth: number | undefined; - try { - try { - typedRoutes = await Promise.all( - pathCreator.getBasePaths().map(async (path: Path) => ({ - type: path.type, - route: await this.routeProvider.getRoute({ - waypoints: path.waypoints, - }), - })), - ); - } catch (e: any) { - throw new Error('Unable to find a route for given waypoints'); - } - - try { - typedRoutes.forEach((typedRoute: TypedRoute) => { - if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) { - driverDistance = typedRoute.route.distance; - driverDuration = typedRoute.route.duration; - points = typedRoute.route.points.map( - (point: Point) => - new PointValueObject({ - lon: point.lon, - lat: point.lat, - }), - ); - fwdAzimuth = typedRoute.route.fwdAzimuth; - backAzimuth = typedRoute.route.backAzimuth; - } - if ( - [PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type) - ) { - passengerDistance = typedRoute.route.distance; - passengerDuration = typedRoute.route.duration; - if (!points) - points = typedRoute.route.points.map( - (point: Point) => - new PointValueObject({ - lon: point.lon, - lat: point.lat, - }), - ); - if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth; - if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth; - } - }); - } catch (error: any) { - throw new Error('Invalid route'); - } - - const ad = AdEntity.create({ - id: command.id, - driver: command.driver, - passenger: command.passenger, - frequency: command.frequency, - fromDate: command.fromDate, - toDate: command.toDate, - schedule: command.schedule, - seatsProposed: command.seatsProposed, - seatsRequested: command.seatsRequested, - strict: command.strict, - waypoints: command.waypoints, - points: points as PointValueObject[], - driverDistance, - driverDuration, - passengerDistance, - passengerDuration, - fwdAzimuth: fwdAzimuth as number, - backAzimuth: backAzimuth as number, - }); + const adFactory = new AdFactory(this.routeProvider); + const ad = await adFactory.create(command); try { await this.repository.insertExtra(ad, 'ad'); diff --git a/src/modules/ad/core/application/queries/match/completer/route.completer.ts b/src/modules/ad/core/application/queries/match/completer/route.completer.ts index aca1a73..672d967 100644 --- a/src/modules/ad/core/application/queries/match/completer/route.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/route.completer.ts @@ -1,9 +1,9 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; -import { Completer } from './completer.abstract'; -import { MatchQuery } from '../match.query'; -import { Step } from '../../../types/step.type'; import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object'; -import { RouteResponse } from '../../../ports/georouter.port'; +import { RouteResponse } from '../../../../domain/georouter.service'; +import { Step } from '../../../types/step.type'; +import { MatchQuery } from '../match.query'; +import { Completer } from './completer.abstract'; export class RouteCompleter extends Completer { protected readonly type: RouteCompleterType; diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 8e41752..ed1341a 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -8,8 +8,8 @@ import { } from '@modules/ad/core/domain/path-creator.service'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; +import { GeorouterService } from '../../../domain/georouter.service'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; -import { GeorouterPort } from '../../ports/georouter.port'; import { AlgorithmType } from '../../types/algorithm.types'; import { Route } from '../../types/route.type'; import { Waypoint } from '../../types/waypoint.type'; @@ -41,10 +41,10 @@ export class MatchQuery extends QueryBase { passengerRoute?: Route; backAzimuth?: number; private readonly originWaypoint: Waypoint; - routeProvider: GeorouterPort; + routeProvider: GeorouterService; // TODO: remove MatchRequestDto depency (here core domain depends on interface /!\) - constructor(props: MatchRequestDto, routeProvider: GeorouterPort) { + constructor(props: MatchRequestDto, routeProvider: GeorouterService) { super(); this.id = props.id; this.driver = props.driver; diff --git a/src/modules/ad/core/domain/ad.factory.ts b/src/modules/ad/core/domain/ad.factory.ts new file mode 100644 index 0000000..076b443 --- /dev/null +++ b/src/modules/ad/core/domain/ad.factory.ts @@ -0,0 +1,94 @@ +import { AdEntity } from './ad.entity'; +import { Role, UserAd } from './ad.types'; +import { GeorouterService } from './georouter.service'; +import { + Path, + PathCreator, + PathType, + TypedRoute, +} from './path-creator.service'; +import { Point } from './value-objects/point.value-object'; + +export class AdFactory { + constructor(private readonly routeProvider: GeorouterService) {} + /** + * Create an AdEntity (a "matcher ad", that is: the data needed to match an ad with a match query) + * from a "user ad" (the data provided by the user). + */ + public async create(ad: UserAd): Promise { + const roles: Role[] = []; + if (ad.driver) roles.push(Role.DRIVER); + if (ad.passenger) roles.push(Role.PASSENGER); + + const pathCreator = new PathCreator( + roles, + ad.waypoints.map((wp) => new Point({ lon: wp.lon, lat: wp.lat })), + ); + + let typedRoutes: TypedRoute[]; + try { + typedRoutes = await Promise.all( + pathCreator.getBasePaths().map(async (path: Path) => ({ + type: path.type, + route: await this.routeProvider.getRoute({ + waypoints: path.waypoints, + }), + })), + ); + } catch (e: any) { + throw new Error('Unable to find a route for given waypoints'); + } + + let driverDistance: number | undefined; + let driverDuration: number | undefined; + let passengerDistance: number | undefined; + let passengerDuration: number | undefined; + let points: Point[]; + let fwdAzimuth: number; + let backAzimuth: number; + try { + typedRoutes.forEach((typedRoute: TypedRoute) => { + if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) { + driverDistance = typedRoute.route.distance; + driverDuration = typedRoute.route.duration; + points = typedRoute.route.points.map((point) => new Point(point)); + fwdAzimuth = typedRoute.route.fwdAzimuth; + backAzimuth = typedRoute.route.backAzimuth; + } + + if ([PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)) { + passengerDistance = typedRoute.route.distance; + passengerDuration = typedRoute.route.duration; + if (!points) { + points = typedRoute.route.points.map((point) => new Point(point)); + } + if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth; + if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth; + } + }); + } catch (error: any) { + throw new Error('Invalid route'); + } + + return AdEntity.create({ + id: ad.id, + driver: ad.driver, + passenger: ad.passenger, + frequency: ad.frequency, + fromDate: ad.fromDate, + toDate: ad.toDate, + schedule: ad.schedule, + seatsProposed: ad.seatsProposed, + seatsRequested: ad.seatsRequested, + strict: ad.strict, + waypoints: ad.waypoints, + points: points!, + driverDistance, + driverDuration, + passengerDistance, + passengerDuration, + fwdAzimuth: fwdAzimuth!, + backAzimuth: backAzimuth!, + }); + } +} diff --git a/src/modules/ad/core/domain/ad.types.ts b/src/modules/ad/core/domain/ad.types.ts index 10f906b..ee68974 100644 --- a/src/modules/ad/core/domain/ad.types.ts +++ b/src/modules/ad/core/domain/ad.types.ts @@ -1,6 +1,23 @@ import { PointProps } from './value-objects/point.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; +/** + * The data provided by the end-user to publish an ad + */ +export interface UserAd { + id: string; + driver: boolean; + passenger: boolean; + frequency: Frequency; + fromDate: string; + toDate: string; + schedule: ScheduleItemProps[]; + seatsProposed: number; + seatsRequested: number; + strict: boolean; + waypoints: PointProps[]; +} + // All properties that an Ad has export interface AdProps { driver: boolean; diff --git a/src/modules/ad/core/application/ports/georouter.port.ts b/src/modules/ad/core/domain/georouter.service.ts similarity index 72% rename from src/modules/ad/core/application/ports/georouter.port.ts rename to src/modules/ad/core/domain/georouter.service.ts index 752e843..ac0c4d4 100644 --- a/src/modules/ad/core/application/ports/georouter.port.ts +++ b/src/modules/ad/core/domain/georouter.service.ts @@ -1,5 +1,3 @@ -import { Injectable } from '@nestjs/common'; - export type Point = { lon: number; lat: number; @@ -25,7 +23,6 @@ export type RouteResponse = { steps?: Step[]; }; -@Injectable() -export abstract class GeorouterPort { - abstract getRoute(request: RouteRequest): Promise; +export interface GeorouterService { + getRoute(request: RouteRequest): Promise; } diff --git a/src/modules/ad/infrastructure/georouter.ts b/src/modules/ad/infrastructure/georouter.ts index 1280791..feddaeb 100644 --- a/src/modules/ad/infrastructure/georouter.ts +++ b/src/modules/ad/infrastructure/georouter.ts @@ -1,26 +1,26 @@ -import { Observable, lastValueFrom } from 'rxjs'; import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { ClientGrpc } from '@nestjs/microservices'; import { GRPC_GEOROUTER_SERVICE_NAME } from '@src/app.constants'; +import { Observable, lastValueFrom } from 'rxjs'; +import { GEOGRAPHY_PACKAGE } from '../ad.di-tokens'; import { - GeorouterPort, + GeorouterService, RouteRequest, RouteResponse, -} from '../core/application/ports/georouter.port'; -import { GEOGRAPHY_PACKAGE } from '../ad.di-tokens'; +} from '../core/domain/georouter.service'; -interface GeorouterService { +interface GeorouterPort { getRoute(request: RouteRequest): Observable; } @Injectable() -export class Georouter implements GeorouterPort, OnModuleInit { - private georouterService: GeorouterService; +export class Georouter implements GeorouterService, OnModuleInit { + private georouterService: GeorouterPort; constructor(@Inject(GEOGRAPHY_PACKAGE) private readonly client: ClientGrpc) {} onModuleInit() { - this.georouterService = this.client.getService( + this.georouterService = this.client.getService( GRPC_GEOROUTER_SERVICE_NAME, ); } diff --git a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index 0801443..4774cd7 100644 --- a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -1,8 +1,8 @@ import { RpcValidationPipe } from '@mobicoop/ddd-library'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; -import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler'; +import { GeorouterService } from '@modules/ad/core/domain/georouter.service'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchMapper } from '@modules/ad/match.mapper'; import { Controller, Inject, UseFilters, UsePipes } from '@nestjs/common'; @@ -24,7 +24,7 @@ export class MatchGrpcController { constructor( private readonly queryBus: QueryBus, @Inject(AD_ROUTE_PROVIDER) - private readonly routeProvider: GeorouterPort, + private readonly routeProvider: GeorouterService, private readonly matchMapper: MatchMapper, ) {} 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 626dc71..fb7ac33 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,18 +1,17 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; import { AD_MESSAGE_PUBLISHER, 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'; -import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; -import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service'; import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command'; +import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; +import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; +import { GeorouterService } from '@modules/ad/core/domain/georouter.service'; import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object'; -import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port'; +import { Test, TestingModule } from '@nestjs/testing'; const originWaypoint: PointProps = { lat: 48.689445, @@ -62,7 +61,7 @@ const mockAdRepository = { }), }; -const mockRouteProvider: GeorouterPort = { +const mockRouteProvider: GeorouterService = { getRoute: jest .fn() .mockImplementationOnce(() => { diff --git a/src/modules/ad/tests/unit/core/match.query.spec.ts b/src/modules/ad/tests/unit/core/match.query.spec.ts index 4379f60..c0ea5de 100644 --- a/src/modules/ad/tests/unit/core/match.query.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query.spec.ts @@ -1,5 +1,4 @@ import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; -import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port'; import { MatchQuery, ScheduleItem, @@ -7,6 +6,7 @@ import { import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { GeorouterService } from '@modules/ad/core/domain/georouter.service'; import { simpleMockGeorouter } from '../georouter.mock'; const originWaypoint: Waypoint = { @@ -61,7 +61,7 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = { time: jest.fn().mockImplementation(() => '23:05'), }; -const mockRouteProvider: GeorouterPort = { +const mockRouteProvider: GeorouterService = { getRoute: jest .fn() .mockImplementationOnce(simpleMockGeorouter.getRoute) diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts index 3e536f9..85dcc62 100644 --- a/src/modules/ad/tests/unit/core/route.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -1,7 +1,3 @@ -import { - RouteRequest, - RouteResponse, -} from '@modules/ad/core/application/ports/georouter.port'; import { RouteCompleter, RouteCompleterType, @@ -12,6 +8,10 @@ import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Target } from '@modules/ad/core/domain/candidate.types'; +import { + RouteRequest, + RouteResponse, +} from '@modules/ad/core/domain/georouter.service'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Step } from '@modules/geography/core/domain/route.types'; import { simpleMockGeorouter } from '../georouter.mock'; diff --git a/src/modules/ad/tests/unit/georouter.mock.ts b/src/modules/ad/tests/unit/georouter.mock.ts index 75332c9..269e79c 100644 --- a/src/modules/ad/tests/unit/georouter.mock.ts +++ b/src/modules/ad/tests/unit/georouter.mock.ts @@ -1,10 +1,10 @@ -import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port'; +import { GeorouterService } from '@modules/ad/core/domain/georouter.service'; -export const bareMockGeorouter: GeorouterPort = { +export const bareMockGeorouter: GeorouterService = { getRoute: jest.fn(), }; -export const simpleMockGeorouter: GeorouterPort = { +export const simpleMockGeorouter: GeorouterService = { getRoute: jest.fn().mockImplementation(() => ({ distance: 350101, duration: 14422,