handle matcher messages

This commit is contained in:
Sylvain Briat 2023-12-06 15:51:46 +01:00
parent dfe4db8276
commit e0a4b07733
29 changed files with 589 additions and 11 deletions

View File

@ -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';

View File

@ -14,6 +14,7 @@ datasource db {
model Ad { model Ad {
uuid String @id @default(uuid()) @db.Uuid uuid String @id @default(uuid()) @db.Uuid
userUuid String @db.Uuid userUuid String @db.Uuid
status Status @default(PENDING)
driver Boolean driver Boolean
passenger Boolean passenger Boolean
frequency Frequency frequency Frequency
@ -66,3 +67,10 @@ enum Frequency {
PUNCTUAL PUNCTUAL
RECURRENT RECURRENT
} }
enum Status {
PENDING
VALID
INVALID
SUSPENDED
}

View File

@ -7,6 +7,14 @@ export const GRPC_SERVICE_NAME = 'AdService';
// messaging // messaging
export const AD_CREATED_ROUTING_KEY = 'ad.created'; 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 // configuration
export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set'; export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set';

View File

@ -8,7 +8,7 @@ import {
WaypointModel, WaypointModel,
ScheduleItemModel, ScheduleItemModel,
} from './infrastructure/ad.repository'; } 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 { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
@ -39,6 +39,7 @@ export class AdMapper
userUuid: copy.userId, userUuid: copy.userId,
driver: copy.driver as boolean, driver: copy.driver as boolean,
passenger: copy.passenger as boolean, passenger: copy.passenger as boolean,
status: copy.status,
frequency: copy.frequency, frequency: copy.frequency,
fromDate: new Date(copy.fromDate), fromDate: new Date(copy.fromDate),
toDate: new Date(copy.toDate), toDate: new Date(copy.toDate),
@ -92,6 +93,7 @@ export class AdMapper
userId: record.userUuid, userId: record.userUuid,
driver: record.driver, driver: record.driver,
passenger: record.passenger, passenger: record.passenger,
status: record.status as Status,
frequency: record.frequency as Frequency, frequency: record.frequency as Frequency,
fromDate: record.fromDate.toISOString().split('T')[0], fromDate: record.fromDate.toISOString().split('T')[0],
toDate: record.toDate.toISOString().split('T')[0], toDate: record.toDate.toISOString().split('T')[0],
@ -135,6 +137,7 @@ export class AdMapper
response.userId = props.userId; response.userId = props.userId;
response.driver = props.driver as boolean; response.driver = props.driver as boolean;
response.passenger = props.passenger as boolean; response.passenger = props.passenger as boolean;
response.status = props.status;
response.frequency = props.frequency; response.frequency = props.frequency;
response.fromDate = this.outputDatetimeTransformer.fromDate( response.fromDate = this.outputDatetimeTransformer.fromDate(
{ {

View File

@ -23,6 +23,10 @@ import { InputDateTimeTransformer } from './infrastructure/input-datetime-transf
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller'; 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 { 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 = [ const grpcControllers = [
CreateAdGrpcController, CreateAdGrpcController,
@ -30,11 +34,20 @@ const grpcControllers = [
FindAdsByIdsGrpcController, FindAdsByIdsGrpcController,
]; ];
const messageHandlers = [
MatcherAdCreatedMessageHandler,
MatcherAdCreationFailedMessageHandler,
];
const eventHandlers: Provider[] = [ const eventHandlers: Provider[] = [
PublishMessageWhenAdIsCreatedDomainEventHandler, PublishMessageWhenAdIsCreatedDomainEventHandler,
]; ];
const commandHandlers: Provider[] = [CreateAdService]; const commandHandlers: Provider[] = [
CreateAdService,
ValidateAdService,
InvalidateAdService,
];
const queryHandlers: Provider[] = [ const queryHandlers: Provider[] = [
FindAdByIdQueryHandler, FindAdByIdQueryHandler,
@ -81,6 +94,7 @@ const adapters: Provider[] = [
imports: [CqrsModule], imports: [CqrsModule],
controllers: [...grpcControllers], controllers: [...grpcControllers],
providers: [ providers: [
...messageHandlers,
...eventHandlers, ...eventHandlers,
...commandHandlers, ...commandHandlers,
...queryHandlers, ...queryHandlers,

View File

@ -0,0 +1,7 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
export class InvalidateAdCommand extends Command {
constructor(props: CommandProps<InvalidateAdCommand>) {
super(props);
}
}

View File

@ -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<AggregateID> {
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;
}
}

View File

@ -0,0 +1,7 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
export class ValidateAdCommand extends Command {
constructor(props: CommandProps<ValidateAdCommand>) {
super(props);
}
}

View File

@ -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<AggregateID> {
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;
}
}

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; 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 { MessagePublisherPort } from '@mobicoop/ddd-library';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens'; import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
import { AD_CREATED_ROUTING_KEY } from '@src/app.constants'; import { AD_CREATED_ROUTING_KEY } from '@src/app.constants';

View File

@ -1,19 +1,26 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { AdCreatedDomainEvent } from './events/ad-created.domain-events'; import { AdCreatedDomainEvent } from './events/ad-created.domain-event';
import { AdProps, CreateAdProps } from './ad.types'; import { AdProps, CreateAdProps, Status } from './ad.types';
import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { WaypointProps } from './value-objects/waypoint.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<AdProps> { export class AdEntity extends AggregateRoot<AdProps> {
protected readonly _id: AggregateID; protected readonly _id: AggregateID;
static create = (create: CreateAdProps): AdEntity => { static create = (create: CreateAdProps): AdEntity => {
const id = v4(); const id = v4();
const props: AdProps = { ...create }; const props: AdProps = { ...create, status: Status.PENDING };
const ad = new AdEntity({ id, props }); const ad = new AdEntity({ id, props });
ad.addEvent( ad.addEvent(
new AdCreatedDomainEvent({ new AdCreatedDomainEvent({
metadata: {
correlationId: id,
timestamp: Date.now(),
},
aggregateId: id, aggregateId: id,
userId: props.userId, userId: props.userId,
driver: props.driver, driver: props.driver,
@ -45,6 +52,48 @@ export class AdEntity extends AggregateRoot<AdProps> {
return ad; 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 { validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database // entity business rules validation to protect it's invariant before saving entity to a database
} }

View File

@ -5,6 +5,7 @@ import { WaypointProps } from './value-objects/waypoint.value-object';
export interface AdProps { export interface AdProps {
userId: string; userId: string;
driver: boolean; driver: boolean;
status: Status;
passenger: boolean; passenger: boolean;
frequency: Frequency; frequency: Frequency;
fromDate: string; fromDate: string;
@ -35,3 +36,10 @@ export enum Frequency {
PUNCTUAL = 'PUNCTUAL', PUNCTUAL = 'PUNCTUAL',
RECURRENT = 'RECURRENT', RECURRENT = 'RECURRENT',
} }
export enum Status {
PENDING = 'PENDING',
VALID = 'VALID',
INVALID = 'INVALID',
SUSPENDED = 'SUSPENDED',
}

View File

@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class AdInvalidatedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<AdInvalidatedDomainEvent>) {
super(props);
}
}

View File

@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class AdSuspendedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<AdSuspendedDomainEvent>) {
super(props);
}
}

View File

@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class AdValidatedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<AdValidatedDomainEvent>) {
super(props);
}
}

View File

@ -17,6 +17,7 @@ export type AdBaseModel = {
userUuid: string; userUuid: string;
driver: boolean; driver: boolean;
passenger: boolean; passenger: boolean;
status: string;
frequency: string; frequency: string;
fromDate: Date; fromDate: Date;
toDate: Date; toDate: Date;

View File

@ -1,10 +1,11 @@
import { ResponseBase } from '@mobicoop/ddd-library'; 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 { export class AdResponseDto extends ResponseBase {
userId: string; userId: string;
driver: boolean; driver: boolean;
passenger: boolean; passenger: boolean;
status: Status;
frequency: Frequency; frequency: Frequency;
fromDate: string; fromDate: string;
toDate: string; toDate: string;

View File

@ -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,
}),
);
}
}

View File

@ -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,
}),
);
}
}

View File

@ -0,0 +1,4 @@
import { IntegrationEvent } from '@mobicoop/ddd-library';
export type MatcherAdCreatedIntegrationEvent = IntegrationEvent;
export type MatcherAdCreationFailedIntegrationEvent = IntegrationEvent;

View File

@ -2,7 +2,7 @@ import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper'; import { AdMapper } from '@modules/ad/ad.mapper';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { AdEntity } from '@modules/ad/core/domain/ad.entity'; 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 { import {
AdReadModel, AdReadModel,
AdWriteModel, AdWriteModel,
@ -17,6 +17,7 @@ const adEntity: AdEntity = new AdEntity({
userId: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e', userId: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
driver: false, driver: false,
passenger: true, passenger: true,
status: Status.PENDING,
frequency: Frequency.PUNCTUAL, frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-21', fromDate: '2023-06-21',
toDate: '2023-06-21', toDate: '2023-06-21',
@ -67,6 +68,7 @@ const adReadModel: AdReadModel = {
userUuid: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e', userUuid: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
driver: false, driver: false,
passenger: true, passenger: true,
status: Status.PENDING,
frequency: Frequency.PUNCTUAL, frequency: Frequency.PUNCTUAL,
fromDate: new Date('2023-06-21'), fromDate: new Date('2023-06-21'),
toDate: new Date('2023-06-21'), toDate: new Date('2023-06-21'),

View File

@ -1,5 +1,9 @@
import { AdEntity } from '@modules/ad/core/domain/ad.entity'; 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'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
const originWaypointProps: WaypointProps = { const originWaypointProps: WaypointProps = {
@ -124,6 +128,7 @@ describe('Ad entity create', () => {
punctualPassengerCreateAdProps, punctualPassengerCreateAdProps,
); );
expect(punctualPassengerAd.id.length).toBe(36); expect(punctualPassengerAd.id.length).toBe(36);
expect(punctualPassengerAd.getProps().status).toBe(Status.PENDING);
expect(punctualPassengerAd.getProps().schedule.length).toBe(1); expect(punctualPassengerAd.getProps().schedule.length).toBe(1);
expect(punctualPassengerAd.getProps().schedule[0].day).toBe(3); expect(punctualPassengerAd.getProps().schedule[0].day).toBe(3);
expect(punctualPassengerAd.getProps().schedule[0].time).toBe('08:30'); 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);
});
});

View File

@ -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>(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);
});
});
});

View File

@ -1,6 +1,6 @@
import { Frequency } from '@modules/ad/core/domain/ad.types'; 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 { 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 { Test, TestingModule } from '@nestjs/testing';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens'; import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
import { AD_CREATED_ROUTING_KEY } from '@src/app.constants'; import { AD_CREATED_ROUTING_KEY } from '@src/app.constants';

View File

@ -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>(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);
});
});
});

View File

@ -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>(
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);
});
});

View File

@ -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>(
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);
});
});

View File

@ -2,7 +2,15 @@ import { Module, Provider } from '@nestjs/common';
import { MESSAGE_PUBLISHER } from './messager.di-tokens'; import { MESSAGE_PUBLISHER } from './messager.di-tokens';
import { ConfigModule, ConfigService } from '@nestjs/config'; 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 { import {
MessageBrokerModule, MessageBrokerModule,
MessageBrokerModuleOptions, MessageBrokerModuleOptions,
@ -24,6 +32,16 @@ const imports = [
) as boolean, ) as boolean,
}, },
name: SERVICE_NAME, 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,
},
},
}), }),
}), }),
]; ];