mirror of
https://gitlab.com/mobicoop/v3/service/matcher.git
synced 2026-01-01 02:02:40 +00:00
Compare commits
34 Commits
amqp-geogr
...
1.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d48d01f051 | ||
|
|
1701fbbeb1 | ||
|
|
a7c281d740 | ||
|
|
01ebac7e74 | ||
|
|
104559d03d | ||
|
|
173e5ebba5 | ||
|
|
0dc01da2b0 | ||
|
|
945ce80840 | ||
|
|
16ebe8d543 | ||
|
|
0c29e522ed | ||
|
|
c51c368d83 | ||
|
|
0446d267ef | ||
|
|
100fb3487d | ||
|
|
e501bef249 | ||
|
|
71ac97410a | ||
|
|
5696ac57bd | ||
|
|
f759581157 | ||
|
|
08b5af7511 | ||
|
|
4581af5e9f | ||
|
|
7f7a51d19b | ||
|
|
739d05b095 | ||
|
|
212b609e26 | ||
|
|
bd6fc1576b | ||
|
|
53df6183bd | ||
|
|
ffeb009497 | ||
|
|
3786fcc2c2 | ||
|
|
e53c12ba74 | ||
|
|
c5a5e33256 | ||
|
|
924547c316 | ||
|
|
5f8dd8b4a0 | ||
|
|
90ae3cf9cb | ||
|
|
6b9bf53b4a | ||
|
|
4fd2950027 | ||
|
|
2ce2a46c95 |
@@ -7,52 +7,7 @@ stages:
|
||||
include:
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
|
||||
##############
|
||||
# TEST STAGE #
|
||||
##############
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: docker/compose:latest
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: ''
|
||||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- docker-compose -f docker-compose.ci.tools.yml -p matcher-tools --env-file ci/.env.ci up -d
|
||||
- sh ci/wait-up.sh
|
||||
- docker-compose -f docker-compose.ci.service.yml -p matcher-service --env-file ci/.env.ci up -d
|
||||
- docker exec -t v3-matcher-api sh -c "npm run test:integration:ci"
|
||||
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
||||
when: always
|
||||
|
||||
###############
|
||||
# BUILD STAGE #
|
||||
###############
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: docker:20.10.22
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: ''
|
||||
services:
|
||||
- docker:dind
|
||||
before_script:
|
||||
- echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
|
||||
script:
|
||||
- export VERSION=$(docker run --rm -v "$PWD":/usr/src/app:ro -w /usr/src/app node:slim node -p "require('./package.json').version")
|
||||
- docker pull $CI_REGISTRY_IMAGE:latest || true
|
||||
- >
|
||||
docker build
|
||||
--pull
|
||||
--cache-from $CI_REGISTRY_IMAGE:latest
|
||||
--tag $CI_REGISTRY_IMAGE:$VERSION
|
||||
--tag $CI_REGISTRY_IMAGE:latest
|
||||
.
|
||||
- docker push $CI_REGISTRY_IMAGE:$VERSION
|
||||
- docker push $CI_REGISTRY_IMAGE:latest
|
||||
only:
|
||||
- main
|
||||
- project: mobicoop/v3/gitlab-templates
|
||||
file:
|
||||
- /ci/release.build-job.yml
|
||||
- /ci/service.test-job.yml
|
||||
|
||||
11
README.md
11
README.md
@@ -156,13 +156,18 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||
- **strict** (boolean, optional): if set to true, allow matching only with similar frequency ads (_default : false_)
|
||||
- **fromDate**: start date for recurrent ad, carpool date for punctual ad
|
||||
- **toDate**: end date for recurrent ad, same as fromDate for punctual ad
|
||||
- **schedule**: an array of schedule items, a schedule item containing :
|
||||
- **schedule**: an optional array of schedule items, a schedule item containing :
|
||||
|
||||
- the week day as a number, from 0 (sunday) to 6 (saturday) if the ad is recurrent (default to fromDate day for punctual search)
|
||||
- the departure time (as HH:MM)
|
||||
- the margin around the departure time in seconds (optional) (_default : 900_)
|
||||
|
||||
_If the schedule is not set, the driver departure time is guessed to be the ideal departure time to reach the passenger, and the passenger departure time is guessed to be the ideal pick up time for the driver_
|
||||
|
||||
- **seatsProposed** (integer, optional): number of seats proposed as driver (_default : 3_)
|
||||
- **seatsRequested** (integer, optional): number of seats requested as passenger (_default : 1_)
|
||||
- **waypoints**: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
|
||||
- **excludedAdId** (optional): the id of an ad to be excluded from the results (useful to avoid self-matchings)
|
||||
- **algorithmType** (optional): the type of algorithm to use (as of 2023-09-28, only the default `PASSENGER_ORIENTED` is accepted)
|
||||
- **remoteness** (integer, optional): an integer to indicate the maximum flying distance (in metres) between the driver route and the passenger pick-up / drop-off points (_default : 15000_)
|
||||
- **useProportion** (boolean, optional): a boolean to indicate if the matching algorithm will compare the distance of the passenger route against the distance of the driver route (_default : 1_). Works in combination with **proportion** parameter
|
||||
@@ -214,10 +219,6 @@ If the matching is successful, you will get a result, containing :
|
||||
Matching is a time-consuming process, so the results of a matching request are stored in cache before being paginated and returned to the requester.
|
||||
An id is attributed to the overall results of a request : on further requests (for example to query for different pages of results), the requester can provide this id and get in return the cached data, avoiding another longer process of computing the results from scratch. Obviously, new computing must be done periodically to get fresh new results !
|
||||
|
||||
There's also a basic cache to store the results of the _same_ request sent multiple times successively.
|
||||
|
||||
Cache TTLs are customizable in the `.env` file.
|
||||
|
||||
## Tests / ESLint / Prettier
|
||||
|
||||
Tests are run outside the container for ease of use (switching between different environments inside containers using prisma is complicated and error prone).
|
||||
|
||||
@@ -11,10 +11,11 @@ services:
|
||||
- .:/usr/src/app
|
||||
env_file:
|
||||
- .env
|
||||
command: npm run start:dev
|
||||
command: npm run start:debug
|
||||
ports:
|
||||
- ${SERVICE_PORT:-5005}:${SERVICE_PORT:-5005}
|
||||
- ${HEALTH_SERVICE_PORT:-6005}:${HEALTH_SERVICE_PORT:-6005}
|
||||
- 9225:9229
|
||||
networks:
|
||||
v3-network:
|
||||
aliases:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mobicoop/matcher",
|
||||
"version": "1.5.5",
|
||||
"version": "1.6.0",
|
||||
"description": "Mobicoop V3 Matcher",
|
||||
"author": "sbriat",
|
||||
"private": true,
|
||||
@@ -11,7 +11,7 @@
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
||||
|
||||
@@ -15,6 +15,9 @@ export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
||||
export const AD_CREATED_MESSAGE_HANDLER = 'adCreated';
|
||||
export const AD_CREATED_ROUTING_KEY = 'ad.created';
|
||||
export const AD_CREATED_QUEUE = 'matcher.ad.created';
|
||||
export const AD_DELETED_MESSAGE_HANDLER = 'adDeleted';
|
||||
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
|
||||
export const AD_DELETED_QUEUE = 'matcher.ad.deleted';
|
||||
|
||||
// health
|
||||
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
||||
|
||||
24
src/log-cause.exception-filter.ts
Normal file
24
src/log-cause.exception-filter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ArgumentsHost, Catch, Logger } from '@nestjs/common';
|
||||
import { BaseRpcExceptionFilter } from '@nestjs/microservices';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Catch()
|
||||
export class LogCauseExceptionFilter extends BaseRpcExceptionFilter {
|
||||
private static readonly causeLogger = new Logger('RpcExceptionsHandler');
|
||||
|
||||
catch(exception: any, host: ArgumentsHost): Observable<any> {
|
||||
const response = super.catch(exception, host);
|
||||
const cause = exception.cause;
|
||||
if (cause) {
|
||||
if (this.isError(cause)) {
|
||||
LogCauseExceptionFilter.causeLogger.error(
|
||||
'Caused by: ' + cause.message,
|
||||
cause.stack,
|
||||
);
|
||||
} else {
|
||||
LogCauseExceptionFilter.causeLogger.error('Caused by: ' + cause);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,3 @@ export const AD_CONFIGURATION_REPOSITORY = Symbol(
|
||||
'AD_CONFIGURATION_REPOSITORY',
|
||||
);
|
||||
export const GEOGRAPHY_PACKAGE = Symbol('GEOGRAPHY_PACKAGE');
|
||||
export const GEOGRAPHY_SERVICE = Symbol('GEOGRAPHY_SERVICE');
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { ExtendedMapper } from '@mobicoop/ddd-library';
|
||||
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AdEntity } from './core/domain/ad.entity';
|
||||
import {
|
||||
AdWriteModel,
|
||||
AdReadModel,
|
||||
ScheduleItemModel,
|
||||
AdWriteExtraModel,
|
||||
} from './infrastructure/ad.repository';
|
||||
import { v4 } from 'uuid';
|
||||
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
|
||||
import { AdEntity } from './core/domain/ad.entity';
|
||||
import {
|
||||
ScheduleItem,
|
||||
ScheduleItemProps,
|
||||
} from './core/domain/value-objects/schedule-item.value-object';
|
||||
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
|
||||
import { ExtendedMapper } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AdReadModel,
|
||||
AdWriteExtraModel,
|
||||
AdWriteModel,
|
||||
ScheduleItemModel,
|
||||
} from './infrastructure/ad.repository';
|
||||
|
||||
/**
|
||||
* Mapper constructs objects that are used in different layers:
|
||||
@@ -97,7 +97,7 @@ export class AdMapper
|
||||
frequency: record.frequency,
|
||||
fromDate: record.fromDate.toISOString().split('T')[0],
|
||||
toDate: record.toDate.toISOString().split('T')[0],
|
||||
schedule: record.schedule.map(
|
||||
schedule: record.schedule?.map(
|
||||
(scheduleItem: ScheduleItemModel) =>
|
||||
new ScheduleItem({
|
||||
day: scheduleItem.day,
|
||||
@@ -111,12 +111,14 @@ export class AdMapper
|
||||
margin: scheduleItem.margin,
|
||||
}),
|
||||
),
|
||||
waypoints: this.directionEncoder
|
||||
.decode(record.waypoints)
|
||||
.map((coordinates, index) => ({
|
||||
position: index,
|
||||
...coordinates,
|
||||
})),
|
||||
waypoints: record.waypoints
|
||||
? this.directionEncoder
|
||||
.decode(record.waypoints)
|
||||
.map((coordinates, index) => ({
|
||||
position: index,
|
||||
...coordinates,
|
||||
}))
|
||||
: [],
|
||||
fwdAzimuth: record.fwdAzimuth,
|
||||
backAzimuth: record.backAzimuth,
|
||||
points: [],
|
||||
|
||||
@@ -1,50 +1,51 @@
|
||||
import { Module, Provider } from '@nestjs/common';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_DIRECTION_ENCODER,
|
||||
AD_ROUTE_PROVIDER,
|
||||
TIMEZONE_FINDER,
|
||||
TIME_CONVERTER,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
OUTPUT_DATETIME_TRANSFORMER,
|
||||
MATCHING_REPOSITORY,
|
||||
AD_CONFIGURATION_REPOSITORY,
|
||||
GEOGRAPHY_PACKAGE,
|
||||
GEOGRAPHY_SERVICE,
|
||||
} from './ad.di-tokens';
|
||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { AdRepository } from './infrastructure/ad.repository';
|
||||
import { PrismaService } from './infrastructure/prisma.service';
|
||||
import { AdMapper } from './ad.mapper';
|
||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||
import { GeographyModule } from '@modules/geography/geography.module';
|
||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
||||
import { MatchQueryHandler } from './core/application/queries/match/match.query-handler';
|
||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||
import { TimeConverter } from './infrastructure/time-converter';
|
||||
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||
import { MatchMapper } from './match.mapper';
|
||||
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
||||
import { MatchingRepository } from './infrastructure/matching.repository';
|
||||
import { MatchingMapper } from './matching.mapper';
|
||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { Module, Provider } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { redisStore } from 'cache-manager-ioredis-yet';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||
import {
|
||||
RedisClientOptions,
|
||||
RedisModule,
|
||||
RedisModuleOptions,
|
||||
} from '@songkeys/nestjs-redis';
|
||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
||||
import { Georouter } from './infrastructure/georouter';
|
||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||
import { GRPC_GEOGRAPHY_PACKAGE_NAME } from '@src/app.constants';
|
||||
import { redisStore } from 'cache-manager-ioredis-yet';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
AD_CONFIGURATION_REPOSITORY,
|
||||
AD_DIRECTION_ENCODER,
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
GEOGRAPHY_PACKAGE,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
MATCHING_REPOSITORY,
|
||||
OUTPUT_DATETIME_TRANSFORMER,
|
||||
TIMEZONE_FINDER,
|
||||
TIME_CONVERTER,
|
||||
} from './ad.di-tokens';
|
||||
import { AdMapper } from './ad.mapper';
|
||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
|
||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
||||
import { MatchQueryHandler } from './core/application/queries/match/match.query-handler';
|
||||
import { AdRepository } from './infrastructure/ad.repository';
|
||||
import { Georouter } from './infrastructure/georouter';
|
||||
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||
import { MatchingRepository } from './infrastructure/matching.repository';
|
||||
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
||||
import { PrismaService } from './infrastructure/prisma.service';
|
||||
import { TimeConverter } from './infrastructure/time-converter';
|
||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||
import { AdDeletedMessageHandler } from './interface/message-handlers/ad-deleted.message-handler';
|
||||
import { MatchMapper } from './match.mapper';
|
||||
import { MatchingMapper } from './matching.mapper';
|
||||
|
||||
const imports = [
|
||||
CqrsModule,
|
||||
@@ -65,20 +66,6 @@ const imports = [
|
||||
}),
|
||||
},
|
||||
]),
|
||||
ClientsModule.register([
|
||||
{
|
||||
name: GEOGRAPHY_SERVICE,
|
||||
transport: Transport.RMQ,
|
||||
options: {
|
||||
//TODO read from config
|
||||
urls: [`${process.env.MESSAGE_BROKER_URI}`],
|
||||
queue: 'geography',
|
||||
queueOptions: {
|
||||
durable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
CacheModule.registerAsync<RedisClientOptions>({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
@@ -111,13 +98,13 @@ const imports = [
|
||||
|
||||
const grpcControllers = [MatchGrpcController];
|
||||
|
||||
const messageHandlers = [AdCreatedMessageHandler];
|
||||
const messageHandlers = [AdCreatedMessageHandler, AdDeletedMessageHandler];
|
||||
|
||||
const eventHandlers: Provider[] = [
|
||||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [CreateAdService];
|
||||
const commandHandlers: Provider[] = [CreateAdService, DeleteAdService];
|
||||
|
||||
const queryHandlers: Provider[] = [MatchQueryHandler];
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class DeleteAdCommand extends Command {
|
||||
constructor(props: CommandProps<DeleteAdCommand>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { DeleteAdCommand } from './delete-ad.command';
|
||||
|
||||
@CommandHandler(DeleteAdCommand)
|
||||
export class DeleteAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY) private readonly adRepository: AdRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteAdCommand): Promise<boolean> {
|
||||
const ad = await this.adRepository.findOneById(command.id);
|
||||
ad.delete();
|
||||
return this.adRepository.delete(ad);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { RouteResponse } from '../../../ports/georouter.port';
|
||||
import { Step } from '../../../types/step.type';
|
||||
import { MatchQuery } from '../match.query';
|
||||
import { Completer } from './completer.abstract';
|
||||
|
||||
export class RouteCompleter extends Completer {
|
||||
protected readonly type: RouteCompleterType;
|
||||
@@ -48,10 +48,7 @@ export class RouteCompleter extends Completer {
|
||||
): Promise<RouteResponse> =>
|
||||
this.query.routeProvider.getRoute({
|
||||
waypoints: (candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
|
||||
(carpoolPathItem: CarpoolPathItem) => ({
|
||||
lon: carpoolPathItem.lon,
|
||||
lat: carpoolPathItem.lat,
|
||||
}),
|
||||
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
|
||||
),
|
||||
detailsSettings: detailsSettings,
|
||||
});
|
||||
|
||||
@@ -165,7 +165,7 @@ export class MatchQueryHandler implements IQueryHandler {
|
||||
frequency: query.frequency,
|
||||
fromDate: query.fromDate,
|
||||
toDate: query.toDate,
|
||||
schedule: query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
schedule: query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||
day: scheduleItem.day as number,
|
||||
time: scheduleItem.time,
|
||||
margin: scheduleItem.margin as number,
|
||||
|
||||
@@ -21,11 +21,12 @@ export class MatchQuery extends QueryBase {
|
||||
readonly frequency: Frequency;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
schedule: ScheduleItem[];
|
||||
schedule?: ScheduleItem[];
|
||||
seatsProposed?: number;
|
||||
seatsRequested?: number;
|
||||
strict?: boolean;
|
||||
readonly waypoints: Waypoint[];
|
||||
excludedAdId?: string;
|
||||
algorithmType?: AlgorithmType;
|
||||
remoteness?: number;
|
||||
useProportion?: boolean;
|
||||
@@ -56,6 +57,7 @@ export class MatchQuery extends QueryBase {
|
||||
this.seatsRequested = props.seatsRequested;
|
||||
this.strict = props.strict;
|
||||
this.waypoints = props.waypoints;
|
||||
this.excludedAdId = props.excludedAdId;
|
||||
this.algorithmType = props.algorithmType;
|
||||
this.remoteness = props.remoteness;
|
||||
this.useProportion = props.useProportion;
|
||||
@@ -73,7 +75,7 @@ export class MatchQuery extends QueryBase {
|
||||
}
|
||||
|
||||
setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => {
|
||||
this.schedule.forEach((day: ScheduleItem) => {
|
||||
this.schedule?.forEach((day: ScheduleItem) => {
|
||||
if (day.margin === undefined) day.margin = defaultMarginDuration;
|
||||
});
|
||||
return this;
|
||||
@@ -136,6 +138,8 @@ export class MatchQuery extends QueryBase {
|
||||
setDatesAndSchedule = (
|
||||
datetimeTransformer: DateTimeTransformerPort,
|
||||
): MatchQuery => {
|
||||
// no transformation if schedule is not set
|
||||
if (this.schedule === undefined) return this;
|
||||
const initialFromDate: string = this.fromDate;
|
||||
this.fromDate = datetimeTransformer.fromDate(
|
||||
{
|
||||
@@ -209,10 +213,7 @@ export class MatchQuery extends QueryBase {
|
||||
pathCreator.getBasePaths().map(async (path: Path) => ({
|
||||
type: path.type,
|
||||
route: await this.routeProvider.getRoute({
|
||||
waypoints: path.waypoints.map((p) => ({
|
||||
lon: p.lon,
|
||||
lat: p.lat,
|
||||
})),
|
||||
waypoints: path.waypoints,
|
||||
}),
|
||||
})),
|
||||
)
|
||||
@@ -228,8 +229,9 @@ export class MatchQuery extends QueryBase {
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.log(e.stack || e);
|
||||
throw new Error('Unable to find a route for given waypoints');
|
||||
throw new Error('Unable to find a route for given waypoints', {
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@ export class PassengerOrientedSelector extends Selector {
|
||||
query: this._createQueryString(Role.PASSENGER),
|
||||
role: Role.PASSENGER,
|
||||
});
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
queryStringRoles.map<Promise<AdsRole>>(
|
||||
@@ -74,7 +73,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||
driverSchedule:
|
||||
adsRole.role == Role.PASSENGER
|
||||
? adEntity.getProps().schedule
|
||||
: this.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
: this.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||
day: scheduleItem.day as number,
|
||||
time: scheduleItem.time,
|
||||
margin: scheduleItem.margin as number,
|
||||
@@ -82,7 +81,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||
passengerSchedule:
|
||||
adsRole.role == Role.DRIVER
|
||||
? adEntity.getProps().schedule
|
||||
: this.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
: this.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||
day: scheduleItem.day as number,
|
||||
time: scheduleItem.time,
|
||||
margin: scheduleItem.margin as number,
|
||||
@@ -137,6 +136,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||
this._whereStrict(),
|
||||
this._whereDate(),
|
||||
this._whereSchedule(role),
|
||||
this._whereExcludedAd(),
|
||||
this._whereAzimuth(),
|
||||
this._whereProportion(role),
|
||||
this._whereRemoteness(role),
|
||||
@@ -155,7 +155,9 @@ export class PassengerOrientedSelector extends Selector {
|
||||
: '';
|
||||
|
||||
private _whereDate = (): string =>
|
||||
`(\
|
||||
this.query.frequency == Frequency.PUNCTUAL
|
||||
? `("fromDate" <= '${this.query.fromDate}' AND "toDate" >= '${this.query.fromDate}')`
|
||||
: `(\
|
||||
(\
|
||||
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
||||
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
||||
@@ -172,6 +174,8 @@ export class PassengerOrientedSelector extends Selector {
|
||||
)`;
|
||||
|
||||
private _whereSchedule = (role: Role): string => {
|
||||
// no schedule filtering if schedule is not set
|
||||
if (this.query.schedule === undefined) return '';
|
||||
const schedule: string[] = [];
|
||||
// we need full dates to compare times, because margins can lead to compare on previous or next day
|
||||
// - first we establish a base calendar (up to a week)
|
||||
@@ -182,7 +186,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||
// - then we compare each resulting day of the schedule with each day of calendar,
|
||||
// adding / removing margin depending on the role
|
||||
scheduleDates.map((date: Date) => {
|
||||
this.query.schedule
|
||||
(this.query.schedule as ScheduleItem[])
|
||||
.filter(
|
||||
(scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day,
|
||||
)
|
||||
@@ -203,6 +207,9 @@ export class PassengerOrientedSelector extends Selector {
|
||||
return '';
|
||||
};
|
||||
|
||||
private _whereExcludedAd = (): string =>
|
||||
this.query.excludedAdId ? `ad.uuid <> '${this.query.excludedAdId}'` : '';
|
||||
|
||||
private _wherePassengerSchedule = (
|
||||
date: Date,
|
||||
scheduleItem: ScheduleItem,
|
||||
@@ -310,6 +317,11 @@ 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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
|
||||
import { AdProps, CreateAdProps } from './ad.types';
|
||||
import { AdDeletedDomainEvent } from './events/ad-delete.domain-event';
|
||||
import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event';
|
||||
|
||||
export class AdEntity extends AggregateRoot<AdProps> {
|
||||
@@ -26,6 +27,14 @@ export class AdEntity extends AggregateRoot<AdProps> {
|
||||
return ad;
|
||||
};
|
||||
|
||||
delete(): void {
|
||||
this.addEvent(
|
||||
new AdDeletedDomainEvent({
|
||||
aggregateId: this.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||
}
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AggregateID,
|
||||
AggregateRoot,
|
||||
ArgumentInvalidException,
|
||||
ValueObject,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Role } from './ad.types';
|
||||
import { CalendarTools } from './calendar-tools.service';
|
||||
import {
|
||||
CandidateProps,
|
||||
CreateCandidateProps,
|
||||
DateInterval,
|
||||
Target,
|
||||
} from './candidate.types';
|
||||
import { ActorTime } from './value-objects/actor-time.value-object';
|
||||
import { Actor } from './value-objects/actor.value-object';
|
||||
import {
|
||||
CarpoolPathItem,
|
||||
CarpoolPathItemProps,
|
||||
} from './value-objects/carpool-path-item.value-object';
|
||||
import { Step, StepProps } from './value-objects/step.value-object';
|
||||
import { ScheduleItem } from './value-objects/schedule-item.value-object';
|
||||
import { Journey } from './value-objects/journey.value-object';
|
||||
import { CalendarTools } from './calendar-tools.service';
|
||||
import { JourneyItem } from './value-objects/journey-item.value-object';
|
||||
import { Actor } from './value-objects/actor.value-object';
|
||||
import { ActorTime } from './value-objects/actor-time.value-object';
|
||||
import { Role } from './ad.types';
|
||||
import { Journey, JourneyProps } from './value-objects/journey.value-object';
|
||||
import {
|
||||
ScheduleItem,
|
||||
ScheduleItemProps,
|
||||
} from './value-objects/schedule-item.value-object';
|
||||
import { Step, StepProps } from './value-objects/step.value-object';
|
||||
|
||||
export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
@@ -53,18 +62,26 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||
* This is a tedious process : additional information can be found in deeper methods !
|
||||
*/
|
||||
createJourneys = (): CandidateEntity => {
|
||||
try {
|
||||
this.props.journeys = this.props.driverSchedule
|
||||
// first we create the journeys
|
||||
.map((driverScheduleItem: ScheduleItem) =>
|
||||
this._createJourney(driverScheduleItem),
|
||||
)
|
||||
// then we filter the ones with invalid pickups
|
||||
.filter((journey: Journey) => journey.hasValidPickUp());
|
||||
} catch (e) {
|
||||
// irrelevant journeys fall here
|
||||
// eg. no available day for the given date range
|
||||
}
|
||||
// driver and passenger schedules are eventually mandatory
|
||||
if (!this.props.driverSchedule) this._createDriverSchedule();
|
||||
if (!this.props.passengerSchedule) this._createPassengerSchedule();
|
||||
this.props.journeys = this.props.driverSchedule!.reduce(
|
||||
(accJourneys: JourneyProps[], driverScheduleItem: ScheduleItem) => {
|
||||
try {
|
||||
// first we create the journeys
|
||||
const journey = this._createJourney(driverScheduleItem);
|
||||
// then we filter the ones with invalid pickups
|
||||
if (journey.hasValidPickUp()) {
|
||||
accJourneys.push(journey);
|
||||
}
|
||||
} catch (e) {
|
||||
// irrelevant journeys fall here
|
||||
// eg. no available day for the given date range
|
||||
}
|
||||
return accJourneys;
|
||||
},
|
||||
new Array<JourneyProps>(),
|
||||
);
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -82,6 +99,49 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||
(1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio)
|
||||
: false;
|
||||
|
||||
/**
|
||||
* Create the driver schedule based on the passenger schedule
|
||||
*/
|
||||
private _createDriverSchedule = (): void => {
|
||||
const passengerSchedule = new Schedule(
|
||||
this.props.passengerSchedule!,
|
||||
this.props.dateInterval,
|
||||
);
|
||||
this.props.driverSchedule = passengerSchedule
|
||||
.adjust(-this._passengerStartDuration())
|
||||
.unpack().items;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the duration to reach the passenger starting point from the driver starting point
|
||||
*/
|
||||
private _passengerStartDuration = (): number => {
|
||||
let passengerStartStepIndex = 0;
|
||||
this.props.carpoolPath?.forEach(
|
||||
(carpoolPathItem: CarpoolPathItem, index: number) => {
|
||||
carpoolPathItem.actors.forEach((actor: Actor) => {
|
||||
if (actor.role == Role.PASSENGER && actor.target == Target.START)
|
||||
passengerStartStepIndex = index;
|
||||
});
|
||||
},
|
||||
);
|
||||
return this.props.steps![passengerStartStepIndex].duration;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the passenger schedule based on the driver schedule
|
||||
*/
|
||||
private _createPassengerSchedule = (): void => {
|
||||
const driverSchedule = new Schedule(
|
||||
this.props.driverSchedule!,
|
||||
this.props.dateInterval,
|
||||
);
|
||||
|
||||
this.props.passengerSchedule = driverSchedule
|
||||
.adjust(this._passengerStartDuration())
|
||||
.unpack().items;
|
||||
};
|
||||
|
||||
private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
|
||||
new Journey({
|
||||
firstDate: CalendarTools.firstDate(
|
||||
@@ -216,7 +276,7 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||
* Find the passenger schedule item with the minimum duration between a given date and the dates of the passenger schedule
|
||||
*/
|
||||
private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap =>
|
||||
this.props.passengerSchedule
|
||||
(this.props.passengerSchedule as ScheduleItemProps[])
|
||||
// first map the passenger schedule to "real" dates (we use unix epoch date as base)
|
||||
.map(
|
||||
(scheduleItem: ScheduleItem) =>
|
||||
@@ -255,6 +315,64 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||
|
||||
validate(): void {
|
||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||
if (!this.props.driverSchedule && !this.props.passengerSchedule)
|
||||
throw new ArgumentInvalidException(
|
||||
'at least the driver or the passenger schedule is required',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO Use this class as part of the CandidateEntity aggregate
|
||||
class Schedule extends ValueObject<{
|
||||
items: ScheduleItemProps[];
|
||||
dateInterval: DateInterval;
|
||||
}> {
|
||||
constructor(items: ScheduleItemProps[], dateInterval: DateInterval) {
|
||||
super({ items, dateInterval });
|
||||
}
|
||||
|
||||
protected validate(): void {}
|
||||
|
||||
/**
|
||||
* Add the given duration to each schedule item
|
||||
* unless the expected new datetime is not possible,
|
||||
* in which case the item is removed from the adjusted schedule
|
||||
* @param duration time increment in seconds (can be negative)
|
||||
* @returns the new adjusted schedule
|
||||
*/
|
||||
adjust(duration: number): Schedule {
|
||||
const newItems = this.props.items.reduce((acc, scheduleItemProps) => {
|
||||
try {
|
||||
const itemDate: Date = CalendarTools.firstDate(
|
||||
scheduleItemProps.day,
|
||||
this.props.dateInterval,
|
||||
);
|
||||
const driverStartDatetime: Date = CalendarTools.datetimeWithSeconds(
|
||||
itemDate,
|
||||
scheduleItemProps.time,
|
||||
duration,
|
||||
);
|
||||
acc.push({
|
||||
day: itemDate.getUTCDay(),
|
||||
margin: scheduleItemProps.margin,
|
||||
time: this._formatTime(driverStartDatetime),
|
||||
});
|
||||
} catch (e) {
|
||||
// no possible driver date or time
|
||||
// TODO : find a test case !
|
||||
}
|
||||
return acc;
|
||||
}, new Array<ScheduleItemProps>());
|
||||
|
||||
return new Schedule(newItems, this.props.dateInterval);
|
||||
}
|
||||
|
||||
private _formatTime(dateTime: Date) {
|
||||
return (
|
||||
dateTime.getUTCHours().toString().padStart(2, '0') +
|
||||
':' +
|
||||
dateTime.getUTCMinutes().toString().padStart(2, '0')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ export interface CandidateProps {
|
||||
frequency: Frequency;
|
||||
driverWaypoints: PointProps[];
|
||||
passengerWaypoints: PointProps[];
|
||||
driverSchedule: ScheduleItemProps[];
|
||||
passengerSchedule: ScheduleItemProps[];
|
||||
driverSchedule?: ScheduleItemProps[];
|
||||
passengerSchedule?: ScheduleItemProps[];
|
||||
driverDistance: number;
|
||||
driverDuration: number;
|
||||
dateInterval: DateInterval;
|
||||
@@ -33,8 +33,8 @@ export interface CreateCandidateProps {
|
||||
driverDuration: number;
|
||||
driverWaypoints: PointProps[];
|
||||
passengerWaypoints: PointProps[];
|
||||
driverSchedule: ScheduleItemProps[];
|
||||
passengerSchedule: ScheduleItemProps[];
|
||||
driverSchedule?: ScheduleItemProps[];
|
||||
passengerSchedule?: ScheduleItemProps[];
|
||||
spacetimeDetourRatio: SpacetimeDetourRatio;
|
||||
dateInterval: DateInterval;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AdDeletedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<AdDeletedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Route } from '../application/types/route.type';
|
||||
import { Role } from './ad.types';
|
||||
import { PathCreatorException } from './match.errors';
|
||||
import { Point } from './value-objects/point.value-object';
|
||||
import { PathCreatorException } from './match.errors';
|
||||
import { Route } from '../application/types/route.type';
|
||||
|
||||
export class PathCreator {
|
||||
constructor(
|
||||
|
||||
@@ -2,8 +2,7 @@ import {
|
||||
ArgumentOutOfRangeException,
|
||||
ValueObject,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Actor, ActorProps } from './actor.value-object';
|
||||
import { Role } from '../ad.types';
|
||||
import { ActorProps } from './actor.value-object';
|
||||
import { Point, PointProps } from './point.value-object';
|
||||
|
||||
/** Note:
|
||||
@@ -36,12 +35,5 @@ export class CarpoolPathItem extends ValueObject<CarpoolPathItemProps> {
|
||||
});
|
||||
if (props.actors.length <= 0)
|
||||
throw new ArgumentOutOfRangeException('at least one actor is required');
|
||||
if (
|
||||
props.actors.filter((actor: Actor) => actor.role == Role.DRIVER).length >
|
||||
1
|
||||
)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
'a carpoolStep can contain only one driver',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ export class JourneyItem extends ValueObject<JourneyItemProps> {
|
||||
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
||||
) as ActorTime
|
||||
).firstDatetime;
|
||||
return `${driverTime.getHours().toString().padStart(2, '0')}:${driverTime
|
||||
.getMinutes()
|
||||
return `${driverTime.getUTCHours().toString().padStart(2, '0')}:${driverTime
|
||||
.getUTCMinutes()
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
@@ -46,6 +46,29 @@ export class Journey extends ValueObject<JourneyProps> {
|
||||
const driverActorTime = passengerDepartureJourneyItem.actorTimes.find(
|
||||
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
||||
) as ActorTime;
|
||||
// TODO : check if the following conditions are even to the ones used in the return
|
||||
// 4 possibilities to be valid :
|
||||
// - 1 : the driver time boundaries are within the passenger time boundaries
|
||||
// - 2 : the max driver time boundary is within the passenger time boundaries
|
||||
// - 3 : the min driver time boundary is within the passenger time boundaries
|
||||
// - 4 : the passenger time boundaries are within the driver time boundaries
|
||||
// return (
|
||||
// // 1
|
||||
// (driverActorTime.firstMinDatetime >=
|
||||
// passengerDepartureActorTime.firstMinDatetime &&
|
||||
// driverActorTime.firstMaxDatetime <=
|
||||
// passengerDepartureActorTime.firstMaxDatetime) ||
|
||||
// // 2 & 4
|
||||
// (driverActorTime.firstMinDatetime <=
|
||||
// passengerDepartureActorTime.firstMinDatetime &&
|
||||
// driverActorTime.firstMaxDatetime >=
|
||||
// passengerDepartureActorTime.firstMinDatetime) ||
|
||||
// // 3
|
||||
// (driverActorTime.firstMinDatetime >=
|
||||
// passengerDepartureActorTime.firstMinDatetime &&
|
||||
// driverActorTime.firstMinDatetime <=
|
||||
// passengerDepartureActorTime.firstMaxDatetime)
|
||||
// );
|
||||
return (
|
||||
(passengerDepartureActorTime.firstMinDatetime <=
|
||||
driverActorTime.firstMaxDatetime &&
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface MatchQueryProps {
|
||||
frequency: Frequency;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
schedule: ScheduleItemProps[];
|
||||
schedule?: ScheduleItemProps[];
|
||||
seatsProposed: number;
|
||||
seatsRequested: number;
|
||||
strict: boolean;
|
||||
@@ -50,7 +50,7 @@ export class MatchQuery extends ValueObject<MatchQueryProps> {
|
||||
return this.props.toDate;
|
||||
}
|
||||
|
||||
get schedule(): ScheduleItemProps[] {
|
||||
get schedule(): ScheduleItemProps[] | undefined {
|
||||
return this.props.schedule;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ClientProxy } from '@nestjs/microservices';
|
||||
import { Observable, lastValueFrom } from 'rxjs';
|
||||
import { GEOGRAPHY_SERVICE } from '../ad.di-tokens';
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { GRPC_GEOROUTER_SERVICE_NAME } from '@src/app.constants';
|
||||
import {
|
||||
GeorouterPort,
|
||||
RouteRequest,
|
||||
RouteResponse,
|
||||
} from '../core/application/ports/georouter.port';
|
||||
import { GEOGRAPHY_PACKAGE } from '../ad.di-tokens';
|
||||
|
||||
interface GeorouterService {
|
||||
getRoute(request: RouteRequest): Observable<RouteResponse>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class Georouter implements GeorouterPort {
|
||||
export class Georouter implements GeorouterPort, OnModuleInit {
|
||||
private georouterService: GeorouterService;
|
||||
|
||||
constructor(
|
||||
@Inject(GEOGRAPHY_SERVICE) private readonly client: ClientProxy,
|
||||
) {}
|
||||
constructor(@Inject(GEOGRAPHY_PACKAGE) private readonly client: ClientGrpc) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.georouterService = this.client.getService<GeorouterService>(
|
||||
GRPC_GEOROUTER_SERVICE_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
getRoute = async (request: RouteRequest): Promise<RouteResponse> => {
|
||||
return lastValueFrom(this.client.send('getRoute', request));
|
||||
try {
|
||||
return await lastValueFrom(this.georouterService.getRoute(request));
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||
import { JourneyResponseDto } from './journey.response.dto';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
|
||||
export class MatchResponseDto extends ResponseBase {
|
||||
adId: string;
|
||||
role: string;
|
||||
frequency: string;
|
||||
frequency: Frequency;
|
||||
distance: number;
|
||||
duration: number;
|
||||
initialDistance: number;
|
||||
|
||||
@@ -58,8 +58,9 @@ export class MatchRequestDto {
|
||||
@Type(() => ScheduleItemDto)
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
schedule: ScheduleItemDto[];
|
||||
schedule?: ScheduleItemDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@@ -80,6 +81,10 @@ export class MatchRequestDto {
|
||||
@ValidateNested({ each: true })
|
||||
waypoints: WaypointDto[];
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
excludedAdId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AlgorithmType)
|
||||
algorithmType?: AlgorithmType;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Controller, Inject, UseInterceptors, UsePipes } from '@nestjs/common';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { MatchingPaginatedResponseDto } from '../dtos/matching.paginated.response.dto';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { MatchRequestDto } from './dtos/match.request.dto';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
|
||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||
import { Controller, Inject, UseFilters, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod } from '@nestjs/microservices';
|
||||
import { LogCauseExceptionFilter } from '@src/log-cause.exception-filter';
|
||||
import { MatchingPaginatedResponseDto } from '../dtos/matching.paginated.response.dto';
|
||||
import { MatchRequestDto } from './dtos/match.request.dto';
|
||||
|
||||
@UseFilters(LogCauseExceptionFilter)
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
@@ -28,28 +28,19 @@ export class MatchGrpcController {
|
||||
private readonly matchMapper: MatchMapper,
|
||||
) {}
|
||||
|
||||
@CacheKey('MatcherServiceMatch')
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@GrpcMethod('MatcherService', 'Match')
|
||||
async match(data: MatchRequestDto): Promise<MatchingPaginatedResponseDto> {
|
||||
try {
|
||||
const matchingResult: MatchingResult = await this.queryBus.execute(
|
||||
new MatchQuery(data, this.routeProvider),
|
||||
);
|
||||
return new MatchingPaginatedResponseDto({
|
||||
id: matchingResult.id,
|
||||
data: matchingResult.matches.map((match: MatchEntity) =>
|
||||
this.matchMapper.toResponse(match),
|
||||
),
|
||||
page: matchingResult.page,
|
||||
perPage: matchingResult.perPage,
|
||||
total: matchingResult.total,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
const matchingResult: MatchingResult = await this.queryBus.execute(
|
||||
new MatchQuery(data, this.routeProvider),
|
||||
);
|
||||
return new MatchingPaginatedResponseDto({
|
||||
id: matchingResult.id,
|
||||
data: matchingResult.matches.map((match: MatchEntity) =>
|
||||
this.matchMapper.toResponse(match),
|
||||
),
|
||||
page: matchingResult.page,
|
||||
perPage: matchingResult.perPage,
|
||||
total: matchingResult.total,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,16 +16,17 @@ message MatchRequest {
|
||||
repeated ScheduleItem schedule = 7;
|
||||
bool strict = 8;
|
||||
repeated Waypoint waypoints = 9;
|
||||
AlgorithmType algorithmType = 10;
|
||||
int32 remoteness = 11;
|
||||
bool useProportion = 12;
|
||||
float proportion = 13;
|
||||
bool useAzimuth = 14;
|
||||
int32 azimuthMargin = 15;
|
||||
float maxDetourDistanceRatio = 16;
|
||||
float maxDetourDurationRatio = 17;
|
||||
optional int32 page = 18;
|
||||
optional int32 perPage = 19;
|
||||
string excludedAdId = 10;
|
||||
AlgorithmType algorithmType = 11;
|
||||
int32 remoteness = 12;
|
||||
bool useProportion = 13;
|
||||
float proportion = 14;
|
||||
bool useAzimuth = 15;
|
||||
int32 azimuthMargin = 16;
|
||||
float maxDetourDistanceRatio = 17;
|
||||
float maxDetourDurationRatio = 18;
|
||||
optional int32 page = 19;
|
||||
optional int32 perPage = 20;
|
||||
}
|
||||
|
||||
message ScheduleItem {
|
||||
@@ -58,15 +59,16 @@ enum AlgorithmType {
|
||||
message Match {
|
||||
string adId = 1;
|
||||
string role = 2;
|
||||
int32 distance = 3;
|
||||
int32 duration = 4;
|
||||
int32 initialDistance = 5;
|
||||
int32 initialDuration = 6;
|
||||
int32 distanceDetour = 7;
|
||||
int32 durationDetour = 8;
|
||||
double distanceDetourPercentage = 9;
|
||||
double durationDetourPercentage = 10;
|
||||
repeated Journey journeys = 11;
|
||||
Frequency frequency = 3;
|
||||
int32 distance = 4;
|
||||
int32 duration = 5;
|
||||
int32 initialDistance = 6;
|
||||
int32 initialDuration = 7;
|
||||
int32 distanceDetour = 8;
|
||||
int32 durationDetour = 9;
|
||||
double distanceDetourPercentage = 10;
|
||||
double durationDetourPercentage = 11;
|
||||
repeated Journey journeys = 12;
|
||||
}
|
||||
|
||||
message Journey {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||
import { DeleteAdCommand } from '@modules/ad/core/application/commands/delete-ad/delete-ad.command';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import {
|
||||
AD_DELETED_MESSAGE_HANDLER,
|
||||
AD_DELETED_ROUTING_KEY,
|
||||
} from '@src/app.constants';
|
||||
import { AdReference } from './ad.types';
|
||||
|
||||
@Injectable()
|
||||
export class AdDeletedMessageHandler {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: AD_DELETED_MESSAGE_HANDLER,
|
||||
routingKey: AD_DELETED_ROUTING_KEY,
|
||||
})
|
||||
public async adDeleted(message: string): Promise<void> {
|
||||
try {
|
||||
const deletedAd: AdReference = JSON.parse(message);
|
||||
await this.commandBus.execute(
|
||||
new DeleteAdCommand({
|
||||
id: deletedAd.aggregateId,
|
||||
}),
|
||||
);
|
||||
} catch (error: any) {
|
||||
// do not throw error to acknowledge incoming message
|
||||
// error handling should be done in the command handler, if relevant
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
|
||||
export type Ad = {
|
||||
export type AdReference = {
|
||||
aggregateId: string;
|
||||
};
|
||||
|
||||
export type Ad = AdReference & {
|
||||
driver: boolean;
|
||||
passenger: boolean;
|
||||
frequency: Frequency;
|
||||
|
||||
@@ -64,7 +64,7 @@ export class MatchingMapper
|
||||
toDate: entity.getProps().query.toDate,
|
||||
schedule: entity
|
||||
.getProps()
|
||||
.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||
day: scheduleItem.day,
|
||||
time: scheduleItem.time,
|
||||
margin: scheduleItem.margin,
|
||||
|
||||
@@ -1,52 +1,17 @@
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
|
||||
const originPointProps: PointProps = {
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
};
|
||||
const destinationPointProps: PointProps = {
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
};
|
||||
|
||||
const createAdProps: CreateAdProps = {
|
||||
id: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
driver: true,
|
||||
passenger: true,
|
||||
fromDate: '2023-06-21',
|
||||
toDate: '2023-06-21',
|
||||
schedule: [
|
||||
{
|
||||
day: 3,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originPointProps, destinationPointProps],
|
||||
driverDistance: 23000,
|
||||
driverDuration: 900,
|
||||
passengerDistance: 23000,
|
||||
passengerDuration: 900,
|
||||
fwdAzimuth: 283,
|
||||
backAzimuth: 93,
|
||||
points: [],
|
||||
};
|
||||
import { createAdProps } from './ad.fixtures';
|
||||
|
||||
describe('Ad entity create', () => {
|
||||
it('should create a new entity', async () => {
|
||||
const ad: AdEntity = AdEntity.create(createAdProps);
|
||||
expect(ad.id.length).toBe(36);
|
||||
expect(ad.getProps().schedule.length).toBe(1);
|
||||
expect(ad.getProps().schedule[0].day).toBe(3);
|
||||
expect(ad.getProps().schedule[0].time).toBe('08:30');
|
||||
expect(ad.getProps().driver).toBeTruthy();
|
||||
expect(ad.getProps().passenger).toBeTruthy();
|
||||
expect(ad.getProps().driverDistance).toBe(23000);
|
||||
describe('create', () => {
|
||||
it('should create a new entity', async () => {
|
||||
const ad: AdEntity = AdEntity.create(createAdProps());
|
||||
expect(ad.id.length).toBe(36);
|
||||
expect(ad.getProps().schedule.length).toBe(1);
|
||||
expect(ad.getProps().schedule[0].day).toBe(3);
|
||||
expect(ad.getProps().schedule[0].time).toBe('08:30');
|
||||
expect(ad.getProps().driver).toBeTruthy();
|
||||
expect(ad.getProps().passenger).toBeTruthy();
|
||||
expect(ad.getProps().driverDistance).toBe(23000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
40
src/modules/ad/tests/unit/core/ad.fixtures.ts
Normal file
40
src/modules/ad/tests/unit/core/ad.fixtures.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
|
||||
const originPointProps: PointProps = {
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
};
|
||||
const destinationPointProps: PointProps = {
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
};
|
||||
|
||||
export function createAdProps(): CreateAdProps {
|
||||
return {
|
||||
id: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
driver: true,
|
||||
passenger: true,
|
||||
fromDate: '2023-06-21',
|
||||
toDate: '2023-06-21',
|
||||
schedule: [
|
||||
{
|
||||
day: 3,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originPointProps, destinationPointProps],
|
||||
driverDistance: 23000,
|
||||
driverDuration: 900,
|
||||
passengerDistance: 23000,
|
||||
passengerDuration: 900,
|
||||
fwdAzimuth: 283,
|
||||
backAzimuth: 93,
|
||||
points: [],
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ArgumentInvalidException } from '@mobicoop/ddd-library';
|
||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import {
|
||||
@@ -6,7 +7,10 @@ import {
|
||||
} from '@modules/ad/core/domain/candidate.types';
|
||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
|
||||
import { CarpoolPathItemProps } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
||||
import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object';
|
||||
import {
|
||||
Journey,
|
||||
JourneyProps,
|
||||
} from '@modules/ad/core/domain/value-objects/journey.value-object';
|
||||
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 { StepProps } from '@modules/ad/core/domain/value-objects/step.value-object';
|
||||
@@ -33,7 +37,7 @@ const waypointsSet2: PointProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const schedule1: ScheduleItemProps[] = [
|
||||
const mondayAt7: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '07:00',
|
||||
@@ -41,7 +45,7 @@ const schedule1: ScheduleItemProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const schedule2: ScheduleItemProps[] = [
|
||||
const mondayAt710: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '07:10',
|
||||
@@ -49,7 +53,7 @@ const schedule2: ScheduleItemProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const schedule3: ScheduleItemProps[] = [
|
||||
const weekdayMornings: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '06:30',
|
||||
@@ -77,7 +81,7 @@ const schedule3: ScheduleItemProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const schedule4: ScheduleItemProps[] = [
|
||||
const schooldayMornings: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '06:50',
|
||||
@@ -100,7 +104,7 @@ const schedule4: ScheduleItemProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const schedule5: ScheduleItemProps[] = [
|
||||
const saturdayNightAndMondayMorning: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 0,
|
||||
time: '00:02',
|
||||
@@ -113,7 +117,7 @@ const schedule5: ScheduleItemProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const schedule6: ScheduleItemProps[] = [
|
||||
const mondayAndSaturdayNights: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '23:10',
|
||||
@@ -126,7 +130,7 @@ const schedule6: ScheduleItemProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const schedule7: ScheduleItemProps[] = [
|
||||
const thursdayEvening: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 4,
|
||||
time: '19:00',
|
||||
@@ -262,8 +266,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
});
|
||||
expect(candidateEntity.id.length).toBe(36);
|
||||
@@ -282,8 +286,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
}).setCarpoolPath(carpoolPath1);
|
||||
expect(candidateEntity.getProps().carpoolPath).toHaveLength(2);
|
||||
@@ -302,8 +306,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
}).setMetrics(352688, 14587);
|
||||
expect(candidateEntity.getProps().distance).toBe(352688);
|
||||
@@ -324,8 +328,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
}).setMetrics(458690, 13980);
|
||||
expect(candidateEntity.isDetourValid()).toBeFalsy();
|
||||
@@ -343,8 +347,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
}).setMetrics(352368, 18314);
|
||||
expect(candidateEntity.isDetourValid()).toBeFalsy();
|
||||
@@ -365,8 +369,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
@@ -374,6 +378,95 @@ describe('Candidate entity', () => {
|
||||
.createJourneys();
|
||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||
});
|
||||
it('should create journeys for a single date without driver schedule', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
role: Role.PASSENGER,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
dateInterval: {
|
||||
lowerDate: '2023-08-28',
|
||||
higherDate: '2023-08-28',
|
||||
},
|
||||
driverWaypoints: waypointsSet1,
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: undefined,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
.setSteps(steps)
|
||||
.createJourneys();
|
||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||
// computed driver start time should be 06:49
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as JourneyProps[]
|
||||
)[0].journeyItems[0].actorTimes[0].firstDatetime.getUTCMinutes(),
|
||||
).toBe(49);
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as JourneyProps[]
|
||||
)[0].journeyItems[0].actorTimes[0].firstDatetime.getUTCHours(),
|
||||
).toBe(6);
|
||||
});
|
||||
it('should create journeys for a single date without passenger schedule', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
role: Role.PASSENGER,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
dateInterval: {
|
||||
lowerDate: '2023-08-28',
|
||||
higherDate: '2023-08-28',
|
||||
},
|
||||
driverWaypoints: waypointsSet1,
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: undefined,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
.setSteps(steps)
|
||||
.createJourneys();
|
||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||
// computed passenger start time should be 07:20
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as JourneyProps[]
|
||||
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(),
|
||||
).toBe(20);
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as JourneyProps[]
|
||||
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCHours(),
|
||||
).toBe(7);
|
||||
});
|
||||
it('should throw without driver and passenger schedule', () => {
|
||||
expect(() =>
|
||||
CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
role: Role.PASSENGER,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
dateInterval: {
|
||||
lowerDate: '2023-08-28',
|
||||
higherDate: '2023-08-28',
|
||||
},
|
||||
driverWaypoints: waypointsSet1,
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: undefined,
|
||||
passengerSchedule: undefined,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
.setSteps(steps)
|
||||
.createJourneys(),
|
||||
).toThrow(ArgumentInvalidException);
|
||||
});
|
||||
it('should create journeys for multiple dates', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
@@ -387,8 +480,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule3,
|
||||
passengerSchedule: schedule4,
|
||||
driverSchedule: weekdayMornings,
|
||||
passengerSchedule: schooldayMornings,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
@@ -424,8 +517,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule5,
|
||||
passengerSchedule: schedule6,
|
||||
driverSchedule: saturdayNightAndMondayMorning,
|
||||
passengerSchedule: mondayAndSaturdayNights,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
@@ -457,6 +550,33 @@ describe('Candidate entity', () => {
|
||||
)[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCMinutes(),
|
||||
).toBe(42);
|
||||
});
|
||||
it('should create a journey for a punctual search from a recurrent driver schedule', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
role: Role.PASSENGER,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
dateInterval: {
|
||||
lowerDate: '2024-04-01', //This is a Monday
|
||||
higherDate: '2024-04-01',
|
||||
},
|
||||
driverWaypoints: waypointsSet1,
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: weekdayMornings,
|
||||
passengerSchedule: mondayAt7,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
.setSteps(steps)
|
||||
.createJourneys();
|
||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as Journey[]
|
||||
)[0].firstDate.getDate(),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should not create journeys if dates does not match', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
@@ -471,8 +591,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule7,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: thursdayEvening,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
@@ -495,8 +615,8 @@ describe('Candidate entity', () => {
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule7,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: thursdayEvening,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
|
||||
@@ -33,22 +33,4 @@ describe('Carpool Path Item value object', () => {
|
||||
});
|
||||
}).toThrow(ArgumentOutOfRangeException);
|
||||
});
|
||||
it('should throw an exception if actors contains more than one driver', () => {
|
||||
expect(() => {
|
||||
new CarpoolPathItem({
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
actors: [
|
||||
new Actor({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
}),
|
||||
new Actor({
|
||||
role: Role.DRIVER,
|
||||
target: Target.START,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}).toThrow(ArgumentOutOfRangeException);
|
||||
});
|
||||
});
|
||||
|
||||
41
src/modules/ad/tests/unit/core/delete-ad.service.spec.ts
Normal file
41
src/modules/ad/tests/unit/core/delete-ad.service.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { DeleteAdCommand } from '@modules/ad/core/application/commands/delete-ad/delete-ad.command';
|
||||
import { DeleteAdService } from '@modules/ad/core/application/commands/delete-ad/delete-ad.service';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { createAdProps } from './ad.fixtures';
|
||||
|
||||
const ad: AdEntity = AdEntity.create(createAdProps());
|
||||
const mockAdRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => ad),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DeleteAdService', () => {
|
||||
let deleteAdService: DeleteAdService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
DeleteAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteAdService = module.get<DeleteAdService>(DeleteAdService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteAdService).toBeDefined();
|
||||
});
|
||||
|
||||
it('should execute the delete logic and delete the ad from the repository', async () => {
|
||||
jest.spyOn(ad, 'delete');
|
||||
await deleteAdService.execute(new DeleteAdCommand(ad.id));
|
||||
expect(ad.delete).toHaveBeenCalled();
|
||||
expect(mockAdRepository.delete).toHaveBeenCalledWith(ad);
|
||||
});
|
||||
});
|
||||
@@ -44,7 +44,7 @@ describe('Match Query value object', () => {
|
||||
expect(matchQueryVO.frequency).toBe(Frequency.PUNCTUAL);
|
||||
expect(matchQueryVO.fromDate).toBe('2023-09-01');
|
||||
expect(matchQueryVO.toDate).toBe('2023-09-01');
|
||||
expect(matchQueryVO.schedule.length).toBe(1);
|
||||
expect(matchQueryVO.schedule?.length).toBe(1);
|
||||
expect(matchQueryVO.seatsProposed).toBe(3);
|
||||
expect(matchQueryVO.seatsRequested).toBe(1);
|
||||
expect(matchQueryVO.strict).toBe(false);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import {
|
||||
MatchQuery,
|
||||
ScheduleItem,
|
||||
} from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
@@ -135,9 +138,9 @@ describe('Match Query', () => {
|
||||
expect(matchQuery.maxDetourDurationRatio).toBe(0.3);
|
||||
expect(matchQuery.fromDate).toBe('2023-08-27');
|
||||
expect(matchQuery.toDate).toBe('2023-08-27');
|
||||
expect(matchQuery.schedule[0].day).toBe(0);
|
||||
expect(matchQuery.schedule[0].time).toBe('23:05');
|
||||
expect(matchQuery.schedule[0].margin).toBe(900);
|
||||
expect((matchQuery.schedule as ScheduleItem[])[0].day).toBe(0);
|
||||
expect((matchQuery.schedule as ScheduleItem[])[0].time).toBe('23:05');
|
||||
expect((matchQuery.schedule as ScheduleItem[])[0].margin).toBe(900);
|
||||
});
|
||||
|
||||
it('should set good values for seats', async () => {
|
||||
|
||||
@@ -47,6 +47,7 @@ const matchQuery = new MatchQuery(
|
||||
],
|
||||
strict: false,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
excludedAdId: '758618c6-dd82-4199-a548-0205161b04d7',
|
||||
},
|
||||
bareMockGeorouter,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { AdDeletedMessageHandler } from '@modules/ad/interface/message-handlers/ad-deleted.message-handler';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const adDeletedMessage =
|
||||
'{"aggregateId":"4eb6a6af-ecfd-41c3-9118-473a507014d4"}';
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Ad Deleted Message Handler', () => {
|
||||
let adDeletedMessageHandler: AdDeletedMessageHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
AdDeletedMessageHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
adDeletedMessageHandler = module.get<AdDeletedMessageHandler>(
|
||||
AdDeletedMessageHandler,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(adDeletedMessageHandler).toBeDefined();
|
||||
});
|
||||
|
||||
it('should call the delete command', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
await adDeletedMessageHandler.adDeleted(adDeletedMessage);
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||
@@ -14,7 +13,6 @@ import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/matc
|
||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
@@ -56,131 +54,126 @@ const recurrentMatchRequestDto: MatchRequestDto = {
|
||||
};
|
||||
|
||||
const mockQueryBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
<MatchingResult>{
|
||||
id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
matches: [
|
||||
MatchEntity.create({
|
||||
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
||||
role: Role.DRIVER,
|
||||
frequency: Frequency.RECURRENT,
|
||||
distance: 356041,
|
||||
duration: 12647,
|
||||
initialDistance: 349251,
|
||||
initialDuration: 12103,
|
||||
journeys: [
|
||||
{
|
||||
firstDate: new Date('2023-09-01'),
|
||||
lastDate: new Date('2024-08-30'),
|
||||
journeyItems: [
|
||||
new JourneyItem({
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.START,
|
||||
firstDatetime: new Date('2023-09-01 07:00'),
|
||||
firstMinDatetime: new Date('2023-09-01 06:45'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:15'),
|
||||
lastDatetime: new Date('2024-08-30 07:00'),
|
||||
lastMinDatetime: new Date('2024-08-30 06:45'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:15'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 48.369445,
|
||||
lon: 6.67487,
|
||||
duration: 2100,
|
||||
distance: 56878,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
firstDatetime: new Date('2023-09-01 07:35'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:20'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:50'),
|
||||
lastDatetime: new Date('2024-08-30 07:35'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:20'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:50'),
|
||||
}),
|
||||
new ActorTime({
|
||||
role: Role.PASSENGER,
|
||||
target: Target.START,
|
||||
firstDatetime: new Date('2023-09-01 07:32'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:17'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:47'),
|
||||
lastDatetime: new Date('2024-08-30 07:32'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:17'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:47'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 47.98487,
|
||||
lon: 6.9427,
|
||||
duration: 3840,
|
||||
distance: 76491,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
firstDatetime: new Date('2023-09-01 08:04'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:51'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:19'),
|
||||
lastDatetime: new Date('2024-08-30 08:04'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:51'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:19'),
|
||||
}),
|
||||
new ActorTime({
|
||||
role: Role.PASSENGER,
|
||||
target: Target.FINISH,
|
||||
firstDatetime: new Date('2023-09-01 08:01'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:46'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:16'),
|
||||
lastDatetime: new Date('2024-08-30 08:01'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:46'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:16'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 47.365987,
|
||||
lon: 7.02154,
|
||||
duration: 4980,
|
||||
distance: 96475,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.FINISH,
|
||||
firstDatetime: new Date('2023-09-01 08:23'),
|
||||
firstMinDatetime: new Date('2023-09-01 08:08'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:38'),
|
||||
lastDatetime: new Date('2024-08-30 08:23'),
|
||||
lastMinDatetime: new Date('2024-08-30 08:08'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:38'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
)
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
execute: jest.fn().mockImplementationOnce(
|
||||
() =>
|
||||
<MatchingResult>{
|
||||
id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
matches: [
|
||||
MatchEntity.create({
|
||||
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
||||
role: Role.DRIVER,
|
||||
frequency: Frequency.RECURRENT,
|
||||
distance: 356041,
|
||||
duration: 12647,
|
||||
initialDistance: 349251,
|
||||
initialDuration: 12103,
|
||||
journeys: [
|
||||
{
|
||||
firstDate: new Date('2023-09-01'),
|
||||
lastDate: new Date('2024-08-30'),
|
||||
journeyItems: [
|
||||
new JourneyItem({
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.START,
|
||||
firstDatetime: new Date('2023-09-01 07:00'),
|
||||
firstMinDatetime: new Date('2023-09-01 06:45'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:15'),
|
||||
lastDatetime: new Date('2024-08-30 07:00'),
|
||||
lastMinDatetime: new Date('2024-08-30 06:45'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:15'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 48.369445,
|
||||
lon: 6.67487,
|
||||
duration: 2100,
|
||||
distance: 56878,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
firstDatetime: new Date('2023-09-01 07:35'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:20'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:50'),
|
||||
lastDatetime: new Date('2024-08-30 07:35'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:20'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:50'),
|
||||
}),
|
||||
new ActorTime({
|
||||
role: Role.PASSENGER,
|
||||
target: Target.START,
|
||||
firstDatetime: new Date('2023-09-01 07:32'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:17'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:47'),
|
||||
lastDatetime: new Date('2024-08-30 07:32'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:17'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:47'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 47.98487,
|
||||
lon: 6.9427,
|
||||
duration: 3840,
|
||||
distance: 76491,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
firstDatetime: new Date('2023-09-01 08:04'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:51'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:19'),
|
||||
lastDatetime: new Date('2024-08-30 08:04'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:51'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:19'),
|
||||
}),
|
||||
new ActorTime({
|
||||
role: Role.PASSENGER,
|
||||
target: Target.FINISH,
|
||||
firstDatetime: new Date('2023-09-01 08:01'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:46'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:16'),
|
||||
lastDatetime: new Date('2024-08-30 08:01'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:46'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:16'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 47.365987,
|
||||
lon: 7.02154,
|
||||
duration: 4980,
|
||||
distance: 96475,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.FINISH,
|
||||
firstDatetime: new Date('2023-09-01 08:23'),
|
||||
firstMinDatetime: new Date('2023-09-01 08:08'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:38'),
|
||||
lastDatetime: new Date('2024-08-30 08:23'),
|
||||
lastMinDatetime: new Date('2024-08-30 08:08'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:38'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
const mockMatchMapper = {
|
||||
@@ -317,16 +310,4 @@ describe('Match Grpc Controller', () => {
|
||||
expect(matchingPaginatedResponseDto.perPage).toBe(10);
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await matchGrpcController.match(recurrentMatchRequestDto);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { Module, Provider } from '@nestjs/common';
|
||||
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
||||
import {
|
||||
MessageBrokerModule,
|
||||
MessageBrokerModuleOptions,
|
||||
MessageBrokerPublisher,
|
||||
} from '@mobicoop/message-broker-module';
|
||||
import { Module, Provider } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
AD_CREATED_MESSAGE_HANDLER,
|
||||
AD_CREATED_QUEUE,
|
||||
AD_CREATED_ROUTING_KEY,
|
||||
AD_DELETED_MESSAGE_HANDLER,
|
||||
AD_DELETED_QUEUE,
|
||||
AD_DELETED_ROUTING_KEY,
|
||||
SERVICE_NAME,
|
||||
} from '@src/app.constants';
|
||||
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
||||
|
||||
const imports = [
|
||||
MessageBrokerModule.forRootAsync({
|
||||
@@ -33,6 +36,10 @@ const imports = [
|
||||
routingKey: AD_CREATED_ROUTING_KEY,
|
||||
queue: AD_CREATED_QUEUE,
|
||||
},
|
||||
[AD_DELETED_MESSAGE_HANDLER]: {
|
||||
routingKey: AD_DELETED_ROUTING_KEY,
|
||||
queue: AD_DELETED_QUEUE,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"lib": ["es2022"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
|
||||
Reference in New Issue
Block a user