From 5e449ad69ae7f79df8e39a39a7e2832adfdbda0a Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Tue, 7 May 2024 10:42:54 +0200 Subject: [PATCH 01/11] Prepare release 2.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ae01ed..8a07dba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mobicoop/ad", - "version": "2.4.5", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mobicoop/ad", - "version": "2.4.5", + "version": "2.6.0", "license": "AGPL", "dependencies": { "@grpc/grpc-js": "^1.9.14", diff --git a/package.json b/package.json index 594c006..98f7f7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mobicoop/ad", - "version": "2.5.0", + "version": "2.6.0", "description": "Mobicoop V3 Ad", "author": "sbriat", "private": true, From c7d47928931fe8717d91e34dddb50c396361761b Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Mon, 29 Apr 2024 16:11:12 +0200 Subject: [PATCH 02/11] Consistent and DRY declarations of ScheduleItem types --- src/modules/ad/ad.mapper.ts | 30 +++++++++---------- .../commands/create-ad/create-ad.command.ts | 8 ++--- .../commands/create-ad/create-ad.service.ts | 16 +++++----- .../core/application/types/schedule-item.ts | 5 ---- src/modules/ad/core/domain/ad.entity.ts | 4 +-- .../domain/events/ad-created.domain-event.ts | 9 ++---- .../schedule-item.value-object.ts | 8 ++--- tests/unit/ad/core/ad.fixtures.ts | 2 ++ .../find-ads-by-ids.query-handler.spec.ts | 6 ++-- .../find-ads-by-user-id.query-handler.spec.ts | 6 ++-- .../ad/core/invalidate-ad.service.spec.ts | 10 ++++--- .../unit/ad/core/validate-ad.service.spec.ts | 10 ++++--- 12 files changed, 57 insertions(+), 57 deletions(-) delete mode 100644 src/modules/ad/core/application/types/schedule-item.ts diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index eb3bf11..255f4fa 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -1,19 +1,19 @@ import { Mapper } from '@mobicoop/ddd-library'; -import { AdResponseDto } from './interface/dtos/ad.response.dto'; import { Inject, Injectable } from '@nestjs/common'; -import { AdEntity } from './core/domain/ad.entity'; -import { - AdWriteModel, - AdReadModel, - WaypointModel, - ScheduleItemModel, -} from './infrastructure/ad.repository'; -import { Frequency, Status } from './core/domain/ad.types'; -import { WaypointProps } from './core/domain/value-objects/waypoint.value-object'; import { v4 } from 'uuid'; -import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens'; import { DateTimeTransformerPort } from './core/application/ports/datetime-transformer.port'; +import { AdEntity } from './core/domain/ad.entity'; +import { Frequency, Status } from './core/domain/ad.types'; +import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; +import { WaypointProps } from './core/domain/value-objects/waypoint.value-object'; +import { + AdReadModel, + AdWriteModel, + ScheduleItemModel, + WaypointModel, +} from './infrastructure/ad.repository'; +import { AdResponseDto } from './interface/dtos/ad.response.dto'; /** * Mapper constructs objects that are used in different layers: @@ -47,7 +47,7 @@ export class AdMapper ? { create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({ uuid: v4(), - day: scheduleItem.day as number, + day: scheduleItem.day, time: new Date( 1970, 0, @@ -55,7 +55,7 @@ export class AdMapper parseInt(scheduleItem.time.split(':')[0]), parseInt(scheduleItem.time.split(':')[1]), ), - margin: scheduleItem.margin as number, + margin: scheduleItem.margin, createdAt: now, updatedAt: now, })), @@ -163,7 +163,7 @@ export class AdMapper response.schedule = props.schedule.map( (scheduleItem: ScheduleItemProps) => ({ day: this.outputDatetimeTransformer.day( - scheduleItem.day as number, + scheduleItem.day, { date: props.fromDate, time: scheduleItem.time, @@ -179,7 +179,7 @@ export class AdMapper }, props.frequency, ), - margin: scheduleItem.margin as number, + margin: scheduleItem.margin, }), ); response.seatsProposed = props.seatsProposed as number; 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 7a939c3..fefe004 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,7 +1,7 @@ -import { ScheduleItem } from '../../types/schedule-item'; -import { Waypoint } from '../../types/waypoint'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; import { Command, CommandProps } from '@mobicoop/ddd-library'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object'; +import { Waypoint } from '../../types/waypoint'; export class CreateAdCommand extends Command { readonly userId: string; @@ -10,7 +10,7 @@ export class CreateAdCommand extends Command { readonly frequency: Frequency; readonly fromDate: string; readonly toDate: string; - readonly schedule: ScheduleItem[]; + readonly schedule: ScheduleItemProps[]; readonly seatsProposed?: number; readonly seatsRequested?: number; readonly strict: 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 5d1d30c..5b3abc3 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,17 +1,17 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { CreateAdCommand } from './create-ad.command'; -import { Inject } from '@nestjs/common'; +import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; import { AD_REPOSITORY, INPUT_DATETIME_TRANSFORMER, } from '@modules/ad/ad.di-tokens'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; -import { Waypoint } from '../../types/waypoint'; -import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; -import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; -import { ScheduleItem } from '../../types/schedule-item'; +import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object'; +import { Inject } from '@nestjs/common'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; +import { Waypoint } from '../../types/waypoint'; +import { CreateAdCommand } from './create-ad.command'; @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { @@ -51,7 +51,7 @@ export class CreateAdService implements ICommandHandler { }, command.frequency, ), - schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({ + schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({ day: this.datetimeTransformer.day( scheduleItem.day, { diff --git a/src/modules/ad/core/application/types/schedule-item.ts b/src/modules/ad/core/application/types/schedule-item.ts deleted file mode 100644 index 92dab99..0000000 --- a/src/modules/ad/core/application/types/schedule-item.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ScheduleItem = { - day: number; - time: string; - margin: number; -}; diff --git a/src/modules/ad/core/domain/ad.entity.ts b/src/modules/ad/core/domain/ad.entity.ts index ffbeb0b..b255263 100644 --- a/src/modules/ad/core/domain/ad.entity.ts +++ b/src/modules/ad/core/domain/ad.entity.ts @@ -30,9 +30,9 @@ export class AdEntity extends AggregateRoot { fromDate: props.fromDate, toDate: props.toDate, schedule: props.schedule.map((day: ScheduleItemProps) => ({ - day: day.day as number, + day: day.day, time: day.time, - margin: day.margin as number, + margin: day.margin, })), seatsProposed: props.seatsProposed, seatsRequested: props.seatsRequested, diff --git a/src/modules/ad/core/domain/events/ad-created.domain-event.ts b/src/modules/ad/core/domain/events/ad-created.domain-event.ts index 50cd191..12c45cf 100644 --- a/src/modules/ad/core/domain/events/ad-created.domain-event.ts +++ b/src/modules/ad/core/domain/events/ad-created.domain-event.ts @@ -1,4 +1,5 @@ import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library'; +import { ScheduleItemProps } from '../value-objects/schedule-item.value-object'; export class AdCreatedDomainEvent extends DomainEvent { readonly userId: string; @@ -7,7 +8,7 @@ export class AdCreatedDomainEvent extends DomainEvent { readonly frequency: string; readonly fromDate: string; readonly toDate: string; - readonly schedule: ScheduleItem[]; + readonly schedule: ScheduleItemProps[]; readonly seatsProposed: number; readonly seatsRequested: number; readonly strict: boolean; @@ -31,12 +32,6 @@ export class AdCreatedDomainEvent extends DomainEvent { } } -export class ScheduleItem { - day: number; - time: string; - margin: number; -} - export class Waypoint { position: number; name?: string; diff --git a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts index 8303eeb..c5f6cc2 100644 --- a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts @@ -6,13 +6,13 @@ import { ValueObject } from '@mobicoop/ddd-library'; * */ export interface ScheduleItemProps { - day?: number; + day: number; time: string; - margin?: number; + margin: number; } export class ScheduleItem extends ValueObject { - get day(): number | undefined { + get day(): number { return this.props.day; } @@ -20,7 +20,7 @@ export class ScheduleItem extends ValueObject { return this.props.time; } - get margin(): number | undefined { + get margin(): number { return this.props.margin; } diff --git a/tests/unit/ad/core/ad.fixtures.ts b/tests/unit/ad/core/ad.fixtures.ts index 2ebf55a..3e2663c 100644 --- a/tests/unit/ad/core/ad.fixtures.ts +++ b/tests/unit/ad/core/ad.fixtures.ts @@ -39,7 +39,9 @@ const punctualCreateAdProps = { toDate: '2023-06-22', schedule: [ { + day: 4, time: '08:30', + margin: 900, }, ], frequency: Frequency.PUNCTUAL, diff --git a/tests/unit/ad/core/find-ads-by-ids.query-handler.spec.ts b/tests/unit/ad/core/find-ads-by-ids.query-handler.spec.ts index c9aef33..0f7e05d 100644 --- a/tests/unit/ad/core/find-ads-by-ids.query-handler.spec.ts +++ b/tests/unit/ad/core/find-ads-by-ids.query-handler.spec.ts @@ -1,10 +1,10 @@ import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query'; +import { FindAdsByIdsQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; import { Test, TestingModule } from '@nestjs/testing'; -import { FindAdsByIdsQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler'; -import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query'; const originWaypointProps: WaypointProps = { position: 0, @@ -44,7 +44,9 @@ const punctualCreateAdProps = { toDate: '2023-06-22', schedule: [ { + day: 4, time: '08:30', + margin: 900, }, ], frequency: Frequency.PUNCTUAL, diff --git a/tests/unit/ad/core/find-ads-by-user-id.query-handler.spec.ts b/tests/unit/ad/core/find-ads-by-user-id.query-handler.spec.ts index 300c84c..4f7751a 100644 --- a/tests/unit/ad/core/find-ads-by-user-id.query-handler.spec.ts +++ b/tests/unit/ad/core/find-ads-by-user-id.query-handler.spec.ts @@ -1,10 +1,10 @@ import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query'; +import { FindAdsByUserIdQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; import { Test, TestingModule } from '@nestjs/testing'; -import { FindAdsByUserIdQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler'; -import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query'; const originWaypointProps: WaypointProps = { position: 0, @@ -44,7 +44,9 @@ const punctualCreateAdProps = { toDate: '2023-06-22', schedule: [ { + day: 4, time: '08:30', + margin: 900, }, ], frequency: Frequency.PUNCTUAL, diff --git a/tests/unit/ad/core/invalidate-ad.service.spec.ts b/tests/unit/ad/core/invalidate-ad.service.spec.ts index db72a24..8bd1568 100644 --- a/tests/unit/ad/core/invalidate-ad.service.spec.ts +++ b/tests/unit/ad/core/invalidate-ad.service.spec.ts @@ -1,11 +1,11 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; import { AggregateID } from '@mobicoop/ddd-library'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command'; +import { InvalidateAdService } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; -import { InvalidateAdService } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service'; -import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command'; +import { Test, TestingModule } from '@nestjs/testing'; const originWaypointProps: WaypointProps = { position: 0, @@ -45,7 +45,9 @@ const punctualCreateAdProps = { toDate: '2023-06-22', schedule: [ { + day: 4, time: '08:30', + margin: 900, }, ], frequency: Frequency.PUNCTUAL, diff --git a/tests/unit/ad/core/validate-ad.service.spec.ts b/tests/unit/ad/core/validate-ad.service.spec.ts index 0a30c89..1ccd147 100644 --- a/tests/unit/ad/core/validate-ad.service.spec.ts +++ b/tests/unit/ad/core/validate-ad.service.spec.ts @@ -1,11 +1,11 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; import { AggregateID } from '@mobicoop/ddd-library'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command'; +import { ValidateAdService } from '@modules/ad/core/application/commands/validate-ad/validate-ad.service'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; -import { ValidateAdService } from '@modules/ad/core/application/commands/validate-ad/validate-ad.service'; -import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; +import { Test, TestingModule } from '@nestjs/testing'; const originWaypointProps: WaypointProps = { position: 0, @@ -45,7 +45,9 @@ const punctualCreateAdProps = { toDate: '2023-06-22', schedule: [ { + day: 4, time: '08:30', + margin: 900, }, ], frequency: Frequency.PUNCTUAL, From 62e5fd56d929f147064e047cd496f45c459d7c9f Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Thu, 16 May 2024 17:12:24 +0200 Subject: [PATCH 03/11] 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 8a07dba..6c60d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@grpc/grpc-js": "^1.9.14", "@grpc/proto-loader": "^0.7.10", - "@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/common": "^10.3.0", @@ -1852,9 +1852,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 98f7f7d..3e5a6df 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@grpc/grpc-js": "^1.9.14", "@grpc/proto-loader": "^0.7.10", "@songkeys/nestjs-redis": "^10.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/common": "^10.3.0", From 3ff5277d5fee64022b68bd743bcdfc436bde909e Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Mon, 29 Apr 2024 16:01:55 +0200 Subject: [PATCH 04/11] Add update method to Ad entity --- src/modules/ad/core/domain/ad.entity.ts | 20 ++++++++++++++++ src/modules/ad/core/domain/ad.types.ts | 22 ++++-------------- .../ad/core/domain/events/ad.domain-event.ts | 23 +++++++++++++++++++ 3 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 src/modules/ad/core/domain/events/ad.domain-event.ts diff --git a/src/modules/ad/core/domain/ad.entity.ts b/src/modules/ad/core/domain/ad.entity.ts index b255263..5b045c1 100644 --- a/src/modules/ad/core/domain/ad.entity.ts +++ b/src/modules/ad/core/domain/ad.entity.ts @@ -6,6 +6,7 @@ import { AdDeletedDomainEvent } from './events/ad-delete.domain-event'; import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event'; import { AdSuspendedDomainEvent } from './events/ad-suspended.domain-event'; import { AdValidatedDomainEvent } from './events/ad-validated.domain-event'; +import { AdUpdatedDomainEvent } from './events/ad.domain-event'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { WaypointProps } from './value-objects/waypoint.value-object'; @@ -96,6 +97,25 @@ export class AdEntity extends AggregateRoot { return this; }; + update = (newProps: CreateAdProps): AdEntity => { + this.props.driver = newProps.driver; + this.props.passenger = newProps.passenger; + this.props.frequency = newProps.frequency; + this.props.fromDate = newProps.fromDate; + this.props.toDate = newProps.toDate; + this.props.seatsProposed = newProps.seatsProposed; + this.props.seatsRequested = newProps.seatsRequested; + this.props.strict = newProps.strict; + this.props.comment = newProps.comment; + this.props.schedule = newProps.schedule.map((item) => ({ ...item })); + this.props.waypoints = newProps.waypoints.map((wp) => ({ ...wp })); + //The ad goes back to pending status until it is validated again + this.props.status = Status.PENDING; + this.addEvent(new AdUpdatedDomainEvent(this)); + this.validate(); + return this; + }; + delete(): void { this.addEvent( new AdDeletedDomainEvent({ diff --git a/src/modules/ad/core/domain/ad.types.ts b/src/modules/ad/core/domain/ad.types.ts index 85bb2bc..d42115a 100644 --- a/src/modules/ad/core/domain/ad.types.ts +++ b/src/modules/ad/core/domain/ad.types.ts @@ -1,23 +1,6 @@ import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { WaypointProps } from './value-objects/waypoint.value-object'; -// All properties that an Ad has -export interface AdProps { - userId: string; - driver: boolean; - status: Status; - passenger: boolean; - frequency: Frequency; - fromDate: string; - toDate: string; - schedule: ScheduleItemProps[]; - seatsProposed: number; - seatsRequested: number; - strict: boolean; - waypoints: WaypointProps[]; - comment?: string; -} - // Properties that are needed for an Ad creation export interface CreateAdProps { userId: string; @@ -34,6 +17,11 @@ export interface CreateAdProps { comment?: string; } +// All properties that an Ad has +export interface AdProps extends CreateAdProps { + status: Status; +} + export enum Frequency { PUNCTUAL = 'PUNCTUAL', RECURRENT = 'RECURRENT', diff --git a/src/modules/ad/core/domain/events/ad.domain-event.ts b/src/modules/ad/core/domain/events/ad.domain-event.ts new file mode 100644 index 0000000..d67b8f5 --- /dev/null +++ b/src/modules/ad/core/domain/events/ad.domain-event.ts @@ -0,0 +1,23 @@ +import { DomainEvent } from '@mobicoop/ddd-library'; +import { AdEntity } from '../ad.entity'; + +export abstract class AdDomainEvent extends DomainEvent { + readonly ad: AdEntity; + + constructor(ad: AdEntity) { + super({ + metadata: { + correlationId: ad.id, + timestamp: Date.now(), + }, + aggregateId: ad.id, + }); + this.ad = ad; + } +} + +export class AdUpdatedDomainEvent extends AdDomainEvent { + constructor(ad: AdEntity) { + super(ad); + } +} From 3d4ff000664af65eed16d1311e6cacc381311db8 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Mon, 29 Apr 2024 16:02:32 +0200 Subject: [PATCH 05/11] publish integration event when an ad is updated --- src/app.constants.ts | 2 +- src/modules/ad/ad.mapper.ts | 1 + src/modules/ad/ad.module.ts | 2 + ...when-ad-is-updated.domain-event-handler.ts | 44 +++++++++++++ ...ad-is-updated.domain-event-handler.spec.ts | 61 +++++++++++++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/modules/ad/core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler.ts create mode 100644 tests/unit/ad/core/publish-message-when-ad-is-updated.domain-event-handler.spec.ts diff --git a/src/app.constants.ts b/src/app.constants.ts index 1f72c93..7f2ec77 100644 --- a/src/app.constants.ts +++ b/src/app.constants.ts @@ -7,7 +7,7 @@ export const GRPC_SERVICE_NAME = 'AdService'; // messaging output export const AD_CREATED_ROUTING_KEY = 'ad.created'; -// messaging output +export const AD_UPDATED_ROUTING_KEY = 'ad.updated'; export const AD_DELETED_ROUTING_KEY = 'ad.deleted'; // messaging input diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 255f4fa..05daf4e 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -141,6 +141,7 @@ export class AdMapper response.userId = props.userId; response.driver = props.driver as boolean; response.passenger = props.passenger as boolean; + response.strict = props.strict; response.status = props.status; response.frequency = props.frequency; response.fromDate = this.outputDatetimeTransformer.fromDate( diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 3d5dfe1..8f0e1d9 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -17,6 +17,7 @@ import { InvalidateAdService } from './core/application/commands/invalidate-ad/i import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service'; import { PublishMessageWhenAdIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-deleted.domain-event-handler'; import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler'; +import { PublishMessageWhenAdIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler'; import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler'; import { FindAdsByIdsQueryHandler } from './core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler'; import { FindAdsByUserIdQueryHandler } from './core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler'; @@ -51,6 +52,7 @@ const messageHandlers = [ const eventHandlers: Provider[] = [ PublishMessageWhenAdIsCreatedDomainEventHandler, + PublishMessageWhenAdIsUpdatedDomainEventHandler, PublishMessageWhenAdIsDeletedDomainEventHandler, ]; diff --git a/src/modules/ad/core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler.ts b/src/modules/ad/core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler.ts new file mode 100644 index 0000000..4715a14 --- /dev/null +++ b/src/modules/ad/core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler.ts @@ -0,0 +1,44 @@ +import { + IntegrationEvent, + IntegrationEventProps, + MessagePublisherPort, +} from '@mobicoop/ddd-library'; +import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens'; +import { AdMapper } from '@modules/ad/ad.mapper'; +import { AdResponseDto } from '@modules/ad/interface/dtos/ad.response.dto'; +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { AD_UPDATED_ROUTING_KEY } from '@src/app.constants'; +import { v4 } from 'uuid'; +import { AdUpdatedDomainEvent } from '../../domain/events/ad.domain-event'; + +class AdIntegrationEvent extends IntegrationEvent { + readonly data: AdResponseDto; + + constructor(props: IntegrationEventProps, data: AdResponseDto) { + super(props); + this.data = data; + } +} + +@Injectable() +export class PublishMessageWhenAdIsUpdatedDomainEventHandler { + constructor( + @Inject(AD_MESSAGE_PUBLISHER) + private readonly messagePublisher: MessagePublisherPort, + private readonly mapper: AdMapper, + ) {} + + @OnEvent(AdUpdatedDomainEvent.name, { async: true, promisify: true }) + async handle(event: AdUpdatedDomainEvent): Promise { + this.messagePublisher.publish( + AD_UPDATED_ROUTING_KEY, + JSON.stringify( + new AdIntegrationEvent( + { id: v4(), metadata: event.metadata }, + this.mapper.toResponse(event.ad), + ), + ), + ); + } +} diff --git a/tests/unit/ad/core/publish-message-when-ad-is-updated.domain-event-handler.spec.ts b/tests/unit/ad/core/publish-message-when-ad-is-updated.domain-event-handler.spec.ts new file mode 100644 index 0000000..c6aa48d --- /dev/null +++ b/tests/unit/ad/core/publish-message-when-ad-is-updated.domain-event-handler.spec.ts @@ -0,0 +1,61 @@ +import { + AD_MESSAGE_PUBLISHER, + OUTPUT_DATETIME_TRANSFORMER, + TIMEZONE_FINDER, + TIME_CONVERTER, +} from '@modules/ad/ad.di-tokens'; +import { AdMapper } from '@modules/ad/ad.mapper'; +import { PublishMessageWhenAdIsUpdatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; +import { AdUpdatedDomainEvent } from '@modules/ad/core/domain/events/ad.domain-event'; +import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer'; +import { TimeConverter } from '@modules/ad/infrastructure/time-converter'; +import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder'; +import { Test, TestingModule } from '@nestjs/testing'; +import { punctualPassengerCreateAdProps } from './ad.fixtures'; + +const mockMessagePublisher = { + publish: jest.fn(), +}; + +describe('Publish message when ad is updated domain event handler', () => { + let updatedDomainEventHandler: PublishMessageWhenAdIsUpdatedDomainEventHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: AD_MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, + }, + { + provide: OUTPUT_DATETIME_TRANSFORMER, + useClass: OutputDateTimeTransformer, + }, + { + provide: TIMEZONE_FINDER, + useClass: TimezoneFinder, + }, + { + provide: TIME_CONVERTER, + useClass: TimeConverter, + }, + AdMapper, + PublishMessageWhenAdIsUpdatedDomainEventHandler, + ], + }).compile(); + + updatedDomainEventHandler = + module.get( + PublishMessageWhenAdIsUpdatedDomainEventHandler, + ); + }); + + it('should publish a message', () => { + expect(updatedDomainEventHandler).toBeDefined(); + const ad = AdEntity.create(punctualPassengerCreateAdProps()); + const adUpdatedDomainEvent = new AdUpdatedDomainEvent(ad); + updatedDomainEventHandler.handle(adUpdatedDomainEvent); + expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1); + }); +}); From 7a84bff260d368fe0da3ecb747894924052b7a3b Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Mon, 29 Apr 2024 17:06:18 +0200 Subject: [PATCH 06/11] Implement update ad command --- src/modules/ad/ad.mapper.ts | 107 +++++++----- src/modules/ad/ad.module.ts | 2 + .../commands/create-ad/create-ad.service.ts | 158 ++++++++++-------- .../commands/update-ad/update-ad.command.ts | 16 ++ .../commands/update-ad/update-ad.service.ts | 29 ++++ .../ad/core/application/types/waypoint.ts | 1 + .../ad/infrastructure/ad.repository.ts | 22 ++- 7 files changed, 216 insertions(+), 119 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 diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 05daf4e..839ced6 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -11,7 +11,9 @@ import { AdReadModel, AdWriteModel, ScheduleItemModel, + ScheduleWriteModel, WaypointModel, + WaypointWriteModel, } from './infrastructure/ad.repository'; import { AdResponseDto } from './interface/dtos/ad.response.dto'; @@ -31,9 +33,8 @@ export class AdMapper private readonly outputDatetimeTransformer: DateTimeTransformerPort, ) {} - toPersistence = (entity: AdEntity): AdWriteModel => { + toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => { const copy = entity.getProps(); - const now = new Date(); const record: AdWriteModel = { uuid: copy.id, userUuid: copy.userId, @@ -43,50 +44,80 @@ export class AdMapper frequency: copy.frequency, fromDate: new Date(copy.fromDate), toDate: new Date(copy.toDate), - schedule: copy.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, - })), - } - : undefined, + schedule: this.toScheduleItemWriteModel(copy.schedule, update), seatsProposed: copy.seatsProposed as number, seatsRequested: copy.seatsRequested as number, strict: copy.strict as boolean, - waypoints: copy.waypoints - ? { - create: copy.waypoints.map((waypoint: WaypointProps) => ({ - uuid: v4(), - position: waypoint.position, - name: waypoint.address.name, - houseNumber: waypoint.address.houseNumber, - street: waypoint.address.street, - locality: waypoint.address.locality, - postalCode: waypoint.address.postalCode, - country: waypoint.address.country, - lon: waypoint.address.coordinates.lon, - lat: waypoint.address.coordinates.lat, - createdAt: now, - updatedAt: now, - })), - } - : undefined, + waypoints: this.toWaypointWriteModel(copy.waypoints, update), comment: copy.comment, }; return record; }; + toScheduleItemWriteModel = ( + schedule: ScheduleItemProps[], + update?: boolean, + ): ScheduleWriteModel | undefined => { + if (!schedule) { + return undefined; + } + 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; + }; + + toWaypointWriteModel = ( + waypoints: WaypointProps[], + update?: boolean, + ): WaypointWriteModel | undefined => { + if (!waypoints) { + return undefined; + } + const now = new Date(); + const record: WaypointWriteModel = { + create: waypoints.map((waypoint: WaypointProps) => ({ + uuid: v4(), + position: waypoint.position, + name: waypoint.address.name, + houseNumber: waypoint.address.houseNumber, + street: waypoint.address.street, + locality: waypoint.address.locality, + postalCode: waypoint.address.postalCode, + country: waypoint.address.country, + lon: waypoint.address.coordinates.lon, + lat: waypoint.address.coordinates.lat, + createdAt: now, + updatedAt: now, + })), + }; + if (update) { + record.deleteMany = { + createdAt: { lt: now }, + }; + } + return record; + }; + toDomain = (record: AdReadModel): AdEntity => { const entity = new AdEntity({ id: record.uuid, diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 8f0e1d9..c27a272 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -14,6 +14,7 @@ import { CreateAdService } from './core/application/commands/create-ad/create-ad import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service'; import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service'; import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service'; +import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service'; import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service'; import { PublishMessageWhenAdIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-deleted.domain-event-handler'; import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler'; @@ -58,6 +59,7 @@ const eventHandlers: Provider[] = [ const commandHandlers: Provider[] = [ CreateAdService, + UpdateAdService, DeleteAdService, DeleteUserAdsService, ValidateAdService, 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 5b3abc3..98a95dc 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 @@ -13,6 +13,87 @@ import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; import { Waypoint } from '../../types/waypoint'; import { CreateAdCommand } from './create-ad.command'; +export function createPropsFromCommand( + command: CreateAdCommand, + datetimeTransformer: DateTimeTransformerPort, +) { + return { + userId: command.userId, + driver: command.driver, + passenger: command.passenger, + frequency: command.frequency, + //TODO Shouldn't that kind of logic be in the domain layer? + fromDate: datetimeTransformer.fromDate( + { + date: command.fromDate, + time: command.schedule[0].time, + coordinates: { + lon: command.waypoints[0].lon, + lat: command.waypoints[0].lat, + }, + }, + command.frequency, + ), + toDate: datetimeTransformer.toDate( + command.toDate, + { + date: command.fromDate, + time: command.schedule[0].time, + coordinates: { + lon: command.waypoints[0].lon, + lat: command.waypoints[0].lat, + }, + }, + command.frequency, + ), + schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({ + day: datetimeTransformer.day( + scheduleItem.day, + { + date: command.fromDate, + time: scheduleItem.time, + coordinates: { + lon: command.waypoints[0].lon, + lat: command.waypoints[0].lat, + }, + }, + command.frequency, + ), + time: datetimeTransformer.time( + { + date: command.fromDate, + time: scheduleItem.time, + coordinates: { + lon: command.waypoints[0].lon, + lat: command.waypoints[0].lat, + }, + }, + command.frequency, + ), + margin: scheduleItem.margin, + })), + seatsProposed: command.seatsProposed ?? 0, + seatsRequested: command.seatsRequested ?? 0, + strict: command.strict, + waypoints: command.waypoints.map((waypoint: Waypoint) => ({ + position: waypoint.position, + address: { + name: waypoint.name, + houseNumber: waypoint.houseNumber, + street: waypoint.street, + postalCode: waypoint.postalCode, + locality: waypoint.locality, + country: waypoint.country, + coordinates: { + lon: waypoint.lon, + lat: waypoint.lat, + }, + }, + })), + comment: command.comment, + }; +} + @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { constructor( @@ -23,80 +104,9 @@ export class CreateAdService implements ICommandHandler { ) {} async execute(command: CreateAdCommand): Promise { - const ad = AdEntity.create({ - userId: command.userId, - driver: command.driver, - passenger: command.passenger, - frequency: command.frequency, - fromDate: this.datetimeTransformer.fromDate( - { - date: command.fromDate, - time: command.schedule[0].time, - coordinates: { - lon: command.waypoints[0].lon, - lat: command.waypoints[0].lat, - }, - }, - command.frequency, - ), - toDate: this.datetimeTransformer.toDate( - command.toDate, - { - date: command.fromDate, - time: command.schedule[0].time, - coordinates: { - lon: command.waypoints[0].lon, - lat: command.waypoints[0].lat, - }, - }, - command.frequency, - ), - schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({ - day: this.datetimeTransformer.day( - scheduleItem.day, - { - date: command.fromDate, - time: scheduleItem.time, - coordinates: { - lon: command.waypoints[0].lon, - lat: command.waypoints[0].lat, - }, - }, - command.frequency, - ), - time: this.datetimeTransformer.time( - { - date: command.fromDate, - time: scheduleItem.time, - coordinates: { - lon: command.waypoints[0].lon, - lat: command.waypoints[0].lat, - }, - }, - command.frequency, - ), - margin: scheduleItem.margin, - })), - seatsProposed: command.seatsProposed ?? 0, - seatsRequested: command.seatsRequested ?? 0, - strict: command.strict, - waypoints: command.waypoints.map((waypoint: Waypoint) => ({ - position: waypoint.position, - address: { - name: waypoint.name, - houseNumber: waypoint.houseNumber, - street: waypoint.street, - postalCode: waypoint.postalCode, - locality: waypoint.locality, - country: waypoint.country, - coordinates: { - lon: waypoint.lon, - lat: waypoint.lat, - }, - }, - })), - comment: command.comment, - }); + const ad = AdEntity.create( + createPropsFromCommand(command, this.datetimeTransformer), + ); try { await this.repository.insert(ad); 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..db2dbc3 --- /dev/null +++ b/src/modules/ad/core/application/commands/update-ad/update-ad.command.ts @@ -0,0 +1,16 @@ +import { CommandProps } from '@mobicoop/ddd-library'; +import { CreateAdCommand } from '../create-ad/create-ad.command'; + +/** + * Ad updates follow the PUT semantics: they replace the entire object. + * Therefore the update command extends the create command to inherit the same properties + * and re-use the data transformation logic. + */ +export class UpdateAdCommand extends CreateAdCommand { + public adId: string; + + constructor(props: CommandProps) { + super(props); + this.adId = props.adId; + } +} 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..a97d093 --- /dev/null +++ b/src/modules/ad/core/application/commands/update-ad/update-ad.service.ts @@ -0,0 +1,29 @@ +import { + AD_REPOSITORY, + INPUT_DATETIME_TRANSFORMER, +} from '@modules/ad/ad.di-tokens'; +import { Inject } from '@nestjs/common'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; +import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; +import { createPropsFromCommand } from '../create-ad/create-ad.service'; +import { UpdateAdCommand } from './update-ad.command'; + +@CommandHandler(UpdateAdCommand) +export class UpdateAdService implements ICommandHandler { + constructor( + @Inject(AD_REPOSITORY) + private readonly repository: AdRepositoryPort, + @Inject(INPUT_DATETIME_TRANSFORMER) + private readonly datetimeTransformer: DateTimeTransformerPort, + ) {} + + async execute(command: UpdateAdCommand): Promise { + const ad = await this.repository.findOneById(command.adId, { + waypoints: true, + schedule: true, + }); + ad.update(createPropsFromCommand(command, this.datetimeTransformer)); + await this.repository.update(ad.id, ad); + } +} diff --git a/src/modules/ad/core/application/types/waypoint.ts b/src/modules/ad/core/application/types/waypoint.ts index 32c5204..57d792e 100644 --- a/src/modules/ad/core/application/types/waypoint.ts +++ b/src/modules/ad/core/application/types/waypoint.ts @@ -1,5 +1,6 @@ import { Address } from './address'; +//TODO Why not use the Waypoint value-object from the domain? export type Waypoint = { position: number; } & Address; diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index b5df96f..fa06d0a 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -1,16 +1,16 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { AdEntity } from '../core/domain/ad.entity'; -import { AdMapper } from '../ad.mapper'; -import { AdRepositoryPort } from '../core/application/ports/ad.repository.port'; import { LoggerBase, MessagePublisherPort, PrismaRepositoryBase, } from '@mobicoop/ddd-library'; -import { PrismaService } from './prisma.service'; -import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; 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 { PrismaService } from './prisma.service'; export type AdBaseModel = { uuid: string; @@ -40,13 +40,21 @@ export type AdWriteModel = AdBaseModel & { }; export type ScheduleWriteModel = { + deleteMany?: PastCreatedFilter; create: ScheduleItemModel[]; }; export type WaypointWriteModel = { + deleteMany?: PastCreatedFilter; create: WaypointModel[]; }; +// 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 ScheduleItemModel = { uuid: string; day: number; From 659c1baea8c764cfc69c43245b9467c1629967b6 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Mon, 29 Apr 2024 17:49:09 +0200 Subject: [PATCH 07/11] Implement the GRPC controller to update ads --- src/modules/ad/ad.module.ts | 2 + .../ad/interface/grpc-controllers/ad.proto | 2 +- .../dtos/update-ad.request.dto.ts | 7 ++ .../update-ad.grpc.controller.ts | 43 +++++++++++ tests/unit/ad/interface/ad.fixtures.ts | 43 +++++++++++ .../create-ad.grpc.controller.spec.ts | 51 ++----------- .../update-ad.grpc.controller.spec.ts | 76 +++++++++++++++++++ 7 files changed, 177 insertions(+), 47 deletions(-) create mode 100644 src/modules/ad/interface/grpc-controllers/dtos/update-ad.request.dto.ts create mode 100644 src/modules/ad/interface/grpc-controllers/update-ad.grpc.controller.ts create mode 100644 tests/unit/ad/interface/ad.fixtures.ts create mode 100644 tests/unit/ad/interface/update-ad.grpc.controller.spec.ts diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index c27a272..286670a 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -33,12 +33,14 @@ import { DeleteAdGrpcController } from './interface/grpc-controllers/delete-ad.g import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller'; import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller'; import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller'; +import { UpdateAdGrpcController } from './interface/grpc-controllers/update-ad.grpc.controller'; import { MatcherAdCreatedMessageHandler } from './interface/message-handlers/matcher-ad-created.message-handler'; import { MatcherAdCreationFailedMessageHandler } from './interface/message-handlers/matcher-ad-creation-failed.message-handler'; import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler'; const grpcControllers = [ CreateAdGrpcController, + UpdateAdGrpcController, DeleteAdGrpcController, FindAdByIdGrpcController, FindAdsByIdsGrpcController, diff --git a/src/modules/ad/interface/grpc-controllers/ad.proto b/src/modules/ad/interface/grpc-controllers/ad.proto index 581aa08..267d106 100644 --- a/src/modules/ad/interface/grpc-controllers/ad.proto +++ b/src/modules/ad/interface/grpc-controllers/ad.proto @@ -7,7 +7,7 @@ service AdService { rpc FindAllByIds(AdsById) returns (Ads); rpc FindAllByUserId(UserById) returns (Ads); rpc Create(Ad) returns (AdById); - rpc Update(Ad) returns (Ad); + rpc Update(Ad) returns (Empty); rpc Delete(AdById) returns (Empty); } diff --git a/src/modules/ad/interface/grpc-controllers/dtos/update-ad.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/update-ad.request.dto.ts new file mode 100644 index 0000000..2fffdb0 --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/dtos/update-ad.request.dto.ts @@ -0,0 +1,7 @@ +import { IsUUID } from 'class-validator'; +import { CreateAdRequestDto } from './create-ad.request.dto'; + +export class UpdateAdRequestDto extends CreateAdRequestDto { + @IsUUID(4) + id: string; +} diff --git a/src/modules/ad/interface/grpc-controllers/update-ad.grpc.controller.ts b/src/modules/ad/interface/grpc-controllers/update-ad.grpc.controller.ts new file mode 100644 index 0000000..326a3df --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/update-ad.grpc.controller.ts @@ -0,0 +1,43 @@ +import { + NotFoundException, + RpcExceptionCode, + RpcValidationPipe, +} from '@mobicoop/ddd-library'; +import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command'; +import { Controller, UsePipes } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { GRPC_SERVICE_NAME } from '@src/app.constants'; +import { UpdateAdRequestDto } from './dtos/update-ad.request.dto'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: false, + forbidUnknownValues: false, + }), +) +@Controller() +export class UpdateAdGrpcController { + constructor(private readonly commandBus: CommandBus) {} + + @GrpcMethod(GRPC_SERVICE_NAME, 'Update') + async update(data: UpdateAdRequestDto): Promise { + try { + const cmdProps = { + adId: data.id, + ...data, + }; + delete (cmdProps as { id?: string }).id; + + await this.commandBus.execute(new UpdateAdCommand(cmdProps)); + } catch (error) { + if (error instanceof NotFoundException) { + throw new RpcException({ + code: RpcExceptionCode.NOT_FOUND, + message: error.message, + }); + } + throw error; + } + } +} diff --git a/tests/unit/ad/interface/ad.fixtures.ts b/tests/unit/ad/interface/ad.fixtures.ts new file mode 100644 index 0000000..6e19747 --- /dev/null +++ b/tests/unit/ad/interface/ad.fixtures.ts @@ -0,0 +1,43 @@ +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; + +const originWaypoint: WaypointDto = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: WaypointDto = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; +export function punctualCreateAdRequest(): CreateAdRequestDto { + return { + userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4', + fromDate: '2023-12-21', + toDate: '2023-12-21', + schedule: [ + { + time: '08:15', + day: 4, + margin: 600, + }, + ], + driver: false, + passenger: true, + seatsRequested: 1, + seatsProposed: 3, + strict: false, + frequency: Frequency.PUNCTUAL, + waypoints: [originWaypoint, destinationWaypoint], + }; +} diff --git a/tests/unit/ad/interface/create-ad.grpc.controller.spec.ts b/tests/unit/ad/interface/create-ad.grpc.controller.spec.ts index b66b4b7..48feaeb 100644 --- a/tests/unit/ad/interface/create-ad.grpc.controller.spec.ts +++ b/tests/unit/ad/interface/create-ad.grpc.controller.spec.ts @@ -1,51 +1,10 @@ -import { IdResponse } from '@mobicoop/ddd-library'; -import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { IdResponse, RpcExceptionCode } from '@mobicoop/ddd-library'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; import { CreateAdGrpcController } from '@modules/ad/interface/grpc-controllers/create-ad.grpc.controller'; -import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto'; -import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { CommandBus } from '@nestjs/cqrs'; import { RpcException } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; - -const originWaypoint: WaypointDto = { - position: 0, - lat: 48.689445, - lon: 6.17651, - houseNumber: '5', - street: 'Avenue Foch', - locality: 'Nancy', - postalCode: '54000', - country: 'France', -}; -const destinationWaypoint: WaypointDto = { - position: 1, - lat: 48.8566, - lon: 2.3522, - locality: 'Paris', - postalCode: '75000', - country: 'France', -}; -const punctualCreateAdRequest: CreateAdRequestDto = { - userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4', - fromDate: '2023-12-21', - toDate: '2023-12-21', - schedule: [ - { - time: '08:15', - day: 4, - margin: 600, - }, - ], - driver: false, - passenger: true, - seatsRequested: 1, - seatsProposed: 3, - strict: false, - frequency: Frequency.PUNCTUAL, - waypoints: [originWaypoint, destinationWaypoint], -}; +import { punctualCreateAdRequest } from './ad.fixtures'; const mockCommandBus = { execute: jest @@ -89,7 +48,7 @@ describe('Create Ad Grpc Controller', () => { it('should create a new ad', async () => { jest.spyOn(mockCommandBus, 'execute'); const result: IdResponse = await createAdGrpcController.create( - punctualCreateAdRequest, + punctualCreateAdRequest(), ); expect(result).toBeInstanceOf(IdResponse); expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2'); @@ -100,7 +59,7 @@ describe('Create Ad Grpc Controller', () => { jest.spyOn(mockCommandBus, 'execute'); expect.assertions(3); try { - await createAdGrpcController.create(punctualCreateAdRequest); + await createAdGrpcController.create(punctualCreateAdRequest()); } catch (e: any) { expect(e).toBeInstanceOf(RpcException); expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS); @@ -112,7 +71,7 @@ describe('Create Ad Grpc Controller', () => { jest.spyOn(mockCommandBus, 'execute'); expect.assertions(3); try { - await createAdGrpcController.create(punctualCreateAdRequest); + await createAdGrpcController.create(punctualCreateAdRequest()); } catch (e: any) { expect(e).toBeInstanceOf(RpcException); expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); diff --git a/tests/unit/ad/interface/update-ad.grpc.controller.spec.ts b/tests/unit/ad/interface/update-ad.grpc.controller.spec.ts new file mode 100644 index 0000000..998e64f --- /dev/null +++ b/tests/unit/ad/interface/update-ad.grpc.controller.spec.ts @@ -0,0 +1,76 @@ +import { NotFoundException, RpcExceptionCode } from '@mobicoop/ddd-library'; +import { UpdateAdGrpcController } from '@modules/ad/interface/grpc-controllers/update-ad.grpc.controller'; +import { CommandBus } from '@nestjs/cqrs'; +import { RpcException } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { punctualCreateAdRequest } from './ad.fixtures'; + +const validAdId = '200d61a8-d878-4378-a609-c19ea71633d2'; +const mockCommandBus = { + execute: jest.fn().mockImplementation(async (command) => { + if (command.adId === '') throw 'Ad id is empty'; + if (command.adId != validAdId) throw new NotFoundException(); + }), +}; + +describe('Update Ad GRPC Controller', () => { + let updateAdGrpcController: UpdateAdGrpcController; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CommandBus, + useValue: mockCommandBus, + }, + UpdateAdGrpcController, + ], + }).compile(); + updateAdGrpcController = module.get( + UpdateAdGrpcController, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(updateAdGrpcController).toBeDefined(); + }); + + it('should execute the update ad command', async () => { + await updateAdGrpcController.update({ + id: validAdId, + ...punctualCreateAdRequest(), + }); + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw a dedicated RpcException if ad is not found', async () => { + expect.assertions(3); + try { + await updateAdGrpcController.update({ + id: 'ac85f5f4-41cd-4c5d-9aee-0a1acb176fb8', + ...punctualCreateAdRequest(), + }); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND); + } + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should rethrow any other exceptions', async () => { + expect.assertions(2); + try { + await updateAdGrpcController.update({ + id: '', + ...punctualCreateAdRequest(), + }); + } catch (e: any) { + expect(e).toBe('Ad id is empty'); + } + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); +}); From f6c320470885b58ca17f5fbb4ae286afe3d50843 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Thu, 2 May 2024 14:45:26 +0200 Subject: [PATCH 08/11] Emit the AdUpdated domain event from the service instead of the repository This is to avoid storing the event in the entity, which prevents serializing it into JSON (because it has a circular dependency to AdEntity) --- .../application/commands/update-ad/update-ad.service.ts | 7 +++++++ src/modules/ad/core/domain/ad.entity.ts | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) 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 index a97d093..cc5790a 100644 --- 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 @@ -2,8 +2,10 @@ import { AD_REPOSITORY, INPUT_DATETIME_TRANSFORMER, } from '@modules/ad/ad.di-tokens'; +import { AdUpdatedDomainEvent } from '@modules/ad/core/domain/events/ad.domain-event'; import { Inject } from '@nestjs/common'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; import { createPropsFromCommand } from '../create-ad/create-ad.service'; @@ -16,6 +18,7 @@ export class UpdateAdService implements ICommandHandler { private readonly repository: AdRepositoryPort, @Inject(INPUT_DATETIME_TRANSFORMER) private readonly datetimeTransformer: DateTimeTransformerPort, + private readonly eventEmitter: EventEmitter2, ) {} async execute(command: UpdateAdCommand): Promise { @@ -25,5 +28,9 @@ export class UpdateAdService implements ICommandHandler { }); ad.update(createPropsFromCommand(command, this.datetimeTransformer)); await this.repository.update(ad.id, ad); + this.eventEmitter.emitAsync( + AdUpdatedDomainEvent.name, + new AdUpdatedDomainEvent(ad), + ); } } diff --git a/src/modules/ad/core/domain/ad.entity.ts b/src/modules/ad/core/domain/ad.entity.ts index 5b045c1..00ea1eb 100644 --- a/src/modules/ad/core/domain/ad.entity.ts +++ b/src/modules/ad/core/domain/ad.entity.ts @@ -6,7 +6,6 @@ import { AdDeletedDomainEvent } from './events/ad-delete.domain-event'; import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event'; import { AdSuspendedDomainEvent } from './events/ad-suspended.domain-event'; import { AdValidatedDomainEvent } from './events/ad-validated.domain-event'; -import { AdUpdatedDomainEvent } from './events/ad.domain-event'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { WaypointProps } from './value-objects/waypoint.value-object'; @@ -111,7 +110,6 @@ export class AdEntity extends AggregateRoot { this.props.waypoints = newProps.waypoints.map((wp) => ({ ...wp })); //The ad goes back to pending status until it is validated again this.props.status = Status.PENDING; - this.addEvent(new AdUpdatedDomainEvent(this)); this.validate(); return this; }; From 5aa4d9e5688ea04c0626ab5716785c414740889d Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Fri, 3 May 2024 11:21:37 +0200 Subject: [PATCH 09/11] Unit tests for the update-ad service --- tests/unit/ad/ad.mocks.ts | 10 +++ tests/unit/ad/core/create-ad.service.spec.ts | 26 +++---- tests/unit/ad/core/update-ad.service.spec.ts | 71 ++++++++++++++++++++ 3 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 tests/unit/ad/ad.mocks.ts create mode 100644 tests/unit/ad/core/update-ad.service.spec.ts diff --git a/tests/unit/ad/ad.mocks.ts b/tests/unit/ad/ad.mocks.ts new file mode 100644 index 0000000..2b97881 --- /dev/null +++ b/tests/unit/ad/ad.mocks.ts @@ -0,0 +1,10 @@ +import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; + +export function mockInputDateTimeTransformer(): DateTimeTransformerPort { + return { + fromDate: jest.fn(), + toDate: jest.fn(), + day: jest.fn(), + time: jest.fn(), + }; +} diff --git a/tests/unit/ad/core/create-ad.service.spec.ts b/tests/unit/ad/core/create-ad.service.spec.ts index 18ed15d..89ac262 100644 --- a/tests/unit/ad/core/create-ad.service.spec.ts +++ b/tests/unit/ad/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_REPOSITORY, INPUT_DATETIME_TRANSFORMER, } from '@modules/ad/ad.di-tokens'; -import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; -import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto'; -import { AggregateID } from '@mobicoop/ddd-library'; -import { AdEntity } from '@modules/ad/core/domain/ad.entity'; -import { ConflictException } from '@mobicoop/ddd-library'; -import { 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 { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; +import { Test, TestingModule } from '@nestjs/testing'; +import { mockInputDateTimeTransformer } from '../ad.mocks'; const originWaypoint: WaypointDto = { position: 0, @@ -64,13 +63,6 @@ const mockAdRepository = { }), }; -const mockInputDateTimeTransformer: DateTimeTransformerPort = { - fromDate: jest.fn(), - toDate: jest.fn(), - day: jest.fn(), - time: jest.fn(), -}; - describe('create-ad.service', () => { let createAdService: CreateAdService; @@ -83,7 +75,7 @@ describe('create-ad.service', () => { }, { provide: INPUT_DATETIME_TRANSFORMER, - useValue: mockInputDateTimeTransformer, + useValue: mockInputDateTimeTransformer(), }, CreateAdService, ], diff --git a/tests/unit/ad/core/update-ad.service.spec.ts b/tests/unit/ad/core/update-ad.service.spec.ts new file mode 100644 index 0000000..9d56fcc --- /dev/null +++ b/tests/unit/ad/core/update-ad.service.spec.ts @@ -0,0 +1,71 @@ +import { + AD_REPOSITORY, + INPUT_DATETIME_TRANSFORMER, +} 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 { AdEntity } from '@modules/ad/core/domain/ad.entity'; +import { Status } from '@modules/ad/core/domain/ad.types'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Test, TestingModule } from '@nestjs/testing'; +import { mockInputDateTimeTransformer } from '../ad.mocks'; +import { punctualCreateAdRequest } from '../interface/ad.fixtures'; +import { punctualPassengerCreateAdProps } from './ad.fixtures'; + +const mockAdRepository = { + findOneById: jest.fn().mockImplementation( + async (id) => + new AdEntity({ + id, + props: { ...punctualPassengerCreateAdProps(), status: Status.VALID }, + }), + ), + update: jest.fn(), +}; + +const mockEventEmitter = { + emitAsync: jest.fn(), +}; + +describe('create-ad.service', () => { + let updateAdService: UpdateAdService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + { + provide: INPUT_DATETIME_TRANSFORMER, + useValue: mockInputDateTimeTransformer(), + }, + { + provide: EventEmitter2, + useValue: mockEventEmitter, + }, + UpdateAdService, + ], + }).compile(); + + updateAdService = module.get(UpdateAdService); + }); + + it('should be defined', () => { + expect(updateAdService).toBeDefined(); + }); + + describe('execute', () => { + it('should update the ad in the repository and emit an event', async () => { + const command = new UpdateAdCommand({ + adId: '200d61a8-d878-4378-a609-c19ea71633d2', + ...punctualCreateAdRequest(), + }); + + await updateAdService.execute(command); + expect(mockAdRepository.update).toHaveBeenCalled(); + expect(mockEventEmitter.emitAsync).toHaveBeenCalled(); + }); + }); +}); From f6f9696620438f5fcb85b0db72e0cb9a94e821a1 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Tue, 7 May 2024 10:41:06 +0200 Subject: [PATCH 10/11] Add a zero-value to the Frequency GRPC enum (required by protobuf specs) --- src/modules/ad/interface/grpc-controllers/ad.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/ad/interface/grpc-controllers/ad.proto b/src/modules/ad/interface/grpc-controllers/ad.proto index 267d106..da93584 100644 --- a/src/modules/ad/interface/grpc-controllers/ad.proto +++ b/src/modules/ad/interface/grpc-controllers/ad.proto @@ -58,6 +58,7 @@ message Waypoint { } enum Frequency { + UNSPECIFIED = 0; PUNCTUAL = 1; RECURRENT = 2; } From 12c237b980df4af9d494359ef3d0b8974aa7c75f Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Fri, 10 May 2024 11:06:40 +0200 Subject: [PATCH 11/11] Update the README doc --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 079a439..b360896 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,9 @@ The app exposes the following [gRPC](https://grpc.io/) services : - waypoints: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives - comment: optional freetext comment / description about the ad +- **Update** : Replace the content of an ad + Accepts the same data as the `Create` function + an ad id, and replace the given ad with the given data. + - **Delete** : Delete permanently an ad ```json @@ -246,13 +249,16 @@ The app exposes the following [gRPC](https://grpc.io/) services : The service listens to these RabbitMQ messages: +- **matcher-ad.created** (to update the status of pending ads) +- **matcher-ad.creation-failed** (to update the status of pending ads) - **user.deleted** (to delete the associated ads) ### Emitted As mentionned earlier, RabbitMQ messages are sent after these events : -- **ad.created** (message: the created ad informations) +- **ad.created** (message: the created ad information) +- **ad.updated** (message: the updated ad information) - **ad.deleted** (message: the id of the deleted ad) ## Tests / ESLint / Prettier