From e0a4b0773369506fa2d54d5d1fcbde76b440dc19 Mon Sep 17 00:00:00 2001 From: Sylvain Briat Date: Wed, 6 Dec 2023 15:51:46 +0100 Subject: [PATCH 1/2] handle matcher messages --- .../20231205142727_status/migration.sql | 5 + prisma/schema.prisma | 8 ++ src/app.constants.ts | 8 ++ src/modules/ad/ad.mapper.ts | 5 +- src/modules/ad/ad.module.ts | 16 ++- .../invalidate-ad/invalidate-ad.command.ts | 7 ++ .../invalidate-ad/invalidate-ad.service.ts | 25 +++++ .../validate-ad/validate-ad.command.ts | 7 ++ .../validate-ad/validate-ad.service.ts | 25 +++++ ...when-ad-is-created.domain-event-handler.ts | 2 +- src/modules/ad/core/domain/ad.entity.ts | 55 ++++++++++- src/modules/ad/core/domain/ad.types.ts | 8 ++ ...n-events.ts => ad-created.domain-event.ts} | 0 .../events/ad-invalidated.domain-event.ts | 7 ++ .../events/ad-suspended.domain-event.ts | 7 ++ .../events/ad-validated.domain-event.ts | 7 ++ .../ad/infrastructure/ad.repository.ts | 1 + .../ad/interface/dtos/ad.response.dto.ts | 3 +- .../matcher-ad-created.message-handler.ts | 24 +++++ ...cher-ad-creation-failed.message-handler.ts | 24 +++++ .../message-handlers/matcher-ad.types.ts | 4 + src/modules/ad/tests/unit/ad.mapper.spec.ts | 4 +- .../ad/tests/unit/core/ad.entity.spec.ts | 37 ++++++- .../unit/core/invalidate-ad.service.spec.ts | 98 +++++++++++++++++++ ...ad-is-created.domain-event-handler.spec.ts | 2 +- .../unit/core/validate-ad.service.spec.ts | 98 +++++++++++++++++++ ...matcher-ad-created.message-handler.spec.ts | 46 +++++++++ ...ad-creation-failed.message-handler.spec.ts | 47 +++++++++ src/modules/messager/messager.module.ts | 20 +++- 29 files changed, 589 insertions(+), 11 deletions(-) create mode 100644 prisma/migrations/20231205142727_status/migration.sql create mode 100644 src/modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command.ts create mode 100644 src/modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service.ts create mode 100644 src/modules/ad/core/application/commands/validate-ad/validate-ad.command.ts create mode 100644 src/modules/ad/core/application/commands/validate-ad/validate-ad.service.ts rename src/modules/ad/core/domain/events/{ad-created.domain-events.ts => ad-created.domain-event.ts} (100%) create mode 100644 src/modules/ad/core/domain/events/ad-invalidated.domain-event.ts create mode 100644 src/modules/ad/core/domain/events/ad-suspended.domain-event.ts create mode 100644 src/modules/ad/core/domain/events/ad-validated.domain-event.ts create mode 100644 src/modules/ad/interface/message-handlers/matcher-ad-created.message-handler.ts create mode 100644 src/modules/ad/interface/message-handlers/matcher-ad-creation-failed.message-handler.ts create mode 100644 src/modules/ad/interface/message-handlers/matcher-ad.types.ts create mode 100644 src/modules/ad/tests/unit/core/invalidate-ad.service.spec.ts create mode 100644 src/modules/ad/tests/unit/core/validate-ad.service.spec.ts create mode 100644 src/modules/ad/tests/unit/interface/matcher-ad-created.message-handler.spec.ts create mode 100644 src/modules/ad/tests/unit/interface/matcher-ad-creation-failed.message-handler.spec.ts diff --git a/prisma/migrations/20231205142727_status/migration.sql b/prisma/migrations/20231205142727_status/migration.sql new file mode 100644 index 0000000..0f3acf3 --- /dev/null +++ b/prisma/migrations/20231205142727_status/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "Status" AS ENUM ('PENDING', 'VALID', 'INVALID', 'SUSPENDED'); + +-- AlterTable +ALTER TABLE "ad" ADD COLUMN "status" "Status" NOT NULL DEFAULT 'PENDING'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ec79a89..13cc35a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,6 +14,7 @@ datasource db { model Ad { uuid String @id @default(uuid()) @db.Uuid userUuid String @db.Uuid + status Status @default(PENDING) driver Boolean passenger Boolean frequency Frequency @@ -66,3 +67,10 @@ enum Frequency { PUNCTUAL RECURRENT } + +enum Status { + PENDING + VALID + INVALID + SUSPENDED +} diff --git a/src/app.constants.ts b/src/app.constants.ts index 921a3b9..a29cf71 100644 --- a/src/app.constants.ts +++ b/src/app.constants.ts @@ -7,6 +7,14 @@ export const GRPC_SERVICE_NAME = 'AdService'; // messaging export const AD_CREATED_ROUTING_KEY = 'ad.created'; +export const MATCHER_AD_CREATED_MESSAGE_HANDLER = 'matcherAdCreated'; +export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher.ad.created'; +export const MATCHER_AD_CREATED_QUEUE = 'matcher-ad-created'; +export const MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER = + 'matcherAdCreationFailed'; +export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY = + 'matcher.ad.creation.failed'; +export const MATCHER_AD_CREATION_FAILED_QUEUE = 'matcher-ad-creation-failed'; // configuration export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set'; diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index ebe9ce7..cf036d9 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -8,7 +8,7 @@ import { WaypointModel, ScheduleItemModel, } from './infrastructure/ad.repository'; -import { Frequency } from './core/domain/ad.types'; +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'; @@ -39,6 +39,7 @@ export class AdMapper userUuid: copy.userId, driver: copy.driver as boolean, passenger: copy.passenger as boolean, + status: copy.status, frequency: copy.frequency, fromDate: new Date(copy.fromDate), toDate: new Date(copy.toDate), @@ -92,6 +93,7 @@ export class AdMapper userId: record.userUuid, driver: record.driver, passenger: record.passenger, + status: record.status as Status, frequency: record.frequency as Frequency, fromDate: record.fromDate.toISOString().split('T')[0], toDate: record.toDate.toISOString().split('T')[0], @@ -135,6 +137,7 @@ export class AdMapper response.userId = props.userId; response.driver = props.driver as boolean; response.passenger = props.passenger as boolean; + 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 28cad5b..29431c4 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -23,6 +23,10 @@ import { InputDateTimeTransformer } from './infrastructure/input-datetime-transf import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller'; import { FindAdsByIdsQueryHandler } from './core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler'; +import { MatcherAdCreatedMessageHandler } from './interface/message-handlers/matcher-ad-created.message-handler'; +import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service'; +import { MatcherAdCreationFailedMessageHandler } from './interface/message-handlers/matcher-ad-creation-failed.message-handler'; +import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service'; const grpcControllers = [ CreateAdGrpcController, @@ -30,11 +34,20 @@ const grpcControllers = [ FindAdsByIdsGrpcController, ]; +const messageHandlers = [ + MatcherAdCreatedMessageHandler, + MatcherAdCreationFailedMessageHandler, +]; + const eventHandlers: Provider[] = [ PublishMessageWhenAdIsCreatedDomainEventHandler, ]; -const commandHandlers: Provider[] = [CreateAdService]; +const commandHandlers: Provider[] = [ + CreateAdService, + ValidateAdService, + InvalidateAdService, +]; const queryHandlers: Provider[] = [ FindAdByIdQueryHandler, @@ -81,6 +94,7 @@ const adapters: Provider[] = [ imports: [CqrsModule], controllers: [...grpcControllers], providers: [ + ...messageHandlers, ...eventHandlers, ...commandHandlers, ...queryHandlers, diff --git a/src/modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command.ts b/src/modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command.ts new file mode 100644 index 0000000..f023b8c --- /dev/null +++ b/src/modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command.ts @@ -0,0 +1,7 @@ +import { Command, CommandProps } from '@mobicoop/ddd-library'; + +export class InvalidateAdCommand extends Command { + constructor(props: CommandProps) { + super(props); + } +} diff --git a/src/modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service.ts b/src/modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service.ts new file mode 100644 index 0000000..9587e0d --- /dev/null +++ b/src/modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service.ts @@ -0,0 +1,25 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { InvalidateAdCommand } from './invalidate-ad.command'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { Inject } from '@nestjs/common'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; +import { AggregateID } from '@mobicoop/ddd-library'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; + +@CommandHandler(InvalidateAdCommand) +export class InvalidateAdService implements ICommandHandler { + constructor( + @Inject(AD_REPOSITORY) + private readonly repository: AdRepositoryPort, + ) {} + + async execute(command: InvalidateAdCommand): Promise { + const ad: AdEntity = await this.repository.findOneById(command.id, { + waypoints: true, + schedule: true, + }); + ad.invalid(); + await this.repository.update(ad.id, ad); + return ad.id; + } +} diff --git a/src/modules/ad/core/application/commands/validate-ad/validate-ad.command.ts b/src/modules/ad/core/application/commands/validate-ad/validate-ad.command.ts new file mode 100644 index 0000000..17c7801 --- /dev/null +++ b/src/modules/ad/core/application/commands/validate-ad/validate-ad.command.ts @@ -0,0 +1,7 @@ +import { Command, CommandProps } from '@mobicoop/ddd-library'; + +export class ValidateAdCommand extends Command { + constructor(props: CommandProps) { + super(props); + } +} diff --git a/src/modules/ad/core/application/commands/validate-ad/validate-ad.service.ts b/src/modules/ad/core/application/commands/validate-ad/validate-ad.service.ts new file mode 100644 index 0000000..4d99750 --- /dev/null +++ b/src/modules/ad/core/application/commands/validate-ad/validate-ad.service.ts @@ -0,0 +1,25 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { ValidateAdCommand } from './validate-ad.command'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { Inject } from '@nestjs/common'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; +import { AggregateID } from '@mobicoop/ddd-library'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; + +@CommandHandler(ValidateAdCommand) +export class ValidateAdService implements ICommandHandler { + constructor( + @Inject(AD_REPOSITORY) + private readonly repository: AdRepositoryPort, + ) {} + + async execute(command: ValidateAdCommand): Promise { + const ad: AdEntity = await this.repository.findOneById(command.id, { + waypoints: true, + schedule: true, + }); + ad.valid(); + await this.repository.update(ad.id, ad); + return ad.id; + } +} diff --git a/src/modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler.ts b/src/modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler.ts index 1acae44..b2acc7a 100644 --- a/src/modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler.ts +++ b/src/modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events'; +import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-event'; import { MessagePublisherPort } from '@mobicoop/ddd-library'; import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens'; import { AD_CREATED_ROUTING_KEY } from '@src/app.constants'; diff --git a/src/modules/ad/core/domain/ad.entity.ts b/src/modules/ad/core/domain/ad.entity.ts index 75f9553..1c4f78c 100644 --- a/src/modules/ad/core/domain/ad.entity.ts +++ b/src/modules/ad/core/domain/ad.entity.ts @@ -1,19 +1,26 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { v4 } from 'uuid'; -import { AdCreatedDomainEvent } from './events/ad-created.domain-events'; -import { AdProps, CreateAdProps } from './ad.types'; +import { AdCreatedDomainEvent } from './events/ad-created.domain-event'; +import { AdProps, CreateAdProps, Status } from './ad.types'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { WaypointProps } from './value-objects/waypoint.value-object'; +import { AdValidatedDomainEvent } from './events/ad-validated.domain-event'; +import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event'; +import { AdSuspendedDomainEvent } from './events/ad-suspended.domain-event'; export class AdEntity extends AggregateRoot { protected readonly _id: AggregateID; static create = (create: CreateAdProps): AdEntity => { const id = v4(); - const props: AdProps = { ...create }; + const props: AdProps = { ...create, status: Status.PENDING }; const ad = new AdEntity({ id, props }); ad.addEvent( new AdCreatedDomainEvent({ + metadata: { + correlationId: id, + timestamp: Date.now(), + }, aggregateId: id, userId: props.userId, driver: props.driver, @@ -45,6 +52,48 @@ export class AdEntity extends AggregateRoot { return ad; }; + valid = (): AdEntity => { + this.props.status = Status.VALID; + this.addEvent( + new AdValidatedDomainEvent({ + metadata: { + correlationId: this.id, + timestamp: Date.now(), + }, + aggregateId: this.id, + }), + ); + return this; + }; + + invalid = (): AdEntity => { + this.props.status = Status.INVALID; + this.addEvent( + new AdInvalidatedDomainEvent({ + metadata: { + correlationId: this.id, + timestamp: Date.now(), + }, + aggregateId: this.id, + }), + ); + return this; + }; + + suspend = (): AdEntity => { + this.props.status = Status.SUSPENDED; + this.addEvent( + new AdSuspendedDomainEvent({ + metadata: { + correlationId: this.id, + timestamp: Date.now(), + }, + aggregateId: this.id, + }), + ); + return this; + }; + 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/ad.types.ts b/src/modules/ad/core/domain/ad.types.ts index c3d98c5..64f53e6 100644 --- a/src/modules/ad/core/domain/ad.types.ts +++ b/src/modules/ad/core/domain/ad.types.ts @@ -5,6 +5,7 @@ import { WaypointProps } from './value-objects/waypoint.value-object'; export interface AdProps { userId: string; driver: boolean; + status: Status; passenger: boolean; frequency: Frequency; fromDate: string; @@ -35,3 +36,10 @@ export enum Frequency { PUNCTUAL = 'PUNCTUAL', RECURRENT = 'RECURRENT', } + +export enum Status { + PENDING = 'PENDING', + VALID = 'VALID', + INVALID = 'INVALID', + SUSPENDED = 'SUSPENDED', +} diff --git a/src/modules/ad/core/domain/events/ad-created.domain-events.ts b/src/modules/ad/core/domain/events/ad-created.domain-event.ts similarity index 100% rename from src/modules/ad/core/domain/events/ad-created.domain-events.ts rename to src/modules/ad/core/domain/events/ad-created.domain-event.ts diff --git a/src/modules/ad/core/domain/events/ad-invalidated.domain-event.ts b/src/modules/ad/core/domain/events/ad-invalidated.domain-event.ts new file mode 100644 index 0000000..7b3ff12 --- /dev/null +++ b/src/modules/ad/core/domain/events/ad-invalidated.domain-event.ts @@ -0,0 +1,7 @@ +import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library'; + +export class AdInvalidatedDomainEvent extends DomainEvent { + constructor(props: DomainEventProps) { + super(props); + } +} diff --git a/src/modules/ad/core/domain/events/ad-suspended.domain-event.ts b/src/modules/ad/core/domain/events/ad-suspended.domain-event.ts new file mode 100644 index 0000000..587148f --- /dev/null +++ b/src/modules/ad/core/domain/events/ad-suspended.domain-event.ts @@ -0,0 +1,7 @@ +import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library'; + +export class AdSuspendedDomainEvent extends DomainEvent { + constructor(props: DomainEventProps) { + super(props); + } +} diff --git a/src/modules/ad/core/domain/events/ad-validated.domain-event.ts b/src/modules/ad/core/domain/events/ad-validated.domain-event.ts new file mode 100644 index 0000000..148394d --- /dev/null +++ b/src/modules/ad/core/domain/events/ad-validated.domain-event.ts @@ -0,0 +1,7 @@ +import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library'; + +export class AdValidatedDomainEvent extends DomainEvent { + constructor(props: DomainEventProps) { + super(props); + } +} diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index 583b39f..7c3f380 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -17,6 +17,7 @@ export type AdBaseModel = { userUuid: string; driver: boolean; passenger: boolean; + status: string; frequency: string; fromDate: Date; toDate: Date; diff --git a/src/modules/ad/interface/dtos/ad.response.dto.ts b/src/modules/ad/interface/dtos/ad.response.dto.ts index 0a2f903..6c22522 100644 --- a/src/modules/ad/interface/dtos/ad.response.dto.ts +++ b/src/modules/ad/interface/dtos/ad.response.dto.ts @@ -1,10 +1,11 @@ import { ResponseBase } from '@mobicoop/ddd-library'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Status } from '@modules/ad/core/domain/ad.types'; export class AdResponseDto extends ResponseBase { userId: string; driver: boolean; passenger: boolean; + status: Status; frequency: Frequency; fromDate: string; toDate: string; diff --git a/src/modules/ad/interface/message-handlers/matcher-ad-created.message-handler.ts b/src/modules/ad/interface/message-handlers/matcher-ad-created.message-handler.ts new file mode 100644 index 0000000..9ed1e4a --- /dev/null +++ b/src/modules/ad/interface/message-handlers/matcher-ad-created.message-handler.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { RabbitSubscribe } from '@mobicoop/message-broker-module'; +import { CommandBus } from '@nestjs/cqrs'; +import { MATCHER_AD_CREATED_MESSAGE_HANDLER } from '@src/app.constants'; +import { MatcherAdCreatedIntegrationEvent } from './matcher-ad.types'; +import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command'; + +@Injectable() +export class MatcherAdCreatedMessageHandler { + constructor(private readonly commandBus: CommandBus) {} + + @RabbitSubscribe({ + name: MATCHER_AD_CREATED_MESSAGE_HANDLER, + }) + public async matcherAdCreated(message: string) { + const matcherAdCreatedIntegrationEvent: MatcherAdCreatedIntegrationEvent = + JSON.parse(message); + await this.commandBus.execute( + new ValidateAdCommand({ + id: matcherAdCreatedIntegrationEvent.id, + }), + ); + } +} diff --git a/src/modules/ad/interface/message-handlers/matcher-ad-creation-failed.message-handler.ts b/src/modules/ad/interface/message-handlers/matcher-ad-creation-failed.message-handler.ts new file mode 100644 index 0000000..05e3691 --- /dev/null +++ b/src/modules/ad/interface/message-handlers/matcher-ad-creation-failed.message-handler.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { RabbitSubscribe } from '@mobicoop/message-broker-module'; +import { CommandBus } from '@nestjs/cqrs'; +import { MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER } from '@src/app.constants'; +import { MatcherAdCreationFailedIntegrationEvent } from './matcher-ad.types'; +import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command'; + +@Injectable() +export class MatcherAdCreationFailedMessageHandler { + constructor(private readonly commandBus: CommandBus) {} + + @RabbitSubscribe({ + name: MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER, + }) + public async matcherAdCreationFailed(message: string) { + const matcherAdCreationFailedIntegrationEvent: MatcherAdCreationFailedIntegrationEvent = + JSON.parse(message); + await this.commandBus.execute( + new InvalidateAdCommand({ + id: matcherAdCreationFailedIntegrationEvent.id, + }), + ); + } +} diff --git a/src/modules/ad/interface/message-handlers/matcher-ad.types.ts b/src/modules/ad/interface/message-handlers/matcher-ad.types.ts new file mode 100644 index 0000000..73df66c --- /dev/null +++ b/src/modules/ad/interface/message-handlers/matcher-ad.types.ts @@ -0,0 +1,4 @@ +import { IntegrationEvent } from '@mobicoop/ddd-library'; + +export type MatcherAdCreatedIntegrationEvent = IntegrationEvent; +export type MatcherAdCreationFailedIntegrationEvent = IntegrationEvent; diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index e47333a..dba2f55 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -2,7 +2,7 @@ import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Status } from '@modules/ad/core/domain/ad.types'; import { AdReadModel, AdWriteModel, @@ -17,6 +17,7 @@ const adEntity: AdEntity = new AdEntity({ userId: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e', driver: false, passenger: true, + status: Status.PENDING, frequency: Frequency.PUNCTUAL, fromDate: '2023-06-21', toDate: '2023-06-21', @@ -67,6 +68,7 @@ const adReadModel: AdReadModel = { userUuid: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e', driver: false, passenger: true, + status: Status.PENDING, frequency: Frequency.PUNCTUAL, fromDate: new Date('2023-06-21'), toDate: new Date('2023-06-21'), 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 7876c08..e6b3c0f 100644 --- a/src/modules/ad/tests/unit/core/ad.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/ad.entity.spec.ts @@ -1,5 +1,9 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity'; -import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; +import { + CreateAdProps, + Frequency, + Status, +} from '@modules/ad/core/domain/ad.types'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; const originWaypointProps: WaypointProps = { @@ -124,6 +128,7 @@ describe('Ad entity create', () => { punctualPassengerCreateAdProps, ); expect(punctualPassengerAd.id.length).toBe(36); + expect(punctualPassengerAd.getProps().status).toBe(Status.PENDING); expect(punctualPassengerAd.getProps().schedule.length).toBe(1); expect(punctualPassengerAd.getProps().schedule[0].day).toBe(3); expect(punctualPassengerAd.getProps().schedule[0].time).toBe('08:30'); @@ -191,3 +196,33 @@ describe('Ad entity create', () => { }); }); }); + +describe('Ad entity validate status', () => { + it('should validate status of a pending ad entity', async () => { + const punctualPassengerAd: AdEntity = AdEntity.create( + punctualPassengerCreateAdProps, + ); + punctualPassengerAd.valid(); + expect(punctualPassengerAd.getProps().status).toBe(Status.VALID); + }); +}); + +describe('Ad entity invalidate status', () => { + it('should invalidate status of a pending ad entity', async () => { + const punctualPassengerAd: AdEntity = AdEntity.create( + punctualPassengerCreateAdProps, + ); + punctualPassengerAd.invalid(); + expect(punctualPassengerAd.getProps().status).toBe(Status.INVALID); + }); +}); + +describe('Ad entity suspend status', () => { + it('should suspend status of a pending ad entity', async () => { + const punctualPassengerAd: AdEntity = AdEntity.create( + punctualPassengerCreateAdProps, + ); + punctualPassengerAd.suspend(); + expect(punctualPassengerAd.getProps().status).toBe(Status.SUSPENDED); + }); +}); diff --git a/src/modules/ad/tests/unit/core/invalidate-ad.service.spec.ts b/src/modules/ad/tests/unit/core/invalidate-ad.service.spec.ts new file mode 100644 index 0000000..db72a24 --- /dev/null +++ b/src/modules/ad/tests/unit/core/invalidate-ad.service.spec.ts @@ -0,0 +1,98 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { AggregateID } from '@mobicoop/ddd-library'; +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'; + +const originWaypointProps: WaypointProps = { + position: 0, + address: { + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', + coordinates: { + lat: 48.689445, + lon: 6.17651, + }, + }, +}; +const destinationWaypointProps: WaypointProps = { + position: 1, + address: { + locality: 'Paris', + postalCode: '75000', + country: 'France', + coordinates: { + lat: 48.8566, + lon: 2.3522, + }, + }, +}; +const baseCreateAdProps = { + userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36', + seatsProposed: 3, + seatsRequested: 1, + strict: false, + waypoints: [originWaypointProps, destinationWaypointProps], +}; +const punctualCreateAdProps = { + fromDate: '2023-06-22', + toDate: '2023-06-22', + schedule: [ + { + time: '08:30', + }, + ], + frequency: Frequency.PUNCTUAL, +}; +const punctualPassengerCreateAdProps: CreateAdProps = { + ...baseCreateAdProps, + ...punctualCreateAdProps, + driver: false, + passenger: true, +}; + +const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps); + +const mockAdRepository = { + findOneById: jest.fn().mockImplementation(() => ad), + update: jest.fn().mockImplementation(() => ad.id), +}; + +describe('Invalidate Ad Service', () => { + let invalidateAdService: InvalidateAdService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + InvalidateAdService, + ], + }).compile(); + + invalidateAdService = module.get(InvalidateAdService); + }); + + it('should be defined', () => { + expect(invalidateAdService).toBeDefined(); + }); + + describe('execution', () => { + it('should invalidate an ad', async () => { + jest.spyOn(ad, 'invalid'); + const invalidateAdCommand = new InvalidateAdCommand(ad.id); + const result: AggregateID = + await invalidateAdService.execute(invalidateAdCommand); + expect(result).toBe(ad.id); + expect(ad.invalid).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/core/publish-message-when-ad-is-created.domain-event-handler.spec.ts b/src/modules/ad/tests/unit/core/publish-message-when-ad-is-created.domain-event-handler.spec.ts index a4c98cb..eeec168 100644 --- a/src/modules/ad/tests/unit/core/publish-message-when-ad-is-created.domain-event-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/publish-message-when-ad-is-created.domain-event-handler.spec.ts @@ -1,6 +1,6 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; import { PublishMessageWhenAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler'; -import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-events'; +import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-event'; import { Test, TestingModule } from '@nestjs/testing'; import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens'; import { AD_CREATED_ROUTING_KEY } from '@src/app.constants'; diff --git a/src/modules/ad/tests/unit/core/validate-ad.service.spec.ts b/src/modules/ad/tests/unit/core/validate-ad.service.spec.ts new file mode 100644 index 0000000..0a30c89 --- /dev/null +++ b/src/modules/ad/tests/unit/core/validate-ad.service.spec.ts @@ -0,0 +1,98 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { AggregateID } from '@mobicoop/ddd-library'; +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'; + +const originWaypointProps: WaypointProps = { + position: 0, + address: { + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', + coordinates: { + lat: 48.689445, + lon: 6.17651, + }, + }, +}; +const destinationWaypointProps: WaypointProps = { + position: 1, + address: { + locality: 'Paris', + postalCode: '75000', + country: 'France', + coordinates: { + lat: 48.8566, + lon: 2.3522, + }, + }, +}; +const baseCreateAdProps = { + userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36', + seatsProposed: 3, + seatsRequested: 1, + strict: false, + waypoints: [originWaypointProps, destinationWaypointProps], +}; +const punctualCreateAdProps = { + fromDate: '2023-06-22', + toDate: '2023-06-22', + schedule: [ + { + time: '08:30', + }, + ], + frequency: Frequency.PUNCTUAL, +}; +const punctualPassengerCreateAdProps: CreateAdProps = { + ...baseCreateAdProps, + ...punctualCreateAdProps, + driver: false, + passenger: true, +}; + +const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps); + +const mockAdRepository = { + findOneById: jest.fn().mockImplementation(() => ad), + update: jest.fn().mockImplementation(() => ad.id), +}; + +describe('Validate Ad Service', () => { + let validateAdService: ValidateAdService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + ValidateAdService, + ], + }).compile(); + + validateAdService = module.get(ValidateAdService); + }); + + it('should be defined', () => { + expect(validateAdService).toBeDefined(); + }); + + describe('execution', () => { + it('should validate an ad', async () => { + jest.spyOn(ad, 'valid'); + const validateAdCommand = new ValidateAdCommand(ad.id); + const result: AggregateID = + await validateAdService.execute(validateAdCommand); + expect(result).toBe(ad.id); + expect(ad.valid).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/interface/matcher-ad-created.message-handler.spec.ts b/src/modules/ad/tests/unit/interface/matcher-ad-created.message-handler.spec.ts new file mode 100644 index 0000000..f69c126 --- /dev/null +++ b/src/modules/ad/tests/unit/interface/matcher-ad-created.message-handler.spec.ts @@ -0,0 +1,46 @@ +import { MatcherAdCreatedMessageHandler } from '@modules/ad/interface/message-handlers/matcher-ad-created.message-handler'; +import { CommandBus } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; + +const matcherAdCreatedMessage = + '{"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4","driverDuration":"3512","driverDistance":"65845","fwdAzimuth":"90","backAzimuth":"270"}'; + +const mockCommandBus = { + execute: jest.fn(), +}; + +describe('Matcher Ad Created Message Handler', () => { + let matcherAdCreatedMessageHandler: MatcherAdCreatedMessageHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CommandBus, + useValue: mockCommandBus, + }, + MatcherAdCreatedMessageHandler, + ], + }).compile(); + + matcherAdCreatedMessageHandler = module.get( + MatcherAdCreatedMessageHandler, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(matcherAdCreatedMessageHandler).toBeDefined(); + }); + + it('should validate an ad', async () => { + jest.spyOn(mockCommandBus, 'execute'); + await matcherAdCreatedMessageHandler.matcherAdCreated( + matcherAdCreatedMessage, + ); + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/ad/tests/unit/interface/matcher-ad-creation-failed.message-handler.spec.ts b/src/modules/ad/tests/unit/interface/matcher-ad-creation-failed.message-handler.spec.ts new file mode 100644 index 0000000..753dfc7 --- /dev/null +++ b/src/modules/ad/tests/unit/interface/matcher-ad-creation-failed.message-handler.spec.ts @@ -0,0 +1,47 @@ +import { MatcherAdCreationFailedMessageHandler } from '@modules/ad/interface/message-handlers/matcher-ad-creation-failed.message-handler'; +import { CommandBus } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; + +const matcherAdCreationFailedMessage = + '{"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4"}'; + +const mockCommandBus = { + execute: jest.fn(), +}; + +describe('Matcher Ad Creation Failed Message Handler', () => { + let matcherAdCreationFailedMessageHandler: MatcherAdCreationFailedMessageHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CommandBus, + useValue: mockCommandBus, + }, + MatcherAdCreationFailedMessageHandler, + ], + }).compile(); + + matcherAdCreationFailedMessageHandler = + module.get( + MatcherAdCreationFailedMessageHandler, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(matcherAdCreationFailedMessageHandler).toBeDefined(); + }); + + it('should invalidate an ad', async () => { + jest.spyOn(mockCommandBus, 'execute'); + await matcherAdCreationFailedMessageHandler.matcherAdCreationFailed( + matcherAdCreationFailedMessage, + ); + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/messager/messager.module.ts b/src/modules/messager/messager.module.ts index d234c61..10dd0ed 100644 --- a/src/modules/messager/messager.module.ts +++ b/src/modules/messager/messager.module.ts @@ -2,7 +2,15 @@ import { Module, Provider } from '@nestjs/common'; import { MESSAGE_PUBLISHER } from './messager.di-tokens'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { SERVICE_NAME } from '@src/app.constants'; +import { + MATCHER_AD_CREATED_MESSAGE_HANDLER, + MATCHER_AD_CREATED_QUEUE, + MATCHER_AD_CREATED_ROUTING_KEY, + MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER, + MATCHER_AD_CREATION_FAILED_QUEUE, + MATCHER_AD_CREATION_FAILED_ROUTING_KEY, + SERVICE_NAME, +} from '@src/app.constants'; import { MessageBrokerModule, MessageBrokerModuleOptions, @@ -24,6 +32,16 @@ const imports = [ ) as boolean, }, name: SERVICE_NAME, + handlers: { + [MATCHER_AD_CREATED_MESSAGE_HANDLER]: { + routingKey: MATCHER_AD_CREATED_ROUTING_KEY, + queue: MATCHER_AD_CREATED_QUEUE, + }, + [MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER]: { + routingKey: MATCHER_AD_CREATION_FAILED_ROUTING_KEY, + queue: MATCHER_AD_CREATION_FAILED_QUEUE, + }, + }, }), }), ]; From c85d6fb7560a4d75acfed6602c828deec658732c Mon Sep 17 00:00:00 2001 From: Sylvain Briat Date: Wed, 6 Dec 2023 15:51:50 +0100 Subject: [PATCH 2/2] 2.4.0 --- 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 64934de..0e0cd4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mobicoop/ad", - "version": "2.3.0", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mobicoop/ad", - "version": "2.3.0", + "version": "2.4.0", "license": "AGPL", "dependencies": { "@grpc/grpc-js": "^1.9.11", diff --git a/package.json b/package.json index 75a46eb..ebfc281 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mobicoop/ad", - "version": "2.3.0", + "version": "2.4.0", "description": "Mobicoop V3 Ad", "author": "sbriat", "private": true,