From 2d9409d147be581c2a4852f654abbea5d2523c30 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Thu, 16 May 2024 17:16:21 +0200 Subject: [PATCH 1/4] Upgrade ddd-library --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 093b05f..43189b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@grpc/grpc-js": "^1.9.14", "@grpc/proto-loader": "^0.7.10", "@mobicoop/configuration-module": "^8.0.0", - "@mobicoop/ddd-library": "^2.4.3", + "@mobicoop/ddd-library": "^2.5.0", "@mobicoop/health-module": "^2.3.2", "@mobicoop/message-broker-module": "^2.1.2", "@nestjs/axios": "^3.0.1", @@ -1881,9 +1881,9 @@ } }, "node_modules/@mobicoop/ddd-library": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-2.4.3.tgz", - "integrity": "sha512-HxNtAfov8ne7XsFTSIDI811r3L1VDV9YUikgX7HPjrB8u2gQh6FQFnIz3Fjb/zWOGxrDEIy8HEM0AYmXkf8ULA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-2.5.0.tgz", + "integrity": "sha512-dTx7KTILs53HCqNx0BDVTzIZxfPW3pi0fZ4UMw/vDNm3oTqGA+jg7YBfNxn8yadM+j2dDIN5Kum43CmKGH8yYA==", "dependencies": { "@nestjs/event-emitter": "^2.0.3", "@nestjs/microservices": "^10.3.0", diff --git a/package.json b/package.json index 84075e6..6344905 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@grpc/proto-loader": "^0.7.10", "@songkeys/nestjs-redis": "^10.0.0", "@mobicoop/configuration-module": "^8.0.0", - "@mobicoop/ddd-library": "^2.4.3", + "@mobicoop/ddd-library": "^2.5.0", "@mobicoop/health-module": "^2.3.2", "@mobicoop/message-broker-module": "^2.1.2", "@nestjs/axios": "^3.0.1", From 34ad357f47360868feb135c6017461ae9adc31e9 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Fri, 3 May 2024 14:45:27 +0200 Subject: [PATCH 2/4] 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, From 3be2d73c601c1f00efa57a0109a5eb15680713d8 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Tue, 7 May 2024 10:13:28 +0200 Subject: [PATCH 3/4] Implement the UpdateAdCommand --- package.json | 2 +- src/app.constants.ts | 2 + src/modules/ad/ad.mapper.ts | 52 ++++++---- src/modules/ad/ad.module.ts | 7 +- .../commands/create-ad/create-ad.service.ts | 3 +- .../commands/update-ad/update-ad.command.ts | 3 + .../commands/update-ad/update-ad.service.ts | 48 ++++++++++ ...er-ad-creation-failed.integration-event.ts | 12 --- .../matcher-ad-failure.integration-event.ts | 15 +++ .../ad/infrastructure/ad.repository.ts | 47 +++++++--- .../tests/unit/core/update-ad.service.spec.ts | 94 +++++++++++++++++++ 11 files changed, 237 insertions(+), 48 deletions(-) create mode 100644 src/modules/ad/core/application/commands/update-ad/update-ad.command.ts create mode 100644 src/modules/ad/core/application/commands/update-ad/update-ad.service.ts delete mode 100644 src/modules/ad/core/application/events/matcher-ad-creation-failed.integration-event.ts create mode 100644 src/modules/ad/core/application/events/matcher-ad-failure.integration-event.ts create mode 100644 src/modules/ad/tests/unit/core/update-ad.service.spec.ts diff --git a/package.json b/package.json index 6344905..ac22430 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage", "test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand", "test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand", - "test:cov": "jest --testPathPattern 'tests/unit/' --coverage", + "test:watch": "jest --testPathPattern 'tests/unit/' --watch", "test:e2e": "jest --config ./test/jest-e2e.json", "migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate deploy'", "migrate:dev": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'", diff --git a/src/app.constants.ts b/src/app.constants.ts index c13df6c..4980881 100644 --- a/src/app.constants.ts +++ b/src/app.constants.ts @@ -10,6 +10,8 @@ export const GRPC_GEOROUTER_SERVICE_NAME = 'GeorouterService'; export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created'; export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY = 'matcher-ad.creation-failed'; +export const MATCHER_AD_UPDATED_ROUTING_KEY = 'matcher-ad.updated'; +export const MATCHER_AD_UPDATE_FAILED_ROUTING_KEY = 'matcher-ad.update-failed'; // messaging input export const AD_CREATED_MESSAGE_HANDLER = 'adCreated'; diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 929f72e..373ec5d 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -13,6 +13,7 @@ import { AdWriteExtraModel, AdWriteModel, ScheduleItemModel, + ScheduleWriteModel, } from './infrastructure/ad.repository'; /** @@ -38,9 +39,8 @@ export class AdMapper private readonly directionEncoder: DirectionEncoderPort, ) {} - toPersistence = (entity: AdEntity): AdWriteModel => { + toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => { const copy = entity.getProps(); - const now = new Date(); const record: AdWriteModel = { uuid: copy.id, driver: copy.driver, @@ -48,22 +48,7 @@ export class AdMapper frequency: copy.frequency, fromDate: new Date(copy.fromDate), toDate: new Date(copy.toDate), - schedule: { - create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({ - uuid: v4(), - day: scheduleItem.day, - time: new Date( - 1970, - 0, - 1, - parseInt(scheduleItem.time.split(':')[0]), - parseInt(scheduleItem.time.split(':')[1]), - ), - margin: scheduleItem.margin, - createdAt: now, - updatedAt: now, - })), - }, + schedule: this.toScheduleItemWriteModel(copy.schedule, update), seatsProposed: copy.seatsProposed, seatsRequested: copy.seatsRequested, strict: copy.strict, @@ -73,12 +58,39 @@ export class AdMapper passengerDistance: copy.passengerDistance, fwdAzimuth: copy.fwdAzimuth, backAzimuth: copy.backAzimuth, - createdAt: copy.createdAt, - updatedAt: copy.updatedAt, }; return record; }; + toScheduleItemWriteModel = ( + schedule: ScheduleItemProps[], + update?: boolean, + ): ScheduleWriteModel => { + const now = new Date(); + const record: ScheduleWriteModel = { + create: schedule.map((scheduleItem: ScheduleItemProps) => ({ + uuid: v4(), + day: scheduleItem.day, + time: new Date( + 1970, + 0, + 1, + parseInt(scheduleItem.time.split(':')[0]), + parseInt(scheduleItem.time.split(':')[1]), + ), + margin: scheduleItem.margin, + createdAt: now, + updatedAt: now, + })), + }; + if (update) { + record.deleteMany = { + createdAt: { lt: now }, + }; + } + return record; + }; + toDomain = (record: AdReadModel): AdEntity => new AdEntity({ id: record.uuid, diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 9040de9..adb71c4 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -31,6 +31,7 @@ import { import { AdMapper } from './ad.mapper'; import { CreateAdService } from './core/application/commands/create-ad/create-ad.service'; import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service'; +import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service'; import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler'; import { MatchQueryHandler } from './core/application/queries/match/match.query-handler'; import { AdRepository } from './infrastructure/ad.repository'; @@ -104,7 +105,11 @@ const eventHandlers: Provider[] = [ PublishMessageWhenMatcherAdIsCreatedDomainEventHandler, ]; -const commandHandlers: Provider[] = [CreateAdService, DeleteAdService]; +const commandHandlers: Provider[] = [ + CreateAdService, + UpdateAdService, + DeleteAdService, +]; const queryHandlers: Provider[] = [MatchQueryHandler]; 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 1f5b78f..e3d92c5 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 @@ -14,7 +14,7 @@ import { Inject } from '@nestjs/common'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants'; import { GeorouterService } from '../../../domain/georouter.service'; -import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event'; +import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-failure.integration-event'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { CreateAdCommand } from './create-ad.command'; @@ -35,6 +35,7 @@ export class CreateAdService implements ICommandHandler { const ad = await adFactory.create(command); try { + //TODO it should not be this service's concern that Prisma does not support postgis types await this.repository.insertExtra(ad, 'ad'); return ad.id; } catch (error: any) { diff --git a/src/modules/ad/core/application/commands/update-ad/update-ad.command.ts b/src/modules/ad/core/application/commands/update-ad/update-ad.command.ts new file mode 100644 index 0000000..898c968 --- /dev/null +++ b/src/modules/ad/core/application/commands/update-ad/update-ad.command.ts @@ -0,0 +1,3 @@ +import { CreateAdCommand } from '../create-ad/create-ad.command'; + +export class UpdateAdCommand extends CreateAdCommand {} diff --git a/src/modules/ad/core/application/commands/update-ad/update-ad.service.ts b/src/modules/ad/core/application/commands/update-ad/update-ad.service.ts new file mode 100644 index 0000000..99aafb9 --- /dev/null +++ b/src/modules/ad/core/application/commands/update-ad/update-ad.service.ts @@ -0,0 +1,48 @@ +import { MessagePublisherPort } from '@mobicoop/ddd-library'; +import { + AD_MESSAGE_PUBLISHER, + AD_REPOSITORY, + AD_ROUTE_PROVIDER, +} from '@modules/ad/ad.di-tokens'; +import { AdFactory } from '@modules/ad/core/domain/ad.factory'; +import { Inject } from '@nestjs/common'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { MATCHER_AD_UPDATE_FAILED_ROUTING_KEY } from '@src/app.constants'; +import { GeorouterService } from '../../../domain/georouter.service'; +import { MatcherAdUpdateFailedIntegrationEvent } from '../../events/matcher-ad-failure.integration-event'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; +import { UpdateAdCommand } from './update-ad.command'; + +@CommandHandler(UpdateAdCommand) +export class UpdateAdService implements ICommandHandler { + constructor( + @Inject(AD_MESSAGE_PUBLISHER) + private readonly messagePublisher: MessagePublisherPort, + @Inject(AD_REPOSITORY) + private readonly repository: AdRepositoryPort, + @Inject(AD_ROUTE_PROVIDER) + private readonly routeProvider: GeorouterService, + ) {} + + async execute(command: UpdateAdCommand): Promise { + try { + const adFactory = new AdFactory(this.routeProvider); + const ad = await adFactory.create(command); + return this.repository.update(ad.id, ad); + } catch (error: any) { + const integrationEvent = new MatcherAdUpdateFailedIntegrationEvent({ + id: command.id, + metadata: { + correlationId: command.id, + timestamp: Date.now(), + }, + cause: error.message, + }); + this.messagePublisher.publish( + MATCHER_AD_UPDATE_FAILED_ROUTING_KEY, + JSON.stringify(integrationEvent), + ); + throw error; + } + } +} diff --git a/src/modules/ad/core/application/events/matcher-ad-creation-failed.integration-event.ts b/src/modules/ad/core/application/events/matcher-ad-creation-failed.integration-event.ts deleted file mode 100644 index c932c6f..0000000 --- a/src/modules/ad/core/application/events/matcher-ad-creation-failed.integration-event.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library'; - -export class MatcherAdCreationFailedIntegrationEvent extends IntegrationEvent { - readonly cause?: string; - - constructor( - props: IntegrationEventProps, - ) { - super(props); - this.cause = props.cause; - } -} diff --git a/src/modules/ad/core/application/events/matcher-ad-failure.integration-event.ts b/src/modules/ad/core/application/events/matcher-ad-failure.integration-event.ts new file mode 100644 index 0000000..ff177c3 --- /dev/null +++ b/src/modules/ad/core/application/events/matcher-ad-failure.integration-event.ts @@ -0,0 +1,15 @@ +import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library'; + +export class MatcherAdFailureIntegrationEvent extends IntegrationEvent { + readonly cause?: string; + + constructor( + props: IntegrationEventProps, + ) { + super(props); + this.cause = props.cause; + } +} + +export class MatcherAdCreationFailedIntegrationEvent extends MatcherAdFailureIntegrationEvent {} +export class MatcherAdUpdateFailedIntegrationEvent extends MatcherAdFailureIntegrationEvent {} diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index eda2c3c..42c2957 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -1,14 +1,14 @@ +import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library'; +import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { AdRepositoryPort } from '../core/application/ports/ad.repository.port'; -import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library'; -import { PrismaService } from './prisma.service'; -import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; -import { AdEntity } from '../core/domain/ad.entity'; -import { AdMapper } from '../ad.mapper'; -import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; -import { Frequency } from '../core/domain/ad.types'; import { SERVICE_NAME } from '@src/app.constants'; +import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; +import { AdMapper } from '../ad.mapper'; +import { AdRepositoryPort } from '../core/application/ports/ad.repository.port'; +import { AdEntity } from '../core/domain/ad.entity'; +import { Frequency } from '../core/domain/ad.types'; +import { PrismaService } from './prisma.service'; export type AdModel = { uuid: string; @@ -26,8 +26,6 @@ export type AdModel = { passengerDistance?: number; fwdAzimuth: number; backAzimuth: number; - createdAt: Date; - updatedAt: Date; }; /** @@ -36,15 +34,26 @@ export type AdModel = { export type AdReadModel = AdModel & { waypoints: string; schedule: ScheduleItemModel[]; + createdAt: Date; + updatedAt: Date; }; /** * The record ready to be sent to the persistence system */ export type AdWriteModel = AdModel & { - schedule: { - create: ScheduleItemModel[]; - }; + schedule: ScheduleWriteModel; +}; + +export type ScheduleWriteModel = { + deleteMany?: PastCreatedFilter; + create: ScheduleItemModel[]; +}; + +// used to delete records created in the past, +// because the order of `create` and `deleteMany` is not guaranteed +export type PastCreatedFilter = { + createdAt: { lt: Date }; }; export type AdWriteExtraModel = { @@ -70,11 +79,15 @@ export type UngroupedAdModel = AdModel & scheduleItemCreatedAt: Date; scheduleItemUpdatedAt: Date; waypoints: string; + createdAt: Date; + updatedAt: Date; }; export type GroupedAdModel = AdModel & { schedule: ScheduleItemModel[]; waypoints: string; + createdAt: Date; + updatedAt: Date; }; /** @@ -169,4 +182,12 @@ export class AdRepository }); return adReadModels; }; + + async update( + id: string, + entity: AdEntity, + identifier?: string, + ): Promise { + this.updateExtra(id, entity, 'ad', identifier); + } } diff --git a/src/modules/ad/tests/unit/core/update-ad.service.spec.ts b/src/modules/ad/tests/unit/core/update-ad.service.spec.ts new file mode 100644 index 0000000..c5f347a --- /dev/null +++ b/src/modules/ad/tests/unit/core/update-ad.service.spec.ts @@ -0,0 +1,94 @@ +import { + AD_MESSAGE_PUBLISHER, + AD_REPOSITORY, + AD_ROUTE_PROVIDER, +} from '@modules/ad/ad.di-tokens'; +import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command'; +import { UpdateAdService } from '@modules/ad/core/application/commands/update-ad/update-ad.service'; +import { GeorouterService } from '@modules/ad/core/domain/georouter.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createAdProps } from './ad.fixtures'; + +const mockAdRepository = { + update: jest.fn().mockImplementation((id) => { + if (id === '42') { + throw 'Bad id!'; + } + }), +}; + +const mockRouteProvider: GeorouterService = { + getRoute: jest.fn().mockImplementation(() => ({ + distance: 350101, + duration: 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, + }, + ], + })), +}; + +const mockMessagePublisher = { + publish: jest.fn().mockImplementation(), +}; + +describe('update-ad.service', () => { + let updateAdService: UpdateAdService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + { + provide: AD_ROUTE_PROVIDER, + useValue: mockRouteProvider, + }, + { + provide: AD_MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, + }, + UpdateAdService, + ], + }).compile(); + + updateAdService = module.get(UpdateAdService); + }); + + it('should be defined', () => { + expect(updateAdService).toBeDefined(); + }); + + describe('execute', () => { + it('should call the repository update method', async () => { + const updateAdCommand = new UpdateAdCommand(createAdProps()); + await updateAdService.execute(updateAdCommand); + expect(mockAdRepository.update).toHaveBeenCalled(); + }); + + it('should emit an event when an error occurs', async () => { + const commandProps = createAdProps(); + commandProps.id = '42'; + const updateAdCommand = new UpdateAdCommand(commandProps); + await expect(updateAdService.execute(updateAdCommand)).rejects.toBe( + 'Bad id!', + ); + expect(mockMessagePublisher.publish).toHaveBeenCalled(); + }); + }); +}); From cc9b45c6a1f6e24d518a3c7f906896f13ce5e6b7 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Fri, 3 May 2024 12:46:11 +0200 Subject: [PATCH 4/4] Handle ad.updated integration events --- src/app.constants.ts | 3 ++ src/modules/ad/ad.module.ts | 7 ++- .../ad-updated.message-handler.ts | 28 ++++++++++++ .../ad-updated.message-handler.spec.ts | 43 +++++++++++++++++++ src/modules/messager/messager.module.ts | 7 +++ 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/modules/ad/interface/message-handlers/ad-updated.message-handler.ts create mode 100644 src/modules/ad/tests/unit/interface/ad-updated.message-handler.spec.ts diff --git a/src/app.constants.ts b/src/app.constants.ts index 4980881..ac3626d 100644 --- a/src/app.constants.ts +++ b/src/app.constants.ts @@ -17,6 +17,9 @@ export const MATCHER_AD_UPDATE_FAILED_ROUTING_KEY = 'matcher-ad.update-failed'; export const AD_CREATED_MESSAGE_HANDLER = 'adCreated'; export const AD_CREATED_ROUTING_KEY = 'ad.created'; export const AD_CREATED_QUEUE = 'matcher.ad.created'; +export const AD_UPDATED_MESSAGE_HANDLER = 'adUpdated'; +export const AD_UPDATED_ROUTING_KEY = 'ad.updated'; +export const AD_UPDATED_QUEUE = 'matcher.ad.updated'; export const AD_DELETED_MESSAGE_HANDLER = 'adDeleted'; export const AD_DELETED_ROUTING_KEY = 'ad.deleted'; export const AD_DELETED_QUEUE = 'matcher.ad.deleted'; diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index adb71c4..a2668d2 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -45,6 +45,7 @@ import { TimezoneFinder } from './infrastructure/timezone-finder'; import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller'; import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler'; import { AdDeletedMessageHandler } from './interface/message-handlers/ad-deleted.message-handler'; +import { AdUpdatedMessageHandler } from './interface/message-handlers/ad-updated.message-handler'; import { MatchMapper } from './match.mapper'; import { MatchingMapper } from './matching.mapper'; @@ -99,7 +100,11 @@ const imports = [ const grpcControllers = [MatchGrpcController]; -const messageHandlers = [AdCreatedMessageHandler, AdDeletedMessageHandler]; +const messageHandlers = [ + AdCreatedMessageHandler, + AdUpdatedMessageHandler, + AdDeletedMessageHandler, +]; const eventHandlers: Provider[] = [ PublishMessageWhenMatcherAdIsCreatedDomainEventHandler, diff --git a/src/modules/ad/interface/message-handlers/ad-updated.message-handler.ts b/src/modules/ad/interface/message-handlers/ad-updated.message-handler.ts new file mode 100644 index 0000000..46c3488 --- /dev/null +++ b/src/modules/ad/interface/message-handlers/ad-updated.message-handler.ts @@ -0,0 +1,28 @@ +import { RabbitSubscribe } from '@mobicoop/message-broker-module'; +import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command'; +import { Injectable } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { + AD_UPDATED_MESSAGE_HANDLER, + AD_UPDATED_ROUTING_KEY, +} from '@src/app.constants'; +import { Ad } from './ad.types'; + +@Injectable() +export class AdUpdatedMessageHandler { + constructor(private readonly commandBus: CommandBus) {} + + @RabbitSubscribe({ + name: AD_UPDATED_MESSAGE_HANDLER, + routingKey: AD_UPDATED_ROUTING_KEY, + }) + public async adUpdated(message: string) { + try { + const updatedAd: { data: Ad } = JSON.parse(message); + await this.commandBus.execute(new UpdateAdCommand(updatedAd.data)); + } catch (error: any) { + // do not throw error to acknowledge incoming message + // error handling should be done in the command handler, if relevant + } + } +} diff --git a/src/modules/ad/tests/unit/interface/ad-updated.message-handler.spec.ts b/src/modules/ad/tests/unit/interface/ad-updated.message-handler.spec.ts new file mode 100644 index 0000000..d827fdf --- /dev/null +++ b/src/modules/ad/tests/unit/interface/ad-updated.message-handler.spec.ts @@ -0,0 +1,43 @@ +import { AdUpdatedMessageHandler } from '@modules/ad/interface/message-handlers/ad-updated.message-handler'; +import { CommandBus } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; + +const adUpdatedMessage = + '{"data": {"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4","driver":"true","passenger":"true","frequency":"PUNCTUAL","fromDate":"2023-08-18","toDate":"2023-08-18","schedule":[{"day":"5","time":"10:00","margin":"900"}],"seatsProposed":"3","seatsRequested":"1","strict":"false","waypoints":[{"position":"0","houseNumber":"5","street":"rue de la monnaie","locality":"Nancy","postalCode":"54000","country":"France","lon":"48.689445","lat":"6.17651"},{"position":"1","locality":"Paris","postalCode":"75000","country":"France","lon":"48.8566","lat":"2.3522"}]}}'; + +const mockCommandBus = { + execute: jest.fn(), +}; + +describe('Ad Updated Message Handler', () => { + let adUpdatedMessageHandler: AdUpdatedMessageHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CommandBus, + useValue: mockCommandBus, + }, + AdUpdatedMessageHandler, + ], + }).compile(); + + adUpdatedMessageHandler = module.get( + AdUpdatedMessageHandler, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(adUpdatedMessageHandler).toBeDefined(); + }); + + it('should update an ad', async () => { + await adUpdatedMessageHandler.adUpdated(adUpdatedMessage); + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/messager/messager.module.ts b/src/modules/messager/messager.module.ts index 73086db..ec29ef4 100644 --- a/src/modules/messager/messager.module.ts +++ b/src/modules/messager/messager.module.ts @@ -12,6 +12,9 @@ import { AD_DELETED_MESSAGE_HANDLER, AD_DELETED_QUEUE, AD_DELETED_ROUTING_KEY, + AD_UPDATED_MESSAGE_HANDLER, + AD_UPDATED_QUEUE, + AD_UPDATED_ROUTING_KEY, SERVICE_NAME, } from '@src/app.constants'; import { MESSAGE_PUBLISHER } from './messager.di-tokens'; @@ -36,6 +39,10 @@ const imports = [ routingKey: AD_CREATED_ROUTING_KEY, queue: AD_CREATED_QUEUE, }, + [AD_UPDATED_MESSAGE_HANDLER]: { + routingKey: AD_UPDATED_ROUTING_KEY, + queue: AD_UPDATED_QUEUE, + }, [AD_DELETED_MESSAGE_HANDLER]: { routingKey: AD_DELETED_ROUTING_KEY, queue: AD_DELETED_QUEUE,