36 Commits

Author SHA1 Message Date
Romain Thouvenin
1fd15430a1 Replace GRPC call to geography with AMQP RPC 2024-03-22 22:33:24 +01:00
Romain Thouvenin
579415c300 Rename GeorouterProvider[Port] to Georouter[Port] 2024-03-19 08:04:28 +01:00
Romain Thouvenin
357746e843 Rename geocoder service to geography 2024-03-14 16:47:54 +01:00
Romain Thouvenin
96c30cb1cc Remove most of the geography module and delegate it to external gRPC microservice 2024-03-14 10:19:15 +01:00
Sylvain Briat
d09bad60f7 1.5.5 2024-02-08 16:18:16 +00:00
Sylvain Briat
3be95fb58c fix wrong carpool crew for a driver query 2024-02-08 16:18:16 +00:00
Fanch
085de292c6 copy file from v3 gitlab template repo 2024-02-05 19:16:08 +01:00
Sylvain Briat
0d537cd6a4 Merge branch 'fixStatus' into 'main'
Removed useless status in matching sql request

See merge request v3/service/matcher!29
2024-02-02 07:53:55 +00:00
Sylvain Briat
50c5b99e54 1.5.4 2024-02-01 16:59:58 +01:00
Sylvain Briat
c42ddef7e4 Removed useless status in matching sql request 2024-02-01 16:55:52 +01:00
Sylvain Briat
21c2bc663c Merge branch 'use_prisma_deploy' into 'main'
use prisma deploy as default for migrate, add migrate:dev command

See merge request v3/service/matcher!28
2024-01-31 14:18:42 +00:00
Fanch
4089619807 use prisma deploy as default for migrate, add migrate:dev command 2024-01-31 12:47:27 +01:00
Fanch
0c272795e9 Merge branch 'fix_test_install' into 'main'
Fix test install

See merge request v3/service/matcher!26
2024-01-24 15:13:16 +00:00
Fanch
6fa8594fa6 use node lts image for docker 2024-01-22 16:57:44 +01:00
Fanch
319cc7b7a7 use full registry path for docker image 2024-01-17 10:25:39 +01:00
Sylvain Briat
c392d87f43 Merge branch 'updatePackages' into 'main'
Update packages

See merge request v3/service/matcher!27
2024-01-17 08:07:07 +00:00
Sylvain Briat
d1942c954a pretty 2024-01-17 08:58:14 +01:00
Sylvain Briat
6df331f990 1.5.3 2024-01-17 08:54:50 +01:00
Sylvain Briat
d47de20588 update packages 2024-01-17 08:54:41 +01:00
Sylvain Briat
957eb93f3e Merge branch 'upgradeGH' into 'main'
Upgrade gh

See merge request v3/service/matcher!25
2023-12-18 14:55:03 +00:00
Sylvain Briat
4ccfba12fa 1.5.2 2023-12-18 15:50:30 +01:00
Sylvain Briat
3aaccfa48f upgrade graphhopper api params, secure broker 2023-12-18 15:49:44 +01:00
Sylvain Briat
d172cac7f4 Merge branch 'fixAdValidation' into 'main'
Fix ad validation

See merge request v3/service/matcher!24
2023-12-07 10:20:14 +00:00
Sylvain Briat
0729670574 1.5.1 2023-12-07 11:14:08 +01:00
Sylvain Briat
ce4107ddd7 fix broker queues and keys 2023-12-07 11:14:01 +01:00
Sylvain Briat
3503e53d79 Merge branch 'adCreatedEvent' into 'main'
Send messages when a matcher ad is created, or when a matcher ad creation has failed

See merge request v3/service/matcher!23
2023-12-06 14:21:24 +00:00
Sylvain Briat
ecab239928 1.5.0 2023-12-06 15:16:38 +01:00
Sylvain Briat
80fac59c43 send messages when matcher ad is created, or when matcher ad creation has failed 2023-12-06 15:16:29 +01:00
Sylvain Briat
73f660bf6d Merge branch 'fixMatchRequestDto' into 'main'
Fix match request dto

See merge request v3/service/matcher!22
2023-11-21 15:36:57 +00:00
Sylvain Briat
df92231f04 1.4.4 2023-11-21 16:32:41 +01:00
Sylvain Briat
de239848c3 fix bad validation for distance and duration ratio, and for proportion 2023-11-21 16:32:32 +01:00
Sylvain Briat
59596fadee Merge branch 'fixEmptyJourneys' into 'main'
Fix empty journeys

See merge request v3/service/matcher!21
2023-11-21 15:15:39 +00:00
Sylvain Briat
eee0fd070a 1.4.3 2023-11-21 16:10:25 +01:00
Sylvain Briat
970260f0d2 fix crash when no date is available in recurrent for a journey 2023-11-21 16:10:19 +01:00
Sylvain Briat
f21fa0e9b0 Merge branch 'updateProto' into 'main'
Update proto

See merge request v3/service/matcher!20
2023-11-06 16:35:15 +00:00
Sylvain Briat
a5bb249193 remove forgotten console log 2023-11-06 17:31:05 +01:00
89 changed files with 3475 additions and 3980 deletions

View File

@@ -8,7 +8,7 @@ HEALTH_SERVICE_PORT=6005
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"
# MESSAGE BROKER
MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
MESSAGE_BROKER_EXCHANGE_DURABILITY=true

View File

@@ -0,0 +1,55 @@
_Replace italic text by your own description_
## Feature Merge Request
### Why this Merge Request
_This merge request addresses, and describe the problem or user story being addressed._
### What is implemented, what is the chosen solution
_Explain the fix or solution implemented. Which other solution have been envisaged._
### Related issues and impact on other project in codebase
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
_And Link to other project Impacted._
### Other Information
_Include any extra information or considerations for reviewers._
## Checklists
### Merge Request
- [ ] Target branch identified.
- [ ] Code based on last version of target branch.
- [ ] Description filled.
- [ ] Impact on other project codebase identified.
- [ ] Documentation reflects the changes made.
- [ ] Test run in gitlab pipeline and locally.
- [ ] One or more reviewer is defined
### Code Review
- [ ] Code follows project coding guidelines.
- [ ] Code follows project designed architecture.
- [ ] Code is easily readable.
- [ ] Everything new have an explicit and pertinent name (variable, method, file ...)
- [ ] No redundant/duplicate code (unless explain by architecture choice)
- [ ] Commit are all related to MR and well written (Atomic commit).
- [ ] New code is tested and covered by automated test.
- [ ] No useless logging or debugging code.
- [ ] No code can be replaced by library or framework code.
### TODO before merge
- [ ] _add any task here_
- [ ] ...
### TODO after merge
- [ ] _add any task here_
- [ ] ...

View File

@@ -0,0 +1,62 @@
_Replace italic text by your own description_
## Release Merge Request
### Why this Merge Request
_This merge request addresses, and describe the problem or user story being addressed._
### What is implemented, what is the chosen solution
_Explain the fix or solution implemented. Which other solution have been envisaged._
### Related issues and impact on other project in codebase
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
_And Link to other project Impacted._
### Other Information
_Include any extra information or considerations for reviewers._
## Checklists
### Merge Request
- [ ] Target branch identified.
- [ ] Code based on last version of target branch.
- [ ] Description filled.
- [ ] Impact on other project codebase identified.
- [ ] Documentation reflects the changes made.
- [ ] Test run in gitlab pipeline and locally.
- [ ] One or more reviewer is defined
### Code Review
- [ ] Code follows project coding guidelines.
- [ ] Code follows project designed architecture.
- [ ] Code is easily readable.
- [ ] Everything new have an explicit and pertinent name (variable, method, file ...)
- [ ] No redundant/duplicate code (unless explain by architecture choice)
- [ ] Commit are all related to MR and well written (Atomic commit).
- [ ] New code is tested and covered by automated test.
- [ ] No useless logging or debugging code.
- [ ] No code can be replaced by library or framework code.
### Change Management
- [ ] Release is planned
- [ ] Merge Request to be included are identified
- [ ] Concerned Team are aware of the change
- [ ] No other change on the same day (if possible)
### TODO before merge
- [ ] _add any task here_
- [ ] ...
### TODO after merge
- [ ] _add any task here_
- [ ] ...

View File

@@ -0,0 +1,37 @@
_Replace italic text by your own description_
## Small Fix Merge Request
### Why this Merge Request
_This merge request addresses, and describe the problem or user story being addressed._
### What is implemented, what is the chosen solution
_Explain the fix or solution implemented. Which other solution have been envisaged._
### Related issues and impact on other project in codebase
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
_And Link to other project Impacted._
### Other Information
_Include any extra information or considerations for reviewers._
## Checklists
### Merge Request
- [ ] Target branch identified.
- [ ] Code based on last version of target branch.
- [ ] Description filled.
- [ ] Impact on other project codebase identified.
- [ ] Test run in gitlab pipeline and locally.
### Code Review
- [ ] Code is easily readable.
- [ ] Commit are all related to MR and well written (Atomic commit).
- [ ] No useless logging or debugging code.

View File

@@ -4,3 +4,4 @@ node_modules
dist
coverage
.prettierrc.json
.gitlab

View File

@@ -2,7 +2,7 @@
# BUILD FOR LOCAL DEVELOPMENT
###################
FROM node:18-alpine3.16 As development
FROM docker.io/node:lts-hydrogen As development
# Create app directory
WORKDIR /usr/src/app
@@ -29,7 +29,7 @@ USER node
# BUILD FOR PRODUCTION
###################
FROM node:18-alpine3.16 As build
FROM docker.io/node:lts-hydrogen As build
WORKDIR /usr/src/app
@@ -63,7 +63,7 @@ USER node
# PRODUCTION
###################
FROM node:18-alpine3.16 As production
FROM docker.io/node:lts-hydrogen As production
# Copy package.json to be able to execute migration command
COPY --chown=node:node package*.json ./

View File

@@ -6,7 +6,7 @@ SERVICE_PORT=5005
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
# MESSAGE BROKER
MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
# REDIS
@@ -15,9 +15,9 @@ REDIS_PASSWORD=redis
REDIS_PORT=6379
# IMAGES
BROKER_IMAGE=rabbitmq:3-alpine
REDIS_IMAGE=redis:7.0-alpine
POSTGRES_IMAGE=postgis/postgis:15-3.3
BROKER_IMAGE=docker.io/rabbitmq:3-alpine
REDIS_IMAGE=docker.io/redis:7.0-alpine
POSTGRES_IMAGE=docker.io/postgis/postgis:15-3.3
# DEFAULT CONFIGURATION
@@ -54,6 +54,3 @@ MAX_DETOUR_DURATION_RATIO=0.3
GEOROUTER_TYPE=graphhopper
# georouter url
GEOROUTER_URL=http://localhost:8989

View File

@@ -2,7 +2,7 @@
# BUILD FOR CI TESTING
###################
FROM node:18-alpine3.16
FROM docker.io/node:lts-hydrogen
# Create app directory
WORKDIR /usr/src/app

4344
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@mobicoop/matcher",
"version": "1.4.2",
"version": "1.5.5",
"description": "Mobicoop V3 Matcher",
"author": "sbriat",
"private": true,
@@ -24,69 +24,69 @@
"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:e2e": "jest --config ./test/jest-e2e.json",
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",
"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:test": "dotenv -e .env.test -- npx prisma migrate deploy",
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
"migrate:deploy": "npx prisma migrate deploy"
},
"dependencies": {
"@grpc/grpc-js": "^1.9.9",
"@grpc/grpc-js": "^1.9.14",
"@grpc/proto-loader": "^0.7.10",
"@songkeys/nestjs-redis": "^10.0.0",
"@mobicoop/configuration-module": "^7.2.1",
"@mobicoop/ddd-library": "^2.1.1",
"@mobicoop/health-module": "^2.3.1",
"@mobicoop/message-broker-module": "^2.1.1",
"@mobicoop/configuration-module": "^8.0.0",
"@mobicoop/ddd-library": "^2.4.3",
"@mobicoop/health-module": "^2.3.2",
"@mobicoop/message-broker-module": "^2.1.2",
"@nestjs/axios": "^3.0.1",
"@nestjs/cache-manager": "^2.1.0",
"@nestjs/common": "^10.2.7",
"@nestjs/cache-manager": "^2.2.0",
"@nestjs/common": "^10.3.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.7",
"@nestjs/core": "^10.3.0",
"@nestjs/cqrs": "^10.2.6",
"@nestjs/event-emitter": "^2.0.2",
"@nestjs/microservices": "^10.2.7",
"@nestjs/platform-express": "^10.2.7",
"@nestjs/terminus": "^10.1.1",
"@prisma/client": "^5.5.2",
"axios": "^1.6.0",
"cache-manager": "^5.2.4",
"@nestjs/event-emitter": "^2.0.3",
"@nestjs/microservices": "^10.3.0",
"@nestjs/platform-express": "^10.3.0",
"@nestjs/terminus": "^10.2.0",
"@prisma/client": "^5.8.1",
"axios": "^1.6.5",
"cache-manager": "^5.3.2",
"cache-manager-ioredis-yet": "^1.2.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"geo-tz": "^7.0.7",
"class-validator": "^0.14.1",
"geo-tz": "^8.0.0",
"geographiclib-geodesic": "^2.0.0",
"got": "^13.0.0",
"got": "^14.0.0",
"ioredis": "^5.3.2",
"nestjs-request-context": "^3.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"timezonecomplete": "^5.12.4"
},
"devDependencies": {
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.7",
"@types/express": "^4.17.20",
"@types/jest": "29.5.7",
"@types/node": "20.8.10",
"@types/supertest": "^2.0.15",
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"@nestjs/cli": "^10.3.0",
"@nestjs/schematics": "^10.1.0",
"@nestjs/testing": "^10.3.0",
"@types/express": "^4.17.21",
"@types/jest": "29.5.11",
"@types/node": "20.11.5",
"@types/supertest": "^6.0.2",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"dotenv-cli": "^7.3.0",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "29.7.0",
"prettier": "^3.0.3",
"prisma": "^5.5.2",
"prettier": "^3.2.3",
"prisma": "^5.8.1",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"supertest": "^6.3.4",
"ts-jest": "29.1.1",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "^5.2.2"
"typescript": "^5.3.3"
},
"jest": {
"moduleFileExtensions": [

View File

@@ -3,24 +3,18 @@ export const SERVICE_NAME = 'matcher';
// grpc
export const GRPC_PACKAGE_NAME = 'matcher';
export const GRPC_GEOGRAPHY_PACKAGE_NAME = 'geography';
export const GRPC_GEOROUTER_SERVICE_NAME = 'GeorouterService';
// messaging
// messaging output
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
'matcher-ad.creation-failed';
// messaging input
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_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_ROUTING_KEY = 'ad.deleted';
export const AD_DELETED_QUEUE = 'matcher-ad-deleted';
// configuration
export const SERVICE_CONFIGURATION_SET_QUEUE = 'matcher-configuration-set';
export const SERVICE_CONFIGURATION_DELETE_QUEUE =
'matcher-configuration-delete';
export const SERVICE_CONFIGURATION_PROPAGATE_QUEUE =
'matcher-configuration-propagate';
export const AD_CREATED_QUEUE = 'matcher.ad.created';
// health
export const GRPC_HEALTH_PACKAGE_NAME = 'health';

View File

@@ -10,6 +10,7 @@ import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
import { HealthModuleOptions } from '@mobicoop/health-module/dist/core/domain/types/health.types';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { GeographyModule } from '@modules/geography/geography.module';
import producerServicesConfig from './config/producer-services.config';
import {
HEALTH_AD_REPOSITORY,
HEALTH_CRITICAL_LOGGING_KEY,
@@ -18,7 +19,7 @@ import {
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ConfigModule.forRoot({ isGlobal: true, load: [producerServicesConfig] }),
EventEmitterModule.forRoot(),
RequestContextModule,
HealthModule.forRootAsync({

View File

@@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
export default registerAs('producerServices', () => ({
geographyUrl: process.env.GEOGRAPHY_SERVICE_URL ?? 'v3-geography-api',
geographyPort: process.env.GEOGRAPHY_SERVICE_PORT
? parseInt(process.env.GEOGRAPHY_SERVICE_PORT, 10)
: 5007,
}));

View File

@@ -1,7 +1,4 @@
import {
ConfigurationDomainGet,
ConfigurationType,
} from '@mobicoop/configuration-module';
import { KeyType, Type } from '@mobicoop/configuration-module';
export const CARPOOL_CONFIG_ROLE = 'role';
export const CARPOOL_CONFIG_SEATS_PROPOSED = 'seatsProposed';
@@ -9,25 +6,25 @@ export const CARPOOL_CONFIG_SEATS_REQUESTED = 'seatsRequested';
export const CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN = 'departureTimeMargin';
export const CARPOOL_CONFIG_STRICT_FREQUENCY = 'strictFrequency';
export const CarpoolConfig: ConfigurationDomainGet[] = [
export const CarpoolKeyTypes: KeyType[] = [
{
key: CARPOOL_CONFIG_ROLE,
type: ConfigurationType.STRING,
type: Type.STRING,
},
{
key: CARPOOL_CONFIG_SEATS_PROPOSED,
type: ConfigurationType.INT,
type: Type.INT,
},
{
key: CARPOOL_CONFIG_SEATS_REQUESTED,
type: ConfigurationType.INT,
type: Type.INT,
},
{
key: CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN,
type: ConfigurationType.INT,
type: Type.INT,
},
{
key: CARPOOL_CONFIG_STRICT_FREQUENCY,
type: ConfigurationType.BOOLEAN,
type: Type.BOOLEAN,
},
];

View File

@@ -18,3 +18,5 @@ export const OUTPUT_DATETIME_TRANSFORMER = Symbol(
export const AD_CONFIGURATION_REPOSITORY = Symbol(
'AD_CONFIGURATION_REPOSITORY',
);
export const GEOGRAPHY_PACKAGE = Symbol('GEOGRAPHY_PACKAGE');
export const GEOGRAPHY_SERVICE = Symbol('GEOGRAPHY_SERVICE');

View File

@@ -5,14 +5,14 @@ import {
AD_REPOSITORY,
AD_DIRECTION_ENCODER,
AD_ROUTE_PROVIDER,
AD_GET_BASIC_ROUTE_CONTROLLER,
TIMEZONE_FINDER,
TIME_CONVERTER,
INPUT_DATETIME_TRANSFORMER,
AD_GET_DETAILED_ROUTE_CONTROLLER,
OUTPUT_DATETIME_TRANSFORMER,
MATCHING_REPOSITORY,
AD_CONFIGURATION_REPOSITORY,
GEOGRAPHY_PACKAGE,
GEOGRAPHY_SERVICE,
} from './ad.di-tokens';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { AdRepository } from './infrastructure/ad.repository';
@@ -20,8 +20,6 @@ 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 { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller';
import { RouteProvider } from './infrastructure/route-provider';
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';
@@ -29,7 +27,6 @@ import { MatchQueryHandler } from './core/application/queries/match/match.query-
import { TimezoneFinder } from './infrastructure/timezone-finder';
import { TimeConverter } from './infrastructure/time-converter';
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller';
import { MatchMapper } from './match.mapper';
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
import { MatchingRepository } from './infrastructure/matching.repository';
@@ -43,9 +40,45 @@ import {
RedisModuleOptions,
} from '@songkeys/nestjs-redis';
import { ConfigurationRepository } from '@mobicoop/configuration-module';
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
import { Georouter } from './infrastructure/georouter';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GRPC_GEOGRAPHY_PACKAGE_NAME } from '@src/app.constants';
import { join } from 'path';
const imports = [
CqrsModule,
ClientsModule.registerAsync([
{
name: GEOGRAPHY_PACKAGE,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
transport: Transport.GRPC,
options: {
package: GRPC_GEOGRAPHY_PACKAGE_NAME,
protoPath: join(__dirname, '/infrastructure/georouter.proto'),
url: `${configService.get<string>(
'producerServices.geographyUrl',
)}:${configService.get<string>('producerServices.geographyPort')}`,
},
}),
},
]),
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) => ({
@@ -80,6 +113,10 @@ const grpcControllers = [MatchGrpcController];
const messageHandlers = [AdCreatedMessageHandler];
const eventHandlers: Provider[] = [
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
];
const commandHandlers: Provider[] = [CreateAdService];
const queryHandlers: Provider[] = [MatchQueryHandler];
@@ -117,15 +154,7 @@ const adapters: Provider[] = [
},
{
provide: AD_ROUTE_PROVIDER,
useClass: RouteProvider,
},
{
provide: AD_GET_BASIC_ROUTE_CONTROLLER,
useClass: GetBasicRouteController,
},
{
provide: AD_GET_DETAILED_ROUTE_CONTROLLER,
useClass: GetDetailedRouteController,
useClass: Georouter,
},
{
provide: TIMEZONE_FINDER,
@@ -150,6 +179,7 @@ const adapters: Provider[] = [
controllers: [...grpcControllers],
providers: [
...messageHandlers,
...eventHandlers,
...commandHandlers,
...queryHandlers,
...mappers,

View File

@@ -1,12 +1,19 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateAdCommand } from './create-ad.command';
import { Inject } from '@nestjs/common';
import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
import {
AD_MESSAGE_PUBLISHER,
AD_REPOSITORY,
AD_ROUTE_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
import {
AggregateID,
ConflictException,
MessagePublisherPort,
} from '@mobicoop/ddd-library';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { RouteProviderPort } from '../../ports/route-provider.port';
import { Role } from '@modules/ad/core/domain/ad.types';
import {
Path,
@@ -17,14 +24,19 @@ import {
import { Waypoint } from '../../types/waypoint.type';
import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects/point.value-object';
import { Point } from '@modules/geography/core/domain/route.types';
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event';
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
import { GeorouterPort } from '../../ports/georouter.port';
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
constructor(
@Inject(AD_MESSAGE_PUBLISHER)
private readonly messagePublisher: MessagePublisherPort,
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
@Inject(AD_ROUTE_PROVIDER)
private readonly routeProvider: RouteProviderPort,
private readonly routeProvider: GeorouterPort,
) {}
async execute(command: CreateAdCommand): Promise<AggregateID> {
@@ -44,17 +56,6 @@ export class CreateAdService implements ICommandHandler {
);
let typedRoutes: TypedRoute[];
try {
typedRoutes = await Promise.all(
pathCreator.getBasePaths().map(async (path: Path) => ({
type: path.type,
route: await this.routeProvider.getBasic(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;
@@ -62,6 +63,21 @@ export class CreateAdService implements ICommandHandler {
let points: PointValueObject[] | undefined;
let fwdAzimuth: number | undefined;
let backAzimuth: number | undefined;
try {
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');
}
try {
typedRoutes.forEach((typedRoute: TypedRoute) => {
if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) {
@@ -77,7 +93,9 @@ export class CreateAdService implements ICommandHandler {
fwdAzimuth = typedRoute.route.fwdAzimuth;
backAzimuth = typedRoute.route.backAzimuth;
}
if ([PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)) {
if (
[PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)
) {
passengerDistance = typedRoute.route.distance;
passengerDuration = typedRoute.route.duration;
if (!points)
@@ -95,6 +113,7 @@ export class CreateAdService implements ICommandHandler {
} catch (error: any) {
throw new Error('Invalid route');
}
const ad = AdEntity.create({
id: command.id,
driver: command.driver,
@@ -115,6 +134,7 @@ export class CreateAdService implements ICommandHandler {
fwdAzimuth: fwdAzimuth as number,
backAzimuth: backAzimuth as number,
});
try {
await this.repository.insertExtra(ad, 'ad');
return ad.id;
@@ -124,5 +144,21 @@ export class CreateAdService implements ICommandHandler {
}
throw error;
}
} catch (error: any) {
const matcherAdCreationFailedIntegrationEvent =
new MatcherAdCreationFailedIntegrationEvent({
id: command.id,
metadata: {
correlationId: command.id,
timestamp: Date.now(),
},
cause: error.message,
});
this.messagePublisher.publish(
MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
JSON.stringify(matcherAdCreationFailedIntegrationEvent),
);
throw error;
}
}
}

View File

@@ -0,0 +1,34 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { MatcherAdCreatedDomainEvent } from '../../domain/events/matcher-ad-created.domain-event';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
import { MATCHER_AD_CREATED_ROUTING_KEY } from '@src/app.constants';
import { MatcherAdCreatedIntegrationEvent } from '../events/matcher-ad-created.integration-event';
@Injectable()
export class PublishMessageWhenMatcherAdIsCreatedDomainEventHandler {
constructor(
@Inject(AD_MESSAGE_PUBLISHER)
private readonly messagePublisher: MessagePublisherPort,
) {}
@OnEvent(MatcherAdCreatedDomainEvent.name, { async: true, promisify: true })
async handle(event: MatcherAdCreatedDomainEvent): Promise<any> {
const matcherAdCreatedIntegrationEvent =
new MatcherAdCreatedIntegrationEvent({
id: event.aggregateId,
driverDuration: event.driverDuration,
driverDistance: event.driverDistance,
passengerDuration: event.passengerDuration,
passengerDistance: event.passengerDistance,
fwdAzimuth: event.fwdAzimuth,
backAzimuth: event.backAzimuth,
metadata: event.metadata,
});
this.messagePublisher.publish(
MATCHER_AD_CREATED_ROUTING_KEY,
JSON.stringify(matcherAdCreatedIntegrationEvent),
);
}
}

View File

@@ -0,0 +1,20 @@
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
export class MatcherAdCreatedIntegrationEvent extends IntegrationEvent {
readonly driverDuration?: number;
readonly driverDistance?: number;
readonly passengerDuration?: number;
readonly passengerDistance?: number;
readonly fwdAzimuth: number;
readonly backAzimuth: number;
constructor(props: IntegrationEventProps<MatcherAdCreatedIntegrationEvent>) {
super(props);
this.driverDuration = props.driverDuration;
this.driverDistance = props.driverDistance;
this.passengerDuration = props.passengerDuration;
this.passengerDistance = props.passengerDistance;
this.fwdAzimuth = props.fwdAzimuth;
this.backAzimuth = props.backAzimuth;
}
}

View File

@@ -0,0 +1,12 @@
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
export class MatcherAdCreationFailedIntegrationEvent extends IntegrationEvent {
readonly cause?: string;
constructor(
props: IntegrationEventProps<MatcherAdCreationFailedIntegrationEvent>,
) {
super(props);
this.cause = props.cause;
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
export type Point = {
lon: number;
lat: number;
};
export type Step = Point & {
duration: number;
distance?: number;
};
export type RouteRequest = {
waypoints: Point[];
detailsSettings?: { points: boolean; steps: boolean };
};
export type RouteResponse = {
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
points: Point[];
steps?: Step[];
};
@Injectable()
export abstract class GeorouterPort {
abstract getRoute(request: RouteRequest): Promise<RouteResponse>;
}

View File

@@ -1,19 +0,0 @@
import { Point } from '../types/point.type';
import { Route } from '../types/route.type';
export interface RouteProviderPort {
/**
* Get a basic route :
* - simple points (coordinates only)
* - overall duration
* - overall distance
*/
getBasic(waypoints: Point[]): Promise<Route>;
/**
* Get a detailed route :
* - detailed points (coordinates and time / distance to reach the point)
* - overall duration
* - overall distance
*/
getDetailed(waypoints: Point[]): Promise<Route>;
}

View File

@@ -21,7 +21,6 @@ export abstract class Algorithm {
for (const processor of this.processors) {
this.candidates = await processor.execute(this.candidates);
}
// console.log(JSON.stringify(this.candidates, null, 2));
return this.candidates.map((candidate: CandidateEntity) =>
MatchEntity.create({
adId: candidate.id,

View File

@@ -33,7 +33,7 @@ export class PassengerOrientedCarpoolPathCompleter extends Completer {
}),
),
candidate.getProps().role == Role.PASSENGER
? candidate.getProps().driverWaypoints.map(
? candidate.getProps().passengerWaypoints.map(
(waypoint: PointProps) =>
new Point({
lon: waypoint.lon,

View File

@@ -1,8 +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;
@@ -18,23 +19,20 @@ export class RouteCompleter extends Completer {
candidates.map(async (candidate: CandidateEntity) => {
switch (this.type) {
case RouteCompleterType.BASIC:
const basicCandidateRoute = await this.query.routeProvider.getBasic(
(candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
),
);
const basicCandidateRoute = await this._getRoute(candidate, {
points: true,
steps: false,
});
candidate.setMetrics(
basicCandidateRoute.distance,
basicCandidateRoute.duration,
);
break;
case RouteCompleterType.DETAILED:
const detailedCandidateRoute =
await this.query.routeProvider.getDetailed(
(candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
),
);
const detailedCandidateRoute = await this._getRoute(candidate, {
points: true,
steps: true,
});
candidate.setSteps(detailedCandidateRoute.steps as Step[]);
break;
}
@@ -43,6 +41,20 @@ export class RouteCompleter extends Completer {
);
return candidates;
};
_getRoute = async (
candidate: CandidateEntity,
detailsSettings: { points: boolean; steps: boolean },
): Promise<RouteResponse> =>
this.query.routeProvider.getRoute({
waypoints: (candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
(carpoolPathItem: CarpoolPathItem) => ({
lon: carpoolPathItem.lon,
lat: carpoolPathItem.lat,
}),
),
detailsSettings: detailsSettings,
});
}
export enum RouteCompleterType {

View File

@@ -17,7 +17,7 @@ import { Paginator } from '@mobicoop/ddd-library';
import { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
import { MatchingRepositoryPort } from '../../ports/matching.repository.port';
import {
ConfigurationDomain,
Domain,
Configurator,
GetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
@@ -27,7 +27,7 @@ import {
CARPOOL_CONFIG_SEATS_PROPOSED,
CARPOOL_CONFIG_SEATS_REQUESTED,
CARPOOL_CONFIG_STRICT_FREQUENCY,
CarpoolConfig,
CarpoolKeyTypes,
} from '@modules/ad/ad.constants';
import {
MATCH_CONFIG_ALGORITHM,
@@ -57,18 +57,12 @@ export class MatchQueryHandler implements IQueryHandler {
execute = async (query: MatchQuery): Promise<MatchingResult> => {
const carpoolConfigurator: Configurator =
await this.configurationRepository.mget(
ConfigurationDomain.CARPOOL,
CarpoolConfig,
);
await this.configurationRepository.mget(Domain.CARPOOL, CarpoolKeyTypes);
const matchConfigurator: Configurator =
await this.configurationRepository.mget(
ConfigurationDomain.MATCH,
MatchConfig,
);
await this.configurationRepository.mget(Domain.MATCH, MatchConfig);
const paginationConfigurator: Configurator =
await this.configurationRepository.mget(
ConfigurationDomain.PAGINATION,
Domain.PAGINATION,
PaginationConfig,
);
query

View File

@@ -1,10 +1,5 @@
import { QueryBase } from '@mobicoop/ddd-library';
import { AlgorithmType } from '../../types/algorithm.types';
import { Waypoint } from '../../types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
import { RouteProviderPort } from '../../ports/route-provider.port';
import {
Path,
PathCreator,
@@ -12,7 +7,12 @@ import {
TypedRoute,
} from '@modules/ad/core/domain/path-creator.service';
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 { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
import { GeorouterPort } from '../../ports/georouter.port';
import { AlgorithmType } from '../../types/algorithm.types';
import { Route } from '../../types/route.type';
import { Waypoint } from '../../types/waypoint.type';
export class MatchQuery extends QueryBase {
id?: string;
@@ -40,9 +40,10 @@ export class MatchQuery extends QueryBase {
passengerRoute?: Route;
backAzimuth?: number;
private readonly originWaypoint: Waypoint;
routeProvider: RouteProviderPort;
routeProvider: GeorouterPort;
constructor(props: MatchRequestDto, routeProvider: RouteProviderPort) {
// TODO: remove MatchRequestDto depency (here core domain depends on interface /!\)
constructor(props: MatchRequestDto, routeProvider: GeorouterPort) {
super();
this.id = props.id;
this.driver = props.driver;
@@ -207,7 +208,12 @@ export class MatchQuery extends QueryBase {
await Promise.all(
pathCreator.getBasePaths().map(async (path: Path) => ({
type: path.type,
route: await this.routeProvider.getBasic(path.waypoints),
route: await this.routeProvider.getRoute({
waypoints: path.waypoints.map((p) => ({
lon: p.lon,
lat: p.lat,
})),
}),
})),
)
).forEach((typedRoute: TypedRoute) => {
@@ -222,6 +228,7 @@ export class MatchQuery extends QueryBase {
}
});
} catch (e: any) {
console.log(e.stack || e);
throw new Error('Unable to find a route for given waypoints');
}
return this;

View File

@@ -174,7 +174,7 @@ export class PassengerOrientedSelector extends Selector {
private _whereSchedule = (role: Role): string => {
const schedule: string[] = [];
// we need full dates to compare times, because margins can lead to compare on previous or next day
// -first we establish a base calendar (up to a week)
// - first we establish a base calendar (up to a week)
const scheduleDates: Date[] = this._datesBetweenBoundaries(
this.query.fromDate,
this.query.toDate,

View File

@@ -1,12 +1,29 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { AdProps, CreateAdProps } from './ad.types';
import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event';
export class AdEntity extends AggregateRoot<AdProps> {
protected readonly _id: AggregateID;
static create = (create: CreateAdProps): AdEntity => {
const props: AdProps = { ...create };
return new AdEntity({ id: create.id, props });
const ad = new AdEntity({ id: create.id, props });
ad.addEvent(
new MatcherAdCreatedDomainEvent({
metadata: {
correlationId: create.id,
timestamp: Date.now(),
},
aggregateId: create.id,
driverDistance: create.driverDistance,
driverDuration: create.driverDuration,
passengerDistance: create.passengerDistance,
passengerDuration: create.passengerDuration,
fwdAzimuth: create.fwdAzimuth,
backAzimuth: create.backAzimuth,
}),
);
return ad;
};
validate(): void {

View File

@@ -53,6 +53,7 @@ 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) =>
@@ -60,6 +61,10 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
)
// 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
}
return this;
};

View File

@@ -0,0 +1,20 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class MatcherAdCreatedDomainEvent extends DomainEvent {
readonly driverDuration?: number;
readonly driverDistance?: number;
readonly passengerDuration?: number;
readonly passengerDistance?: number;
readonly fwdAzimuth: number;
readonly backAzimuth: number;
constructor(props: DomainEventProps<MatcherAdCreatedDomainEvent>) {
super(props);
this.driverDuration = props.driverDuration;
this.driverDistance = props.driverDistance;
this.passengerDuration = props.passengerDuration;
this.passengerDistance = props.passengerDistance;
this.fwdAzimuth = props.fwdAzimuth;
this.backAzimuth = props.backAzimuth;
}
}

View File

@@ -1,7 +1,7 @@
import { Role } from './ad.types';
import { Point } from './value-objects/point.value-object';
import { PathCreatorException } from './match.errors';
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';
export class PathCreator {
constructor(

View File

@@ -0,0 +1,39 @@
syntax = "proto3";
package geography;
service GeorouterService {
rpc GetRoute(RouteRequest) returns (Route);
}
message RouteRequest {
repeated Point waypoints = 1;
optional DetailsSettings detailsSettings = 2;
}
message Point {
double lon = 1;
double lat = 2;
}
message DetailsSettings {
bool points = 1;
bool steps = 2;
}
message Route {
int32 distance = 1;
int32 duration = 2;
int32 fwdAzimuth = 3;
int32 backAzimuth = 4;
int32 distanceAzimuth = 5;
repeated Point points = 6;
repeated Step steps = 7;
}
message Step {
double lon = 1;
double lat = 2;
int32 duration = 3;
int32 distance = 4;
}

View File

@@ -0,0 +1,26 @@
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Observable, lastValueFrom } from 'rxjs';
import { GEOGRAPHY_SERVICE } from '../ad.di-tokens';
import {
GeorouterPort,
RouteRequest,
RouteResponse,
} from '../core/application/ports/georouter.port';
interface GeorouterService {
getRoute(request: RouteRequest): Observable<RouteResponse>;
}
@Injectable()
export class Georouter implements GeorouterPort {
private georouterService: GeorouterService;
constructor(
@Inject(GEOGRAPHY_SERVICE) private readonly client: ClientProxy,
) {}
getRoute = async (request: RouteRequest): Promise<RouteResponse> => {
return lastValueFrom(this.client.send('getRoute', request));
};
}

View File

@@ -1,28 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { RouteProviderPort } from '../core/application/ports/route-provider.port';
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
import {
AD_GET_BASIC_ROUTE_CONTROLLER,
AD_GET_DETAILED_ROUTE_CONTROLLER,
} from '../ad.di-tokens';
import { Point, Route } from '@modules/geography/core/domain/route.types';
@Injectable()
export class RouteProvider implements RouteProviderPort {
constructor(
@Inject(AD_GET_BASIC_ROUTE_CONTROLLER)
private readonly getBasicRouteController: GetRouteControllerPort,
@Inject(AD_GET_DETAILED_ROUTE_CONTROLLER)
private readonly getDetailedRouteController: GetRouteControllerPort,
) {}
getBasic = async (waypoints: Point[]): Promise<Route> =>
await this.getBasicRouteController.get({
waypoints,
});
getDetailed = async (waypoints: Point[]): Promise<Route> =>
await this.getDetailedRouteController.get({
waypoints,
});
}

View File

@@ -2,10 +2,10 @@ import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDecimal,
IsEnum,
IsISO8601,
IsInt,
IsNumber,
IsOptional,
IsUUID,
Max,
@@ -93,7 +93,7 @@ export class MatchRequestDto {
useProportion?: boolean;
@IsOptional()
@IsDecimal()
@IsNumber()
@Min(0)
@Max(1)
proportion?: number;
@@ -109,13 +109,13 @@ export class MatchRequestDto {
azimuthMargin?: number;
@IsOptional()
@IsDecimal()
@IsNumber()
@Min(0)
@Max(1)
maxDetourDistanceRatio?: number;
@IsOptional()
@IsDecimal()
@IsNumber()
@Min(0)
@Max(1)
maxDetourDurationRatio?: number;

View File

@@ -8,10 +8,10 @@ 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 { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
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';
@UsePipes(
new RpcValidationPipe({
@@ -24,7 +24,7 @@ export class MatchGrpcController {
constructor(
private readonly queryBus: QueryBus,
@Inject(AD_ROUTE_PROVIDER)
private readonly routeProvider: RouteProviderPort,
private readonly routeProvider: GeorouterPort,
private readonly matchMapper: MatchMapper,
) {}
@@ -32,7 +32,6 @@ export class MatchGrpcController {
@UseInterceptors(CacheInterceptor)
@GrpcMethod('MatcherService', 'Match')
async match(data: MatchRequestDto): Promise<MatchingPaginatedResponseDto> {
console.log(data);
try {
const matchingResult: MatchingResult = await this.queryBus.execute(
new MatchQuery(data, this.routeProvider),

View File

@@ -19,7 +19,7 @@ message MatchRequest {
AlgorithmType algorithmType = 10;
int32 remoteness = 11;
bool useProportion = 12;
int32 proportion = 13;
float proportion = 13;
bool useAzimuth = 14;
int32 azimuthMargin = 15;
float maxDetourDistanceRatio = 16;

View File

@@ -3,7 +3,10 @@ import { RabbitSubscribe } from '@mobicoop/message-broker-module';
import { CommandBus } from '@nestjs/cqrs';
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
import { Ad } from './ad.types';
import { AD_CREATED_MESSAGE_HANDLER } from '@src/app.constants';
import {
AD_CREATED_MESSAGE_HANDLER,
AD_CREATED_ROUTING_KEY,
} from '@src/app.constants';
@Injectable()
export class AdCreatedMessageHandler {
@@ -11,6 +14,7 @@ export class AdCreatedMessageHandler {
@RabbitSubscribe({
name: AD_CREATED_MESSAGE_HANDLER,
routingKey: AD_CREATED_ROUTING_KEY,
})
public async adCreated(message: string) {
try {
@@ -30,8 +34,9 @@ export class AdCreatedMessageHandler {
waypoints: createdAd.waypoints,
}),
);
} catch (e: any) {
console.log(e);
} catch (error: any) {
// do not throw error to acknowledge incoming message
// error handling should be done in the command handler, if relevant
}
}
}

View File

@@ -1,7 +1,4 @@
import {
ConfigurationDomainGet,
ConfigurationType,
} from '@mobicoop/configuration-module';
import { KeyType, Type } from '@mobicoop/configuration-module';
export const MATCH_CONFIG_ALGORITHM = 'algorithm';
export const MATCH_CONFIG_REMOTENESS = 'remoteness';
@@ -13,44 +10,44 @@ export const MATCH_CONFIG_MAX_DETOUR_DISTANCE_RATIO = 'maxDetourDistanceRatio';
export const MATCH_CONFIG_MAX_DETOUR_DURATION_RATIO = 'maxDetourDurationRatio';
export const PAGINATION_CONFIG_PER_PAGE = 'perPage';
export const MatchConfig: ConfigurationDomainGet[] = [
export const MatchConfig: KeyType[] = [
{
key: MATCH_CONFIG_ALGORITHM,
type: ConfigurationType.STRING,
type: Type.STRING,
},
{
key: MATCH_CONFIG_REMOTENESS,
type: ConfigurationType.INT,
type: Type.INT,
},
{
key: MATCH_CONFIG_USE_PROPORTION,
type: ConfigurationType.BOOLEAN,
type: Type.BOOLEAN,
},
{
key: MATCH_CONFIG_PROPORTION,
type: ConfigurationType.FLOAT,
type: Type.FLOAT,
},
{
key: MATCH_CONFIG_USE_AZIMUTH,
type: ConfigurationType.BOOLEAN,
type: Type.BOOLEAN,
},
{
key: MATCH_CONFIG_AZIMUTH_MARGIN,
type: ConfigurationType.INT,
type: Type.INT,
},
{
key: MATCH_CONFIG_MAX_DETOUR_DISTANCE_RATIO,
type: ConfigurationType.FLOAT,
type: Type.FLOAT,
},
{
key: MATCH_CONFIG_MAX_DETOUR_DURATION_RATIO,
type: ConfigurationType.FLOAT,
type: Type.FLOAT,
},
];
export const PaginationConfig: ConfigurationDomainGet[] = [
export const PaginationConfig: KeyType[] = [
{
key: PAGINATION_CONFIG_PER_PAGE,
type: ConfigurationType.INT,
type: Type.INT,
},
];

View File

@@ -1,5 +1,4 @@
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import {
Algorithm,
Selector,
@@ -9,6 +8,7 @@ import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -29,11 +29,6 @@ const destinationWaypoint: Waypoint = {
country: 'France',
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(),
getDetailed: jest.fn(),
};
const matchQuery = new MatchQuery(
{
frequency: Frequency.PUNCTUAL,
@@ -46,7 +41,7 @@ const matchQuery = new MatchQuery(
],
waypoints: [originWaypoint, destinationWaypoint],
},
mockRouteProvider,
bareMockGeorouter,
);
const mockAdRepository: AdRepositoryPort = {
@@ -54,6 +49,7 @@ const mockAdRepository: AdRepositoryPort = {
findOneById: jest.fn(),
findOne: jest.fn(),
findAll: jest.fn(),
findAllByIds: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
updateWhere: jest.fn(),

View File

@@ -1,5 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
import {
AD_MESSAGE_PUBLISHER,
AD_REPOSITORY,
AD_ROUTE_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { ConflictException } from '@mobicoop/ddd-library';
@@ -7,8 +11,8 @@ 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 { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
const originWaypoint: PointProps = {
lat: 48.689445,
@@ -58,8 +62,8 @@ const mockAdRepository = {
}),
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest
const mockRouteProvider: GeorouterPort = {
getRoute: jest
.fn()
.mockImplementationOnce(() => {
throw new Error();
@@ -93,7 +97,10 @@ const mockRouteProvider: RouteProviderPort = {
},
],
})),
getDetailed: jest.fn(),
};
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
describe('create-ad.service', () => {
@@ -110,6 +117,10 @@ describe('create-ad.service', () => {
provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider,
},
{
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
CreateAdService,
],
}).compile();

View File

@@ -6,6 +6,7 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { simpleMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -42,24 +43,7 @@ const matchQuery = new MatchQuery(
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn().mockImplementation(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
})),
getDetailed: jest.fn().mockImplementation(() => ({
distance: 350102,
duration: 14423,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
})),
},
simpleMockGeorouter,
);
const candidate: CandidateEntity = CandidateEntity.create({

View File

@@ -4,6 +4,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
bareMockGeorouter,
);
const candidate: CandidateEntity = CandidateEntity.create({

View File

@@ -1,6 +1,6 @@
import {
ConfigurationDomain,
ConfigurationDomainGet,
Domain,
KeyType,
Configurator,
GetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
@@ -19,7 +19,6 @@ import {
} 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 { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
import {
MatchQueryHandler,
@@ -43,6 +42,7 @@ import {
PAGINATION_CONFIG_PER_PAGE,
} from '@modules/ad/match.constants';
import { Test, TestingModule } from '@nestjs/testing';
import { simpleMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -258,83 +258,83 @@ const mockConfigurationRepository: GetConfigurationRepositoryPort = {
get: jest.fn(),
mget: jest.fn().mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(domain: ConfigurationDomain, configs: ConfigurationDomainGet[]) => {
(domain: Domain, keyTypes: KeyType[]) => {
switch (domain) {
case ConfigurationDomain.CARPOOL:
return new Configurator(ConfigurationDomain.CARPOOL, [
case Domain.CARPOOL:
return new Configurator(Domain.CARPOOL, [
{
domain: ConfigurationDomain.CARPOOL,
domain: Domain.CARPOOL,
key: CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN,
value: 900,
},
{
domain: ConfigurationDomain.CARPOOL,
domain: Domain.CARPOOL,
key: CARPOOL_CONFIG_ROLE,
value: 'passenger',
},
{
domain: ConfigurationDomain.CARPOOL,
domain: Domain.CARPOOL,
key: CARPOOL_CONFIG_SEATS_PROPOSED,
value: 3,
},
{
domain: ConfigurationDomain.CARPOOL,
domain: Domain.CARPOOL,
key: CARPOOL_CONFIG_SEATS_REQUESTED,
value: 1,
},
{
domain: ConfigurationDomain.CARPOOL,
domain: Domain.CARPOOL,
key: CARPOOL_CONFIG_STRICT_FREQUENCY,
value: false,
},
]);
case ConfigurationDomain.MATCH:
return new Configurator(ConfigurationDomain.MATCH, [
case Domain.MATCH:
return new Configurator(Domain.MATCH, [
{
domain: ConfigurationDomain.MATCH,
domain: Domain.MATCH,
key: MATCH_CONFIG_ALGORITHM,
value: 'PASSENGER_ORIENTED',
},
{
domain: ConfigurationDomain.MATCH,
domain: Domain.MATCH,
key: MATCH_CONFIG_REMOTENESS,
value: 15000,
},
{
domain: ConfigurationDomain.MATCH,
domain: Domain.MATCH,
key: MATCH_CONFIG_USE_PROPORTION,
value: true,
},
{
domain: ConfigurationDomain.MATCH,
domain: Domain.MATCH,
key: MATCH_CONFIG_PROPORTION,
value: 0.3,
},
{
domain: ConfigurationDomain.MATCH,
domain: Domain.MATCH,
key: MATCH_CONFIG_USE_AZIMUTH,
value: true,
},
{
domain: ConfigurationDomain.MATCH,
domain: Domain.MATCH,
key: MATCH_CONFIG_AZIMUTH_MARGIN,
value: 10,
},
{
domain: ConfigurationDomain.MATCH,
domain: Domain.MATCH,
key: MATCH_CONFIG_MAX_DETOUR_DISTANCE_RATIO,
value: 0.3,
},
{
domain: ConfigurationDomain.MATCH,
domain: Domain.MATCH,
key: MATCH_CONFIG_MAX_DETOUR_DURATION_RATIO,
value: 0.3,
},
]);
case ConfigurationDomain.PAGINATION:
return new Configurator(ConfigurationDomain.PAGINATION, [
case Domain.PAGINATION:
return new Configurator(Domain.PAGINATION, [
{
domain: ConfigurationDomain.PAGINATION,
domain: Domain.PAGINATION,
key: PAGINATION_CONFIG_PER_PAGE,
value: 10,
},
@@ -351,17 +351,7 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
time: jest.fn(),
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn().mockImplementation(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
})),
getDetailed: jest.fn(),
};
const mockRouteProvider = simpleMockGeorouter;
describe('Match Query Handler', () => {
let matchQueryHandler: MatchQueryHandler;

View File

@@ -1,9 +1,10 @@
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
import { MatchQuery } 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';
import { simpleMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -57,17 +58,10 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
time: jest.fn().mockImplementation(() => '23:05'),
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest
const mockRouteProvider: GeorouterPort = {
getRoute: jest
.fn()
.mockImplementationOnce(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
}))
.mockImplementationOnce(simpleMockGeorouter.getRoute)
.mockImplementationOnce(() => ({
distance: 340102,
duration: 13423,
@@ -76,22 +70,8 @@ const mockRouteProvider: RouteProviderPort = {
distanceAzimuth: 336544,
points: [],
}))
.mockImplementationOnce(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
}))
.mockImplementationOnce(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
}))
.mockImplementationOnce(simpleMockGeorouter.getRoute)
.mockImplementationOnce(simpleMockGeorouter.getRoute)
.mockImplementationOnce(() => ({
distance: 340102,
duration: 13423,
@@ -103,7 +83,6 @@ const mockRouteProvider: RouteProviderPort = {
.mockImplementationOnce(() => {
throw new Error();
}),
getDetailed: jest.fn(),
};
describe('Match Query', () => {

View File

@@ -42,11 +42,10 @@ const matchQuery = new MatchQuery(
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn().mockImplementation(() => ({
getRoute: jest.fn().mockImplementation(() => ({
duration: 6500,
distance: 89745,
})),
getDetailed: jest.fn(),
},
);
@@ -55,6 +54,7 @@ const mockMatcherRepository: AdRepositoryPort = {
findOneById: jest.fn(),
findOne: jest.fn(),
findAll: jest.fn(),
findAllByIds: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
updateWhere: jest.fn(),

View File

@@ -4,6 +4,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
bareMockGeorouter,
);
const candidates: CandidateEntity[] = [

View File

@@ -4,6 +4,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
bareMockGeorouter,
);
const candidate: CandidateEntity = CandidateEntity.create({

View File

@@ -5,6 +5,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -47,10 +48,7 @@ const matchQuery = new MatchQuery(
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
bareMockGeorouter,
);
matchQuery.driverRoute = {
distance: 150120,
@@ -100,6 +98,7 @@ const mockMatcherRepository: AdRepositoryPort = {
findOneById: jest.fn(),
findOne: jest.fn(),
findAll: jest.fn(),
findAllByIds: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
updateWhere: jest.fn(),

View File

@@ -0,0 +1,57 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
import { MATCHER_AD_CREATED_ROUTING_KEY } from '@src/app.constants';
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
import { MatcherAdCreatedDomainEvent } from '@modules/ad/core/domain/events/matcher-ad-created.domain-event';
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
describe('Publish message when matcher ad is created domain event handler', () => {
let publishMessageWhenMatcherAdIsCreatedDomainEventHandler: PublishMessageWhenMatcherAdIsCreatedDomainEventHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
],
}).compile();
publishMessageWhenMatcherAdIsCreatedDomainEventHandler =
module.get<PublishMessageWhenMatcherAdIsCreatedDomainEventHandler>(
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
);
});
it('should publish a message', () => {
jest.spyOn(mockMessagePublisher, 'publish');
const matcherAdCreatedDomainEvent: MatcherAdCreatedDomainEvent = {
id: 'some-domain-event-id',
aggregateId: 'some-aggregate-id',
metadata: {
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
correlationId: 'some-correlation-id',
},
driverDistance: 65845,
driverDuration: 3254,
fwdAzimuth: 90,
backAzimuth: 270,
};
publishMessageWhenMatcherAdIsCreatedDomainEventHandler.handle(
matcherAdCreatedDomainEvent,
);
expect(
publishMessageWhenMatcherAdIsCreatedDomainEventHandler,
).toBeDefined();
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
MATCHER_AD_CREATED_ROUTING_KEY,
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"driverDuration":3254,"driverDistance":65845,"fwdAzimuth":90,"backAzimuth":270}',
);
});
});

View File

@@ -1,3 +1,7 @@
import {
RouteRequest,
RouteResponse,
} from '@modules/ad/core/application/ports/georouter.port';
import {
RouteCompleter,
RouteCompleterType,
@@ -9,6 +13,8 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { Step } from '@modules/geography/core/domain/route.types';
import { simpleMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = {
position: 0,
@@ -46,23 +52,16 @@ const matchQuery = new MatchQuery(
waypoints: [originWaypoint, destinationWaypoint],
},
{
getBasic: jest.fn().mockImplementation(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
})),
getDetailed: jest.fn().mockImplementation(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
steps: [jest.fn(), jest.fn(), jest.fn(), jest.fn()],
})),
getRoute: jest
.fn()
.mockImplementation(async (req: RouteRequest): Promise<RouteResponse> => {
const response = await simpleMockGeorouter.getRoute(req);
if (req.detailsSettings?.steps) {
const step: Step = { lon: 0, lat: 0, duration: 0 };
response.steps = [step, step, step, step];
}
return response;
}),
},
);

View File

@@ -0,0 +1,16 @@
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
export const bareMockGeorouter: GeorouterPort = {
getRoute: jest.fn(),
};
export const simpleMockGeorouter: GeorouterPort = {
getRoute: jest.fn().mockImplementation(() => ({
distance: 350101,
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
})),
};

View File

@@ -4,7 +4,6 @@ import {
AD_ROUTE_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
@@ -12,6 +11,7 @@ import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
import { bareMockGeorouter } from '../georouter.mock';
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
@@ -73,11 +73,6 @@ const mockDirectionEncoder: DirectionEncoderPort = {
]),
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(),
getDetailed: jest.fn(),
};
const mockPrismaService = {
$queryRawUnsafe: jest
.fn()
@@ -239,7 +234,7 @@ describe('Ad repository', () => {
},
{
provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider,
useValue: bareMockGeorouter,
},
{
provide: AD_MESSAGE_PUBLISHER,

View File

@@ -1,110 +0,0 @@
import {
AD_GET_BASIC_ROUTE_CONTROLLER,
AD_GET_DETAILED_ROUTE_CONTROLLER,
} from '@modules/ad/ad.di-tokens';
import { Point } from '@modules/ad/core/application/types/point.type';
import { Route } from '@modules/ad/core/application/types/route.type';
import { RouteProvider } from '@modules/ad/infrastructure/route-provider';
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
import { Test, TestingModule } from '@nestjs/testing';
const originPoint: Point = {
lat: 48.689445,
lon: 6.17651,
};
const destinationPoint: Point = {
lat: 48.8566,
lon: 2.3522,
};
const mockGetBasicRouteController: GetRouteControllerPort = {
get: jest.fn().mockImplementationOnce(() => ({
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 mockGetDetailedRouteController: GetRouteControllerPort = {
get: jest.fn().mockImplementationOnce(() => ({
distance: 350102,
duration: 14423,
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,
},
],
})),
};
describe('Route provider', () => {
let routeProvider: RouteProvider;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RouteProvider,
{
provide: AD_GET_BASIC_ROUTE_CONTROLLER,
useValue: mockGetBasicRouteController,
},
{
provide: AD_GET_DETAILED_ROUTE_CONTROLLER,
useValue: mockGetDetailedRouteController,
},
],
}).compile();
routeProvider = module.get<RouteProvider>(RouteProvider);
});
it('should be defined', () => {
expect(routeProvider).toBeDefined();
});
it('should provide a basic route', async () => {
const route: Route = await routeProvider.getBasic([
originPoint,
destinationPoint,
]);
expect(route.distance).toBe(350101);
expect(route.duration).toBe(14422);
});
it('should provide a detailed route', async () => {
const route: Route = await routeProvider.getDetailed([
originPoint,
destinationPoint,
]);
expect(route.distance).toBe(350102);
expect(route.duration).toBe(14423);
});
});

View File

@@ -1,6 +1,5 @@
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
@@ -17,6 +16,7 @@ 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';
const originWaypoint: WaypointDto = {
position: 0,
@@ -183,11 +183,6 @@ const mockQueryBus = {
}),
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(),
getDetailed: jest.fn(),
};
const mockMatchMapper = {
toResponse: jest.fn().mockImplementation(() => ({
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
@@ -286,7 +281,7 @@ describe('Match Grpc Controller', () => {
},
{
provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider,
useValue: bareMockGeorouter,
},
{
provide: MatchMapper,

View File

@@ -1,13 +0,0 @@
export interface GeodesicPort {
inverse(
lon1: number,
lat1: number,
lon2: number,
lat2: number,
): {
azimuth: number;
distance: number;
};
distance(lon1: number, lat1: number, lon2: number, lat2: number): number;
azimuth(lon1: number, lat1: number, lon2: number, lat2: number): number;
}

View File

@@ -1,6 +0,0 @@
import { Route, Point } from '../../domain/route.types';
import { GeorouterSettings } from '../types/georouter-settings.type';
export interface GeorouterPort {
route(waypoints: Point[], settings: GeorouterSettings): Promise<Route>;
}

View File

@@ -1,6 +0,0 @@
import { GetRouteRequestDto } from '@modules/geography/interface/controllers/dtos/get-route.request.dto';
import { RouteResponseDto } from '@modules/geography/interface/dtos/route.response.dto';
export interface GetRouteControllerPort {
get(data: GetRouteRequestDto): Promise<RouteResponseDto>;
}

View File

@@ -1,18 +0,0 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetRouteQuery } from './get-route.query';
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { Inject } from '@nestjs/common';
import { GEOROUTER } from '@modules/geography/geography.di-tokens';
import { GeorouterPort } from '../../ports/georouter.port';
@QueryHandler(GetRouteQuery)
export class GetRouteQueryHandler implements IQueryHandler {
constructor(@Inject(GEOROUTER) private readonly georouter: GeorouterPort) {}
execute = async (query: GetRouteQuery): Promise<RouteEntity> =>
await RouteEntity.create({
waypoints: query.waypoints,
georouter: this.georouter,
georouterSettings: query.georouterSettings,
});
}

View File

@@ -1,21 +0,0 @@
import { QueryBase } from '@mobicoop/ddd-library';
import { GeorouterSettings } from '../../types/georouter-settings.type';
import { Point } from '@modules/geography/core/domain/route.types';
export class GetRouteQuery extends QueryBase {
readonly waypoints: Point[];
readonly georouterSettings: GeorouterSettings;
constructor(
waypoints: Point[],
georouterSettings: GeorouterSettings = {
detailedDistance: false,
detailedDuration: false,
points: true,
},
) {
super();
this.waypoints = waypoints;
this.georouterSettings = georouterSettings;
}
}

View File

@@ -1,5 +0,0 @@
export type GeorouterSettings = {
points: boolean;
detailedDuration: boolean;
detailedDistance: boolean;
};

View File

@@ -1,33 +0,0 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { CreateRouteProps, RouteProps, Route } from './route.types';
import { v4 } from 'uuid';
import { RouteNotFoundException } from './route.errors';
export class RouteEntity extends AggregateRoot<RouteProps> {
protected readonly _id: AggregateID;
static create = async (create: CreateRouteProps): Promise<RouteEntity> => {
const route: Route = await create.georouter.route(
create.waypoints,
create.georouterSettings,
);
if (!route) throw new RouteNotFoundException();
const routeProps: RouteProps = {
distance: route.distance,
duration: route.duration,
fwdAzimuth: route.fwdAzimuth,
backAzimuth: route.backAzimuth,
distanceAzimuth: route.distanceAzimuth,
points: route.points,
steps: route.steps,
};
return new RouteEntity({
id: v4(),
props: routeProps,
});
};
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}
}

View File

@@ -1,21 +0,0 @@
import { ExceptionBase } from '@mobicoop/ddd-library';
export class RouteNotFoundException extends ExceptionBase {
static readonly message = 'Route not found';
public readonly code = 'ROUTE.NOT_FOUND';
constructor(cause?: Error, metadata?: unknown) {
super(RouteNotFoundException.message, cause, metadata);
}
}
export class GeorouterUnavailableException extends ExceptionBase {
static readonly message = 'Georouter unavailable';
public readonly code = 'GEOROUTER.UNAVAILABLE';
constructor(cause?: Error, metadata?: unknown) {
super(GeorouterUnavailableException.message, cause, metadata);
}
}

View File

@@ -1,26 +1,3 @@
import { GeorouterPort } from '../application/ports/georouter.port';
import { GeorouterSettings } from '../application/types/georouter-settings.type';
import { PointProps } from './value-objects/point.value-object';
import { StepProps } from './value-objects/step.value-object';
// All properties that a Route has
export interface RouteProps {
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
points: PointProps[];
steps?: StepProps[];
}
// Properties that are needed for a Route creation
export interface CreateRouteProps {
waypoints: PointProps[];
georouter: GeorouterPort;
georouterSettings: GeorouterSettings;
}
// Types used outside the domain
export type Route = {
distance: number;

View File

@@ -1,31 +0,0 @@
import {
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface PointProps {
lon: number;
lat: number;
}
export class Point extends ValueObject<PointProps> {
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
validate(props: PointProps): void {
if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
if (props.lat > 90 || props.lat < -90)
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
}
}

View File

@@ -1,46 +0,0 @@
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
import { Point, PointProps } from './point.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface StepProps extends PointProps {
duration: number;
distance?: number;
}
export class Step extends ValueObject<StepProps> {
get duration(): number {
return this.props.duration;
}
get distance(): number | undefined {
return this.props.distance;
}
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
protected validate(props: StepProps): void {
// validate point props
new Point({
lon: props.lon,
lat: props.lat,
});
if (props.duration < 0)
throw new ArgumentInvalidException(
'duration must be greater than or equal to 0',
);
if (props.distance !== undefined && props.distance < 0)
throw new ArgumentInvalidException(
'distance must be greater than or equal to 0',
);
}
}

View File

@@ -1,18 +0,0 @@
import {
ConfigurationDomainGet,
ConfigurationType,
} from '@mobicoop/configuration-module';
export const GEOGRAPHY_CONFIG_GEOROUTER_TYPE = 'georouterType';
export const GEOGRAPHY_CONFIG_GEOROUTER_URL = 'georouterUrl';
export const GeographyConfig: ConfigurationDomainGet[] = [
{
key: GEOGRAPHY_CONFIG_GEOROUTER_TYPE,
type: ConfigurationType.STRING,
},
{
key: GEOGRAPHY_CONFIG_GEOROUTER_URL,
type: ConfigurationType.STRING,
},
];

View File

@@ -1,6 +1,4 @@
export const DIRECTION_ENCODER = Symbol('DIRECTION_ENCODER');
export const GEOROUTER = Symbol('GEOROUTER');
export const GEODESIC = Symbol('GEODESIC');
export const GEOGRAPHY_CONFIGURATION_REPOSITORY = Symbol(
'GEOGRAPHY_CONFIGURATION_REPOSITORY',
);

View File

@@ -2,24 +2,12 @@ import { Module, Provider } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import {
DIRECTION_ENCODER,
GEODESIC,
GEOGRAPHY_CONFIGURATION_REPOSITORY,
GEOROUTER,
} from './geography.di-tokens';
import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder';
import { GetBasicRouteController } from './interface/controllers/get-basic-route.controller';
import { RouteMapper } from './route.mapper';
import { Geodesic } from './infrastructure/geodesic';
import { GraphhopperGeorouter } from './infrastructure/graphhopper-georouter';
import { HttpModule } from '@nestjs/axios';
import { GetRouteQueryHandler } from './core/application/queries/get-route/get-route.query-handler';
import { GetDetailedRouteController } from './interface/controllers/get-detailed-route.controller';
import { ConfigurationRepository } from '@mobicoop/configuration-module';
const queryHandlers: Provider[] = [GetRouteQueryHandler];
const mappers: Provider[] = [RouteMapper];
const adapters: Provider[] = [
{
provide: GEOGRAPHY_CONFIGURATION_REPOSITORY,
@@ -29,26 +17,11 @@ const adapters: Provider[] = [
provide: DIRECTION_ENCODER,
useClass: PostgresDirectionEncoder,
},
{
provide: GEOROUTER,
useClass: GraphhopperGeorouter,
},
{
provide: GEODESIC,
useClass: Geodesic,
},
GetBasicRouteController,
GetDetailedRouteController,
];
@Module({
imports: [CqrsModule, HttpModule],
providers: [...queryHandlers, ...mappers, ...adapters],
exports: [
RouteMapper,
DIRECTION_ENCODER,
GetBasicRouteController,
GetDetailedRouteController,
],
providers: [...adapters],
exports: [DIRECTION_ENCODER],
})
export class GeographyModule {}

View File

@@ -1,59 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Geodesic as Geolib, GeodesicClass } from 'geographiclib-geodesic';
import { GeodesicPort } from '../core/application/ports/geodesic.port';
@Injectable()
export class Geodesic implements GeodesicPort {
private geod: GeodesicClass;
constructor() {
this.geod = Geolib.WGS84;
}
inverse = (
lon1: number,
lat1: number,
lon2: number,
lat2: number,
): { azimuth: number; distance: number } => {
const { azi2: azimuth, s12: distance } = this.geod.Inverse(
lat1,
lon1,
lat2,
lon2,
);
if (!azimuth || !distance)
throw new Error(
`Inverse not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`,
);
return { azimuth, distance };
};
azimuth = (
lon1: number,
lat1: number,
lon2: number,
lat2: number,
): number => {
const { azi2: azimuth } = this.geod.Inverse(lat1, lon1, lat2, lon2);
if (!azimuth)
throw new Error(
`Azimuth not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`,
);
return azimuth;
};
distance = (
lon1: number,
lat1: number,
lon2: number,
lat2: number,
): number => {
const { s12: distance } = this.geod.Inverse(lat1, lon1, lat2, lon2);
if (!distance)
throw new Error(
`Distance not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`,
);
return distance;
};
}

View File

@@ -1,342 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { GeorouterPort } from '../core/application/ports/georouter.port';
import { GeorouterSettings } from '../core/application/types/georouter-settings.type';
import { Route, Step, Point } from '../core/domain/route.types';
import {
GEODESIC,
GEOGRAPHY_CONFIGURATION_REPOSITORY,
} from '../geography.di-tokens';
import { catchError, lastValueFrom, map } from 'rxjs';
import { AxiosError, AxiosResponse } from 'axios';
import {
GeorouterUnavailableException,
RouteNotFoundException,
} from '../core/domain/route.errors';
import { GeodesicPort } from '../core/application/ports/geodesic.port';
import {
ConfigurationDomain,
Configurator,
GetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
import {
GEOGRAPHY_CONFIG_GEOROUTER_URL,
GeographyConfig,
} from '../geography.constants';
@Injectable()
export class GraphhopperGeorouter implements GeorouterPort {
private url: string;
private urlArgs: string[];
constructor(
private readonly httpService: HttpService,
@Inject(GEOGRAPHY_CONFIGURATION_REPOSITORY)
private readonly configurationRepository: GetConfigurationRepositoryPort,
@Inject(GEODESIC) private readonly geodesic: GeodesicPort,
) {}
route = async (
waypoints: Point[],
settings: GeorouterSettings,
): Promise<Route> => {
const geographyConfigurator: Configurator =
await this.configurationRepository.mget(
ConfigurationDomain.GEOGRAPHY,
GeographyConfig,
);
this.url = [
geographyConfigurator.get<string>(GEOGRAPHY_CONFIG_GEOROUTER_URL),
'/route?',
].join('');
this._setDefaultUrlArgs();
this._setSettings(settings);
return this._getRoute(waypoints);
};
private _setDefaultUrlArgs = (): void => {
this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false'];
};
private _setSettings = (settings: GeorouterSettings): void => {
if (settings.detailedDuration) {
this.urlArgs.push('details=time');
}
if (settings.detailedDistance) {
this.urlArgs.push('instructions=true');
} else {
this.urlArgs.push('instructions=false');
}
if (!settings.points) {
this.urlArgs.push('calc_points=false');
}
};
private _getRoute = async (waypoints: Point[]): Promise<Route> => {
const url: string = [
this.getUrl(),
'&point=',
waypoints
.map((point: Point) => [point.lat, point.lon].join('%2C'))
.join('&point='),
].join('');
return await lastValueFrom(
this.httpService.get(url).pipe(
map((response) => {
if (response.data) return this.createRoute(response);
throw new Error();
}),
catchError((error: AxiosError) => {
if (error.code == AxiosError.ERR_BAD_REQUEST) {
throw new RouteNotFoundException(
error,
'No route found for given coordinates',
);
}
throw new GeorouterUnavailableException(error);
}),
),
);
};
private getUrl = (): string => [this.url, this.urlArgs.join('&')].join('');
private createRoute = (
response: AxiosResponse<GraphhopperResponse>,
): Route => {
const route = {} as Route;
if (response.data.paths && response.data.paths[0]) {
const shortestPath = response.data.paths[0];
route.distance = shortestPath.distance ?? 0;
route.duration = shortestPath.time ? shortestPath.time / 1000 : 0;
if (shortestPath.points && shortestPath.points.coordinates) {
route.points = shortestPath.points.coordinates.map((coordinate) => ({
lon: coordinate[0],
lat: coordinate[1],
}));
const inverse = this.geodesic.inverse(
route.points[0].lon,
route.points[0].lat,
route.points[route.points.length - 1].lon,
route.points[route.points.length - 1].lat,
);
route.fwdAzimuth =
inverse.azimuth >= 0
? inverse.azimuth
: 360 - Math.abs(inverse.azimuth);
route.backAzimuth =
route.fwdAzimuth > 180
? route.fwdAzimuth - 180
: route.fwdAzimuth + 180;
route.distanceAzimuth = inverse.distance;
if (
shortestPath.details &&
shortestPath.details.time &&
shortestPath.snapped_waypoints &&
shortestPath.snapped_waypoints.coordinates
) {
let instructions: GraphhopperInstruction[] = [];
if (shortestPath.instructions)
instructions = shortestPath.instructions;
route.steps = this.generateSteps(
shortestPath.points.coordinates,
shortestPath.snapped_waypoints.coordinates,
shortestPath.details.time,
instructions,
);
}
}
}
return route;
};
private generateSteps = (
points: [[number, number]],
snappedWaypoints: [[number, number]],
durations: [[number, number, number]],
instructions: GraphhopperInstruction[],
): Step[] => {
const indices = this.getIndices(points, snappedWaypoints);
const times = this.getTimes(durations, indices);
const distances = this.getDistances(instructions, indices);
return indices.map((index) => {
const duration = times.find((time) => time.index == index);
if (!duration)
throw new Error(`Duration not found for waypoint #${index}`);
const distance = distances.find((distance) => distance.index == index);
if (!distance && instructions.length > 0)
throw new Error(`Distance not found for waypoint #${index}`);
return {
lon: points[index][1],
lat: points[index][0],
distance: distance?.distance,
duration: duration.duration,
};
});
};
private getIndices = (
points: [[number, number]],
snappedWaypoints: [[number, number]],
): number[] => {
const indices: number[] = snappedWaypoints.map(
(waypoint: [number, number]) =>
points.findIndex(
(point) => point[0] == waypoint[0] && point[1] == waypoint[1],
),
);
if (indices.find((index: number) => index == -1) === undefined)
return indices;
const missedWaypoints = indices
.map(
(value, index) =>
<
{
index: number;
originIndex: number;
waypoint: number[];
nearest?: number;
distance: number;
}
>{
index: value,
originIndex: index,
waypoint: snappedWaypoints[index],
nearest: undefined,
distance: 999999999,
},
)
.filter((element) => element.index == -1);
for (const index in points) {
for (const missedWaypoint of missedWaypoints) {
const distance = this.geodesic.distance(
missedWaypoint.waypoint[0],
missedWaypoint.waypoint[1],
points[index][0],
points[index][1],
);
if (distance < missedWaypoint.distance) {
missedWaypoint.distance = distance;
missedWaypoint.nearest = parseInt(index);
}
}
}
for (const missedWaypoint of missedWaypoints) {
indices[missedWaypoint.originIndex] = missedWaypoint.nearest as number;
}
return indices;
};
private getTimes = (
durations: [[number, number, number]],
indices: number[],
): Array<{ index: number; duration: number }> => {
const times: Array<{ index: number; duration: number }> = [];
let duration = 0;
for (const [origin, destination, stepDuration] of durations) {
let indexFound = false;
const indexAsOrigin = indices.find((index) => index == origin);
if (
indexAsOrigin !== undefined &&
times.find((time) => origin == time.index) == undefined
) {
times.push({
index: indexAsOrigin,
duration: Math.round(stepDuration / 1000),
});
indexFound = true;
}
if (!indexFound) {
const indexAsDestination = indices.find(
(index) => index == destination,
);
if (
indexAsDestination !== undefined &&
times.find((time) => destination == time.index) == undefined
) {
times.push({
index: indexAsDestination,
duration: Math.round((duration + stepDuration) / 1000),
});
indexFound = true;
}
}
if (!indexFound) {
const indexInBetween = indices.find(
(index) => origin < index && index < destination,
);
if (indexInBetween !== undefined) {
times.push({
index: indexInBetween,
duration: Math.round((duration + stepDuration / 2) / 1000),
});
}
}
duration += stepDuration;
}
return times;
};
private getDistances = (
instructions: GraphhopperInstruction[],
indices: number[],
): Array<{ index: number; distance: number }> => {
let distance = 0;
const distances: Array<{ index: number; distance: number }> = [
{
index: 0,
distance,
},
];
for (const instruction of instructions) {
distance += instruction.distance;
if (
(instruction.sign == GraphhopperSign.SIGN_WAYPOINT ||
instruction.sign == GraphhopperSign.SIGN_FINISH) &&
indices.find((index) => index == instruction.interval[0]) !== undefined
) {
distances.push({
index: instruction.interval[0],
distance: Math.round(distance),
});
}
}
return distances;
};
}
type GraphhopperResponse = {
paths: [
{
distance: number;
weight: number;
time: number;
points_encoded: boolean;
bbox: number[];
points: GraphhopperCoordinates;
snapped_waypoints: GraphhopperCoordinates;
details: {
time: [[number, number, number]];
};
instructions: GraphhopperInstruction[];
},
];
};
type GraphhopperCoordinates = {
coordinates: [[number, number]];
};
type GraphhopperInstruction = {
distance: number;
heading: number;
sign: GraphhopperSign;
interval: [number, number];
text: string;
};
enum GraphhopperSign {
SIGN_START = 0,
SIGN_FINISH = 4,
SIGN_WAYPOINT = 5,
}

View File

@@ -1,5 +0,0 @@
import { Point } from '@modules/geography/core/domain/route.types';
export type GetRouteRequestDto = {
waypoints: Point[];
};

View File

@@ -1,23 +0,0 @@
import { QueryBus } from '@nestjs/cqrs';
import { RouteResponseDto } from '../dtos/route.response.dto';
import { GetRouteRequestDto } from './dtos/get-route.request.dto';
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query';
import { RouteMapper } from '@modules/geography/route.mapper';
import { Controller } from '@nestjs/common';
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
@Controller()
export class GetBasicRouteController implements GetRouteControllerPort {
constructor(
private readonly queryBus: QueryBus,
private readonly mapper: RouteMapper,
) {}
async get(data: GetRouteRequestDto): Promise<RouteResponseDto> {
const route: RouteEntity = await this.queryBus.execute(
new GetRouteQuery(data.waypoints),
);
return this.mapper.toResponse(route);
}
}

View File

@@ -1,27 +0,0 @@
import { QueryBus } from '@nestjs/cqrs';
import { RouteResponseDto } from '../dtos/route.response.dto';
import { GetRouteRequestDto } from './dtos/get-route.request.dto';
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query';
import { RouteMapper } from '@modules/geography/route.mapper';
import { Controller } from '@nestjs/common';
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
@Controller()
export class GetDetailedRouteController implements GetRouteControllerPort {
constructor(
private readonly queryBus: QueryBus,
private readonly mapper: RouteMapper,
) {}
async get(data: GetRouteRequestDto): Promise<RouteResponseDto> {
const route: RouteEntity = await this.queryBus.execute(
new GetRouteQuery(data.waypoints, {
detailedDistance: true,
detailedDuration: true,
points: true,
}),
);
return this.mapper.toResponse(route);
}
}

View File

@@ -1,11 +0,0 @@
import { Point, Step } from '@modules/geography/core/domain/route.types';
export class RouteResponseDto {
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
points: Point[];
steps?: Step[];
}

View File

@@ -1,28 +0,0 @@
import { Mapper } from '@mobicoop/ddd-library';
import { Injectable } from '@nestjs/common';
import { RouteEntity } from './core/domain/route.entity';
import { RouteResponseDto } from './interface/dtos/route.response.dto';
/**
* Mapper constructs objects that are used in different layers:
* Record is an object that is stored in a database,
* Entity is an object that is used in application domain layer,
* and a ResponseDTO is an object returned to a user (usually as json).
*/
@Injectable()
export class RouteMapper
implements Mapper<RouteEntity, undefined, undefined, RouteResponseDto>
{
toResponse = (entity: RouteEntity): RouteResponseDto => {
const response = new RouteResponseDto();
response.distance = Math.round(entity.getProps().distance);
response.duration = Math.round(entity.getProps().duration);
response.fwdAzimuth = Math.round(entity.getProps().fwdAzimuth);
response.backAzimuth = Math.round(entity.getProps().backAzimuth);
response.distanceAzimuth = Math.round(entity.getProps().distanceAzimuth);
response.points = entity.getProps().points;
response.steps = entity.getProps().steps;
return response;
};
}

View File

@@ -1,61 +0,0 @@
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query';
import { GetRouteQueryHandler } from '@modules/geography/core/application/queries/get-route/get-route.query-handler';
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { Point } from '@modules/geography/core/domain/route.types';
import { GEOROUTER } from '@modules/geography/geography.di-tokens';
import { Test, TestingModule } from '@nestjs/testing';
const originWaypoint: Point = {
lat: 48.689445,
lon: 6.17651,
};
const destinationWaypoint: Point = {
lat: 48.8566,
lon: 2.3522,
};
const mockGeorouter: GeorouterPort = {
route: jest.fn(),
};
describe('Get route query handler', () => {
let getRoutequeryHandler: GetRouteQueryHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: GEOROUTER,
useValue: mockGeorouter,
},
GetRouteQueryHandler,
],
}).compile();
getRoutequeryHandler =
module.get<GetRouteQueryHandler>(GetRouteQueryHandler);
});
it('should be defined', () => {
expect(getRoutequeryHandler).toBeDefined();
});
describe('execution', () => {
it('should get a route', async () => {
const getRoutequery = new GetRouteQuery(
[originWaypoint, destinationWaypoint],
{
detailedDistance: false,
detailedDuration: false,
points: true,
},
);
RouteEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});
const result = await getRoutequeryHandler.execute(getRoutequery);
expect(result.id).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
});
});
});

View File

@@ -1,41 +0,0 @@
import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library';
import { Point } from '@modules/geography/core/domain/value-objects/point.value-object';
describe('Point value object', () => {
it('should create a point value object', () => {
const pointVO = new Point({
lat: 48.689445,
lon: 6.17651,
});
expect(pointVO.lat).toBe(48.689445);
expect(pointVO.lon).toBe(6.17651);
});
it('should throw an exception if longitude is invalid', () => {
expect(() => {
new Point({
lat: 48.689445,
lon: 186.17651,
});
}).toThrow(ArgumentOutOfRangeException);
expect(() => {
new Point({
lat: 48.689445,
lon: -186.17651,
});
}).toThrow(ArgumentOutOfRangeException);
});
it('should throw an exception if latitude is invalid', () => {
expect(() => {
new Point({
lat: 148.689445,
lon: 6.17651,
});
}).toThrow(ArgumentOutOfRangeException);
expect(() => {
new Point({
lat: -148.689445,
lon: 6.17651,
});
}).toThrow(ArgumentOutOfRangeException);
});
});

View File

@@ -1,70 +0,0 @@
import { GeorouterPort } from '@modules/geography/core/application/ports/georouter.port';
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { RouteNotFoundException } from '@modules/geography/core/domain/route.errors';
import {
Point,
CreateRouteProps,
} from '@modules/geography/core/domain/route.types';
const originPoint: Point = {
lat: 48.689445,
lon: 6.17651,
};
const destinationPoint: Point = {
lat: 48.8566,
lon: 2.3522,
};
const mockGeorouter: GeorouterPort = {
route: jest
.fn()
.mockImplementationOnce(() => ({
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,
},
],
steps: [],
}))
.mockImplementationOnce(() => []),
};
const createRouteProps: CreateRouteProps = {
waypoints: [originPoint, destinationPoint],
georouter: mockGeorouter,
georouterSettings: {
points: true,
detailedDistance: false,
detailedDuration: false,
},
};
describe('Route entity create', () => {
it('should create a new entity', async () => {
const route: RouteEntity = await RouteEntity.create(createRouteProps);
expect(route.id.length).toBe(36);
expect(route.getProps().duration).toBe(14422);
});
it('should throw an exception if route is not found', async () => {
try {
await RouteEntity.create(createRouteProps);
} catch (e: any) {
expect(e).toBeInstanceOf(RouteNotFoundException);
}
});
});

View File

@@ -1,76 +0,0 @@
import {
ArgumentInvalidException,
ArgumentOutOfRangeException,
} from '@mobicoop/ddd-library';
import { Step } from '@modules/geography/core/domain/value-objects/step.value-object';
describe('Step value object', () => {
it('should create a step value object', () => {
const stepVO = new Step({
lat: 48.689445,
lon: 6.17651,
duration: 150,
distance: 12000,
});
expect(stepVO.duration).toBe(150);
expect(stepVO.distance).toBe(12000);
expect(stepVO.lat).toBe(48.689445);
expect(stepVO.lon).toBe(6.17651);
});
it('should throw an exception if longitude is invalid', () => {
expect(() => {
new Step({
lat: 48.689445,
lon: 186.17651,
duration: 150,
distance: 12000,
});
}).toThrow(ArgumentOutOfRangeException);
expect(() => {
new Step({
lat: 48.689445,
lon: -186.17651,
duration: 150,
distance: 12000,
});
}).toThrow(ArgumentOutOfRangeException);
});
it('should throw an exception if latitude is invalid', () => {
expect(() => {
new Step({
lat: 248.689445,
lon: 6.17651,
duration: 150,
distance: 12000,
});
}).toThrow(ArgumentOutOfRangeException);
expect(() => {
new Step({
lat: -148.689445,
lon: 6.17651,
duration: 150,
distance: 12000,
});
}).toThrow(ArgumentOutOfRangeException);
});
it('should throw an exception if distance is invalid', () => {
expect(() => {
new Step({
lat: 48.689445,
lon: 6.17651,
duration: 150,
distance: -12000,
});
}).toThrow(ArgumentInvalidException);
});
it('should throw an exception if duration is invalid', () => {
expect(() => {
new Step({
lat: 48.689445,
lon: 6.17651,
duration: -150,
distance: 12000,
});
}).toThrow(ArgumentInvalidException);
});
});

View File

@@ -1,36 +0,0 @@
import { Geodesic } from '@modules/geography/infrastructure/geodesic';
describe('Matcher geodesic', () => {
it('should be defined', () => {
const geodesic: Geodesic = new Geodesic();
expect(geodesic).toBeDefined();
});
it('should get inverse values', () => {
const geodesic: Geodesic = new Geodesic();
const inv = geodesic.inverse(0, 0, 1, 1);
expect(Math.round(inv.azimuth as number)).toBe(45);
expect(Math.round(inv.distance as number)).toBe(156900);
});
it('should get azimuth value', () => {
const geodesic: Geodesic = new Geodesic();
const azimuth = geodesic.azimuth(0, 0, 1, 1);
expect(Math.round(azimuth as number)).toBe(45);
});
it('should get distance value', () => {
const geodesic: Geodesic = new Geodesic();
const distance = geodesic.distance(0, 0, 1, 1);
expect(Math.round(distance as number)).toBe(156900);
});
it('should throw an exception if inverse fails', () => {
const geodesic: Geodesic = new Geodesic();
expect(() => {
geodesic.inverse(7.74547, 48.583035, 7.74547, 48.583036);
}).toThrow();
});
it('should throw an exception if azimuth fails', () => {
const geodesic: Geodesic = new Geodesic();
expect(() => {
geodesic.azimuth(7.74547, 48.583035, 7.74547, 48.583036);
}).toThrow();
});
});

View File

@@ -1,508 +0,0 @@
import {
ConfigurationDomain,
ConfigurationDomainGet,
Configurator,
GetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
import { GeodesicPort } from '@modules/geography/core/application/ports/geodesic.port';
import {
GeorouterUnavailableException,
RouteNotFoundException,
} from '@modules/geography/core/domain/route.errors';
import { Route, Step } from '@modules/geography/core/domain/route.types';
import { GEOGRAPHY_CONFIG_GEOROUTER_URL } from '@modules/geography/geography.constants';
import {
GEODESIC,
GEOGRAPHY_CONFIGURATION_REPOSITORY,
} from '@modules/geography/geography.di-tokens';
import { GraphhopperGeorouter } from '@modules/geography/infrastructure/graphhopper-georouter';
import { HttpService } from '@nestjs/axios';
import { Test, TestingModule } from '@nestjs/testing';
import { AxiosError } from 'axios';
import { of, throwError } from 'rxjs';
const mockHttpService = {
get: jest
.fn()
.mockImplementationOnce(() => {
return throwError(
() => new AxiosError('Axios error', AxiosError.ERR_BAD_REQUEST),
);
})
.mockImplementationOnce(() => {
return throwError(() => 'Router unavailable');
})
.mockImplementationOnce(() => {
return of({
status: 200,
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 5, 180000],
[5, 6, 180000],
[6, 7, 180000],
[7, 9, 360000],
[9, 10, 180000],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[5, 5],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 7, 540000],
[7, 9, 360000],
[9, 10, 180000],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[5, 5],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 7, 540000],
[7, 9, 360000],
[9, 10, 180000],
],
},
instructions: [
{
distance: 25000,
sign: 0,
interval: [0, 5],
text: 'Some instructions',
time: 900000,
},
{
distance: 0,
sign: 5,
interval: [5, 5],
text: 'Waypoint 1',
time: 0,
},
{
distance: 25000,
sign: 2,
interval: [5, 10],
text: 'Some instructions',
time: 900000,
},
{
distance: 0.0,
sign: 4,
interval: [10, 10],
text: 'Arrive at destination',
time: 0,
},
],
},
],
},
});
}),
};
const mockGeodesic: GeodesicPort = {
inverse: jest.fn().mockImplementation(() => ({
azimuth: 45,
distance: 50000,
})),
azimuth: jest.fn().mockImplementation(() => 45),
distance: jest.fn().mockImplementation(() => 50000),
};
const mockConfigurationRepository: GetConfigurationRepositoryPort = {
get: jest.fn(),
mget: jest.fn().mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(domain: ConfigurationDomain, configs: ConfigurationDomainGet[]) => {
switch (domain) {
case ConfigurationDomain.GEOGRAPHY:
return new Configurator(ConfigurationDomain.GEOGRAPHY, [
{
domain: ConfigurationDomain.GEOGRAPHY,
key: GEOGRAPHY_CONFIG_GEOROUTER_URL,
value: 'http://localhost:8989',
},
]);
}
},
),
};
describe('Graphhopper Georouter', () => {
let graphhopperGeorouter: GraphhopperGeorouter;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
GraphhopperGeorouter,
{
provide: HttpService,
useValue: mockHttpService,
},
{
provide: GEOGRAPHY_CONFIGURATION_REPOSITORY,
useValue: mockConfigurationRepository,
},
{
provide: GEODESIC,
useValue: mockGeodesic,
},
],
}).compile();
graphhopperGeorouter =
module.get<GraphhopperGeorouter>(GraphhopperGeorouter);
});
it('should be defined', () => {
expect(graphhopperGeorouter).toBeDefined();
});
it('should fail if route is not found', async () => {
await expect(
graphhopperGeorouter.route(
[
{
lon: 0,
lat: 0,
},
{
lon: 1,
lat: 1,
},
],
{
detailedDistance: false,
detailedDuration: false,
points: false,
},
),
).rejects.toBeInstanceOf(RouteNotFoundException);
});
it('should fail if georouter is unavailable', async () => {
await expect(
graphhopperGeorouter.route(
[
{
lon: 0,
lat: 0,
},
{
lon: 1,
lat: 1,
},
],
{
detailedDistance: false,
detailedDuration: false,
points: false,
},
),
).rejects.toBeInstanceOf(GeorouterUnavailableException);
});
it('should fail if georouter response is corrupted', async () => {
await expect(
graphhopperGeorouter.route(
[
{
lon: 0,
lat: 0,
},
{
lon: 1,
lat: 1,
},
],
{
detailedDistance: false,
detailedDuration: false,
points: false,
},
),
).rejects.toBeInstanceOf(GeorouterUnavailableException);
});
it('should create a basic route', async () => {
const route: Route = await graphhopperGeorouter.route(
[
{
lon: 0,
lat: 0,
},
{
lon: 10,
lat: 10,
},
],
{
detailedDistance: false,
detailedDuration: false,
points: false,
},
);
expect(route.distance).toBe(50000);
});
it('should create a route with points', async () => {
const route: Route = await graphhopperGeorouter.route(
[
{
lon: 0,
lat: 0,
},
{
lon: 10,
lat: 10,
},
],
{
detailedDistance: false,
detailedDuration: false,
points: true,
},
);
expect(route.distance).toBe(50000);
expect(route.duration).toBe(1800);
expect(route.fwdAzimuth).toBe(45);
expect(route.backAzimuth).toBe(225);
expect(route.points).toHaveLength(11);
});
it('should create a route with points and time', async () => {
const route: Route = await graphhopperGeorouter.route(
[
{
lon: 0,
lat: 0,
},
{
lon: 10,
lat: 10,
},
],
{
detailedDistance: false,
detailedDuration: true,
points: true,
},
);
expect(route.steps).toHaveLength(2);
expect((route.steps as Step[])[1].duration).toBe(1800);
expect((route.steps as Step[])[1].distance).toBeUndefined();
});
it('should create one route with points and missed waypoints extrapolations', async () => {
const route: Route = await graphhopperGeorouter.route(
[
{
lon: 0,
lat: 0,
},
{
lon: 5,
lat: 5,
},
{
lon: 10,
lat: 10,
},
],
{
detailedDistance: false,
detailedDuration: true,
points: true,
},
);
expect(route.steps).toHaveLength(3);
expect(route.distance).toBe(50000);
expect(route.duration).toBe(1800);
expect(route.fwdAzimuth).toBe(45);
expect(route.backAzimuth).toBe(225);
expect(route.points.length).toBe(9);
});
it('should create a route with points, time and distance', async () => {
const route: Route = await graphhopperGeorouter.route(
[
{
lon: 0,
lat: 0,
},
{
lon: 10,
lat: 10,
},
],
{
detailedDistance: true,
detailedDuration: true,
points: true,
},
);
expect(route.steps).toHaveLength(3);
expect((route.steps as Step[])[1].duration).toBe(990);
expect((route.steps as Step[])[1].distance).toBe(25000);
});
});

View File

@@ -1,63 +0,0 @@
import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller';
import { RouteMapper } from '@modules/geography/route.mapper';
import { QueryBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
const mockQueryBus = {
execute: jest.fn(),
};
const mockRouteMapper = {
toPersistence: jest.fn(),
toDomain: jest.fn(),
toResponse: jest.fn(),
};
describe('Get Basic Route Controller', () => {
let getBasicRouteController: GetBasicRouteController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: QueryBus,
useValue: mockQueryBus,
},
{
provide: RouteMapper,
useValue: mockRouteMapper,
},
GetBasicRouteController,
],
}).compile();
getBasicRouteController = module.get<GetBasicRouteController>(
GetBasicRouteController,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(getBasicRouteController).toBeDefined();
});
it('should get a route', async () => {
jest.spyOn(mockQueryBus, 'execute');
await getBasicRouteController.get({
waypoints: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
});
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,63 +0,0 @@
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller';
import { RouteMapper } from '@modules/geography/route.mapper';
import { QueryBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
const mockQueryBus = {
execute: jest.fn(),
};
const mockRouteMapper = {
toPersistence: jest.fn(),
toDomain: jest.fn(),
toResponse: jest.fn(),
};
describe('Get Detailed Route Controller', () => {
let getDetailedRouteController: GetDetailedRouteController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: QueryBus,
useValue: mockQueryBus,
},
{
provide: RouteMapper,
useValue: mockRouteMapper,
},
GetDetailedRouteController,
],
}).compile();
getDetailedRouteController = module.get<GetDetailedRouteController>(
GetDetailedRouteController,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(getDetailedRouteController).toBeDefined();
});
it('should get a route', async () => {
jest.spyOn(mockQueryBus, 'execute');
await getDetailedRouteController.get({
waypoints: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
});
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,45 +0,0 @@
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
import { RouteMapper } from '@modules/geography/route.mapper';
import { Test } from '@nestjs/testing';
describe('Route Mapper', () => {
let routeMapper: RouteMapper;
beforeAll(async () => {
const module = await Test.createTestingModule({
providers: [RouteMapper],
}).compile();
routeMapper = module.get<RouteMapper>(RouteMapper);
});
it('should be defined', () => {
expect(routeMapper).toBeDefined();
});
it('should map domain entity to response', async () => {
const now = new Date();
const routeEntity: RouteEntity = new RouteEntity({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
createdAt: now,
updatedAt: now,
props: {
distance: 23000,
duration: 900,
fwdAzimuth: 283,
backAzimuth: 93,
distanceAzimuth: 19840,
points: [
{
lon: 6.1765103,
lat: 48.689446,
},
{
lon: 2.3523,
lat: 48.8567,
},
],
},
});
expect(routeMapper.toResponse(routeEntity).distance).toBe(23000);
});
});

View File

@@ -10,12 +10,6 @@ import {
AD_CREATED_MESSAGE_HANDLER,
AD_CREATED_QUEUE,
AD_CREATED_ROUTING_KEY,
AD_DELETED_MESSAGE_HANDLER,
AD_DELETED_QUEUE,
AD_DELETED_ROUTING_KEY,
AD_UPDATED_MESSAGE_HANDLER,
AD_UPDATED_QUEUE,
AD_UPDATED_ROUTING_KEY,
SERVICE_NAME,
} from '@src/app.constants';
@@ -39,14 +33,6 @@ const imports = [
routingKey: AD_CREATED_ROUTING_KEY,
queue: AD_CREATED_QUEUE,
},
[AD_UPDATED_MESSAGE_HANDLER]: {
routingKey: AD_UPDATED_ROUTING_KEY,
queue: AD_UPDATED_QUEUE,
},
[AD_DELETED_MESSAGE_HANDLER]: {
routingKey: AD_DELETED_ROUTING_KEY,
queue: AD_DELETED_QUEUE,
},
},
}),
}),

View File

@@ -20,7 +20,7 @@
"paths": {
"@libs/*": ["src/libs/*"],
"@modules/*": ["src/modules/*"],
"@src/*": ["src/*"]
}
}
"@src/*": ["src/*"],
},
},
}