Merge branch 'adCreatedEvent' into 'main'
Send messages when a matcher ad is created, or when a matcher ad creation has failed See merge request v3/service/matcher!23
This commit is contained in:
commit
3503e53d79
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@mobicoop/matcher",
|
||||
"version": "1.4.4",
|
||||
"version": "1.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@mobicoop/matcher",
|
||||
"version": "1.4.4",
|
||||
"version": "1.5.0",
|
||||
"license": "AGPL",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.9.9",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mobicoop/matcher",
|
||||
"version": "1.4.4",
|
||||
"version": "1.5.0",
|
||||
"description": "Mobicoop V3 Matcher",
|
||||
"author": "sbriat",
|
||||
"private": true,
|
||||
|
|
|
@ -14,13 +14,9 @@ export const AD_UPDATED_QUEUE = 'matcher-ad-updated';
|
|||
export const AD_DELETED_MESSAGE_HANDLER = 'adDeleted';
|
||||
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
|
||||
export const AD_DELETED_QUEUE = 'matcher-ad-deleted';
|
||||
|
||||
// configuration
|
||||
export const SERVICE_CONFIGURATION_SET_QUEUE = 'matcher-configuration-set';
|
||||
export const SERVICE_CONFIGURATION_DELETE_QUEUE =
|
||||
'matcher-configuration-delete';
|
||||
export const SERVICE_CONFIGURATION_PROPAGATE_QUEUE =
|
||||
'matcher-configuration-propagate';
|
||||
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher.ad.created';
|
||||
export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
||||
'matcher.ad.creation.failed';
|
||||
|
||||
// health
|
||||
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
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';
|
||||
|
||||
const imports = [
|
||||
CqrsModule,
|
||||
|
@ -80,6 +81,10 @@ const grpcControllers = [MatchGrpcController];
|
|||
|
||||
const messageHandlers = [AdCreatedMessageHandler];
|
||||
|
||||
const eventHandlers: Provider[] = [
|
||||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [CreateAdService];
|
||||
|
||||
const queryHandlers: Provider[] = [MatchQueryHandler];
|
||||
|
@ -150,6 +155,7 @@ const adapters: Provider[] = [
|
|||
controllers: [...grpcControllers],
|
||||
providers: [
|
||||
...messageHandlers,
|
||||
...eventHandlers,
|
||||
...commandHandlers,
|
||||
...queryHandlers,
|
||||
...mappers,
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { CreateAdCommand } from './create-ad.command';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AggregateID,
|
||||
ConflictException,
|
||||
MessagePublisherPort,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||
import { RouteProviderPort } from '../../ports/route-provider.port';
|
||||
import { Role } from '@modules/ad/core/domain/ad.types';
|
||||
|
@ -17,10 +25,14 @@ import {
|
|||
import { Waypoint } from '../../types/waypoint.type';
|
||||
import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
import { Point } from '@modules/geography/core/domain/route.types';
|
||||
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event';
|
||||
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||
|
||||
@CommandHandler(CreateAdCommand)
|
||||
export class CreateAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
@Inject(AD_ROUTE_PROVIDER)
|
||||
|
@ -44,6 +56,15 @@ export class CreateAdService implements ICommandHandler {
|
|||
);
|
||||
|
||||
let typedRoutes: TypedRoute[];
|
||||
let driverDistance: number | undefined;
|
||||
let driverDuration: number | undefined;
|
||||
let passengerDistance: number | undefined;
|
||||
let passengerDuration: number | undefined;
|
||||
let points: PointValueObject[] | undefined;
|
||||
let fwdAzimuth: number | undefined;
|
||||
let backAzimuth: number | undefined;
|
||||
|
||||
try {
|
||||
try {
|
||||
typedRoutes = await Promise.all(
|
||||
pathCreator.getBasePaths().map(async (path: Path) => ({
|
||||
|
@ -55,13 +76,6 @@ export class CreateAdService implements ICommandHandler {
|
|||
throw new Error('Unable to find a route for given waypoints');
|
||||
}
|
||||
|
||||
let driverDistance: number | undefined;
|
||||
let driverDuration: number | undefined;
|
||||
let passengerDistance: number | undefined;
|
||||
let passengerDuration: number | undefined;
|
||||
let points: PointValueObject[] | undefined;
|
||||
let fwdAzimuth: number | undefined;
|
||||
let backAzimuth: number | undefined;
|
||||
try {
|
||||
typedRoutes.forEach((typedRoute: TypedRoute) => {
|
||||
if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) {
|
||||
|
@ -77,7 +91,9 @@ export class CreateAdService implements ICommandHandler {
|
|||
fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||
backAzimuth = typedRoute.route.backAzimuth;
|
||||
}
|
||||
if ([PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)) {
|
||||
if (
|
||||
[PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)
|
||||
) {
|
||||
passengerDistance = typedRoute.route.distance;
|
||||
passengerDuration = typedRoute.route.duration;
|
||||
if (!points)
|
||||
|
@ -95,6 +111,7 @@ export class CreateAdService implements ICommandHandler {
|
|||
} catch (error: any) {
|
||||
throw new Error('Invalid route');
|
||||
}
|
||||
|
||||
const ad = AdEntity.create({
|
||||
id: command.id,
|
||||
driver: command.driver,
|
||||
|
@ -115,6 +132,7 @@ export class CreateAdService implements ICommandHandler {
|
|||
fwdAzimuth: fwdAzimuth as number,
|
||||
backAzimuth: backAzimuth as number,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.repository.insertExtra(ad, 'ad');
|
||||
return ad.id;
|
||||
|
@ -124,5 +142,21 @@ export class CreateAdService implements ICommandHandler {
|
|||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error: any) {
|
||||
const matcherAdCreationFailedIntegrationEvent =
|
||||
new MatcherAdCreationFailedIntegrationEvent({
|
||||
id: command.id,
|
||||
metadata: {
|
||||
correlationId: command.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
cause: error.message,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
|
||||
JSON.stringify(matcherAdCreationFailedIntegrationEvent),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { MatcherAdCreatedDomainEvent } from '../../domain/events/matcher-ad-created.domain-event';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
|
||||
import { MATCHER_AD_CREATED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { MatcherAdCreatedIntegrationEvent } from '../events/matcher-ad-created.integration-event';
|
||||
|
||||
@Injectable()
|
||||
export class PublishMessageWhenMatcherAdIsCreatedDomainEventHandler {
|
||||
constructor(
|
||||
@Inject(AD_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
) {}
|
||||
|
||||
@OnEvent(MatcherAdCreatedDomainEvent.name, { async: true, promisify: true })
|
||||
async handle(event: MatcherAdCreatedDomainEvent): Promise<any> {
|
||||
const matcherAdCreatedIntegrationEvent =
|
||||
new MatcherAdCreatedIntegrationEvent({
|
||||
id: event.aggregateId,
|
||||
driverDuration: event.driverDuration,
|
||||
driverDistance: event.driverDistance,
|
||||
passengerDuration: event.passengerDuration,
|
||||
passengerDistance: event.passengerDistance,
|
||||
fwdAzimuth: event.fwdAzimuth,
|
||||
backAzimuth: event.backAzimuth,
|
||||
metadata: event.metadata,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
MATCHER_AD_CREATED_ROUTING_KEY,
|
||||
JSON.stringify(matcherAdCreatedIntegrationEvent),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class MatcherAdCreatedIntegrationEvent extends IntegrationEvent {
|
||||
readonly driverDuration?: number;
|
||||
readonly driverDistance?: number;
|
||||
readonly passengerDuration?: number;
|
||||
readonly passengerDistance?: number;
|
||||
readonly fwdAzimuth: number;
|
||||
readonly backAzimuth: number;
|
||||
|
||||
constructor(props: IntegrationEventProps<MatcherAdCreatedIntegrationEvent>) {
|
||||
super(props);
|
||||
this.driverDuration = props.driverDuration;
|
||||
this.driverDistance = props.driverDistance;
|
||||
this.passengerDuration = props.passengerDuration;
|
||||
this.passengerDistance = props.passengerDistance;
|
||||
this.fwdAzimuth = props.fwdAzimuth;
|
||||
this.backAzimuth = props.backAzimuth;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class MatcherAdCreationFailedIntegrationEvent extends IntegrationEvent {
|
||||
readonly cause?: string;
|
||||
|
||||
constructor(
|
||||
props: IntegrationEventProps<MatcherAdCreationFailedIntegrationEvent>,
|
||||
) {
|
||||
super(props);
|
||||
this.cause = props.cause;
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ export abstract class Algorithm {
|
|||
for (const processor of this.processors) {
|
||||
this.candidates = await processor.execute(this.candidates);
|
||||
}
|
||||
// console.log(JSON.stringify(this.candidates, null, 2));
|
||||
return this.candidates.map((candidate: CandidateEntity) =>
|
||||
MatchEntity.create({
|
||||
adId: candidate.id,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { AdStatus, Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { Selector } from '../algorithm.abstract';
|
||||
import { Waypoint } from '../../../types/waypoint.type';
|
||||
import { Point } from '../../../types/point.type';
|
||||
|
@ -133,6 +133,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||
|
||||
private _createWhere = (role: Role): string =>
|
||||
[
|
||||
this._whereStatus(),
|
||||
this._whereRole(role),
|
||||
this._whereStrict(),
|
||||
this._whereDate(),
|
||||
|
@ -144,6 +145,8 @@ export class PassengerOrientedSelector extends Selector {
|
|||
.filter((where: string) => where != '')
|
||||
.join(' AND ');
|
||||
|
||||
private _whereStatus = (): string => `status='${AdStatus.VALID}'`;
|
||||
|
||||
private _whereRole = (role: Role): string =>
|
||||
role == Role.PASSENGER ? 'driver=True' : 'passenger=True';
|
||||
|
||||
|
@ -174,7 +177,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||
private _whereSchedule = (role: Role): string => {
|
||||
const schedule: string[] = [];
|
||||
// we need full dates to compare times, because margins can lead to compare on previous or next day
|
||||
// -first we establish a base calendar (up to a week)
|
||||
// - first we establish a base calendar (up to a week)
|
||||
const scheduleDates: Date[] = this._datesBetweenBoundaries(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
|
|
|
@ -1,12 +1,29 @@
|
|||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AdProps, CreateAdProps } from './ad.types';
|
||||
import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event';
|
||||
|
||||
export class AdEntity extends AggregateRoot<AdProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
|
||||
static create = (create: CreateAdProps): AdEntity => {
|
||||
const props: AdProps = { ...create };
|
||||
return new AdEntity({ id: create.id, props });
|
||||
const ad = new AdEntity({ id: create.id, props });
|
||||
ad.addEvent(
|
||||
new MatcherAdCreatedDomainEvent({
|
||||
metadata: {
|
||||
correlationId: create.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
aggregateId: create.id,
|
||||
driverDistance: create.driverDistance,
|
||||
driverDuration: create.driverDuration,
|
||||
passengerDistance: create.passengerDistance,
|
||||
passengerDuration: create.passengerDuration,
|
||||
fwdAzimuth: create.fwdAzimuth,
|
||||
backAzimuth: create.backAzimuth,
|
||||
}),
|
||||
);
|
||||
return ad;
|
||||
};
|
||||
|
||||
validate(): void {
|
||||
|
|
|
@ -53,3 +53,10 @@ export enum Role {
|
|||
DRIVER = 'DRIVER',
|
||||
PASSENGER = 'PASSENGER',
|
||||
}
|
||||
|
||||
export enum AdStatus {
|
||||
PENDING = 'PENDING',
|
||||
VALID = 'VALID',
|
||||
INVALID = 'INVALID',
|
||||
SUSPENDED = 'SUSPENDED',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class MatcherAdCreatedDomainEvent extends DomainEvent {
|
||||
readonly driverDuration?: number;
|
||||
readonly driverDistance?: number;
|
||||
readonly passengerDuration?: number;
|
||||
readonly passengerDistance?: number;
|
||||
readonly fwdAzimuth: number;
|
||||
readonly backAzimuth: number;
|
||||
|
||||
constructor(props: DomainEventProps<MatcherAdCreatedDomainEvent>) {
|
||||
super(props);
|
||||
this.driverDuration = props.driverDuration;
|
||||
this.driverDistance = props.driverDistance;
|
||||
this.passengerDuration = props.passengerDuration;
|
||||
this.passengerDistance = props.passengerDistance;
|
||||
this.fwdAzimuth = props.fwdAzimuth;
|
||||
this.backAzimuth = props.backAzimuth;
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ export class AdCreatedMessageHandler {
|
|||
name: AD_CREATED_MESSAGE_HANDLER,
|
||||
})
|
||||
public async adCreated(message: string) {
|
||||
try {
|
||||
const createdAd: Ad = JSON.parse(message);
|
||||
await this.commandBus.execute(
|
||||
new CreateAdCommand({
|
||||
|
@ -30,8 +29,5 @@ export class AdCreatedMessageHandler {
|
|||
waypoints: createdAd.waypoints,
|
||||
}),
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { ConflictException } from '@mobicoop/ddd-library';
|
||||
|
@ -96,6 +100,10 @@ const mockRouteProvider: RouteProviderPort = {
|
|||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('create-ad.service', () => {
|
||||
let createAdService: CreateAdService;
|
||||
|
||||
|
@ -110,6 +118,10 @@ describe('create-ad.service', () => {
|
|||
provide: AD_ROUTE_PROVIDER,
|
||||
useValue: mockRouteProvider,
|
||||
},
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
CreateAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
|
||||
import { MATCHER_AD_CREATED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
||||
import { MatcherAdCreatedDomainEvent } from '@modules/ad/core/domain/events/matcher-ad-created.domain-event';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Publish message when matcher ad is created domain event handler', () => {
|
||||
let publishMessageWhenMatcherAdIsCreatedDomainEventHandler: PublishMessageWhenMatcherAdIsCreatedDomainEventHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
publishMessageWhenMatcherAdIsCreatedDomainEventHandler =
|
||||
module.get<PublishMessageWhenMatcherAdIsCreatedDomainEventHandler>(
|
||||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish a message', () => {
|
||||
jest.spyOn(mockMessagePublisher, 'publish');
|
||||
const matcherAdCreatedDomainEvent: MatcherAdCreatedDomainEvent = {
|
||||
id: 'some-domain-event-id',
|
||||
aggregateId: 'some-aggregate-id',
|
||||
metadata: {
|
||||
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
||||
correlationId: 'some-correlation-id',
|
||||
},
|
||||
driverDistance: 65845,
|
||||
driverDuration: 3254,
|
||||
fwdAzimuth: 90,
|
||||
backAzimuth: 270,
|
||||
};
|
||||
publishMessageWhenMatcherAdIsCreatedDomainEventHandler.handle(
|
||||
matcherAdCreatedDomainEvent,
|
||||
);
|
||||
expect(
|
||||
publishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||
).toBeDefined();
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
|
||||
MATCHER_AD_CREATED_ROUTING_KEY,
|
||||
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"driverDuration":3254,"driverDistance":65845,"fwdAzimuth":90,"backAzimuth":270}',
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue