Merge branch 'release-1.8' into 'main'
Release 1.8 See merge request mobicoop/v3/service/matcher!49
This commit is contained in:
commit
f53f48f521
|
@ -1,18 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/matcher",
|
"name": "@mobicoop/matcher",
|
||||||
"version": "1.5.5",
|
"version": "1.8.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@mobicoop/matcher",
|
"name": "@mobicoop/matcher",
|
||||||
"version": "1.5.5",
|
"version": "1.8.0",
|
||||||
"license": "AGPL",
|
"license": "AGPL",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.9.14",
|
"@grpc/grpc-js": "^1.9.14",
|
||||||
"@grpc/proto-loader": "^0.7.10",
|
"@grpc/proto-loader": "^0.7.10",
|
||||||
"@mobicoop/configuration-module": "^8.0.0",
|
"@mobicoop/configuration-module": "^8.0.0",
|
||||||
"@mobicoop/ddd-library": "^2.4.3",
|
"@mobicoop/ddd-library": "^2.5.0",
|
||||||
"@mobicoop/health-module": "^2.3.2",
|
"@mobicoop/health-module": "^2.3.2",
|
||||||
"@mobicoop/message-broker-module": "^2.1.2",
|
"@mobicoop/message-broker-module": "^2.1.2",
|
||||||
"@nestjs/axios": "^3.0.1",
|
"@nestjs/axios": "^3.0.1",
|
||||||
|
@ -1881,9 +1881,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mobicoop/ddd-library": {
|
"node_modules/@mobicoop/ddd-library": {
|
||||||
"version": "2.4.3",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-2.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-2.5.0.tgz",
|
||||||
"integrity": "sha512-HxNtAfov8ne7XsFTSIDI811r3L1VDV9YUikgX7HPjrB8u2gQh6FQFnIz3Fjb/zWOGxrDEIy8HEM0AYmXkf8ULA==",
|
"integrity": "sha512-dTx7KTILs53HCqNx0BDVTzIZxfPW3pi0fZ4UMw/vDNm3oTqGA+jg7YBfNxn8yadM+j2dDIN5Kum43CmKGH8yYA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/event-emitter": "^2.0.3",
|
"@nestjs/event-emitter": "^2.0.3",
|
||||||
"@nestjs/microservices": "^10.3.0",
|
"@nestjs/microservices": "^10.3.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/matcher",
|
"name": "@mobicoop/matcher",
|
||||||
"version": "1.6.0",
|
"version": "1.8.0",
|
||||||
"description": "Mobicoop V3 Matcher",
|
"description": "Mobicoop V3 Matcher",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||||
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
|
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
|
||||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
||||||
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
"test:watch": "jest --testPathPattern 'tests/unit/' --watch",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate deploy'",
|
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate deploy'",
|
||||||
"migrate:dev": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",
|
"migrate:dev": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
"@grpc/proto-loader": "^0.7.10",
|
"@grpc/proto-loader": "^0.7.10",
|
||||||
"@songkeys/nestjs-redis": "^10.0.0",
|
"@songkeys/nestjs-redis": "^10.0.0",
|
||||||
"@mobicoop/configuration-module": "^8.0.0",
|
"@mobicoop/configuration-module": "^8.0.0",
|
||||||
"@mobicoop/ddd-library": "^2.4.3",
|
"@mobicoop/ddd-library": "^2.5.0",
|
||||||
"@mobicoop/health-module": "^2.3.2",
|
"@mobicoop/health-module": "^2.3.2",
|
||||||
"@mobicoop/message-broker-module": "^2.1.2",
|
"@mobicoop/message-broker-module": "^2.1.2",
|
||||||
"@nestjs/axios": "^3.0.1",
|
"@nestjs/axios": "^3.0.1",
|
||||||
|
|
|
@ -10,11 +10,16 @@ export const GRPC_GEOROUTER_SERVICE_NAME = 'GeorouterService';
|
||||||
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
|
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
|
||||||
export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
||||||
'matcher-ad.creation-failed';
|
'matcher-ad.creation-failed';
|
||||||
|
export const MATCHER_AD_UPDATED_ROUTING_KEY = 'matcher-ad.updated';
|
||||||
|
export const MATCHER_AD_UPDATE_FAILED_ROUTING_KEY = 'matcher-ad.update-failed';
|
||||||
|
|
||||||
// messaging input
|
// messaging input
|
||||||
export const AD_CREATED_MESSAGE_HANDLER = 'adCreated';
|
export const AD_CREATED_MESSAGE_HANDLER = 'adCreated';
|
||||||
export const AD_CREATED_ROUTING_KEY = 'ad.created';
|
export const AD_CREATED_ROUTING_KEY = 'ad.created';
|
||||||
export const AD_CREATED_QUEUE = 'matcher.ad.created';
|
export const AD_CREATED_QUEUE = 'matcher.ad.created';
|
||||||
|
export const AD_UPDATED_MESSAGE_HANDLER = 'adUpdated';
|
||||||
|
export const AD_UPDATED_ROUTING_KEY = 'ad.updated';
|
||||||
|
export const AD_UPDATED_QUEUE = 'matcher.ad.updated';
|
||||||
export const AD_DELETED_MESSAGE_HANDLER = 'adDeleted';
|
export const AD_DELETED_MESSAGE_HANDLER = 'adDeleted';
|
||||||
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
|
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
|
||||||
export const AD_DELETED_QUEUE = 'matcher.ad.deleted';
|
export const AD_DELETED_QUEUE = 'matcher.ad.deleted';
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
AdWriteExtraModel,
|
AdWriteExtraModel,
|
||||||
AdWriteModel,
|
AdWriteModel,
|
||||||
ScheduleItemModel,
|
ScheduleItemModel,
|
||||||
|
ScheduleWriteModel,
|
||||||
} from './infrastructure/ad.repository';
|
} from './infrastructure/ad.repository';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,9 +39,8 @@ export class AdMapper
|
||||||
private readonly directionEncoder: DirectionEncoderPort,
|
private readonly directionEncoder: DirectionEncoderPort,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
toPersistence = (entity: AdEntity): AdWriteModel => {
|
toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => {
|
||||||
const copy = entity.getProps();
|
const copy = entity.getProps();
|
||||||
const now = new Date();
|
|
||||||
const record: AdWriteModel = {
|
const record: AdWriteModel = {
|
||||||
uuid: copy.id,
|
uuid: copy.id,
|
||||||
driver: copy.driver,
|
driver: copy.driver,
|
||||||
|
@ -48,22 +48,7 @@ export class AdMapper
|
||||||
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),
|
||||||
schedule: {
|
schedule: this.toScheduleItemWriteModel(copy.schedule, update),
|
||||||
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
|
|
||||||
uuid: v4(),
|
|
||||||
day: scheduleItem.day,
|
|
||||||
time: new Date(
|
|
||||||
1970,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
parseInt(scheduleItem.time.split(':')[0]),
|
|
||||||
parseInt(scheduleItem.time.split(':')[1]),
|
|
||||||
),
|
|
||||||
margin: scheduleItem.margin,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
seatsProposed: copy.seatsProposed,
|
seatsProposed: copy.seatsProposed,
|
||||||
seatsRequested: copy.seatsRequested,
|
seatsRequested: copy.seatsRequested,
|
||||||
strict: copy.strict,
|
strict: copy.strict,
|
||||||
|
@ -73,12 +58,39 @@ export class AdMapper
|
||||||
passengerDistance: copy.passengerDistance,
|
passengerDistance: copy.passengerDistance,
|
||||||
fwdAzimuth: copy.fwdAzimuth,
|
fwdAzimuth: copy.fwdAzimuth,
|
||||||
backAzimuth: copy.backAzimuth,
|
backAzimuth: copy.backAzimuth,
|
||||||
createdAt: copy.createdAt,
|
|
||||||
updatedAt: copy.updatedAt,
|
|
||||||
};
|
};
|
||||||
return record;
|
return record;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toScheduleItemWriteModel = (
|
||||||
|
schedule: ScheduleItemProps[],
|
||||||
|
update?: boolean,
|
||||||
|
): ScheduleWriteModel => {
|
||||||
|
const now = new Date();
|
||||||
|
const record: ScheduleWriteModel = {
|
||||||
|
create: schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||||
|
uuid: v4(),
|
||||||
|
day: scheduleItem.day,
|
||||||
|
time: new Date(
|
||||||
|
1970,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
parseInt(scheduleItem.time.split(':')[0]),
|
||||||
|
parseInt(scheduleItem.time.split(':')[1]),
|
||||||
|
),
|
||||||
|
margin: scheduleItem.margin,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
if (update) {
|
||||||
|
record.deleteMany = {
|
||||||
|
createdAt: { lt: now },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
toDomain = (record: AdReadModel): AdEntity =>
|
toDomain = (record: AdReadModel): AdEntity =>
|
||||||
new AdEntity({
|
new AdEntity({
|
||||||
id: record.uuid,
|
id: record.uuid,
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
import { AdMapper } from './ad.mapper';
|
import { AdMapper } from './ad.mapper';
|
||||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||||
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
|
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
|
||||||
|
import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service';
|
||||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
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 { MatchQueryHandler } from './core/application/queries/match/match.query-handler';
|
||||||
import { AdRepository } from './infrastructure/ad.repository';
|
import { AdRepository } from './infrastructure/ad.repository';
|
||||||
|
@ -44,6 +45,7 @@ import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||||
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
||||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||||
import { AdDeletedMessageHandler } from './interface/message-handlers/ad-deleted.message-handler';
|
import { AdDeletedMessageHandler } from './interface/message-handlers/ad-deleted.message-handler';
|
||||||
|
import { AdUpdatedMessageHandler } from './interface/message-handlers/ad-updated.message-handler';
|
||||||
import { MatchMapper } from './match.mapper';
|
import { MatchMapper } from './match.mapper';
|
||||||
import { MatchingMapper } from './matching.mapper';
|
import { MatchingMapper } from './matching.mapper';
|
||||||
|
|
||||||
|
@ -98,13 +100,21 @@ const imports = [
|
||||||
|
|
||||||
const grpcControllers = [MatchGrpcController];
|
const grpcControllers = [MatchGrpcController];
|
||||||
|
|
||||||
const messageHandlers = [AdCreatedMessageHandler, AdDeletedMessageHandler];
|
const messageHandlers = [
|
||||||
|
AdCreatedMessageHandler,
|
||||||
|
AdUpdatedMessageHandler,
|
||||||
|
AdDeletedMessageHandler,
|
||||||
|
];
|
||||||
|
|
||||||
const eventHandlers: Provider[] = [
|
const eventHandlers: Provider[] = [
|
||||||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||||
];
|
];
|
||||||
|
|
||||||
const commandHandlers: Provider[] = [CreateAdService, DeleteAdService];
|
const commandHandlers: Provider[] = [
|
||||||
|
CreateAdService,
|
||||||
|
UpdateAdService,
|
||||||
|
DeleteAdService,
|
||||||
|
];
|
||||||
|
|
||||||
const queryHandlers: Provider[] = [MatchQueryHandler];
|
const queryHandlers: Provider[] = [MatchQueryHandler];
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
|
||||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
import { ScheduleItem } from '../../types/schedule-item.type';
|
import { Frequency, UserAd } from '@modules/ad/core/domain/ad.types';
|
||||||
import { Address } from '../../types/address.type';
|
import { Address } from '../../types/address.type';
|
||||||
|
import { ScheduleItem } from '../../types/schedule-item.type';
|
||||||
|
|
||||||
export class CreateAdCommand extends Command {
|
export class CreateAdCommand extends Command implements UserAd {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly driver: boolean;
|
readonly driver: boolean;
|
||||||
readonly passenger: boolean;
|
readonly passenger: boolean;
|
||||||
|
|
|
@ -1,32 +1,22 @@
|
||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { CreateAdCommand } from './create-ad.command';
|
|
||||||
import { Inject } from '@nestjs/common';
|
|
||||||
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 {
|
import {
|
||||||
AggregateID,
|
AggregateID,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
MessagePublisherPort,
|
MessagePublisherPort,
|
||||||
} from '@mobicoop/ddd-library';
|
} from '@mobicoop/ddd-library';
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
|
||||||
import { Role } from '@modules/ad/core/domain/ad.types';
|
|
||||||
import {
|
import {
|
||||||
Path,
|
AD_MESSAGE_PUBLISHER,
|
||||||
PathCreator,
|
AD_REPOSITORY,
|
||||||
PathType,
|
AD_ROUTE_PROVIDER,
|
||||||
TypedRoute,
|
} from '@modules/ad/ad.di-tokens';
|
||||||
} from '@modules/ad/core/domain/path-creator.service';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
import { Waypoint } from '../../types/waypoint.type';
|
import { AdFactory } from '@modules/ad/core/domain/ad.factory';
|
||||||
import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects/point.value-object';
|
import { Inject } from '@nestjs/common';
|
||||||
import { Point } from '@modules/geography/core/domain/route.types';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event';
|
|
||||||
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
|
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||||
import { GeorouterPort } from '../../ports/georouter.port';
|
import { GeorouterService } from '../../../domain/georouter.service';
|
||||||
|
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-failure.integration-event';
|
||||||
|
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||||
|
import { CreateAdCommand } from './create-ad.command';
|
||||||
|
|
||||||
@CommandHandler(CreateAdCommand)
|
@CommandHandler(CreateAdCommand)
|
||||||
export class CreateAdService implements ICommandHandler {
|
export class CreateAdService implements ICommandHandler {
|
||||||
|
@ -36,106 +26,16 @@ export class CreateAdService implements ICommandHandler {
|
||||||
@Inject(AD_REPOSITORY)
|
@Inject(AD_REPOSITORY)
|
||||||
private readonly repository: AdRepositoryPort,
|
private readonly repository: AdRepositoryPort,
|
||||||
@Inject(AD_ROUTE_PROVIDER)
|
@Inject(AD_ROUTE_PROVIDER)
|
||||||
private readonly routeProvider: GeorouterPort,
|
private readonly routeProvider: GeorouterService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||||
const roles: Role[] = [];
|
|
||||||
if (command.driver) roles.push(Role.DRIVER);
|
|
||||||
if (command.passenger) roles.push(Role.PASSENGER);
|
|
||||||
|
|
||||||
const pathCreator: PathCreator = new PathCreator(
|
|
||||||
roles,
|
|
||||||
command.waypoints.map(
|
|
||||||
(waypoint: Waypoint) =>
|
|
||||||
new PointValueObject({
|
|
||||||
lon: waypoint.lon,
|
|
||||||
lat: waypoint.lat,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
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 {
|
||||||
try {
|
const adFactory = new AdFactory(this.routeProvider);
|
||||||
typedRoutes = await Promise.all(
|
const ad = await adFactory.create(command);
|
||||||
pathCreator.getBasePaths().map(async (path: Path) => ({
|
|
||||||
type: path.type,
|
|
||||||
route: await this.routeProvider.getRoute({
|
|
||||||
waypoints: path.waypoints,
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
throw new Error('Unable to find a route for given waypoints');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
typedRoutes.forEach((typedRoute: TypedRoute) => {
|
|
||||||
if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) {
|
|
||||||
driverDistance = typedRoute.route.distance;
|
|
||||||
driverDuration = typedRoute.route.duration;
|
|
||||||
points = typedRoute.route.points.map(
|
|
||||||
(point: Point) =>
|
|
||||||
new PointValueObject({
|
|
||||||
lon: point.lon,
|
|
||||||
lat: point.lat,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
fwdAzimuth = typedRoute.route.fwdAzimuth;
|
|
||||||
backAzimuth = typedRoute.route.backAzimuth;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
[PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)
|
|
||||||
) {
|
|
||||||
passengerDistance = typedRoute.route.distance;
|
|
||||||
passengerDuration = typedRoute.route.duration;
|
|
||||||
if (!points)
|
|
||||||
points = typedRoute.route.points.map(
|
|
||||||
(point: Point) =>
|
|
||||||
new PointValueObject({
|
|
||||||
lon: point.lon,
|
|
||||||
lat: point.lat,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth;
|
|
||||||
if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error('Invalid route');
|
|
||||||
}
|
|
||||||
|
|
||||||
const ad = AdEntity.create({
|
|
||||||
id: command.id,
|
|
||||||
driver: command.driver,
|
|
||||||
passenger: command.passenger,
|
|
||||||
frequency: command.frequency,
|
|
||||||
fromDate: command.fromDate,
|
|
||||||
toDate: command.toDate,
|
|
||||||
schedule: command.schedule,
|
|
||||||
seatsProposed: command.seatsProposed,
|
|
||||||
seatsRequested: command.seatsRequested,
|
|
||||||
strict: command.strict,
|
|
||||||
waypoints: command.waypoints,
|
|
||||||
points: points as PointValueObject[],
|
|
||||||
driverDistance,
|
|
||||||
driverDuration,
|
|
||||||
passengerDistance,
|
|
||||||
passengerDuration,
|
|
||||||
fwdAzimuth: fwdAzimuth as number,
|
|
||||||
backAzimuth: backAzimuth as number,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
//TODO it should not be this service's concern that Prisma does not support postgis types
|
||||||
await this.repository.insertExtra(ad, 'ad');
|
await this.repository.insertExtra(ad, 'ad');
|
||||||
return ad.id;
|
return ad.id;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { CreateAdCommand } from '../create-ad/create-ad.command';
|
||||||
|
|
||||||
|
export class UpdateAdCommand extends CreateAdCommand {}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
import {
|
||||||
|
AD_MESSAGE_PUBLISHER,
|
||||||
|
AD_REPOSITORY,
|
||||||
|
AD_ROUTE_PROVIDER,
|
||||||
|
} from '@modules/ad/ad.di-tokens';
|
||||||
|
import { AdFactory } from '@modules/ad/core/domain/ad.factory';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { MATCHER_AD_UPDATE_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||||
|
import { GeorouterService } from '../../../domain/georouter.service';
|
||||||
|
import { MatcherAdUpdateFailedIntegrationEvent } from '../../events/matcher-ad-failure.integration-event';
|
||||||
|
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||||
|
import { UpdateAdCommand } from './update-ad.command';
|
||||||
|
|
||||||
|
@CommandHandler(UpdateAdCommand)
|
||||||
|
export class UpdateAdService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_MESSAGE_PUBLISHER)
|
||||||
|
private readonly messagePublisher: MessagePublisherPort,
|
||||||
|
@Inject(AD_REPOSITORY)
|
||||||
|
private readonly repository: AdRepositoryPort,
|
||||||
|
@Inject(AD_ROUTE_PROVIDER)
|
||||||
|
private readonly routeProvider: GeorouterService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: UpdateAdCommand): Promise<void> {
|
||||||
|
try {
|
||||||
|
const adFactory = new AdFactory(this.routeProvider);
|
||||||
|
const ad = await adFactory.create(command);
|
||||||
|
return this.repository.update(ad.id, ad);
|
||||||
|
} catch (error: any) {
|
||||||
|
const integrationEvent = new MatcherAdUpdateFailedIntegrationEvent({
|
||||||
|
id: command.id,
|
||||||
|
metadata: {
|
||||||
|
correlationId: command.id,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
cause: error.message,
|
||||||
|
});
|
||||||
|
this.messagePublisher.publish(
|
||||||
|
MATCHER_AD_UPDATE_FAILED_ROUTING_KEY,
|
||||||
|
JSON.stringify(integrationEvent),
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class MatcherAdFailureIntegrationEvent extends IntegrationEvent {
|
||||||
|
readonly cause?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
props: IntegrationEventProps<MatcherAdCreationFailedIntegrationEvent>,
|
||||||
|
) {
|
||||||
|
super(props);
|
||||||
|
this.cause = props.cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatcherAdCreationFailedIntegrationEvent extends MatcherAdFailureIntegrationEvent {}
|
||||||
|
export class MatcherAdUpdateFailedIntegrationEvent extends MatcherAdFailureIntegrationEvent {}
|
|
@ -1,9 +1,9 @@
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
import { Completer } from './completer.abstract';
|
|
||||||
import { MatchQuery } from '../match.query';
|
|
||||||
import { Step } from '../../../types/step.type';
|
|
||||||
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
||||||
import { RouteResponse } from '../../../ports/georouter.port';
|
import { RouteResponse } from '../../../../domain/georouter.service';
|
||||||
|
import { Step } from '../../../types/step.type';
|
||||||
|
import { MatchQuery } from '../match.query';
|
||||||
|
import { Completer } from './completer.abstract';
|
||||||
|
|
||||||
export class RouteCompleter extends Completer {
|
export class RouteCompleter extends Completer {
|
||||||
protected readonly type: RouteCompleterType;
|
protected readonly type: RouteCompleterType;
|
||||||
|
|
|
@ -8,8 +8,8 @@ import {
|
||||||
} from '@modules/ad/core/domain/path-creator.service';
|
} from '@modules/ad/core/domain/path-creator.service';
|
||||||
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
|
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||||
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
|
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
|
||||||
|
import { GeorouterService } from '../../../domain/georouter.service';
|
||||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||||
import { GeorouterPort } from '../../ports/georouter.port';
|
|
||||||
import { AlgorithmType } from '../../types/algorithm.types';
|
import { AlgorithmType } from '../../types/algorithm.types';
|
||||||
import { Route } from '../../types/route.type';
|
import { Route } from '../../types/route.type';
|
||||||
import { Waypoint } from '../../types/waypoint.type';
|
import { Waypoint } from '../../types/waypoint.type';
|
||||||
|
@ -41,10 +41,10 @@ export class MatchQuery extends QueryBase {
|
||||||
passengerRoute?: Route;
|
passengerRoute?: Route;
|
||||||
backAzimuth?: number;
|
backAzimuth?: number;
|
||||||
private readonly originWaypoint: Waypoint;
|
private readonly originWaypoint: Waypoint;
|
||||||
routeProvider: GeorouterPort;
|
routeProvider: GeorouterService;
|
||||||
|
|
||||||
// TODO: remove MatchRequestDto depency (here core domain depends on interface /!\)
|
// TODO: remove MatchRequestDto depency (here core domain depends on interface /!\)
|
||||||
constructor(props: MatchRequestDto, routeProvider: GeorouterPort) {
|
constructor(props: MatchRequestDto, routeProvider: GeorouterService) {
|
||||||
super();
|
super();
|
||||||
this.id = props.id;
|
this.id = props.id;
|
||||||
this.driver = props.driver;
|
this.driver = props.driver;
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
import { 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';
|
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
|
||||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
import { DateInterval } from '../../../../domain/candidate.types';
|
||||||
|
import { Point } from '../../../types/point.type';
|
||||||
|
import { Waypoint } from '../../../types/waypoint.type';
|
||||||
|
import { Selector } from '../algorithm.abstract';
|
||||||
|
import { ScheduleItem } from '../match.query';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class complements the AdRepository prisma service by turning a match query object into a SQL query,
|
||||||
|
* with the assumption that the query is passenger-oriented (i.e. it is up to the driver to go out of his way to pick up the passenger).
|
||||||
|
* The idea is to make a rough filter of the ads in DB to limit the number of ads to be processed more precisely by the application code.
|
||||||
|
* TODO: Converting the query object into a SQL query is a job for the repository implementation
|
||||||
|
* (or anything behind the repository interface),
|
||||||
|
* any logic related to being passenger-oriented should be in the domain layer.
|
||||||
|
* (though it might be difficult to describe generically the search criteria with a query object)
|
||||||
|
*/
|
||||||
export class PassengerOrientedSelector extends Selector {
|
export class PassengerOrientedSelector extends Selector {
|
||||||
select = async (): Promise<CandidateEntity[]> => {
|
select = async (): Promise<CandidateEntity[]> => {
|
||||||
const queryStringRoles: QueryStringRole[] = [];
|
const queryStringRoles: QueryStringRole[] = [];
|
||||||
|
@ -19,6 +29,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||||
query: this._createQueryString(Role.PASSENGER),
|
query: this._createQueryString(Role.PASSENGER),
|
||||||
role: Role.PASSENGER,
|
role: Role.PASSENGER,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
queryStringRoles.map<Promise<AdsRole>>(
|
queryStringRoles.map<Promise<AdsRole>>(
|
||||||
|
@ -36,7 +47,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||||
id: adEntity.id,
|
id: adEntity.id,
|
||||||
role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER,
|
role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER,
|
||||||
frequency: adEntity.getProps().frequency,
|
frequency: adEntity.getProps().frequency,
|
||||||
dateInterval: {
|
dateInterval: this._fixDateInterval({
|
||||||
lowerDate: this._maxDateString(
|
lowerDate: this._maxDateString(
|
||||||
this.query.fromDate,
|
this.query.fromDate,
|
||||||
adEntity.getProps().fromDate,
|
adEntity.getProps().fromDate,
|
||||||
|
@ -45,7 +56,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||||
this.query.toDate,
|
this.query.toDate,
|
||||||
adEntity.getProps().toDate,
|
adEntity.getProps().toDate,
|
||||||
),
|
),
|
||||||
},
|
}),
|
||||||
driverWaypoints:
|
driverWaypoints:
|
||||||
adsRole.role == Role.PASSENGER
|
adsRole.role == Role.PASSENGER
|
||||||
? adEntity.getProps().waypoints
|
? adEntity.getProps().waypoints
|
||||||
|
@ -134,8 +145,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||||
[
|
[
|
||||||
this._whereRole(role),
|
this._whereRole(role),
|
||||||
this._whereStrict(),
|
this._whereStrict(),
|
||||||
this._whereDate(),
|
this._whereDate(role),
|
||||||
this._whereSchedule(role),
|
|
||||||
this._whereExcludedAd(),
|
this._whereExcludedAd(),
|
||||||
this._whereAzimuth(),
|
this._whereAzimuth(),
|
||||||
this._whereProportion(role),
|
this._whereProportion(role),
|
||||||
|
@ -154,110 +164,58 @@ export class PassengerOrientedSelector extends Selector {
|
||||||
: `frequency='${Frequency.RECURRENT}'`
|
: `frequency='${Frequency.RECURRENT}'`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
private _whereDate = (): string =>
|
/**
|
||||||
this.query.frequency == Frequency.PUNCTUAL
|
* Generates the WHERE clause checking that the date range of the query intersects with the range of the ad.
|
||||||
? `("fromDate" <= '${this.query.fromDate}' AND "toDate" >= '${this.query.fromDate}')`
|
* Note that driver dates might not be comparable with passenger dates when the trip is by night or very long.
|
||||||
: `(\
|
* For this reason, the pickup date is adjusted with the driver duration,
|
||||||
(\
|
* so as to compare with the maximum / minimum driver date that could make sense for the passenger.
|
||||||
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
* This may return more ads than necessary, but they will be filtered out in further processing.
|
||||||
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
*/
|
||||||
) OR (\
|
private _whereDate = (role: Role): string => {
|
||||||
"fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
const maxFromDate = this._maxFromDate(role);
|
||||||
"toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
const minToDate = this._minToDate(role);
|
||||||
) OR (\
|
return `("fromDate" <= ${maxFromDate} AND "toDate" >= ${minToDate})`;
|
||||||
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
};
|
||||||
"toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
|
||||||
) OR (\
|
|
||||||
"fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
|
||||||
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
|
||||||
)\
|
|
||||||
)`;
|
|
||||||
|
|
||||||
private _whereSchedule = (role: Role): string => {
|
private _maxFromDate = (role: Role): string => {
|
||||||
// no schedule filtering if schedule is not set
|
if (role == Role.DRIVER) {
|
||||||
if (this.query.schedule === undefined) return '';
|
//When looking for a passenger, we add the duration of the driver route to the latest toDate
|
||||||
const schedule: string[] = [];
|
//to compute the maximum sensible passenger fromDate, in case the pickup date could be on the next day
|
||||||
// we need full dates to compare times, because margins can lead to compare on previous or next day
|
const querySchedule = this.query.schedule;
|
||||||
// - first we establish a base calendar (up to a week)
|
// When there is no schedule (search whole day), we consider the driver accepts to depart until 23:59
|
||||||
const scheduleDates: Date[] = this._datesBetweenBoundaries(
|
const maxScheduleTime =
|
||||||
this.query.fromDate,
|
querySchedule === undefined
|
||||||
this.query.toDate,
|
? '23:59'
|
||||||
);
|
: querySchedule.reduce(
|
||||||
// - then we compare each resulting day of the schedule with each day of calendar,
|
(max, s) => (s.time > max ? s.time : max),
|
||||||
// adding / removing margin depending on the role
|
'00:00',
|
||||||
scheduleDates.map((date: Date) => {
|
);
|
||||||
(this.query.schedule as ScheduleItem[])
|
const [h, m] = maxScheduleTime.split(':');
|
||||||
.filter(
|
const maxFromDate = new Date(this.query.toDate);
|
||||||
(scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day,
|
maxFromDate.setHours(parseInt(h));
|
||||||
)
|
maxFromDate.setMinutes(parseInt(m));
|
||||||
.map((scheduleItem: ScheduleItem) => {
|
maxFromDate.setSeconds(this.query.driverRoute!.duration);
|
||||||
switch (role) {
|
return `'${maxFromDate.getUTCFullYear()}-${maxFromDate.getUTCMonth() + 1}-${maxFromDate.getUTCDate()}'`;
|
||||||
case Role.PASSENGER:
|
} else {
|
||||||
schedule.push(this._wherePassengerSchedule(date, scheduleItem));
|
return `'${this.query.toDate}'`;
|
||||||
break;
|
}
|
||||||
case Role.DRIVER:
|
};
|
||||||
schedule.push(this._whereDriverSchedule(date, scheduleItem));
|
|
||||||
break;
|
private _minToDate = (role: Role): string => {
|
||||||
}
|
if (role == Role.PASSENGER) {
|
||||||
});
|
// When looking for a driver, we look for a toDate that is one day before the fromDate of the query
|
||||||
});
|
// so that the driver will be able to pick up the passenger even during a long trip that starts the day before
|
||||||
if (schedule.length > 0) {
|
const oneDayBeforeFromDate = new Date(this.query.fromDate);
|
||||||
return ['(', schedule.join(' OR '), ')'].join('');
|
oneDayBeforeFromDate.setDate(oneDayBeforeFromDate.getDate() - 1);
|
||||||
|
return `'${oneDayBeforeFromDate.getUTCFullYear()}-${oneDayBeforeFromDate.getUTCMonth() + 1}-${oneDayBeforeFromDate.getUTCDate()}'`;
|
||||||
|
} else {
|
||||||
|
return `'${this.query.fromDate}'`;
|
||||||
}
|
}
|
||||||
return '';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private _whereExcludedAd = (): string =>
|
private _whereExcludedAd = (): string =>
|
||||||
this.query.excludedAdId ? `ad.uuid <> '${this.query.excludedAdId}'` : '';
|
this.query.excludedAdId ? `ad.uuid <> '${this.query.excludedAdId}'` : '';
|
||||||
|
|
||||||
private _wherePassengerSchedule = (
|
|
||||||
date: Date,
|
|
||||||
scheduleItem: ScheduleItem,
|
|
||||||
): string => {
|
|
||||||
let maxDepartureDatetime: Date = new Date(date);
|
|
||||||
maxDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0]));
|
|
||||||
maxDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1]));
|
|
||||||
maxDepartureDatetime = this._addMargin(
|
|
||||||
maxDepartureDatetime,
|
|
||||||
scheduleItem.margin as number,
|
|
||||||
);
|
|
||||||
// we want the min departure time of the driver to be before the max departure time of the passenger
|
|
||||||
return `make_timestamp(\
|
|
||||||
${maxDepartureDatetime.getUTCFullYear()},\
|
|
||||||
${maxDepartureDatetime.getUTCMonth() + 1},\
|
|
||||||
${maxDepartureDatetime.getUTCDate()},\
|
|
||||||
CAST(EXTRACT(hour from time) as integer),\
|
|
||||||
CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\
|
|
||||||
make_timestamp(\
|
|
||||||
${maxDepartureDatetime.getUTCFullYear()},\
|
|
||||||
${maxDepartureDatetime.getUTCMonth() + 1},\
|
|
||||||
${maxDepartureDatetime.getUTCDate()},${maxDepartureDatetime.getUTCHours()},${maxDepartureDatetime.getUTCMinutes()},0)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _whereDriverSchedule = (
|
|
||||||
date: Date,
|
|
||||||
scheduleItem: ScheduleItem,
|
|
||||||
): string => {
|
|
||||||
let minDepartureDatetime: Date = new Date(date);
|
|
||||||
minDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0]));
|
|
||||||
minDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1]));
|
|
||||||
minDepartureDatetime = this._addMargin(
|
|
||||||
minDepartureDatetime,
|
|
||||||
-(scheduleItem.margin as number),
|
|
||||||
);
|
|
||||||
// we want the max departure time of the passenger to be after the min departure time of the driver
|
|
||||||
return `make_timestamp(\
|
|
||||||
${minDepartureDatetime.getUTCFullYear()},
|
|
||||||
${minDepartureDatetime.getUTCMonth() + 1},
|
|
||||||
${minDepartureDatetime.getUTCDate()},\
|
|
||||||
CAST(EXTRACT(hour from time) as integer),\
|
|
||||||
CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\
|
|
||||||
make_timestamp(\
|
|
||||||
${minDepartureDatetime.getUTCFullYear()},
|
|
||||||
${minDepartureDatetime.getUTCMonth() + 1},
|
|
||||||
${minDepartureDatetime.getUTCDate()},${minDepartureDatetime.getUTCHours()},${minDepartureDatetime.getUTCMinutes()},0)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _whereAzimuth = (): string => {
|
private _whereAzimuth = (): string => {
|
||||||
if (!this.query.useAzimuth) return '';
|
if (!this.query.useAzimuth) return '';
|
||||||
const { minAzimuth, maxAzimuth } = this._azimuthRange(
|
const { minAzimuth, maxAzimuth } = this._azimuthRange(
|
||||||
|
@ -317,37 +275,6 @@ export class PassengerOrientedSelector extends Selector {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of dates containing all the dates (limited to 7 by default) between 2 boundary dates.
|
|
||||||
*
|
|
||||||
* The array length can be limited to a _max_ number of dates (default: 7)
|
|
||||||
*/
|
|
||||||
private _datesBetweenBoundaries = (
|
|
||||||
firstDate: string,
|
|
||||||
lastDate: string,
|
|
||||||
max = 7,
|
|
||||||
): Date[] => {
|
|
||||||
const fromDate: Date = new Date(firstDate);
|
|
||||||
const toDate: Date = new Date(lastDate);
|
|
||||||
const dates: Date[] = [];
|
|
||||||
let count = 0;
|
|
||||||
for (
|
|
||||||
let date = fromDate;
|
|
||||||
date <= toDate;
|
|
||||||
date.setUTCDate(date.getUTCDate() + 1)
|
|
||||||
) {
|
|
||||||
dates.push(new Date(date));
|
|
||||||
count++;
|
|
||||||
if (count == max) break;
|
|
||||||
}
|
|
||||||
return dates;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _addMargin = (date: Date, marginInSeconds: number): Date => {
|
|
||||||
date.setUTCSeconds(marginInSeconds);
|
|
||||||
return date;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _azimuthRange = (
|
private _azimuthRange = (
|
||||||
azimuth: number,
|
azimuth: number,
|
||||||
margin: number,
|
margin: number,
|
||||||
|
@ -358,11 +285,26 @@ export class PassengerOrientedSelector extends Selector {
|
||||||
azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin,
|
azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//TODO If the dates are always formatted with '%Y-%m-%d', no conversion to Date is needed
|
||||||
private _maxDateString = (date1: string, date2: string): string =>
|
private _maxDateString = (date1: string, date2: string): string =>
|
||||||
new Date(date1) > new Date(date2) ? date1 : date2;
|
new Date(date1) > new Date(date2) ? date1 : date2;
|
||||||
|
|
||||||
private _minDateString = (date1: string, date2: string): string =>
|
private _minDateString = (date1: string, date2: string): string =>
|
||||||
new Date(date1) < new Date(date2) ? date1 : date2;
|
new Date(date1) < new Date(date2) ? date1 : date2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a punctual ad matches a punctual query, it may be on a different date than the query
|
||||||
|
* (for routes by night), and the range produced by _minDateString and _maxDateString is not correct.
|
||||||
|
* This function fixes that by inverting the dates if necessary.
|
||||||
|
*/
|
||||||
|
private _fixDateInterval(interval: DateInterval): DateInterval {
|
||||||
|
if (interval.lowerDate > interval.higherDate) {
|
||||||
|
const tmp = interval.lowerDate;
|
||||||
|
interval.lowerDate = interval.higherDate;
|
||||||
|
interval.higherDate = tmp;
|
||||||
|
}
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueryStringRole = {
|
export type QueryStringRole = {
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { AdEntity } from './ad.entity';
|
||||||
|
import { Role, UserAd } from './ad.types';
|
||||||
|
import { GeorouterService } from './georouter.service';
|
||||||
|
import {
|
||||||
|
Path,
|
||||||
|
PathCreator,
|
||||||
|
PathType,
|
||||||
|
TypedRoute,
|
||||||
|
} from './path-creator.service';
|
||||||
|
import { Point } from './value-objects/point.value-object';
|
||||||
|
|
||||||
|
export class AdFactory {
|
||||||
|
constructor(private readonly routeProvider: GeorouterService) {}
|
||||||
|
/**
|
||||||
|
* Create an AdEntity (a "matcher ad", that is: the data needed to match an ad with a match query)
|
||||||
|
* from a "user ad" (the data provided by the user).
|
||||||
|
*/
|
||||||
|
public async create(ad: UserAd): Promise<AdEntity> {
|
||||||
|
const roles: Role[] = [];
|
||||||
|
if (ad.driver) roles.push(Role.DRIVER);
|
||||||
|
if (ad.passenger) roles.push(Role.PASSENGER);
|
||||||
|
|
||||||
|
const pathCreator = new PathCreator(
|
||||||
|
roles,
|
||||||
|
ad.waypoints.map((wp) => new Point({ lon: wp.lon, lat: wp.lat })),
|
||||||
|
);
|
||||||
|
|
||||||
|
let typedRoutes: TypedRoute[];
|
||||||
|
try {
|
||||||
|
typedRoutes = await Promise.all(
|
||||||
|
pathCreator.getBasePaths().map(async (path: Path) => ({
|
||||||
|
type: path.type,
|
||||||
|
route: await this.routeProvider.getRoute({
|
||||||
|
waypoints: path.waypoints,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
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: Point[];
|
||||||
|
let fwdAzimuth: number;
|
||||||
|
let backAzimuth: number;
|
||||||
|
try {
|
||||||
|
typedRoutes.forEach((typedRoute: TypedRoute) => {
|
||||||
|
if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) {
|
||||||
|
driverDistance = typedRoute.route.distance;
|
||||||
|
driverDuration = typedRoute.route.duration;
|
||||||
|
points = typedRoute.route.points.map((point) => new Point(point));
|
||||||
|
fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||||
|
backAzimuth = typedRoute.route.backAzimuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)) {
|
||||||
|
passengerDistance = typedRoute.route.distance;
|
||||||
|
passengerDuration = typedRoute.route.duration;
|
||||||
|
if (!points) {
|
||||||
|
points = typedRoute.route.points.map((point) => new Point(point));
|
||||||
|
}
|
||||||
|
if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||||
|
if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error('Invalid route');
|
||||||
|
}
|
||||||
|
|
||||||
|
return AdEntity.create({
|
||||||
|
id: ad.id,
|
||||||
|
driver: ad.driver,
|
||||||
|
passenger: ad.passenger,
|
||||||
|
frequency: ad.frequency,
|
||||||
|
fromDate: ad.fromDate,
|
||||||
|
toDate: ad.toDate,
|
||||||
|
schedule: ad.schedule,
|
||||||
|
seatsProposed: ad.seatsProposed,
|
||||||
|
seatsRequested: ad.seatsRequested,
|
||||||
|
strict: ad.strict,
|
||||||
|
waypoints: ad.waypoints,
|
||||||
|
points: points!,
|
||||||
|
driverDistance,
|
||||||
|
driverDuration,
|
||||||
|
passengerDistance,
|
||||||
|
passengerDuration,
|
||||||
|
fwdAzimuth: fwdAzimuth!,
|
||||||
|
backAzimuth: backAzimuth!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,23 @@
|
||||||
import { PointProps } from './value-objects/point.value-object';
|
import { PointProps } from './value-objects/point.value-object';
|
||||||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
|
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data provided by the end-user to publish an ad
|
||||||
|
*/
|
||||||
|
export interface UserAd {
|
||||||
|
id: string;
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
frequency: Frequency;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
schedule: ScheduleItemProps[];
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
strict: boolean;
|
||||||
|
waypoints: PointProps[];
|
||||||
|
}
|
||||||
|
|
||||||
// All properties that an Ad has
|
// All properties that an Ad has
|
||||||
export interface AdProps {
|
export interface AdProps {
|
||||||
driver: boolean;
|
driver: boolean;
|
||||||
|
|
|
@ -323,7 +323,7 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO Use this class as part of the CandidateEntity aggregate
|
//TODO Use this class as part of the CandidateEntity aggregate
|
||||||
class Schedule extends ValueObject<{
|
export class Schedule extends ValueObject<{
|
||||||
items: ScheduleItemProps[];
|
items: ScheduleItemProps[];
|
||||||
dateInterval: DateInterval;
|
dateInterval: DateInterval;
|
||||||
}> {
|
}> {
|
||||||
|
@ -353,7 +353,7 @@ class Schedule extends ValueObject<{
|
||||||
duration,
|
duration,
|
||||||
);
|
);
|
||||||
acc.push({
|
acc.push({
|
||||||
day: itemDate.getUTCDay(),
|
day: driverStartDatetime.getUTCDay(),
|
||||||
margin: scheduleItemProps.margin,
|
margin: scheduleItemProps.margin,
|
||||||
time: this._formatTime(driverStartDatetime),
|
time: this._formatTime(driverStartDatetime),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
export type Point = {
|
export type Point = {
|
||||||
lon: number;
|
lon: number;
|
||||||
lat: number;
|
lat: number;
|
||||||
|
@ -25,7 +23,6 @@ export type RouteResponse = {
|
||||||
steps?: Step[];
|
steps?: Step[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
export interface GeorouterService {
|
||||||
export abstract class GeorouterPort {
|
getRoute(request: RouteRequest): Promise<RouteResponse>;
|
||||||
abstract getRoute(request: RouteRequest): Promise<RouteResponse>;
|
|
||||||
}
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
|
import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
|
||||||
import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library';
|
|
||||||
import { PrismaService } from './prisma.service';
|
|
||||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
|
||||||
import { AdEntity } from '../core/domain/ad.entity';
|
|
||||||
import { AdMapper } from '../ad.mapper';
|
|
||||||
import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base';
|
|
||||||
import { Frequency } from '../core/domain/ad.types';
|
|
||||||
import { SERVICE_NAME } from '@src/app.constants';
|
import { SERVICE_NAME } from '@src/app.constants';
|
||||||
|
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
||||||
|
import { AdMapper } from '../ad.mapper';
|
||||||
|
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
||||||
|
import { AdEntity } from '../core/domain/ad.entity';
|
||||||
|
import { Frequency } from '../core/domain/ad.types';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
export type AdModel = {
|
export type AdModel = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
@ -26,8 +26,6 @@ export type AdModel = {
|
||||||
passengerDistance?: number;
|
passengerDistance?: number;
|
||||||
fwdAzimuth: number;
|
fwdAzimuth: number;
|
||||||
backAzimuth: number;
|
backAzimuth: number;
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,15 +34,26 @@ export type AdModel = {
|
||||||
export type AdReadModel = AdModel & {
|
export type AdReadModel = AdModel & {
|
||||||
waypoints: string;
|
waypoints: string;
|
||||||
schedule: ScheduleItemModel[];
|
schedule: ScheduleItemModel[];
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The record ready to be sent to the persistence system
|
* The record ready to be sent to the persistence system
|
||||||
*/
|
*/
|
||||||
export type AdWriteModel = AdModel & {
|
export type AdWriteModel = AdModel & {
|
||||||
schedule: {
|
schedule: ScheduleWriteModel;
|
||||||
create: ScheduleItemModel[];
|
};
|
||||||
};
|
|
||||||
|
export type ScheduleWriteModel = {
|
||||||
|
deleteMany?: PastCreatedFilter;
|
||||||
|
create: ScheduleItemModel[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// used to delete records created in the past,
|
||||||
|
// because the order of `create` and `deleteMany` is not guaranteed
|
||||||
|
export type PastCreatedFilter = {
|
||||||
|
createdAt: { lt: Date };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdWriteExtraModel = {
|
export type AdWriteExtraModel = {
|
||||||
|
@ -70,11 +79,15 @@ export type UngroupedAdModel = AdModel &
|
||||||
scheduleItemCreatedAt: Date;
|
scheduleItemCreatedAt: Date;
|
||||||
scheduleItemUpdatedAt: Date;
|
scheduleItemUpdatedAt: Date;
|
||||||
waypoints: string;
|
waypoints: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GroupedAdModel = AdModel & {
|
export type GroupedAdModel = AdModel & {
|
||||||
schedule: ScheduleItemModel[];
|
schedule: ScheduleItemModel[];
|
||||||
waypoints: string;
|
waypoints: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -169,4 +182,12 @@ export class AdRepository
|
||||||
});
|
});
|
||||||
return adReadModels;
|
return adReadModels;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
entity: AdEntity,
|
||||||
|
identifier?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
this.updateExtra(id, entity, 'ad', identifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import { Observable, lastValueFrom } from 'rxjs';
|
|
||||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { ClientGrpc } from '@nestjs/microservices';
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
import { GRPC_GEOROUTER_SERVICE_NAME } from '@src/app.constants';
|
import { GRPC_GEOROUTER_SERVICE_NAME } from '@src/app.constants';
|
||||||
|
import { Observable, lastValueFrom } from 'rxjs';
|
||||||
|
import { GEOGRAPHY_PACKAGE } from '../ad.di-tokens';
|
||||||
import {
|
import {
|
||||||
GeorouterPort,
|
GeorouterService,
|
||||||
RouteRequest,
|
RouteRequest,
|
||||||
RouteResponse,
|
RouteResponse,
|
||||||
} from '../core/application/ports/georouter.port';
|
} from '../core/domain/georouter.service';
|
||||||
import { GEOGRAPHY_PACKAGE } from '../ad.di-tokens';
|
|
||||||
|
|
||||||
interface GeorouterService {
|
interface GeorouterPort {
|
||||||
getRoute(request: RouteRequest): Observable<RouteResponse>;
|
getRoute(request: RouteRequest): Observable<RouteResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Georouter implements GeorouterPort, OnModuleInit {
|
export class Georouter implements GeorouterService, OnModuleInit {
|
||||||
private georouterService: GeorouterService;
|
private georouterService: GeorouterPort;
|
||||||
|
|
||||||
constructor(@Inject(GEOGRAPHY_PACKAGE) private readonly client: ClientGrpc) {}
|
constructor(@Inject(GEOGRAPHY_PACKAGE) private readonly client: ClientGrpc) {}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
this.georouterService = this.client.getService<GeorouterService>(
|
this.georouterService = this.client.getService<GeorouterPort>(
|
||||||
GRPC_GEOROUTER_SERVICE_NAME,
|
GRPC_GEOROUTER_SERVICE_NAME,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||||
|
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||||
import { Controller, Inject, UseFilters, UsePipes } from '@nestjs/common';
|
import { Controller, Inject, UseFilters, UsePipes } from '@nestjs/common';
|
||||||
|
@ -24,7 +24,7 @@ export class MatchGrpcController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly queryBus: QueryBus,
|
private readonly queryBus: QueryBus,
|
||||||
@Inject(AD_ROUTE_PROVIDER)
|
@Inject(AD_ROUTE_PROVIDER)
|
||||||
private readonly routeProvider: GeorouterPort,
|
private readonly routeProvider: GeorouterService,
|
||||||
private readonly matchMapper: MatchMapper,
|
private readonly matchMapper: MatchMapper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||||
|
import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import {
|
||||||
|
AD_UPDATED_MESSAGE_HANDLER,
|
||||||
|
AD_UPDATED_ROUTING_KEY,
|
||||||
|
} from '@src/app.constants';
|
||||||
|
import { Ad } from './ad.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdUpdatedMessageHandler {
|
||||||
|
constructor(private readonly commandBus: CommandBus) {}
|
||||||
|
|
||||||
|
@RabbitSubscribe({
|
||||||
|
name: AD_UPDATED_MESSAGE_HANDLER,
|
||||||
|
routingKey: AD_UPDATED_ROUTING_KEY,
|
||||||
|
})
|
||||||
|
public async adUpdated(message: string) {
|
||||||
|
try {
|
||||||
|
const updatedAd: { data: Ad } = JSON.parse(message);
|
||||||
|
await this.commandBus.execute(new UpdateAdCommand(updatedAd.data));
|
||||||
|
} catch (error: any) {
|
||||||
|
// do not throw error to acknowledge incoming message
|
||||||
|
// error handling should be done in the command handler, if relevant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||||
|
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export const Nice: PointProps = {
|
||||||
|
lat: 43.7102,
|
||||||
|
lon: 7.262,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Marseille: PointProps = {
|
||||||
|
lat: 43.2965,
|
||||||
|
lon: 5.3698,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SaintRaphael: PointProps = {
|
||||||
|
lat: 43.4268,
|
||||||
|
lon: 6.769,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Toulon: PointProps = {
|
||||||
|
lat: 43.1167,
|
||||||
|
lon: 5.95,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function monday(time: string): ScheduleItemProps {
|
||||||
|
return { day: 1, time: time, margin: 900 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wednesday(time: string): ScheduleItemProps {
|
||||||
|
return { day: 3, time: time, margin: 900 };
|
||||||
|
}
|
||||||
|
export function thursday(time: string): ScheduleItemProps {
|
||||||
|
return { day: 4, time: time, margin: 900 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function weekdays(time: string): ScheduleItemProps[] {
|
||||||
|
return [1, 2, 3, 4, 5].map<ScheduleItemProps>((day) => ({
|
||||||
|
day: day,
|
||||||
|
time: time,
|
||||||
|
margin: 900,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAdPropsDefaults(): CreateAdProps {
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
driver: false,
|
||||||
|
passenger: false,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
schedule: [],
|
||||||
|
seatsProposed: 1,
|
||||||
|
seatsRequested: 1,
|
||||||
|
strict: false,
|
||||||
|
waypoints: [],
|
||||||
|
points: [],
|
||||||
|
driverDuration: 0,
|
||||||
|
driverDistance: 0,
|
||||||
|
passengerDuration: 0,
|
||||||
|
passengerDistance: 0,
|
||||||
|
fwdAzimuth: 0,
|
||||||
|
backAzimuth: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function driverNiceMarseille(
|
||||||
|
frequency: Frequency,
|
||||||
|
dates: string[],
|
||||||
|
schedule: ScheduleItemProps[],
|
||||||
|
): CreateAdProps {
|
||||||
|
return {
|
||||||
|
...createAdPropsDefaults(),
|
||||||
|
driver: true,
|
||||||
|
frequency: frequency,
|
||||||
|
fromDate: dates[0],
|
||||||
|
toDate: dates[1],
|
||||||
|
schedule: schedule,
|
||||||
|
waypoints: [Nice, Marseille],
|
||||||
|
points: [Nice, SaintRaphael, Toulon, Marseille],
|
||||||
|
driverDuration: 7668,
|
||||||
|
driverDistance: 199000,
|
||||||
|
passengerDuration: 7668,
|
||||||
|
passengerDistance: 199000,
|
||||||
|
fwdAzimuth: 273,
|
||||||
|
backAzimuth: 93,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function passengerToulonMarseille(
|
||||||
|
frequency: Frequency,
|
||||||
|
dates: string[],
|
||||||
|
schedule: ScheduleItemProps[],
|
||||||
|
): CreateAdProps {
|
||||||
|
return {
|
||||||
|
...createAdPropsDefaults(),
|
||||||
|
passenger: true,
|
||||||
|
frequency: frequency,
|
||||||
|
fromDate: dates[0],
|
||||||
|
toDate: dates[1],
|
||||||
|
schedule: schedule,
|
||||||
|
waypoints: [Toulon, Marseille],
|
||||||
|
points: [Toulon, Marseille],
|
||||||
|
driverDuration: 2460,
|
||||||
|
driverDistance: 64000,
|
||||||
|
passengerDuration: 2460,
|
||||||
|
passengerDistance: 64000,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,61 +1,16 @@
|
||||||
import {
|
|
||||||
AD_DIRECTION_ENCODER,
|
|
||||||
AD_MESSAGE_PUBLISHER,
|
|
||||||
AD_REPOSITORY,
|
|
||||||
} from '@modules/ad/ad.di-tokens';
|
|
||||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
|
||||||
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 { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||||
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
import { driverNiceMarseille, wednesday, weekdays } from './ad.fixtures';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { integrationTestingModule } from './integration.setup';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
|
||||||
import { Test } from '@nestjs/testing';
|
|
||||||
|
|
||||||
describe('Ad Repository', () => {
|
describe('Ad Repository', () => {
|
||||||
let prismaService: PrismaService;
|
let prismaService: PrismaService;
|
||||||
let adRepository: AdRepository;
|
let adRepository: AdRepository;
|
||||||
|
|
||||||
const mockMessagePublisher = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockLogger = {
|
|
||||||
log: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const module = await Test.createTestingModule({
|
({ prismaService, adRepository } = await integrationTestingModule());
|
||||||
imports: [
|
|
||||||
EventEmitterModule.forRoot(),
|
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
PrismaService,
|
|
||||||
AdMapper,
|
|
||||||
{
|
|
||||||
provide: AD_REPOSITORY,
|
|
||||||
useClass: AdRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: AD_MESSAGE_PUBLISHER,
|
|
||||||
useValue: mockMessagePublisher,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: AD_DIRECTION_ENCODER,
|
|
||||||
useClass: PostgresDirectionEncoder,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
// disable logging
|
|
||||||
.setLogger(mockLogger)
|
|
||||||
.compile();
|
|
||||||
|
|
||||||
prismaService = module.get<PrismaService>(PrismaService);
|
|
||||||
adRepository = module.get<AdRepository>(AD_REPOSITORY);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -70,60 +25,12 @@ describe('Ad Repository', () => {
|
||||||
it('should create a punctual ad', async () => {
|
it('should create a punctual ad', async () => {
|
||||||
const beforeCount = await prismaService.ad.count();
|
const beforeCount = await prismaService.ad.count();
|
||||||
|
|
||||||
const createAdProps: CreateAdProps = {
|
const createAdProps = driverNiceMarseille(
|
||||||
id: 'b4b56444-f8d3-4110-917c-e37bba77f383',
|
Frequency.PUNCTUAL,
|
||||||
driver: true,
|
['2023-02-01', '2023-02-01'],
|
||||||
passenger: false,
|
[wednesday('08:30')],
|
||||||
frequency: Frequency.PUNCTUAL,
|
);
|
||||||
fromDate: '2023-02-01',
|
const adToCreate = AdEntity.create(createAdProps);
|
||||||
toDate: '2023-02-01',
|
|
||||||
schedule: [
|
|
||||||
{
|
|
||||||
day: 3,
|
|
||||||
time: '12:05',
|
|
||||||
margin: 900,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
seatsProposed: 3,
|
|
||||||
seatsRequested: 1,
|
|
||||||
strict: false,
|
|
||||||
waypoints: [
|
|
||||||
{
|
|
||||||
lon: 43.7102,
|
|
||||||
lat: 7.262,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 43.2965,
|
|
||||||
lat: 5.3698,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
points: [
|
|
||||||
{
|
|
||||||
lon: 7.262,
|
|
||||||
lat: 43.7102,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 6.797838,
|
|
||||||
lat: 43.547031,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 6.18535,
|
|
||||||
lat: 43.407517,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 5.3698,
|
|
||||||
lat: 43.2965,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
driverDuration: 7668,
|
|
||||||
driverDistance: 199000,
|
|
||||||
passengerDuration: 7668,
|
|
||||||
passengerDistance: 199000,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
};
|
|
||||||
|
|
||||||
const adToCreate: AdEntity = AdEntity.create(createAdProps);
|
|
||||||
await adRepository.insertExtra(adToCreate, 'ad');
|
await adRepository.insertExtra(adToCreate, 'ad');
|
||||||
|
|
||||||
const afterCount = await prismaService.ad.count();
|
const afterCount = await prismaService.ad.count();
|
||||||
|
@ -134,80 +41,13 @@ describe('Ad Repository', () => {
|
||||||
it('should create a recurrent ad', async () => {
|
it('should create a recurrent ad', async () => {
|
||||||
const beforeCount = await prismaService.ad.count();
|
const beforeCount = await prismaService.ad.count();
|
||||||
|
|
||||||
const createAdProps: CreateAdProps = {
|
const createAdProps = driverNiceMarseille(
|
||||||
id: 'b4b56444-f8d3-4110-917c-e37bba77f383',
|
Frequency.RECURRENT,
|
||||||
driver: true,
|
['2023-02-01', '2024-01-31'],
|
||||||
passenger: false,
|
weekdays('08:30'),
|
||||||
frequency: Frequency.RECURRENT,
|
);
|
||||||
fromDate: '2023-02-01',
|
|
||||||
toDate: '2024-01-31',
|
|
||||||
schedule: [
|
|
||||||
{
|
|
||||||
day: 1,
|
|
||||||
time: '08:00',
|
|
||||||
margin: 900,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
day: 2,
|
|
||||||
time: '08:00',
|
|
||||||
margin: 900,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
day: 3,
|
|
||||||
time: '09:00',
|
|
||||||
margin: 900,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
day: 4,
|
|
||||||
time: '08:00',
|
|
||||||
margin: 900,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
day: 5,
|
|
||||||
time: '08:00',
|
|
||||||
margin: 900,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
seatsProposed: 3,
|
|
||||||
seatsRequested: 1,
|
|
||||||
strict: false,
|
|
||||||
waypoints: [
|
|
||||||
{
|
|
||||||
lon: 43.7102,
|
|
||||||
lat: 7.262,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 43.2965,
|
|
||||||
lat: 5.3698,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
points: [
|
|
||||||
{
|
|
||||||
lon: 7.262,
|
|
||||||
lat: 43.7102,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 6.797838,
|
|
||||||
lat: 43.547031,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 6.18535,
|
|
||||||
lat: 43.407517,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 5.3698,
|
|
||||||
lat: 43.2965,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
driverDuration: 7668,
|
|
||||||
driverDistance: 199000,
|
|
||||||
passengerDuration: 7668,
|
|
||||||
passengerDistance: 199000,
|
|
||||||
fwdAzimuth: 273,
|
|
||||||
backAzimuth: 93,
|
|
||||||
};
|
|
||||||
|
|
||||||
const adToCreate: AdEntity = AdEntity.create(createAdProps);
|
const adToCreate = AdEntity.create(createAdProps);
|
||||||
await adRepository.insertExtra(adToCreate, 'ad');
|
await adRepository.insertExtra(adToCreate, 'ad');
|
||||||
|
|
||||||
const afterCount = await prismaService.ad.count();
|
const afterCount = await prismaService.ad.count();
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import {
|
||||||
|
AD_DIRECTION_ENCODER,
|
||||||
|
AD_MESSAGE_PUBLISHER,
|
||||||
|
AD_REPOSITORY,
|
||||||
|
} from '@modules/ad/ad.di-tokens';
|
||||||
|
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||||
|
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||||
|
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||||
|
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
|
export async function integrationTestingModule(): Promise<{
|
||||||
|
prismaService: PrismaService;
|
||||||
|
adRepository: AdRepository;
|
||||||
|
}> {
|
||||||
|
const mockMessagePublisher = {
|
||||||
|
publish: jest.fn().mockImplementation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockLogger = {
|
||||||
|
log: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
PrismaService,
|
||||||
|
AdMapper,
|
||||||
|
{
|
||||||
|
provide: AD_REPOSITORY,
|
||||||
|
useClass: AdRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AD_MESSAGE_PUBLISHER,
|
||||||
|
useValue: mockMessagePublisher,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AD_DIRECTION_ENCODER,
|
||||||
|
useClass: PostgresDirectionEncoder,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.setLogger(mockLogger)
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
return {
|
||||||
|
prismaService: module.get<PrismaService>(PrismaService),
|
||||||
|
adRepository: module.get<AdRepository>(AD_REPOSITORY),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,467 @@
|
||||||
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
|
import { PassengerOrientedSelector } from '@modules/ad/core/application/queries/match/selector/passenger-oriented.selector';
|
||||||
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||||
|
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||||
|
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||||
|
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||||
|
import { bareMockGeorouter } from '../unit/georouter.mock';
|
||||||
|
import {
|
||||||
|
Marseille,
|
||||||
|
Nice,
|
||||||
|
SaintRaphael,
|
||||||
|
Toulon,
|
||||||
|
driverNiceMarseille,
|
||||||
|
monday,
|
||||||
|
passengerToulonMarseille,
|
||||||
|
thursday,
|
||||||
|
wednesday,
|
||||||
|
} from './ad.fixtures';
|
||||||
|
import { integrationTestingModule } from './integration.setup';
|
||||||
|
function baseMatchQuery(
|
||||||
|
frequency: Frequency,
|
||||||
|
dates: [string, string],
|
||||||
|
scheduleItems: ScheduleItemProps[],
|
||||||
|
waypoints: WaypointDto[],
|
||||||
|
): MatchQuery {
|
||||||
|
return new MatchQuery(
|
||||||
|
{
|
||||||
|
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
|
||||||
|
driver: false,
|
||||||
|
passenger: false,
|
||||||
|
frequency: frequency,
|
||||||
|
fromDate: dates[0],
|
||||||
|
toDate: dates[1],
|
||||||
|
useAzimuth: false,
|
||||||
|
useProportion: false,
|
||||||
|
remoteness: 15000,
|
||||||
|
schedule: scheduleItems,
|
||||||
|
strict: false,
|
||||||
|
waypoints: waypoints,
|
||||||
|
},
|
||||||
|
bareMockGeorouter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function passengerQueryToulonMarseille(
|
||||||
|
frequency: Frequency,
|
||||||
|
dates: [string, string],
|
||||||
|
scheduleItems: ScheduleItemProps[],
|
||||||
|
): MatchQuery {
|
||||||
|
const matchQuery = baseMatchQuery(frequency, dates, scheduleItems, [
|
||||||
|
{ position: 0, ...Toulon },
|
||||||
|
{ position: 1, ...Marseille },
|
||||||
|
]);
|
||||||
|
matchQuery.passenger = true;
|
||||||
|
matchQuery.passengerRoute = {
|
||||||
|
distance: 64000,
|
||||||
|
duration: 2460,
|
||||||
|
points: [Toulon, Marseille],
|
||||||
|
// Not used by this query
|
||||||
|
fwdAzimuth: 0,
|
||||||
|
backAzimuth: 0,
|
||||||
|
distanceAzimuth: 0,
|
||||||
|
};
|
||||||
|
return matchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
function driverQueryNiceMarseille(
|
||||||
|
frequency: Frequency,
|
||||||
|
dates: [string, string],
|
||||||
|
scheduleItems: ScheduleItemProps[],
|
||||||
|
): MatchQuery {
|
||||||
|
const matchQuery = baseMatchQuery(frequency, dates, scheduleItems, [
|
||||||
|
{ position: 0, ...Nice },
|
||||||
|
{ position: 1, ...Marseille },
|
||||||
|
]);
|
||||||
|
matchQuery.driver = true;
|
||||||
|
matchQuery.driverRoute = {
|
||||||
|
distance: 199000,
|
||||||
|
duration: 7668,
|
||||||
|
points: [Nice, SaintRaphael, Toulon, Marseille],
|
||||||
|
// Not used by this query
|
||||||
|
fwdAzimuth: 0,
|
||||||
|
backAzimuth: 0,
|
||||||
|
distanceAzimuth: 0,
|
||||||
|
};
|
||||||
|
return matchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PassengerOriented selector', () => {
|
||||||
|
let prismaService: PrismaService;
|
||||||
|
let adRepository: AdRepository;
|
||||||
|
|
||||||
|
const insertAd = async (adProps: CreateAdProps): Promise<void> => {
|
||||||
|
const ad = AdEntity.create(adProps);
|
||||||
|
return adRepository.insertExtra(ad, 'ad');
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
({ prismaService, adRepository } = await integrationTestingModule());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prismaService.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await prismaService.ad.deleteMany();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('select', () => {
|
||||||
|
it('should find a driver that departs on the same day', async () => {
|
||||||
|
await insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
passengerQueryToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a passenger that departs on the same day', async () => {
|
||||||
|
await insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
driverQueryNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a driver that departs the day before', async () => {
|
||||||
|
await insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('23:45')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
passengerQueryToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
[thursday('01:15')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a passenger that departs the day after', async () => {
|
||||||
|
await insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
[thursday('01:15')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
driverQueryNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('23:45')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a driver that departs shortly after midnight', async () => {
|
||||||
|
await insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
//01:30 in Nice is 00:30 in UTC
|
||||||
|
[thursday('01:30')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
passengerQueryToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
[thursday('03:00')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a passenger that departs shortly after midnight', async () => {
|
||||||
|
await insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
[thursday('03:00')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
driverQueryNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
[thursday('01:30')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT find a driver that departs the day after', async () => {
|
||||||
|
await insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
[thursday('08:30')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
passengerQueryToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT find a passenger that departs the day before', async () => {
|
||||||
|
await insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
driverQueryNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
[thursday('08:30')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a recurring driver that interesects', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-01', '2023-02-28'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-01', '2023-02-18'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-12', '2023-02-28'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-12', '2023-02-18'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
passengerQueryToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-10', '2023-02-20'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT find a recurring driver that doesn't interesect", async () => {
|
||||||
|
await Promise.all([
|
||||||
|
insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-01', '2023-02-10'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-20', '2023-02-28'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
passengerQueryToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-12', '2023-02-18'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a recurring passenger that interesects', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-01', '2023-02-28'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-01', '2023-02-18'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-12', '2023-02-28'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-12', '2023-02-18'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
driverQueryNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-10', '2023-02-20'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT find a recurring passenger that doesn't interesect", async () => {
|
||||||
|
await Promise.all([
|
||||||
|
insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-01', '2023-02-10'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-20', '2023-02-28'],
|
||||||
|
[wednesday('10:00')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
driverQueryNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-12', '2023-02-18'],
|
||||||
|
[wednesday('08:30')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a borderline driver that departs the day before a recurring query', async () => {
|
||||||
|
await insertAd(
|
||||||
|
driverNiceMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-01', '2023-02-01'],
|
||||||
|
[wednesday('23:45')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
passengerQueryToulonMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-02-02', '2023-02-28'],
|
||||||
|
[monday('13:45'), thursday('01:15')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a borderline passenger that departs the day after a recurring query', async () => {
|
||||||
|
await insertAd(
|
||||||
|
passengerToulonMarseille(
|
||||||
|
Frequency.PUNCTUAL,
|
||||||
|
['2023-02-02', '2023-02-02'],
|
||||||
|
[thursday('01:15')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||||
|
driverQueryNiceMarseille(
|
||||||
|
Frequency.RECURRENT,
|
||||||
|
['2023-01-01', '2023-02-01'],
|
||||||
|
[monday('13:45'), wednesday('23:45')],
|
||||||
|
),
|
||||||
|
adRepository,
|
||||||
|
);
|
||||||
|
const candidates = await passengerOrientedSelector.select();
|
||||||
|
expect(candidates.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,18 +1,17 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||||
import {
|
import {
|
||||||
AD_MESSAGE_PUBLISHER,
|
AD_MESSAGE_PUBLISHER,
|
||||||
AD_REPOSITORY,
|
AD_REPOSITORY,
|
||||||
AD_ROUTE_PROVIDER,
|
AD_ROUTE_PROVIDER,
|
||||||
} from '@modules/ad/ad.di-tokens';
|
} 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';
|
|
||||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
|
||||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
|
||||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||||
|
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
||||||
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
const originWaypoint: PointProps = {
|
const originWaypoint: PointProps = {
|
||||||
lat: 48.689445,
|
lat: 48.689445,
|
||||||
|
@ -62,7 +61,7 @@ const mockAdRepository = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: GeorouterPort = {
|
const mockRouteProvider: GeorouterService = {
|
||||||
getRoute: jest
|
getRoute: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {
|
import {
|
||||||
Domain,
|
|
||||||
KeyType,
|
|
||||||
Configurator,
|
Configurator,
|
||||||
|
Domain,
|
||||||
GetConfigurationRepositoryPort,
|
GetConfigurationRepositoryPort,
|
||||||
|
KeyType,
|
||||||
} from '@mobicoop/configuration-module';
|
} from '@mobicoop/configuration-module';
|
||||||
import {
|
import {
|
||||||
CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN,
|
CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN,
|
||||||
|
@ -16,8 +16,9 @@ import {
|
||||||
AD_REPOSITORY,
|
AD_REPOSITORY,
|
||||||
INPUT_DATETIME_TRANSFORMER,
|
INPUT_DATETIME_TRANSFORMER,
|
||||||
MATCHING_REPOSITORY,
|
MATCHING_REPOSITORY,
|
||||||
|
TIMEZONE_FINDER,
|
||||||
|
TIME_CONVERTER,
|
||||||
} from '@modules/ad/ad.di-tokens';
|
} from '@modules/ad/ad.di-tokens';
|
||||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
|
||||||
import { MatchingRepositoryPort } from '@modules/ad/core/application/ports/matching.repository.port';
|
import { MatchingRepositoryPort } from '@modules/ad/core/application/ports/matching.repository.port';
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import {
|
import {
|
||||||
|
@ -30,6 +31,9 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { Target } from '@modules/ad/core/domain/candidate.types';
|
import { Target } from '@modules/ad/core/domain/candidate.types';
|
||||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||||
import { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
|
import { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
|
||||||
|
import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer';
|
||||||
|
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
|
||||||
|
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
|
||||||
import {
|
import {
|
||||||
MATCH_CONFIG_ALGORITHM,
|
MATCH_CONFIG_ALGORITHM,
|
||||||
MATCH_CONFIG_AZIMUTH_MARGIN,
|
MATCH_CONFIG_AZIMUTH_MARGIN,
|
||||||
|
@ -344,13 +348,6 @@ const mockConfigurationRepository: GetConfigurationRepositoryPort = {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
|
||||||
fromDate: jest.fn(),
|
|
||||||
toDate: jest.fn(),
|
|
||||||
day: jest.fn(),
|
|
||||||
time: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockRouteProvider = simpleMockGeorouter;
|
const mockRouteProvider = simpleMockGeorouter;
|
||||||
|
|
||||||
describe('Match Query Handler', () => {
|
describe('Match Query Handler', () => {
|
||||||
|
@ -372,9 +369,17 @@ describe('Match Query Handler', () => {
|
||||||
provide: AD_CONFIGURATION_REPOSITORY,
|
provide: AD_CONFIGURATION_REPOSITORY,
|
||||||
useValue: mockConfigurationRepository,
|
useValue: mockConfigurationRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: TIMEZONE_FINDER,
|
||||||
|
useClass: TimezoneFinder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TIME_CONVERTER,
|
||||||
|
useClass: TimeConverter,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: INPUT_DATETIME_TRANSFORMER,
|
provide: INPUT_DATETIME_TRANSFORMER,
|
||||||
useValue: mockInputDateTimeTransformer,
|
useClass: InputDateTimeTransformer,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
|
||||||
import {
|
import {
|
||||||
MatchQuery,
|
MatchQuery,
|
||||||
ScheduleItem,
|
ScheduleItem,
|
||||||
|
@ -7,6 +6,7 @@ import {
|
||||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||||
import { simpleMockGeorouter } from '../georouter.mock';
|
import { simpleMockGeorouter } from '../georouter.mock';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
|
@ -61,7 +61,7 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
||||||
time: jest.fn().mockImplementation(() => '23:05'),
|
time: jest.fn().mockImplementation(() => '23:05'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: GeorouterPort = {
|
const mockRouteProvider: GeorouterService = {
|
||||||
getRoute: jest
|
getRoute: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(simpleMockGeorouter.getRoute)
|
.mockImplementationOnce(simpleMockGeorouter.getRoute)
|
||||||
|
|
|
@ -72,27 +72,7 @@ matchQuery.driverRoute = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
matchQuery.passengerRoute = {
|
matchQuery.passengerRoute = { ...matchQuery.driverRoute };
|
||||||
distance: 150120,
|
|
||||||
duration: 6540,
|
|
||||||
fwdAzimuth: 276,
|
|
||||||
backAzimuth: 96,
|
|
||||||
distanceAzimuth: 148321,
|
|
||||||
points: [
|
|
||||||
{
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lat: 48.7566,
|
|
||||||
lon: 4.3522,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lat: 48.8566,
|
|
||||||
lon: 2.3522,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMatcherRepository: AdRepositoryPort = {
|
const mockMatcherRepository: AdRepositoryPort = {
|
||||||
insertExtra: jest.fn(),
|
insertExtra: jest.fn(),
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
import {
|
|
||||||
RouteRequest,
|
|
||||||
RouteResponse,
|
|
||||||
} from '@modules/ad/core/application/ports/georouter.port';
|
|
||||||
import {
|
import {
|
||||||
RouteCompleter,
|
RouteCompleter,
|
||||||
RouteCompleterType,
|
RouteCompleterType,
|
||||||
|
@ -12,6 +8,10 @@ import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
import { Target } from '@modules/ad/core/domain/candidate.types';
|
import { Target } from '@modules/ad/core/domain/candidate.types';
|
||||||
|
import {
|
||||||
|
RouteRequest,
|
||||||
|
RouteResponse,
|
||||||
|
} from '@modules/ad/core/domain/georouter.service';
|
||||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
|
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
|
||||||
import { Step } from '@modules/geography/core/domain/route.types';
|
import { Step } from '@modules/geography/core/domain/route.types';
|
||||||
import { simpleMockGeorouter } from '../georouter.mock';
|
import { simpleMockGeorouter } from '../georouter.mock';
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import {
|
||||||
|
AD_MESSAGE_PUBLISHER,
|
||||||
|
AD_REPOSITORY,
|
||||||
|
AD_ROUTE_PROVIDER,
|
||||||
|
} from '@modules/ad/ad.di-tokens';
|
||||||
|
import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command';
|
||||||
|
import { UpdateAdService } from '@modules/ad/core/application/commands/update-ad/update-ad.service';
|
||||||
|
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { createAdProps } from './ad.fixtures';
|
||||||
|
|
||||||
|
const mockAdRepository = {
|
||||||
|
update: jest.fn().mockImplementation((id) => {
|
||||||
|
if (id === '42') {
|
||||||
|
throw 'Bad id!';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRouteProvider: GeorouterService = {
|
||||||
|
getRoute: jest.fn().mockImplementation(() => ({
|
||||||
|
distance: 350101,
|
||||||
|
duration: 14422,
|
||||||
|
fwdAzimuth: 273,
|
||||||
|
backAzimuth: 93,
|
||||||
|
distanceAzimuth: 336544,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lon: 6.1765102,
|
||||||
|
lat: 48.689445,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 4.984578,
|
||||||
|
lat: 48.725687,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 2.3522,
|
||||||
|
lat: 48.8566,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockMessagePublisher = {
|
||||||
|
publish: jest.fn().mockImplementation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('update-ad.service', () => {
|
||||||
|
let updateAdService: UpdateAdService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: AD_REPOSITORY,
|
||||||
|
useValue: mockAdRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AD_ROUTE_PROVIDER,
|
||||||
|
useValue: mockRouteProvider,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AD_MESSAGE_PUBLISHER,
|
||||||
|
useValue: mockMessagePublisher,
|
||||||
|
},
|
||||||
|
UpdateAdService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
updateAdService = module.get<UpdateAdService>(UpdateAdService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(updateAdService).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execute', () => {
|
||||||
|
it('should call the repository update method', async () => {
|
||||||
|
const updateAdCommand = new UpdateAdCommand(createAdProps());
|
||||||
|
await updateAdService.execute(updateAdCommand);
|
||||||
|
expect(mockAdRepository.update).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit an event when an error occurs', async () => {
|
||||||
|
const commandProps = createAdProps();
|
||||||
|
commandProps.id = '42';
|
||||||
|
const updateAdCommand = new UpdateAdCommand(commandProps);
|
||||||
|
await expect(updateAdService.execute(updateAdCommand)).rejects.toBe(
|
||||||
|
'Bad id!',
|
||||||
|
);
|
||||||
|
expect(mockMessagePublisher.publish).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,10 +1,10 @@
|
||||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||||
|
|
||||||
export const bareMockGeorouter: GeorouterPort = {
|
export const bareMockGeorouter: GeorouterService = {
|
||||||
getRoute: jest.fn(),
|
getRoute: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const simpleMockGeorouter: GeorouterPort = {
|
export const simpleMockGeorouter: GeorouterService = {
|
||||||
getRoute: jest.fn().mockImplementation(() => ({
|
getRoute: jest.fn().mockImplementation(() => ({
|
||||||
distance: 350101,
|
distance: 350101,
|
||||||
duration: 14422,
|
duration: 14422,
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { AdUpdatedMessageHandler } from '@modules/ad/interface/message-handlers/ad-updated.message-handler';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const adUpdatedMessage =
|
||||||
|
'{"data": {"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4","driver":"true","passenger":"true","frequency":"PUNCTUAL","fromDate":"2023-08-18","toDate":"2023-08-18","schedule":[{"day":"5","time":"10:00","margin":"900"}],"seatsProposed":"3","seatsRequested":"1","strict":"false","waypoints":[{"position":"0","houseNumber":"5","street":"rue de la monnaie","locality":"Nancy","postalCode":"54000","country":"France","lon":"48.689445","lat":"6.17651"},{"position":"1","locality":"Paris","postalCode":"75000","country":"France","lon":"48.8566","lat":"2.3522"}]}}';
|
||||||
|
|
||||||
|
const mockCommandBus = {
|
||||||
|
execute: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Ad Updated Message Handler', () => {
|
||||||
|
let adUpdatedMessageHandler: AdUpdatedMessageHandler;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CommandBus,
|
||||||
|
useValue: mockCommandBus,
|
||||||
|
},
|
||||||
|
AdUpdatedMessageHandler,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
adUpdatedMessageHandler = module.get<AdUpdatedMessageHandler>(
|
||||||
|
AdUpdatedMessageHandler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(adUpdatedMessageHandler).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update an ad', async () => {
|
||||||
|
await adUpdatedMessageHandler.adUpdated(adUpdatedMessage);
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,6 +12,9 @@ import {
|
||||||
AD_DELETED_MESSAGE_HANDLER,
|
AD_DELETED_MESSAGE_HANDLER,
|
||||||
AD_DELETED_QUEUE,
|
AD_DELETED_QUEUE,
|
||||||
AD_DELETED_ROUTING_KEY,
|
AD_DELETED_ROUTING_KEY,
|
||||||
|
AD_UPDATED_MESSAGE_HANDLER,
|
||||||
|
AD_UPDATED_QUEUE,
|
||||||
|
AD_UPDATED_ROUTING_KEY,
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
} from '@src/app.constants';
|
} from '@src/app.constants';
|
||||||
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
||||||
|
@ -36,6 +39,10 @@ const imports = [
|
||||||
routingKey: AD_CREATED_ROUTING_KEY,
|
routingKey: AD_CREATED_ROUTING_KEY,
|
||||||
queue: AD_CREATED_QUEUE,
|
queue: AD_CREATED_QUEUE,
|
||||||
},
|
},
|
||||||
|
[AD_UPDATED_MESSAGE_HANDLER]: {
|
||||||
|
routingKey: AD_UPDATED_ROUTING_KEY,
|
||||||
|
queue: AD_UPDATED_QUEUE,
|
||||||
|
},
|
||||||
[AD_DELETED_MESSAGE_HANDLER]: {
|
[AD_DELETED_MESSAGE_HANDLER]: {
|
||||||
routingKey: AD_DELETED_ROUTING_KEY,
|
routingKey: AD_DELETED_ROUTING_KEY,
|
||||||
queue: AD_DELETED_QUEUE,
|
queue: AD_DELETED_QUEUE,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
"exclude": ["node_modules", "tests", "dist", "**/*spec.ts"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue