42 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
Sylvain Briat
fef0779aaa 1.4.2 2023-11-06 17:27:56 +01:00
Sylvain Briat
fb3f1cf4df updateProto 2023-11-06 17:27:49 +01:00
Sylvain Briat
703865dc38 Merge branch 'fixReadme' into 'main'
Fix readme

See merge request v3/service/matcher!19
2023-11-06 09:10:22 +00:00
Sylvain Briat
75bb82094d 1.4.1 2023-11-06 10:05:24 +01:00
Sylvain Briat
8d2964659b remove match id from doc 2023-11-06 10:05:10 +01:00
Sylvain Briat
8eb1b51457 Merge branch 'removeMatchId' into 'main'
Remove useless match id from matching in proto file

See merge request v3/service/matcher!18
2023-11-06 08:59:54 +00:00
90 changed files with 3477 additions and 3983 deletions

View File

@@ -8,7 +8,7 @@ HEALTH_SERVICE_PORT=6005
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher" DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"
# MESSAGE BROKER # 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=mobicoop
MESSAGE_BROKER_EXCHANGE_DURABILITY=true 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 dist
coverage coverage
.prettierrc.json .prettierrc.json
.gitlab

View File

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

View File

@@ -181,7 +181,6 @@ If the matching is successful, you will get a result, containing :
- **page**: the number of the page that is returned (may be different than the number of the required page, if number of results does not match with perPage parameter) - **page**: the number of the page that is returned (may be different than the number of the required page, if number of results does not match with perPage parameter)
- **perPage**: the number of results per page (as it may not be specified in the request) - **perPage**: the number of results per page (as it may not be specified in the request)
- **data**: an array of the results themselves, each including: - **data**: an array of the results themselves, each including:
- **id**: an id for the result
- **adId**: the id of the ad that matches - **adId**: the id of the ad that matches
- **role**: the role of the ad owner in that match - **role**: the role of the ad owner in that match
- **distance**: the distance in metres of the resulting carpool - **distance**: the distance in metres of the resulting carpool

View File

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

View File

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

View File

@@ -3,24 +3,18 @@ export const SERVICE_NAME = 'matcher';
// grpc // grpc
export const GRPC_PACKAGE_NAME = 'matcher'; 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_MESSAGE_HANDLER = 'adCreated';
export const AD_CREATED_ROUTING_KEY = 'ad.created'; export const AD_CREATED_ROUTING_KEY = 'ad.created';
export const AD_CREATED_QUEUE = 'matcher-ad-created'; export const AD_CREATED_QUEUE = 'matcher.ad.created';
export const AD_UPDATED_MESSAGE_HANDLER = 'adUpdated';
export const AD_UPDATED_ROUTING_KEY = 'ad.updated';
export const AD_UPDATED_QUEUE = 'matcher-ad-updated';
export const AD_DELETED_MESSAGE_HANDLER = 'adDeleted';
export const AD_DELETED_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';
// health // health
export const GRPC_HEALTH_PACKAGE_NAME = '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 { HealthModuleOptions } from '@mobicoop/health-module/dist/core/domain/types/health.types';
import { MessagePublisherPort } from '@mobicoop/ddd-library'; import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { GeographyModule } from '@modules/geography/geography.module'; import { GeographyModule } from '@modules/geography/geography.module';
import producerServicesConfig from './config/producer-services.config';
import { import {
HEALTH_AD_REPOSITORY, HEALTH_AD_REPOSITORY,
HEALTH_CRITICAL_LOGGING_KEY, HEALTH_CRITICAL_LOGGING_KEY,
@@ -18,7 +19,7 @@ import {
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true, load: [producerServicesConfig] }),
EventEmitterModule.forRoot(), EventEmitterModule.forRoot(),
RequestContextModule, RequestContextModule,
HealthModule.forRootAsync({ 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 { import { KeyType, Type } from '@mobicoop/configuration-module';
ConfigurationDomainGet,
ConfigurationType,
} from '@mobicoop/configuration-module';
export const CARPOOL_CONFIG_ROLE = 'role'; export const CARPOOL_CONFIG_ROLE = 'role';
export const CARPOOL_CONFIG_SEATS_PROPOSED = 'seatsProposed'; 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_DEPARTURE_TIME_MARGIN = 'departureTimeMargin';
export const CARPOOL_CONFIG_STRICT_FREQUENCY = 'strictFrequency'; export const CARPOOL_CONFIG_STRICT_FREQUENCY = 'strictFrequency';
export const CarpoolConfig: ConfigurationDomainGet[] = [ export const CarpoolKeyTypes: KeyType[] = [
{ {
key: CARPOOL_CONFIG_ROLE, key: CARPOOL_CONFIG_ROLE,
type: ConfigurationType.STRING, type: Type.STRING,
}, },
{ {
key: CARPOOL_CONFIG_SEATS_PROPOSED, key: CARPOOL_CONFIG_SEATS_PROPOSED,
type: ConfigurationType.INT, type: Type.INT,
}, },
{ {
key: CARPOOL_CONFIG_SEATS_REQUESTED, key: CARPOOL_CONFIG_SEATS_REQUESTED,
type: ConfigurationType.INT, type: Type.INT,
}, },
{ {
key: CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN, key: CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN,
type: ConfigurationType.INT, type: Type.INT,
}, },
{ {
key: CARPOOL_CONFIG_STRICT_FREQUENCY, 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( export const AD_CONFIGURATION_REPOSITORY = Symbol(
'AD_CONFIGURATION_REPOSITORY', '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_REPOSITORY,
AD_DIRECTION_ENCODER, AD_DIRECTION_ENCODER,
AD_ROUTE_PROVIDER, AD_ROUTE_PROVIDER,
AD_GET_BASIC_ROUTE_CONTROLLER,
TIMEZONE_FINDER, TIMEZONE_FINDER,
TIME_CONVERTER, TIME_CONVERTER,
INPUT_DATETIME_TRANSFORMER, INPUT_DATETIME_TRANSFORMER,
AD_GET_DETAILED_ROUTE_CONTROLLER,
OUTPUT_DATETIME_TRANSFORMER, OUTPUT_DATETIME_TRANSFORMER,
MATCHING_REPOSITORY, MATCHING_REPOSITORY,
AD_CONFIGURATION_REPOSITORY, AD_CONFIGURATION_REPOSITORY,
GEOGRAPHY_PACKAGE,
GEOGRAPHY_SERVICE,
} from './ad.di-tokens'; } from './ad.di-tokens';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { AdRepository } from './infrastructure/ad.repository'; import { AdRepository } from './infrastructure/ad.repository';
@@ -20,8 +20,6 @@ import { PrismaService } from './infrastructure/prisma.service';
import { AdMapper } from './ad.mapper'; import { AdMapper } from './ad.mapper';
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler'; import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder'; 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 { GeographyModule } from '@modules/geography/geography.module';
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service'; import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller'; 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 { TimezoneFinder } from './infrastructure/timezone-finder';
import { TimeConverter } from './infrastructure/time-converter'; import { TimeConverter } from './infrastructure/time-converter';
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller';
import { MatchMapper } from './match.mapper'; import { MatchMapper } from './match.mapper';
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
import { MatchingRepository } from './infrastructure/matching.repository'; import { MatchingRepository } from './infrastructure/matching.repository';
@@ -43,9 +40,45 @@ import {
RedisModuleOptions, RedisModuleOptions,
} from '@songkeys/nestjs-redis'; } from '@songkeys/nestjs-redis';
import { ConfigurationRepository } from '@mobicoop/configuration-module'; 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 = [ const imports = [
CqrsModule, 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>({ CacheModule.registerAsync<RedisClientOptions>({
imports: [ConfigModule], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
@@ -80,6 +113,10 @@ const grpcControllers = [MatchGrpcController];
const messageHandlers = [AdCreatedMessageHandler]; const messageHandlers = [AdCreatedMessageHandler];
const eventHandlers: Provider[] = [
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
];
const commandHandlers: Provider[] = [CreateAdService]; const commandHandlers: Provider[] = [CreateAdService];
const queryHandlers: Provider[] = [MatchQueryHandler]; const queryHandlers: Provider[] = [MatchQueryHandler];
@@ -117,15 +154,7 @@ const adapters: Provider[] = [
}, },
{ {
provide: AD_ROUTE_PROVIDER, provide: AD_ROUTE_PROVIDER,
useClass: RouteProvider, useClass: Georouter,
},
{
provide: AD_GET_BASIC_ROUTE_CONTROLLER,
useClass: GetBasicRouteController,
},
{
provide: AD_GET_DETAILED_ROUTE_CONTROLLER,
useClass: GetDetailedRouteController,
}, },
{ {
provide: TIMEZONE_FINDER, provide: TIMEZONE_FINDER,
@@ -150,6 +179,7 @@ const adapters: Provider[] = [
controllers: [...grpcControllers], controllers: [...grpcControllers],
providers: [ providers: [
...messageHandlers, ...messageHandlers,
...eventHandlers,
...commandHandlers, ...commandHandlers,
...queryHandlers, ...queryHandlers,
...mappers, ...mappers,

View File

@@ -1,12 +1,19 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateAdCommand } from './create-ad.command'; import { CreateAdCommand } from './create-ad.command';
import { Inject } from '@nestjs/common'; 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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { AdRepositoryPort } from '../../ports/ad.repository.port'; 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 { 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 { Role } from '@modules/ad/core/domain/ad.types';
import { import {
Path, Path,
@@ -17,14 +24,19 @@ import {
import { Waypoint } from '../../types/waypoint.type'; import { Waypoint } from '../../types/waypoint.type';
import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects/point.value-object'; import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects/point.value-object';
import { Point } from '@modules/geography/core/domain/route.types'; 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) @CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler { export class CreateAdService implements ICommandHandler {
constructor( constructor(
@Inject(AD_MESSAGE_PUBLISHER)
private readonly messagePublisher: MessagePublisherPort,
@Inject(AD_REPOSITORY) @Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort, private readonly repository: AdRepositoryPort,
@Inject(AD_ROUTE_PROVIDER) @Inject(AD_ROUTE_PROVIDER)
private readonly routeProvider: RouteProviderPort, private readonly routeProvider: GeorouterPort,
) {} ) {}
async execute(command: CreateAdCommand): Promise<AggregateID> { async execute(command: CreateAdCommand): Promise<AggregateID> {
@@ -44,17 +56,6 @@ export class CreateAdService implements ICommandHandler {
); );
let typedRoutes: TypedRoute[]; 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 driverDistance: number | undefined;
let driverDuration: number | undefined; let driverDuration: number | undefined;
let passengerDistance: number | undefined; let passengerDistance: number | undefined;
@@ -62,6 +63,21 @@ export class CreateAdService implements ICommandHandler {
let points: PointValueObject[] | undefined; let points: PointValueObject[] | undefined;
let fwdAzimuth: number | undefined; let fwdAzimuth: number | undefined;
let backAzimuth: 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 { try {
typedRoutes.forEach((typedRoute: TypedRoute) => { typedRoutes.forEach((typedRoute: TypedRoute) => {
if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) { if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) {
@@ -77,7 +93,9 @@ export class CreateAdService implements ICommandHandler {
fwdAzimuth = typedRoute.route.fwdAzimuth; fwdAzimuth = typedRoute.route.fwdAzimuth;
backAzimuth = typedRoute.route.backAzimuth; backAzimuth = typedRoute.route.backAzimuth;
} }
if ([PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)) { if (
[PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)
) {
passengerDistance = typedRoute.route.distance; passengerDistance = typedRoute.route.distance;
passengerDuration = typedRoute.route.duration; passengerDuration = typedRoute.route.duration;
if (!points) if (!points)
@@ -95,6 +113,7 @@ export class CreateAdService implements ICommandHandler {
} catch (error: any) { } catch (error: any) {
throw new Error('Invalid route'); throw new Error('Invalid route');
} }
const ad = AdEntity.create({ const ad = AdEntity.create({
id: command.id, id: command.id,
driver: command.driver, driver: command.driver,
@@ -115,6 +134,7 @@ export class CreateAdService implements ICommandHandler {
fwdAzimuth: fwdAzimuth as number, fwdAzimuth: fwdAzimuth as number,
backAzimuth: backAzimuth as number, backAzimuth: backAzimuth as number,
}); });
try { try {
await this.repository.insertExtra(ad, 'ad'); await this.repository.insertExtra(ad, 'ad');
return ad.id; return ad.id;
@@ -124,5 +144,21 @@ export class CreateAdService implements ICommandHandler {
} }
throw error; 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) { for (const processor of this.processors) {
this.candidates = await processor.execute(this.candidates); this.candidates = await processor.execute(this.candidates);
} }
// console.log(JSON.stringify(this.candidates, null, 2));
return this.candidates.map((candidate: CandidateEntity) => return this.candidates.map((candidate: CandidateEntity) =>
MatchEntity.create({ MatchEntity.create({
adId: candidate.id, adId: candidate.id,

View File

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

View File

@@ -1,8 +1,9 @@
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Completer } from './completer.abstract';
import { MatchQuery } from '../match.query';
import { Step } from '../../../types/step.type';
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object'; import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
import { RouteResponse } from '../../../ports/georouter.port';
import { Step } from '../../../types/step.type';
import { MatchQuery } from '../match.query';
import { Completer } from './completer.abstract';
export class RouteCompleter extends Completer { export class RouteCompleter extends Completer {
protected readonly type: RouteCompleterType; protected readonly type: RouteCompleterType;
@@ -18,23 +19,20 @@ export class RouteCompleter extends Completer {
candidates.map(async (candidate: CandidateEntity) => { candidates.map(async (candidate: CandidateEntity) => {
switch (this.type) { switch (this.type) {
case RouteCompleterType.BASIC: case RouteCompleterType.BASIC:
const basicCandidateRoute = await this.query.routeProvider.getBasic( const basicCandidateRoute = await this._getRoute(candidate, {
(candidate.getProps().carpoolPath as CarpoolPathItem[]).map( points: true,
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem, steps: false,
), });
);
candidate.setMetrics( candidate.setMetrics(
basicCandidateRoute.distance, basicCandidateRoute.distance,
basicCandidateRoute.duration, basicCandidateRoute.duration,
); );
break; break;
case RouteCompleterType.DETAILED: case RouteCompleterType.DETAILED:
const detailedCandidateRoute = const detailedCandidateRoute = await this._getRoute(candidate, {
await this.query.routeProvider.getDetailed( points: true,
(candidate.getProps().carpoolPath as CarpoolPathItem[]).map( steps: true,
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem, });
),
);
candidate.setSteps(detailedCandidateRoute.steps as Step[]); candidate.setSteps(detailedCandidateRoute.steps as Step[]);
break; break;
} }
@@ -43,6 +41,20 @@ export class RouteCompleter extends Completer {
); );
return candidates; 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 { 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 { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
import { MatchingRepositoryPort } from '../../ports/matching.repository.port'; import { MatchingRepositoryPort } from '../../ports/matching.repository.port';
import { import {
ConfigurationDomain, Domain,
Configurator, Configurator,
GetConfigurationRepositoryPort, GetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module'; } from '@mobicoop/configuration-module';
@@ -27,7 +27,7 @@ import {
CARPOOL_CONFIG_SEATS_PROPOSED, CARPOOL_CONFIG_SEATS_PROPOSED,
CARPOOL_CONFIG_SEATS_REQUESTED, CARPOOL_CONFIG_SEATS_REQUESTED,
CARPOOL_CONFIG_STRICT_FREQUENCY, CARPOOL_CONFIG_STRICT_FREQUENCY,
CarpoolConfig, CarpoolKeyTypes,
} from '@modules/ad/ad.constants'; } from '@modules/ad/ad.constants';
import { import {
MATCH_CONFIG_ALGORITHM, MATCH_CONFIG_ALGORITHM,
@@ -57,18 +57,12 @@ export class MatchQueryHandler implements IQueryHandler {
execute = async (query: MatchQuery): Promise<MatchingResult> => { execute = async (query: MatchQuery): Promise<MatchingResult> => {
const carpoolConfigurator: Configurator = const carpoolConfigurator: Configurator =
await this.configurationRepository.mget( await this.configurationRepository.mget(Domain.CARPOOL, CarpoolKeyTypes);
ConfigurationDomain.CARPOOL,
CarpoolConfig,
);
const matchConfigurator: Configurator = const matchConfigurator: Configurator =
await this.configurationRepository.mget( await this.configurationRepository.mget(Domain.MATCH, MatchConfig);
ConfigurationDomain.MATCH,
MatchConfig,
);
const paginationConfigurator: Configurator = const paginationConfigurator: Configurator =
await this.configurationRepository.mget( await this.configurationRepository.mget(
ConfigurationDomain.PAGINATION, Domain.PAGINATION,
PaginationConfig, PaginationConfig,
); );
query query

View File

@@ -1,10 +1,5 @@
import { QueryBase } from '@mobicoop/ddd-library'; 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 { 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 { import {
Path, Path,
PathCreator, PathCreator,
@@ -12,7 +7,12 @@ import {
TypedRoute, TypedRoute,
} from '@modules/ad/core/domain/path-creator.service'; } from '@modules/ad/core/domain/path-creator.service';
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
import { 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 { Route } from '../../types/route.type';
import { Waypoint } from '../../types/waypoint.type';
export class MatchQuery extends QueryBase { export class MatchQuery extends QueryBase {
id?: string; id?: string;
@@ -40,9 +40,10 @@ export class MatchQuery extends QueryBase {
passengerRoute?: Route; passengerRoute?: Route;
backAzimuth?: number; backAzimuth?: number;
private readonly originWaypoint: Waypoint; 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(); super();
this.id = props.id; this.id = props.id;
this.driver = props.driver; this.driver = props.driver;
@@ -207,7 +208,12 @@ export class MatchQuery extends QueryBase {
await Promise.all( await Promise.all(
pathCreator.getBasePaths().map(async (path: Path) => ({ pathCreator.getBasePaths().map(async (path: Path) => ({
type: path.type, 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) => { ).forEach((typedRoute: TypedRoute) => {
@@ -222,6 +228,7 @@ export class MatchQuery extends QueryBase {
} }
}); });
} catch (e: any) { } catch (e: any) {
console.log(e.stack || e);
throw new Error('Unable to find a route for given waypoints'); throw new Error('Unable to find a route for given waypoints');
} }
return this; return this;

View File

@@ -1,12 +1,29 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { AdProps, CreateAdProps } from './ad.types'; import { AdProps, CreateAdProps } from './ad.types';
import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event';
export class AdEntity extends AggregateRoot<AdProps> { export class AdEntity extends AggregateRoot<AdProps> {
protected readonly _id: AggregateID; protected readonly _id: AggregateID;
static create = (create: CreateAdProps): AdEntity => { static create = (create: CreateAdProps): AdEntity => {
const props: AdProps = { ...create }; 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 { 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 ! * This is a tedious process : additional information can be found in deeper methods !
*/ */
createJourneys = (): CandidateEntity => { createJourneys = (): CandidateEntity => {
try {
this.props.journeys = this.props.driverSchedule this.props.journeys = this.props.driverSchedule
// first we create the journeys // first we create the journeys
.map((driverScheduleItem: ScheduleItem) => .map((driverScheduleItem: ScheduleItem) =>
@@ -60,6 +61,10 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
) )
// then we filter the ones with invalid pickups // then we filter the ones with invalid pickups
.filter((journey: Journey) => journey.hasValidPickUp()); .filter((journey: Journey) => journey.hasValidPickUp());
} catch (e) {
// irrelevant journeys fall here
// eg. no available day for the given date range
}
return this; 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 { 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 { export class PathCreator {
constructor( 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, ArrayMinSize,
IsArray, IsArray,
IsBoolean, IsBoolean,
IsDecimal,
IsEnum, IsEnum,
IsISO8601, IsISO8601,
IsInt, IsInt,
IsNumber,
IsOptional, IsOptional,
IsUUID, IsUUID,
Max, Max,
@@ -93,7 +93,7 @@ export class MatchRequestDto {
useProportion?: boolean; useProportion?: boolean;
@IsOptional() @IsOptional()
@IsDecimal() @IsNumber()
@Min(0) @Min(0)
@Max(1) @Max(1)
proportion?: number; proportion?: number;
@@ -109,13 +109,13 @@ export class MatchRequestDto {
azimuthMargin?: number; azimuthMargin?: number;
@IsOptional() @IsOptional()
@IsDecimal() @IsNumber()
@Min(0) @Min(0)
@Max(1) @Max(1)
maxDetourDistanceRatio?: number; maxDetourDistanceRatio?: number;
@IsOptional() @IsOptional()
@IsDecimal() @IsNumber()
@Min(0) @Min(0)
@Max(1) @Max(1)
maxDetourDurationRatio?: number; 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 { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchEntity } from '@modules/ad/core/domain/match.entity';
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; 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 { MatchMapper } from '@modules/ad/match.mapper';
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler'; import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager'; import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
@UsePipes( @UsePipes(
new RpcValidationPipe({ new RpcValidationPipe({
@@ -24,7 +24,7 @@ export class MatchGrpcController {
constructor( constructor(
private readonly queryBus: QueryBus, private readonly queryBus: QueryBus,
@Inject(AD_ROUTE_PROVIDER) @Inject(AD_ROUTE_PROVIDER)
private readonly routeProvider: RouteProviderPort, private readonly routeProvider: GeorouterPort,
private readonly matchMapper: MatchMapper, private readonly matchMapper: MatchMapper,
) {} ) {}

View File

@@ -19,14 +19,13 @@ message MatchRequest {
AlgorithmType algorithmType = 10; AlgorithmType algorithmType = 10;
int32 remoteness = 11; int32 remoteness = 11;
bool useProportion = 12; bool useProportion = 12;
int32 proportion = 13; float proportion = 13;
bool useAzimuth = 14; bool useAzimuth = 14;
int32 azimuthMargin = 15; int32 azimuthMargin = 15;
float maxDetourDistanceRatio = 16; float maxDetourDistanceRatio = 16;
float maxDetourDurationRatio = 17; float maxDetourDurationRatio = 17;
int32 identifier = 18; optional int32 page = 18;
optional int32 page = 19; optional int32 perPage = 19;
optional int32 perPage = 20;
} }
message ScheduleItem { message ScheduleItem {

View File

@@ -3,7 +3,10 @@ import { RabbitSubscribe } from '@mobicoop/message-broker-module';
import { CommandBus } from '@nestjs/cqrs'; import { CommandBus } from '@nestjs/cqrs';
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command'; import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
import { Ad } from './ad.types'; 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() @Injectable()
export class AdCreatedMessageHandler { export class AdCreatedMessageHandler {
@@ -11,6 +14,7 @@ export class AdCreatedMessageHandler {
@RabbitSubscribe({ @RabbitSubscribe({
name: AD_CREATED_MESSAGE_HANDLER, name: AD_CREATED_MESSAGE_HANDLER,
routingKey: AD_CREATED_ROUTING_KEY,
}) })
public async adCreated(message: string) { public async adCreated(message: string) {
try { try {
@@ -30,8 +34,9 @@ export class AdCreatedMessageHandler {
waypoints: createdAd.waypoints, waypoints: createdAd.waypoints,
}), }),
); );
} catch (e: any) { } catch (error: any) {
console.log(e); // 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 { import { KeyType, Type } from '@mobicoop/configuration-module';
ConfigurationDomainGet,
ConfigurationType,
} from '@mobicoop/configuration-module';
export const MATCH_CONFIG_ALGORITHM = 'algorithm'; export const MATCH_CONFIG_ALGORITHM = 'algorithm';
export const MATCH_CONFIG_REMOTENESS = 'remoteness'; 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 MATCH_CONFIG_MAX_DETOUR_DURATION_RATIO = 'maxDetourDurationRatio';
export const PAGINATION_CONFIG_PER_PAGE = 'perPage'; export const PAGINATION_CONFIG_PER_PAGE = 'perPage';
export const MatchConfig: ConfigurationDomainGet[] = [ export const MatchConfig: KeyType[] = [
{ {
key: MATCH_CONFIG_ALGORITHM, key: MATCH_CONFIG_ALGORITHM,
type: ConfigurationType.STRING, type: Type.STRING,
}, },
{ {
key: MATCH_CONFIG_REMOTENESS, key: MATCH_CONFIG_REMOTENESS,
type: ConfigurationType.INT, type: Type.INT,
}, },
{ {
key: MATCH_CONFIG_USE_PROPORTION, key: MATCH_CONFIG_USE_PROPORTION,
type: ConfigurationType.BOOLEAN, type: Type.BOOLEAN,
}, },
{ {
key: MATCH_CONFIG_PROPORTION, key: MATCH_CONFIG_PROPORTION,
type: ConfigurationType.FLOAT, type: Type.FLOAT,
}, },
{ {
key: MATCH_CONFIG_USE_AZIMUTH, key: MATCH_CONFIG_USE_AZIMUTH,
type: ConfigurationType.BOOLEAN, type: Type.BOOLEAN,
}, },
{ {
key: MATCH_CONFIG_AZIMUTH_MARGIN, key: MATCH_CONFIG_AZIMUTH_MARGIN,
type: ConfigurationType.INT, type: Type.INT,
}, },
{ {
key: MATCH_CONFIG_MAX_DETOUR_DISTANCE_RATIO, key: MATCH_CONFIG_MAX_DETOUR_DISTANCE_RATIO,
type: ConfigurationType.FLOAT, type: Type.FLOAT,
}, },
{ {
key: MATCH_CONFIG_MAX_DETOUR_DURATION_RATIO, 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, 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 { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { import {
Algorithm, Algorithm,
Selector, 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 { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchEntity } from '@modules/ad/core/domain/match.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@@ -29,11 +29,6 @@ const destinationWaypoint: Waypoint = {
country: 'France', country: 'France',
}; };
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(),
getDetailed: jest.fn(),
};
const matchQuery = new MatchQuery( const matchQuery = new MatchQuery(
{ {
frequency: Frequency.PUNCTUAL, frequency: Frequency.PUNCTUAL,
@@ -46,7 +41,7 @@ const matchQuery = new MatchQuery(
], ],
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}, },
mockRouteProvider, bareMockGeorouter,
); );
const mockAdRepository: AdRepositoryPort = { const mockAdRepository: AdRepositoryPort = {
@@ -54,6 +49,7 @@ const mockAdRepository: AdRepositoryPort = {
findOneById: jest.fn(), findOneById: jest.fn(),
findOne: jest.fn(), findOne: jest.fn(),
findAll: jest.fn(), findAll: jest.fn(),
findAllByIds: jest.fn(),
insert: jest.fn(), insert: jest.fn(),
update: jest.fn(), update: jest.fn(),
updateWhere: jest.fn(), updateWhere: jest.fn(),

View File

@@ -1,5 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing'; 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 { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { ConflictException } from '@mobicoop/ddd-library'; 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 { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command'; import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; 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 { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
const originWaypoint: PointProps = { const originWaypoint: PointProps = {
lat: 48.689445, lat: 48.689445,
@@ -58,8 +62,8 @@ const mockAdRepository = {
}), }),
}; };
const mockRouteProvider: RouteProviderPort = { const mockRouteProvider: GeorouterPort = {
getBasic: jest getRoute: jest
.fn() .fn()
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
throw new Error(); throw new Error();
@@ -93,7 +97,10 @@ const mockRouteProvider: RouteProviderPort = {
}, },
], ],
})), })),
getDetailed: jest.fn(), };
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
}; };
describe('create-ad.service', () => { describe('create-ad.service', () => {
@@ -110,6 +117,10 @@ describe('create-ad.service', () => {
provide: AD_ROUTE_PROVIDER, provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider, useValue: mockRouteProvider,
}, },
{
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
CreateAdService, CreateAdService,
], ],
}).compile(); }).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 { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { simpleMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@@ -42,24 +43,7 @@ const matchQuery = new MatchQuery(
strict: false, strict: false,
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}, },
{ simpleMockGeorouter,
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: [],
})),
},
); );
const candidate: CandidateEntity = CandidateEntity.create({ 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 { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
strict: false, strict: false,
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}, },
{ bareMockGeorouter,
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
); );
const candidate: CandidateEntity = CandidateEntity.create({ const candidate: CandidateEntity = CandidateEntity.create({

View File

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

View File

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

View File

@@ -42,11 +42,10 @@ const matchQuery = new MatchQuery(
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}, },
{ {
getBasic: jest.fn().mockImplementation(() => ({ getRoute: jest.fn().mockImplementation(() => ({
duration: 6500, duration: 6500,
distance: 89745, distance: 89745,
})), })),
getDetailed: jest.fn(),
}, },
); );
@@ -55,6 +54,7 @@ const mockMatcherRepository: AdRepositoryPort = {
findOneById: jest.fn(), findOneById: jest.fn(),
findOne: jest.fn(), findOne: jest.fn(),
findAll: jest.fn(), findAll: jest.fn(),
findAllByIds: jest.fn(),
insert: jest.fn(), insert: jest.fn(),
update: jest.fn(), update: jest.fn(),
updateWhere: 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 { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
strict: false, strict: false,
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}, },
{ bareMockGeorouter,
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
); );
const candidates: CandidateEntity[] = [ 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 { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
strict: false, strict: false,
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}, },
{ bareMockGeorouter,
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
); );
const candidate: CandidateEntity = CandidateEntity.create({ 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 { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency } from '@modules/ad/core/domain/ad.types'; import { Frequency } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@@ -47,10 +48,7 @@ const matchQuery = new MatchQuery(
strict: false, strict: false,
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}, },
{ bareMockGeorouter,
getBasic: jest.fn(),
getDetailed: jest.fn(),
},
); );
matchQuery.driverRoute = { matchQuery.driverRoute = {
distance: 150120, distance: 150120,
@@ -100,6 +98,7 @@ const mockMatcherRepository: AdRepositoryPort = {
findOneById: jest.fn(), findOneById: jest.fn(),
findOne: jest.fn(), findOne: jest.fn(),
findAll: jest.fn(), findAll: jest.fn(),
findAllByIds: jest.fn(),
insert: jest.fn(), insert: jest.fn(),
update: jest.fn(), update: jest.fn(),
updateWhere: 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 { import {
RouteCompleter, RouteCompleter,
RouteCompleterType, 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 { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
import { Step } from '@modules/geography/core/domain/route.types';
import { simpleMockGeorouter } from '../georouter.mock';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = {
position: 0, position: 0,
@@ -46,23 +52,16 @@ const matchQuery = new MatchQuery(
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}, },
{ {
getBasic: jest.fn().mockImplementation(() => ({ getRoute: jest
distance: 350101, .fn()
duration: 14422, .mockImplementation(async (req: RouteRequest): Promise<RouteResponse> => {
fwdAzimuth: 273, const response = await simpleMockGeorouter.getRoute(req);
backAzimuth: 93, if (req.detailsSettings?.steps) {
distanceAzimuth: 336544, const step: Step = { lon: 0, lat: 0, duration: 0 };
points: [], response.steps = [step, step, step, step];
})), }
getDetailed: jest.fn().mockImplementation(() => ({ return response;
distance: 350101, }),
duration: 14422,
fwdAzimuth: 273,
backAzimuth: 93,
distanceAzimuth: 336544,
points: [],
steps: [jest.fn(), jest.fn(), jest.fn(), jest.fn()],
})),
}, },
); );

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, AD_ROUTE_PROVIDER,
} from '@modules/ad/ad.di-tokens'; } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper'; 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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency } from '@modules/ad/core/domain/ad.types'; import { Frequency } from '@modules/ad/core/domain/ad.types';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
@@ -12,6 +11,7 @@ import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { bareMockGeorouter } from '../georouter.mock';
const mockMessagePublisher = { const mockMessagePublisher = {
publish: jest.fn().mockImplementation(), publish: jest.fn().mockImplementation(),
@@ -73,11 +73,6 @@ const mockDirectionEncoder: DirectionEncoderPort = {
]), ]),
}; };
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(),
getDetailed: jest.fn(),
};
const mockPrismaService = { const mockPrismaService = {
$queryRawUnsafe: jest $queryRawUnsafe: jest
.fn() .fn()
@@ -239,7 +234,7 @@ describe('Ad repository', () => {
}, },
{ {
provide: AD_ROUTE_PROVIDER, provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider, useValue: bareMockGeorouter,
}, },
{ {
provide: AD_MESSAGE_PUBLISHER, 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 { RpcExceptionCode } from '@mobicoop/ddd-library';
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler'; import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { Frequency, Role } from '@modules/ad/core/domain/ad.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 { QueryBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices'; import { RpcException } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { bareMockGeorouter } from '../georouter.mock';
const originWaypoint: WaypointDto = { const originWaypoint: WaypointDto = {
position: 0, position: 0,
@@ -183,11 +183,6 @@ const mockQueryBus = {
}), }),
}; };
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(),
getDetailed: jest.fn(),
};
const mockMatchMapper = { const mockMatchMapper = {
toResponse: jest.fn().mockImplementation(() => ({ toResponse: jest.fn().mockImplementation(() => ({
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
@@ -286,7 +281,7 @@ describe('Match Grpc Controller', () => {
}, },
{ {
provide: AD_ROUTE_PROVIDER, provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider, useValue: bareMockGeorouter,
}, },
{ {
provide: MatchMapper, 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 // Types used outside the domain
export type Route = { export type Route = {
distance: number; 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 DIRECTION_ENCODER = Symbol('DIRECTION_ENCODER');
export const GEOROUTER = Symbol('GEOROUTER');
export const GEODESIC = Symbol('GEODESIC');
export const GEOGRAPHY_CONFIGURATION_REPOSITORY = Symbol( export const GEOGRAPHY_CONFIGURATION_REPOSITORY = Symbol(
'GEOGRAPHY_CONFIGURATION_REPOSITORY', 'GEOGRAPHY_CONFIGURATION_REPOSITORY',
); );

View File

@@ -2,24 +2,12 @@ import { Module, Provider } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs'; import { CqrsModule } from '@nestjs/cqrs';
import { import {
DIRECTION_ENCODER, DIRECTION_ENCODER,
GEODESIC,
GEOGRAPHY_CONFIGURATION_REPOSITORY, GEOGRAPHY_CONFIGURATION_REPOSITORY,
GEOROUTER,
} from './geography.di-tokens'; } from './geography.di-tokens';
import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder'; 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 { 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'; import { ConfigurationRepository } from '@mobicoop/configuration-module';
const queryHandlers: Provider[] = [GetRouteQueryHandler];
const mappers: Provider[] = [RouteMapper];
const adapters: Provider[] = [ const adapters: Provider[] = [
{ {
provide: GEOGRAPHY_CONFIGURATION_REPOSITORY, provide: GEOGRAPHY_CONFIGURATION_REPOSITORY,
@@ -29,26 +17,11 @@ const adapters: Provider[] = [
provide: DIRECTION_ENCODER, provide: DIRECTION_ENCODER,
useClass: PostgresDirectionEncoder, useClass: PostgresDirectionEncoder,
}, },
{
provide: GEOROUTER,
useClass: GraphhopperGeorouter,
},
{
provide: GEODESIC,
useClass: Geodesic,
},
GetBasicRouteController,
GetDetailedRouteController,
]; ];
@Module({ @Module({
imports: [CqrsModule, HttpModule], imports: [CqrsModule, HttpModule],
providers: [...queryHandlers, ...mappers, ...adapters], providers: [...adapters],
exports: [ exports: [DIRECTION_ENCODER],
RouteMapper,
DIRECTION_ENCODER,
GetBasicRouteController,
GetDetailedRouteController,
],
}) })
export class GeographyModule {} 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_MESSAGE_HANDLER,
AD_CREATED_QUEUE, AD_CREATED_QUEUE,
AD_CREATED_ROUTING_KEY, 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, SERVICE_NAME,
} from '@src/app.constants'; } from '@src/app.constants';
@@ -39,14 +33,6 @@ const imports = [
routingKey: AD_CREATED_ROUTING_KEY, routingKey: AD_CREATED_ROUTING_KEY,
queue: AD_CREATED_QUEUE, queue: AD_CREATED_QUEUE,
}, },
[AD_UPDATED_MESSAGE_HANDLER]: {
routingKey: AD_UPDATED_ROUTING_KEY,
queue: AD_UPDATED_QUEUE,
},
[AD_DELETED_MESSAGE_HANDLER]: {
routingKey: AD_DELETED_ROUTING_KEY,
queue: AD_DELETED_QUEUE,
},
}, },
}), }),
}), }),

View File

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