From 1701fbbeb14c80b34b2a2a646c71b3b60bada387 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Thu, 25 Apr 2024 15:27:21 +0200 Subject: [PATCH] Implement a DeleteAdCommand --- src/modules/ad/ad.module.ts | 73 ++++++++++--------- .../commands/delete-ad/delete-ad.command.ts | 7 ++ .../commands/delete-ad/delete-ad.service.ts | 18 +++++ src/modules/ad/core/domain/ad.entity.ts | 11 ++- .../domain/events/ad-delete.domain-event.ts | 7 ++ .../ad/tests/unit/core/ad.entity.spec.ts | 59 +++------------ src/modules/ad/tests/unit/core/ad.fixtures.ts | 40 ++++++++++ .../tests/unit/core/delete-ad.service.spec.ts | 41 +++++++++++ 8 files changed, 172 insertions(+), 84 deletions(-) create mode 100644 src/modules/ad/core/application/commands/delete-ad/delete-ad.command.ts create mode 100644 src/modules/ad/core/application/commands/delete-ad/delete-ad.service.ts create mode 100644 src/modules/ad/core/domain/events/ad-delete.domain-event.ts create mode 100644 src/modules/ad/tests/unit/core/ad.fixtures.ts create mode 100644 src/modules/ad/tests/unit/core/delete-ad.service.spec.ts diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 03c760b..e1e765b 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -1,49 +1,50 @@ -import { Module, Provider } from '@nestjs/common'; -import { CqrsModule } from '@nestjs/cqrs'; -import { - AD_MESSAGE_PUBLISHER, - AD_REPOSITORY, - AD_DIRECTION_ENCODER, - AD_ROUTE_PROVIDER, - TIMEZONE_FINDER, - TIME_CONVERTER, - INPUT_DATETIME_TRANSFORMER, - OUTPUT_DATETIME_TRANSFORMER, - MATCHING_REPOSITORY, - AD_CONFIGURATION_REPOSITORY, - GEOGRAPHY_PACKAGE, -} from './ad.di-tokens'; +import { ConfigurationRepository } from '@mobicoop/configuration-module'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; -import { AdRepository } from './infrastructure/ad.repository'; -import { PrismaService } from './infrastructure/prisma.service'; -import { AdMapper } from './ad.mapper'; -import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler'; -import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder'; import { GeographyModule } from '@modules/geography/geography.module'; -import { CreateAdService } from './core/application/commands/create-ad/create-ad.service'; -import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller'; -import { MatchQueryHandler } from './core/application/queries/match/match.query-handler'; -import { TimezoneFinder } from './infrastructure/timezone-finder'; -import { TimeConverter } from './infrastructure/time-converter'; -import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; -import { MatchMapper } from './match.mapper'; -import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; -import { MatchingRepository } from './infrastructure/matching.repository'; -import { MatchingMapper } from './matching.mapper'; +import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder'; import { CacheModule } from '@nestjs/cache-manager'; +import { Module, Provider } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { redisStore } from 'cache-manager-ioredis-yet'; +import { CqrsModule } from '@nestjs/cqrs'; +import { ClientsModule, Transport } from '@nestjs/microservices'; import { RedisClientOptions, RedisModule, RedisModuleOptions, } from '@songkeys/nestjs-redis'; -import { ConfigurationRepository } from '@mobicoop/configuration-module'; -import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler'; -import { Georouter } from './infrastructure/georouter'; -import { ClientsModule, Transport } from '@nestjs/microservices'; import { GRPC_GEOGRAPHY_PACKAGE_NAME } from '@src/app.constants'; +import { redisStore } from 'cache-manager-ioredis-yet'; import { join } from 'path'; +import { + AD_CONFIGURATION_REPOSITORY, + AD_DIRECTION_ENCODER, + AD_MESSAGE_PUBLISHER, + AD_REPOSITORY, + AD_ROUTE_PROVIDER, + GEOGRAPHY_PACKAGE, + INPUT_DATETIME_TRANSFORMER, + MATCHING_REPOSITORY, + OUTPUT_DATETIME_TRANSFORMER, + TIMEZONE_FINDER, + TIME_CONVERTER, +} from './ad.di-tokens'; +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 { 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'; +import { Georouter } from './infrastructure/georouter'; +import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; +import { MatchingRepository } from './infrastructure/matching.repository'; +import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; +import { PrismaService } from './infrastructure/prisma.service'; +import { TimeConverter } from './infrastructure/time-converter'; +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 { MatchMapper } from './match.mapper'; +import { MatchingMapper } from './matching.mapper'; const imports = [ CqrsModule, @@ -102,7 +103,7 @@ const eventHandlers: Provider[] = [ PublishMessageWhenMatcherAdIsCreatedDomainEventHandler, ]; -const commandHandlers: Provider[] = [CreateAdService]; +const commandHandlers: Provider[] = [CreateAdService, DeleteAdService]; const queryHandlers: Provider[] = [MatchQueryHandler]; diff --git a/src/modules/ad/core/application/commands/delete-ad/delete-ad.command.ts b/src/modules/ad/core/application/commands/delete-ad/delete-ad.command.ts new file mode 100644 index 0000000..1db4234 --- /dev/null +++ b/src/modules/ad/core/application/commands/delete-ad/delete-ad.command.ts @@ -0,0 +1,7 @@ +import { Command, CommandProps } from '@mobicoop/ddd-library'; + +export class DeleteAdCommand extends Command { + constructor(props: CommandProps) { + super(props); + } +} diff --git a/src/modules/ad/core/application/commands/delete-ad/delete-ad.service.ts b/src/modules/ad/core/application/commands/delete-ad/delete-ad.service.ts new file mode 100644 index 0000000..a8d89ee --- /dev/null +++ b/src/modules/ad/core/application/commands/delete-ad/delete-ad.service.ts @@ -0,0 +1,18 @@ +import { AD_REPOSITORY } 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 { DeleteAdCommand } from './delete-ad.command'; + +@CommandHandler(DeleteAdCommand) +export class DeleteAdService implements ICommandHandler { + constructor( + @Inject(AD_REPOSITORY) private readonly adRepository: AdRepositoryPort, + ) {} + + async execute(command: DeleteAdCommand): Promise { + const ad = await this.adRepository.findOneById(command.id); + ad.delete(); + return this.adRepository.delete(ad); + } +} diff --git a/src/modules/ad/core/domain/ad.entity.ts b/src/modules/ad/core/domain/ad.entity.ts index d8a6c37..513485b 100644 --- a/src/modules/ad/core/domain/ad.entity.ts +++ b/src/modules/ad/core/domain/ad.entity.ts @@ -1,5 +1,6 @@ -import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; +import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library'; import { AdProps, CreateAdProps } from './ad.types'; +import { AdDeletedDomainEvent } from './events/ad-delete.domain-event'; import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event'; export class AdEntity extends AggregateRoot { @@ -26,6 +27,14 @@ export class AdEntity extends AggregateRoot { return ad; }; + delete(): void { + this.addEvent( + new AdDeletedDomainEvent({ + aggregateId: this.id, + }), + ); + } + validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } diff --git a/src/modules/ad/core/domain/events/ad-delete.domain-event.ts b/src/modules/ad/core/domain/events/ad-delete.domain-event.ts new file mode 100644 index 0000000..3c4930d --- /dev/null +++ b/src/modules/ad/core/domain/events/ad-delete.domain-event.ts @@ -0,0 +1,7 @@ +import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library'; + +export class AdDeletedDomainEvent extends DomainEvent { + constructor(props: DomainEventProps) { + super(props); + } +} diff --git a/src/modules/ad/tests/unit/core/ad.entity.spec.ts b/src/modules/ad/tests/unit/core/ad.entity.spec.ts index 6a8fe97..a173b2b 100644 --- a/src/modules/ad/tests/unit/core/ad.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/ad.entity.spec.ts @@ -1,52 +1,17 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity'; -import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; -import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object'; - -const originPointProps: PointProps = { - lat: 48.689445, - lon: 6.17651, -}; -const destinationPointProps: PointProps = { - lat: 48.8566, - lon: 2.3522, -}; - -const createAdProps: CreateAdProps = { - id: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36', - driver: true, - passenger: true, - fromDate: '2023-06-21', - toDate: '2023-06-21', - schedule: [ - { - day: 3, - time: '08:30', - margin: 900, - }, - ], - frequency: Frequency.PUNCTUAL, - seatsProposed: 3, - seatsRequested: 1, - strict: false, - waypoints: [originPointProps, destinationPointProps], - driverDistance: 23000, - driverDuration: 900, - passengerDistance: 23000, - passengerDuration: 900, - fwdAzimuth: 283, - backAzimuth: 93, - points: [], -}; +import { createAdProps } from './ad.fixtures'; describe('Ad entity create', () => { - it('should create a new entity', async () => { - const ad: AdEntity = AdEntity.create(createAdProps); - expect(ad.id.length).toBe(36); - expect(ad.getProps().schedule.length).toBe(1); - expect(ad.getProps().schedule[0].day).toBe(3); - expect(ad.getProps().schedule[0].time).toBe('08:30'); - expect(ad.getProps().driver).toBeTruthy(); - expect(ad.getProps().passenger).toBeTruthy(); - expect(ad.getProps().driverDistance).toBe(23000); + describe('create', () => { + it('should create a new entity', async () => { + const ad: AdEntity = AdEntity.create(createAdProps()); + expect(ad.id.length).toBe(36); + expect(ad.getProps().schedule.length).toBe(1); + expect(ad.getProps().schedule[0].day).toBe(3); + expect(ad.getProps().schedule[0].time).toBe('08:30'); + expect(ad.getProps().driver).toBeTruthy(); + expect(ad.getProps().passenger).toBeTruthy(); + expect(ad.getProps().driverDistance).toBe(23000); + }); }); }); diff --git a/src/modules/ad/tests/unit/core/ad.fixtures.ts b/src/modules/ad/tests/unit/core/ad.fixtures.ts new file mode 100644 index 0000000..da6db28 --- /dev/null +++ b/src/modules/ad/tests/unit/core/ad.fixtures.ts @@ -0,0 +1,40 @@ +import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; +import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object'; + +const originPointProps: PointProps = { + lat: 48.689445, + lon: 6.17651, +}; +const destinationPointProps: PointProps = { + lat: 48.8566, + lon: 2.3522, +}; + +export function createAdProps(): CreateAdProps { + return { + id: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36', + driver: true, + passenger: true, + fromDate: '2023-06-21', + toDate: '2023-06-21', + schedule: [ + { + day: 3, + time: '08:30', + margin: 900, + }, + ], + frequency: Frequency.PUNCTUAL, + seatsProposed: 3, + seatsRequested: 1, + strict: false, + waypoints: [originPointProps, destinationPointProps], + driverDistance: 23000, + driverDuration: 900, + passengerDistance: 23000, + passengerDuration: 900, + fwdAzimuth: 283, + backAzimuth: 93, + points: [], + }; +} diff --git a/src/modules/ad/tests/unit/core/delete-ad.service.spec.ts b/src/modules/ad/tests/unit/core/delete-ad.service.spec.ts new file mode 100644 index 0000000..bd9eff3 --- /dev/null +++ b/src/modules/ad/tests/unit/core/delete-ad.service.spec.ts @@ -0,0 +1,41 @@ +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { DeleteAdCommand } from '@modules/ad/core/application/commands/delete-ad/delete-ad.command'; +import { DeleteAdService } from '@modules/ad/core/application/commands/delete-ad/delete-ad.service'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createAdProps } from './ad.fixtures'; + +const ad: AdEntity = AdEntity.create(createAdProps()); +const mockAdRepository = { + findOneById: jest.fn().mockImplementation(() => ad), + delete: jest.fn(), +}; + +describe('DeleteAdService', () => { + let deleteAdService: DeleteAdService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + DeleteAdService, + ], + }).compile(); + + deleteAdService = module.get(DeleteAdService); + }); + + it('should be defined', () => { + expect(deleteAdService).toBeDefined(); + }); + + it('should execute the delete logic and delete the ad from the repository', async () => { + jest.spyOn(ad, 'delete'); + await deleteAdService.execute(new DeleteAdCommand(ad.id)); + expect(ad.delete).toHaveBeenCalled(); + expect(mockAdRepository.delete).toHaveBeenCalledWith(ad); + }); +});