mirror of
https://gitlab.com/mobicoop/v3/service/matcher.git
synced 2026-01-01 04:32:41 +00:00
Compare commits
56 Commits
v1.4.1
...
1.6_checkc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f759581157 | ||
|
|
08b5af7511 | ||
|
|
4581af5e9f | ||
|
|
7f7a51d19b | ||
|
|
739d05b095 | ||
|
|
212b609e26 | ||
|
|
bd6fc1576b | ||
|
|
53df6183bd | ||
|
|
ffeb009497 | ||
|
|
3786fcc2c2 | ||
|
|
e53c12ba74 | ||
|
|
c5a5e33256 | ||
|
|
924547c316 | ||
|
|
5f8dd8b4a0 | ||
|
|
90ae3cf9cb | ||
|
|
6b9bf53b4a | ||
|
|
4fd2950027 | ||
|
|
2ce2a46c95 | ||
|
|
579415c300 | ||
|
|
357746e843 | ||
|
|
96c30cb1cc | ||
|
|
d09bad60f7 | ||
|
|
3be95fb58c | ||
|
|
085de292c6 | ||
|
|
0d537cd6a4 | ||
|
|
50c5b99e54 | ||
|
|
c42ddef7e4 | ||
|
|
21c2bc663c | ||
|
|
4089619807 | ||
|
|
0c272795e9 | ||
|
|
6fa8594fa6 | ||
|
|
319cc7b7a7 | ||
|
|
c392d87f43 | ||
|
|
d1942c954a | ||
|
|
6df331f990 | ||
|
|
d47de20588 | ||
|
|
957eb93f3e | ||
|
|
4ccfba12fa | ||
|
|
3aaccfa48f | ||
|
|
d172cac7f4 | ||
|
|
0729670574 | ||
|
|
ce4107ddd7 | ||
|
|
3503e53d79 | ||
|
|
ecab239928 | ||
|
|
80fac59c43 | ||
|
|
73f660bf6d | ||
|
|
df92231f04 | ||
|
|
de239848c3 | ||
|
|
59596fadee | ||
|
|
eee0fd070a | ||
|
|
970260f0d2 | ||
|
|
f21fa0e9b0 | ||
|
|
a5bb249193 | ||
|
|
fef0779aaa | ||
|
|
fb3f1cf4df | ||
|
|
703865dc38 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ stages:
|
|||||||
include:
|
include:
|
||||||
- template: Security/SAST.gitlab-ci.yml
|
- template: Security/SAST.gitlab-ci.yml
|
||||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||||
|
- project: mobicoop/v3/gitlab-templates
|
||||||
|
file: /ci/release.build-job.yml
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# TEST STAGE #
|
# TEST STAGE #
|
||||||
@@ -28,31 +30,3 @@ test:
|
|||||||
rules:
|
rules:
|
||||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
||||||
when: always
|
when: always
|
||||||
|
|
||||||
###############
|
|
||||||
# BUILD STAGE #
|
|
||||||
###############
|
|
||||||
|
|
||||||
build:
|
|
||||||
stage: build
|
|
||||||
image: docker:20.10.22
|
|
||||||
variables:
|
|
||||||
DOCKER_TLS_CERTDIR: ''
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
before_script:
|
|
||||||
- echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
|
|
||||||
script:
|
|
||||||
- export VERSION=$(docker run --rm -v "$PWD":/usr/src/app:ro -w /usr/src/app node:slim node -p "require('./package.json').version")
|
|
||||||
- docker pull $CI_REGISTRY_IMAGE:latest || true
|
|
||||||
- >
|
|
||||||
docker build
|
|
||||||
--pull
|
|
||||||
--cache-from $CI_REGISTRY_IMAGE:latest
|
|
||||||
--tag $CI_REGISTRY_IMAGE:$VERSION
|
|
||||||
--tag $CI_REGISTRY_IMAGE:latest
|
|
||||||
.
|
|
||||||
- docker push $CI_REGISTRY_IMAGE:$VERSION
|
|
||||||
- docker push $CI_REGISTRY_IMAGE:latest
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
|
|||||||
55
.gitlab/merge_request_templates/default.md
Normal file
55
.gitlab/merge_request_templates/default.md
Normal 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_
|
||||||
|
- [ ] ...
|
||||||
62
.gitlab/merge_request_templates/release.md
Normal file
62
.gitlab/merge_request_templates/release.md
Normal 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_
|
||||||
|
- [ ] ...
|
||||||
37
.gitlab/merge_request_templates/smallfix.md
Normal file
37
.gitlab/merge_request_templates/smallfix.md
Normal 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.
|
||||||
@@ -4,3 +4,4 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
coverage
|
coverage
|
||||||
.prettierrc.json
|
.prettierrc.json
|
||||||
|
.gitlab
|
||||||
|
|||||||
@@ -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 ./
|
||||||
|
|||||||
@@ -156,10 +156,14 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||||||
- **strict** (boolean, optional): if set to true, allow matching only with similar frequency ads (_default : false_)
|
- **strict** (boolean, optional): if set to true, allow matching only with similar frequency ads (_default : false_)
|
||||||
- **fromDate**: start date for recurrent ad, carpool date for punctual ad
|
- **fromDate**: start date for recurrent ad, carpool date for punctual ad
|
||||||
- **toDate**: end date for recurrent ad, same as fromDate for punctual ad
|
- **toDate**: end date for recurrent ad, same as fromDate for punctual ad
|
||||||
- **schedule**: an array of schedule items, a schedule item containing :
|
- **schedule**: an optional array of schedule items, a schedule item containing :
|
||||||
|
|
||||||
- the week day as a number, from 0 (sunday) to 6 (saturday) if the ad is recurrent (default to fromDate day for punctual search)
|
- the week day as a number, from 0 (sunday) to 6 (saturday) if the ad is recurrent (default to fromDate day for punctual search)
|
||||||
- the departure time (as HH:MM)
|
- the departure time (as HH:MM)
|
||||||
- the margin around the departure time in seconds (optional) (_default : 900_)
|
- the margin around the departure time in seconds (optional) (_default : 900_)
|
||||||
|
|
||||||
|
_If the schedule is not set, the driver departure time is guessed to be the ideal departure time to reach the passenger, and the passenger departure time is guessed to be the ideal pick up time for the driver_
|
||||||
|
|
||||||
- **seatsProposed** (integer, optional): number of seats proposed as driver (_default : 3_)
|
- **seatsProposed** (integer, optional): number of seats proposed as driver (_default : 3_)
|
||||||
- **seatsRequested** (integer, optional): number of seats requested as passenger (_default : 1_)
|
- **seatsRequested** (integer, optional): number of seats requested as passenger (_default : 1_)
|
||||||
- **waypoints**: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
|
- **waypoints**: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
|
||||||
|
|||||||
11
ci/.env.ci
11
ci/.env.ci
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ services:
|
|||||||
- .:/usr/src/app
|
- .:/usr/src/app
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: npm run start:dev
|
command: npm run start:debug
|
||||||
ports:
|
ports:
|
||||||
- ${SERVICE_PORT:-5005}:${SERVICE_PORT:-5005}
|
- ${SERVICE_PORT:-5005}:${SERVICE_PORT:-5005}
|
||||||
- ${HEALTH_SERVICE_PORT:-6005}:${HEALTH_SERVICE_PORT:-6005}
|
- ${HEALTH_SERVICE_PORT:-6005}:${HEALTH_SERVICE_PORT:-6005}
|
||||||
|
- 9225:9229
|
||||||
networks:
|
networks:
|
||||||
v3-network:
|
v3-network:
|
||||||
aliases:
|
aliases:
|
||||||
|
|||||||
4344
package-lock.json
generated
4344
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mobicoop/matcher",
|
"name": "@mobicoop/matcher",
|
||||||
"version": "1.4.1",
|
"version": "1.6.0",
|
||||||
"description": "Mobicoop V3 Matcher",
|
"description": "Mobicoop V3 Matcher",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
8
src/config/producer-services.config.ts
Normal file
8
src/config/producer-services.config.ts
Normal 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,
|
||||||
|
}));
|
||||||
24
src/log-cause.exception-filter.ts
Normal file
24
src/log-cause.exception-filter.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { ArgumentsHost, Catch, Logger } from '@nestjs/common';
|
||||||
|
import { BaseRpcExceptionFilter } from '@nestjs/microservices';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class LogCauseExceptionFilter extends BaseRpcExceptionFilter {
|
||||||
|
private static readonly causeLogger = new Logger('RpcExceptionsHandler');
|
||||||
|
|
||||||
|
catch(exception: any, host: ArgumentsHost): Observable<any> {
|
||||||
|
const response = super.catch(exception, host);
|
||||||
|
const cause = exception.cause;
|
||||||
|
if (cause) {
|
||||||
|
if (this.isError(cause)) {
|
||||||
|
LogCauseExceptionFilter.causeLogger.error(
|
||||||
|
'Caused by: ' + cause.message,
|
||||||
|
cause.stack,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
LogCauseExceptionFilter.causeLogger.error('Caused by: ' + cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ 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');
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ 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,
|
||||||
} 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 +19,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 +26,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 +39,31 @@ 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')}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
CacheModule.registerAsync<RedisClientOptions>({
|
CacheModule.registerAsync<RedisClientOptions>({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
@@ -80,6 +98,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 +139,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 +164,7 @@ const adapters: Provider[] = [
|
|||||||
controllers: [...grpcControllers],
|
controllers: [...grpcControllers],
|
||||||
providers: [
|
providers: [
|
||||||
...messageHandlers,
|
...messageHandlers,
|
||||||
|
...eventHandlers,
|
||||||
...commandHandlers,
|
...commandHandlers,
|
||||||
...queryHandlers,
|
...queryHandlers,
|
||||||
...mappers,
|
...mappers,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { MatcherAdCreatedDomainEvent } from '../../domain/events/matcher-ad-created.domain-event';
|
||||||
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { MATCHER_AD_CREATED_ROUTING_KEY } from '@src/app.constants';
|
||||||
|
import { MatcherAdCreatedIntegrationEvent } from '../events/matcher-ad-created.integration-event';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PublishMessageWhenMatcherAdIsCreatedDomainEventHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_MESSAGE_PUBLISHER)
|
||||||
|
private readonly messagePublisher: MessagePublisherPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnEvent(MatcherAdCreatedDomainEvent.name, { async: true, promisify: true })
|
||||||
|
async handle(event: MatcherAdCreatedDomainEvent): Promise<any> {
|
||||||
|
const matcherAdCreatedIntegrationEvent =
|
||||||
|
new MatcherAdCreatedIntegrationEvent({
|
||||||
|
id: event.aggregateId,
|
||||||
|
driverDuration: event.driverDuration,
|
||||||
|
driverDistance: event.driverDistance,
|
||||||
|
passengerDuration: event.passengerDuration,
|
||||||
|
passengerDistance: event.passengerDistance,
|
||||||
|
fwdAzimuth: event.fwdAzimuth,
|
||||||
|
backAzimuth: event.backAzimuth,
|
||||||
|
metadata: event.metadata,
|
||||||
|
});
|
||||||
|
this.messagePublisher.publish(
|
||||||
|
MATCHER_AD_CREATED_ROUTING_KEY,
|
||||||
|
JSON.stringify(matcherAdCreatedIntegrationEvent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class MatcherAdCreatedIntegrationEvent extends IntegrationEvent {
|
||||||
|
readonly driverDuration?: number;
|
||||||
|
readonly driverDistance?: number;
|
||||||
|
readonly passengerDuration?: number;
|
||||||
|
readonly passengerDistance?: number;
|
||||||
|
readonly fwdAzimuth: number;
|
||||||
|
readonly backAzimuth: number;
|
||||||
|
|
||||||
|
constructor(props: IntegrationEventProps<MatcherAdCreatedIntegrationEvent>) {
|
||||||
|
super(props);
|
||||||
|
this.driverDuration = props.driverDuration;
|
||||||
|
this.driverDistance = props.driverDistance;
|
||||||
|
this.passengerDuration = props.passengerDuration;
|
||||||
|
this.passengerDistance = props.passengerDistance;
|
||||||
|
this.fwdAzimuth = props.fwdAzimuth;
|
||||||
|
this.backAzimuth = props.backAzimuth;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class MatcherAdCreationFailedIntegrationEvent extends IntegrationEvent {
|
||||||
|
readonly cause?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
props: IntegrationEventProps<MatcherAdCreationFailedIntegrationEvent>,
|
||||||
|
) {
|
||||||
|
super(props);
|
||||||
|
this.cause = props.cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/modules/ad/core/application/ports/georouter.port.ts
Normal file
31
src/modules/ad/core/application/ports/georouter.port.ts
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Completer } from './completer.abstract';
|
|||||||
import { MatchQuery } from '../match.query';
|
import { MatchQuery } from '../match.query';
|
||||||
import { Step } from '../../../types/step.type';
|
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';
|
||||||
|
|
||||||
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,17 @@ 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) => carpoolPathItem,
|
||||||
|
),
|
||||||
|
detailsSettings: detailsSettings,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RouteCompleterType {
|
export enum RouteCompleterType {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -171,7 +165,7 @@ export class MatchQueryHandler implements IQueryHandler {
|
|||||||
frequency: query.frequency,
|
frequency: query.frequency,
|
||||||
fromDate: query.fromDate,
|
fromDate: query.fromDate,
|
||||||
toDate: query.toDate,
|
toDate: query.toDate,
|
||||||
schedule: query.schedule.map((scheduleItem: ScheduleItem) => ({
|
schedule: query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||||
day: scheduleItem.day as number,
|
day: scheduleItem.day as number,
|
||||||
time: scheduleItem.time,
|
time: scheduleItem.time,
|
||||||
margin: scheduleItem.margin as number,
|
margin: scheduleItem.margin as number,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -21,7 +21,7 @@ export class MatchQuery extends QueryBase {
|
|||||||
readonly frequency: Frequency;
|
readonly frequency: Frequency;
|
||||||
fromDate: string;
|
fromDate: string;
|
||||||
toDate: string;
|
toDate: string;
|
||||||
schedule: ScheduleItem[];
|
schedule?: ScheduleItem[];
|
||||||
seatsProposed?: number;
|
seatsProposed?: number;
|
||||||
seatsRequested?: number;
|
seatsRequested?: number;
|
||||||
strict?: boolean;
|
strict?: boolean;
|
||||||
@@ -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;
|
||||||
@@ -72,7 +73,7 @@ export class MatchQuery extends QueryBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => {
|
setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => {
|
||||||
this.schedule.forEach((day: ScheduleItem) => {
|
this.schedule?.forEach((day: ScheduleItem) => {
|
||||||
if (day.margin === undefined) day.margin = defaultMarginDuration;
|
if (day.margin === undefined) day.margin = defaultMarginDuration;
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
@@ -135,6 +136,8 @@ export class MatchQuery extends QueryBase {
|
|||||||
setDatesAndSchedule = (
|
setDatesAndSchedule = (
|
||||||
datetimeTransformer: DateTimeTransformerPort,
|
datetimeTransformer: DateTimeTransformerPort,
|
||||||
): MatchQuery => {
|
): MatchQuery => {
|
||||||
|
// no transformation if schedule is not set
|
||||||
|
if (this.schedule === undefined) return this;
|
||||||
const initialFromDate: string = this.fromDate;
|
const initialFromDate: string = this.fromDate;
|
||||||
this.fromDate = datetimeTransformer.fromDate(
|
this.fromDate = datetimeTransformer.fromDate(
|
||||||
{
|
{
|
||||||
@@ -207,7 +210,9 @@ 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,
|
||||||
|
}),
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
).forEach((typedRoute: TypedRoute) => {
|
).forEach((typedRoute: TypedRoute) => {
|
||||||
@@ -222,7 +227,9 @@ export class MatchQuery extends QueryBase {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new Error('Unable to find a route for given waypoints');
|
throw new Error('Unable to find a route for given waypoints', {
|
||||||
|
cause: e,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export class PassengerOrientedSelector extends Selector {
|
|||||||
query: this._createQueryString(Role.PASSENGER),
|
query: this._createQueryString(Role.PASSENGER),
|
||||||
role: Role.PASSENGER,
|
role: Role.PASSENGER,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
queryStringRoles.map<Promise<AdsRole>>(
|
queryStringRoles.map<Promise<AdsRole>>(
|
||||||
@@ -74,7 +73,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||||||
driverSchedule:
|
driverSchedule:
|
||||||
adsRole.role == Role.PASSENGER
|
adsRole.role == Role.PASSENGER
|
||||||
? adEntity.getProps().schedule
|
? adEntity.getProps().schedule
|
||||||
: this.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
: this.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||||
day: scheduleItem.day as number,
|
day: scheduleItem.day as number,
|
||||||
time: scheduleItem.time,
|
time: scheduleItem.time,
|
||||||
margin: scheduleItem.margin as number,
|
margin: scheduleItem.margin as number,
|
||||||
@@ -82,7 +81,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||||||
passengerSchedule:
|
passengerSchedule:
|
||||||
adsRole.role == Role.DRIVER
|
adsRole.role == Role.DRIVER
|
||||||
? adEntity.getProps().schedule
|
? adEntity.getProps().schedule
|
||||||
: this.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
: this.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||||
day: scheduleItem.day as number,
|
day: scheduleItem.day as number,
|
||||||
time: scheduleItem.time,
|
time: scheduleItem.time,
|
||||||
margin: scheduleItem.margin as number,
|
margin: scheduleItem.margin as number,
|
||||||
@@ -155,7 +154,9 @@ export class PassengerOrientedSelector extends Selector {
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
private _whereDate = (): string =>
|
private _whereDate = (): string =>
|
||||||
`(\
|
this.query.frequency == Frequency.PUNCTUAL
|
||||||
|
? `("fromDate" <= '${this.query.fromDate}' AND "toDate" >= '${this.query.fromDate}')`
|
||||||
|
: `(\
|
||||||
(\
|
(\
|
||||||
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
||||||
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
||||||
@@ -172,6 +173,8 @@ export class PassengerOrientedSelector extends Selector {
|
|||||||
)`;
|
)`;
|
||||||
|
|
||||||
private _whereSchedule = (role: Role): string => {
|
private _whereSchedule = (role: Role): string => {
|
||||||
|
// no schedule filtering if schedule is not set
|
||||||
|
if (this.query.schedule === undefined) return '';
|
||||||
const schedule: string[] = [];
|
const schedule: string[] = [];
|
||||||
// we need full dates to compare times, because margins can lead to compare on previous or next day
|
// we need full dates to compare times, because margins can lead to compare on previous or next day
|
||||||
// - first we establish a base calendar (up to a week)
|
// - first we establish a base calendar (up to a week)
|
||||||
@@ -182,7 +185,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||||||
// - then we compare each resulting day of the schedule with each day of calendar,
|
// - then we compare each resulting day of the schedule with each day of calendar,
|
||||||
// adding / removing margin depending on the role
|
// adding / removing margin depending on the role
|
||||||
scheduleDates.map((date: Date) => {
|
scheduleDates.map((date: Date) => {
|
||||||
this.query.schedule
|
(this.query.schedule as ScheduleItem[])
|
||||||
.filter(
|
.filter(
|
||||||
(scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day,
|
(scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day,
|
||||||
)
|
)
|
||||||
@@ -310,6 +313,11 @@ export class PassengerOrientedSelector extends Selector {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of dates containing all the dates (limited to 7 by default) between 2 boundary dates.
|
||||||
|
*
|
||||||
|
* The array length can be limited to a _max_ number of dates (default: 7)
|
||||||
|
*/
|
||||||
private _datesBetweenBoundaries = (
|
private _datesBetweenBoundaries = (
|
||||||
firstDate: string,
|
firstDate: string,
|
||||||
lastDate: string,
|
lastDate: string,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
import {
|
||||||
|
AggregateRoot,
|
||||||
|
AggregateID,
|
||||||
|
ArgumentInvalidException,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
import {
|
import {
|
||||||
CandidateProps,
|
CandidateProps,
|
||||||
CreateCandidateProps,
|
CreateCandidateProps,
|
||||||
@@ -9,7 +13,10 @@ import {
|
|||||||
CarpoolPathItemProps,
|
CarpoolPathItemProps,
|
||||||
} from './value-objects/carpool-path-item.value-object';
|
} from './value-objects/carpool-path-item.value-object';
|
||||||
import { Step, StepProps } from './value-objects/step.value-object';
|
import { Step, StepProps } from './value-objects/step.value-object';
|
||||||
import { ScheduleItem } from './value-objects/schedule-item.value-object';
|
import {
|
||||||
|
ScheduleItem,
|
||||||
|
ScheduleItemProps,
|
||||||
|
} from './value-objects/schedule-item.value-object';
|
||||||
import { Journey } from './value-objects/journey.value-object';
|
import { Journey } from './value-objects/journey.value-object';
|
||||||
import { CalendarTools } from './calendar-tools.service';
|
import { CalendarTools } from './calendar-tools.service';
|
||||||
import { JourneyItem } from './value-objects/journey-item.value-object';
|
import { JourneyItem } from './value-objects/journey-item.value-object';
|
||||||
@@ -53,13 +60,21 @@ 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 => {
|
||||||
this.props.journeys = this.props.driverSchedule
|
// driver and passenger schedules are eventually mandatory
|
||||||
|
if (!this.props.driverSchedule) this._createDriverSchedule();
|
||||||
|
if (!this.props.passengerSchedule) this._createPassengerSchedule();
|
||||||
|
try {
|
||||||
|
this.props.journeys = (this.props.driverSchedule as ScheduleItemProps[])
|
||||||
// first we create the journeys
|
// first we create the journeys
|
||||||
.map((driverScheduleItem: ScheduleItem) =>
|
.map((driverScheduleItem: ScheduleItem) =>
|
||||||
this._createJourney(driverScheduleItem),
|
this._createJourney(driverScheduleItem),
|
||||||
)
|
)
|
||||||
// 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,6 +92,116 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||||||
(1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio)
|
(1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the driver schedule based on the passenger schedule
|
||||||
|
*/
|
||||||
|
private _createDriverSchedule = (): void => {
|
||||||
|
let driverSchedule: ScheduleItemProps[] = this.props.passengerSchedule!.map(
|
||||||
|
(scheduleItemProps: ScheduleItemProps) => ({
|
||||||
|
day: scheduleItemProps.day,
|
||||||
|
time: scheduleItemProps.time,
|
||||||
|
margin: scheduleItemProps.margin,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// adjust the driver theoretical schedule :
|
||||||
|
// we guess the ideal driver departure time based on the duration to
|
||||||
|
// reach the passenger starting point from the driver starting point
|
||||||
|
driverSchedule = driverSchedule.map(
|
||||||
|
(scheduleItemProps: ScheduleItemProps) => {
|
||||||
|
const driverDate: Date = CalendarTools.firstDate(
|
||||||
|
scheduleItemProps.day,
|
||||||
|
this.props.dateInterval,
|
||||||
|
);
|
||||||
|
const driverStartDatetime: Date = CalendarTools.datetimeWithSeconds(
|
||||||
|
driverDate,
|
||||||
|
scheduleItemProps.time,
|
||||||
|
-this._passengerStartDuration(),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
day: driverDate.getUTCDay(),
|
||||||
|
margin: scheduleItemProps.margin,
|
||||||
|
time: `${driverStartDatetime
|
||||||
|
.getUTCHours()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}:${driverStartDatetime
|
||||||
|
.getUTCMinutes()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.props.driverSchedule = driverSchedule.map(
|
||||||
|
(scheduleItemProps: ScheduleItemProps) => ({
|
||||||
|
day: scheduleItemProps.day,
|
||||||
|
time: scheduleItemProps.time,
|
||||||
|
margin: scheduleItemProps.margin,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the duration to reach the passenger starting point from the driver starting point
|
||||||
|
*/
|
||||||
|
private _passengerStartDuration = (): number => {
|
||||||
|
let passengerStartStepIndex = 0;
|
||||||
|
this.props.carpoolPath?.forEach(
|
||||||
|
(carpoolPathItem: CarpoolPathItem, index: number) => {
|
||||||
|
carpoolPathItem.actors.forEach((actor: Actor) => {
|
||||||
|
if (actor.role == Role.PASSENGER && actor.target == Target.START)
|
||||||
|
passengerStartStepIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return this.props.steps![passengerStartStepIndex].duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the passenger schedule based on the driver schedule
|
||||||
|
*/
|
||||||
|
private _createPassengerSchedule = (): void => {
|
||||||
|
let passengerSchedule: ScheduleItemProps[] = this.props.driverSchedule!.map(
|
||||||
|
(scheduleItemProps: ScheduleItemProps) => ({
|
||||||
|
day: scheduleItemProps.day,
|
||||||
|
time: scheduleItemProps.time,
|
||||||
|
margin: scheduleItemProps.margin,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// adjust the passenger theoretical schedule :
|
||||||
|
// we guess the ideal passenger departure time based on the duration to
|
||||||
|
// reach the passenger starting point from the driver starting point
|
||||||
|
passengerSchedule = passengerSchedule.map(
|
||||||
|
(scheduleItemProps: ScheduleItemProps) => {
|
||||||
|
const passengerDate: Date = CalendarTools.firstDate(
|
||||||
|
scheduleItemProps.day,
|
||||||
|
this.props.dateInterval,
|
||||||
|
);
|
||||||
|
const passengeStartDatetime: Date = CalendarTools.datetimeWithSeconds(
|
||||||
|
passengerDate,
|
||||||
|
scheduleItemProps.time,
|
||||||
|
this._passengerStartDuration(),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
day: passengerDate.getUTCDay(),
|
||||||
|
margin: scheduleItemProps.margin,
|
||||||
|
time: `${passengeStartDatetime
|
||||||
|
.getUTCHours()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}:${passengeStartDatetime
|
||||||
|
.getUTCMinutes()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.props.passengerSchedule = passengerSchedule.map(
|
||||||
|
(scheduleItemProps: ScheduleItemProps) => ({
|
||||||
|
day: scheduleItemProps.day,
|
||||||
|
time: scheduleItemProps.time,
|
||||||
|
margin: scheduleItemProps.margin,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
|
private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
|
||||||
new Journey({
|
new Journey({
|
||||||
firstDate: CalendarTools.firstDate(
|
firstDate: CalendarTools.firstDate(
|
||||||
@@ -211,7 +336,7 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||||||
* Find the passenger schedule item with the minimum duration between a given date and the dates of the passenger schedule
|
* Find the passenger schedule item with the minimum duration between a given date and the dates of the passenger schedule
|
||||||
*/
|
*/
|
||||||
private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap =>
|
private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap =>
|
||||||
this.props.passengerSchedule
|
(this.props.passengerSchedule as ScheduleItemProps[])
|
||||||
// first map the passenger schedule to "real" dates (we use unix epoch date as base)
|
// first map the passenger schedule to "real" dates (we use unix epoch date as base)
|
||||||
.map(
|
.map(
|
||||||
(scheduleItem: ScheduleItem) =>
|
(scheduleItem: ScheduleItem) =>
|
||||||
@@ -250,6 +375,10 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||||||
|
|
||||||
validate(): void {
|
validate(): void {
|
||||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
|
if (!this.props.driverSchedule && !this.props.passengerSchedule)
|
||||||
|
throw new ArgumentInvalidException(
|
||||||
|
'at least the driver or the passenger schedule is required',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export interface CandidateProps {
|
|||||||
frequency: Frequency;
|
frequency: Frequency;
|
||||||
driverWaypoints: PointProps[];
|
driverWaypoints: PointProps[];
|
||||||
passengerWaypoints: PointProps[];
|
passengerWaypoints: PointProps[];
|
||||||
driverSchedule: ScheduleItemProps[];
|
driverSchedule?: ScheduleItemProps[];
|
||||||
passengerSchedule: ScheduleItemProps[];
|
passengerSchedule?: ScheduleItemProps[];
|
||||||
driverDistance: number;
|
driverDistance: number;
|
||||||
driverDuration: number;
|
driverDuration: number;
|
||||||
dateInterval: DateInterval;
|
dateInterval: DateInterval;
|
||||||
@@ -33,8 +33,8 @@ export interface CreateCandidateProps {
|
|||||||
driverDuration: number;
|
driverDuration: number;
|
||||||
driverWaypoints: PointProps[];
|
driverWaypoints: PointProps[];
|
||||||
passengerWaypoints: PointProps[];
|
passengerWaypoints: PointProps[];
|
||||||
driverSchedule: ScheduleItemProps[];
|
driverSchedule?: ScheduleItemProps[];
|
||||||
passengerSchedule: ScheduleItemProps[];
|
passengerSchedule?: ScheduleItemProps[];
|
||||||
spacetimeDetourRatio: SpacetimeDetourRatio;
|
spacetimeDetourRatio: SpacetimeDetourRatio;
|
||||||
dateInterval: DateInterval;
|
dateInterval: DateInterval;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,8 +42,8 @@ export class JourneyItem extends ValueObject<JourneyItemProps> {
|
|||||||
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
||||||
) as ActorTime
|
) as ActorTime
|
||||||
).firstDatetime;
|
).firstDatetime;
|
||||||
return `${driverTime.getHours().toString().padStart(2, '0')}:${driverTime
|
return `${driverTime.getUTCHours().toString().padStart(2, '0')}:${driverTime
|
||||||
.getMinutes()
|
.getUTCMinutes()
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0')}`;
|
.padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,6 +46,29 @@ export class Journey extends ValueObject<JourneyProps> {
|
|||||||
const driverActorTime = passengerDepartureJourneyItem.actorTimes.find(
|
const driverActorTime = passengerDepartureJourneyItem.actorTimes.find(
|
||||||
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
||||||
) as ActorTime;
|
) as ActorTime;
|
||||||
|
// TODO : check if the following conditions are even to the ones used in the return
|
||||||
|
// 4 possibilities to be valid :
|
||||||
|
// - 1 : the driver time boundaries are within the passenger time boundaries
|
||||||
|
// - 2 : the max driver time boundary is within the passenger time boundaries
|
||||||
|
// - 3 : the min driver time boundary is within the passenger time boundaries
|
||||||
|
// - 4 : the passenger time boundaries are within the driver time boundaries
|
||||||
|
// return (
|
||||||
|
// // 1
|
||||||
|
// (driverActorTime.firstMinDatetime >=
|
||||||
|
// passengerDepartureActorTime.firstMinDatetime &&
|
||||||
|
// driverActorTime.firstMaxDatetime <=
|
||||||
|
// passengerDepartureActorTime.firstMaxDatetime) ||
|
||||||
|
// // 2 & 4
|
||||||
|
// (driverActorTime.firstMinDatetime <=
|
||||||
|
// passengerDepartureActorTime.firstMinDatetime &&
|
||||||
|
// driverActorTime.firstMaxDatetime >=
|
||||||
|
// passengerDepartureActorTime.firstMinDatetime) ||
|
||||||
|
// // 3
|
||||||
|
// (driverActorTime.firstMinDatetime >=
|
||||||
|
// passengerDepartureActorTime.firstMinDatetime &&
|
||||||
|
// driverActorTime.firstMinDatetime <=
|
||||||
|
// passengerDepartureActorTime.firstMaxDatetime)
|
||||||
|
// );
|
||||||
return (
|
return (
|
||||||
(passengerDepartureActorTime.firstMinDatetime <=
|
(passengerDepartureActorTime.firstMinDatetime <=
|
||||||
driverActorTime.firstMaxDatetime &&
|
driverActorTime.firstMaxDatetime &&
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface MatchQueryProps {
|
|||||||
frequency: Frequency;
|
frequency: Frequency;
|
||||||
fromDate: string;
|
fromDate: string;
|
||||||
toDate: string;
|
toDate: string;
|
||||||
schedule: ScheduleItemProps[];
|
schedule?: ScheduleItemProps[];
|
||||||
seatsProposed: number;
|
seatsProposed: number;
|
||||||
seatsRequested: number;
|
seatsRequested: number;
|
||||||
strict: boolean;
|
strict: boolean;
|
||||||
@@ -50,7 +50,7 @@ export class MatchQuery extends ValueObject<MatchQueryProps> {
|
|||||||
return this.props.toDate;
|
return this.props.toDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
get schedule(): ScheduleItemProps[] {
|
get schedule(): ScheduleItemProps[] | undefined {
|
||||||
return this.props.schedule;
|
return this.props.schedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
src/modules/ad/infrastructure/georouter.proto
Normal file
39
src/modules/ad/infrastructure/georouter.proto
Normal 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;
|
||||||
|
}
|
||||||
35
src/modules/ad/infrastructure/georouter.ts
Normal file
35
src/modules/ad/infrastructure/georouter.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Observable, lastValueFrom } from 'rxjs';
|
||||||
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
|
import { GRPC_GEOROUTER_SERVICE_NAME } from '@src/app.constants';
|
||||||
|
import {
|
||||||
|
GeorouterPort,
|
||||||
|
RouteRequest,
|
||||||
|
RouteResponse,
|
||||||
|
} from '../core/application/ports/georouter.port';
|
||||||
|
import { GEOGRAPHY_PACKAGE } from '../ad.di-tokens';
|
||||||
|
|
||||||
|
interface GeorouterService {
|
||||||
|
getRoute(request: RouteRequest): Observable<RouteResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class Georouter implements GeorouterPort, OnModuleInit {
|
||||||
|
private georouterService: GeorouterService;
|
||||||
|
|
||||||
|
constructor(@Inject(GEOGRAPHY_PACKAGE) private readonly client: ClientGrpc) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.georouterService = this.client.getService<GeorouterService>(
|
||||||
|
GRPC_GEOROUTER_SERVICE_NAME,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoute = async (request: RouteRequest): Promise<RouteResponse> => {
|
||||||
|
try {
|
||||||
|
return await lastValueFrom(this.georouterService.getRoute(request));
|
||||||
|
} catch (error: any) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
@@ -58,8 +58,9 @@ export class MatchRequestDto {
|
|||||||
@Type(() => ScheduleItemDto)
|
@Type(() => ScheduleItemDto)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ArrayMinSize(1)
|
@ArrayMinSize(1)
|
||||||
|
@IsOptional()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
schedule: ScheduleItemDto[];
|
schedule?: ScheduleItemDto[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@@ -93,7 +94,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 +110,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;
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import { Controller, Inject, UseInterceptors, UsePipes } from '@nestjs/common';
|
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
|
||||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
|
||||||
import { MatchingPaginatedResponseDto } from '../dtos/matching.paginated.response.dto';
|
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { MatchRequestDto } from './dtos/match.request.dto';
|
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
|
||||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
|
||||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
||||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||||
|
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||||
|
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||||
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
|
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Inject,
|
||||||
|
UseFilters,
|
||||||
|
UseInterceptors,
|
||||||
|
UsePipes,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
|
import { GrpcMethod } from '@nestjs/microservices';
|
||||||
|
import { LogCauseExceptionFilter } from '@src/log-cause.exception-filter';
|
||||||
|
import { MatchingPaginatedResponseDto } from '../dtos/matching.paginated.response.dto';
|
||||||
|
import { MatchRequestDto } from './dtos/match.request.dto';
|
||||||
|
|
||||||
|
@UseFilters(LogCauseExceptionFilter)
|
||||||
@UsePipes(
|
@UsePipes(
|
||||||
new RpcValidationPipe({
|
new RpcValidationPipe({
|
||||||
whitelist: false,
|
whitelist: false,
|
||||||
@@ -24,7 +31,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,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -32,7 +39,6 @@ export class MatchGrpcController {
|
|||||||
@UseInterceptors(CacheInterceptor)
|
@UseInterceptors(CacheInterceptor)
|
||||||
@GrpcMethod('MatcherService', 'Match')
|
@GrpcMethod('MatcherService', 'Match')
|
||||||
async match(data: MatchRequestDto): Promise<MatchingPaginatedResponseDto> {
|
async match(data: MatchRequestDto): Promise<MatchingPaginatedResponseDto> {
|
||||||
try {
|
|
||||||
const matchingResult: MatchingResult = await this.queryBus.execute(
|
const matchingResult: MatchingResult = await this.queryBus.execute(
|
||||||
new MatchQuery(data, this.routeProvider),
|
new MatchQuery(data, this.routeProvider),
|
||||||
);
|
);
|
||||||
@@ -45,11 +51,5 @@ export class MatchGrpcController {
|
|||||||
perPage: matchingResult.perPage,
|
perPage: matchingResult.perPage,
|
||||||
total: matchingResult.total,
|
total: matchingResult.total,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: RpcExceptionCode.UNKNOWN,
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export class MatchingMapper
|
|||||||
toDate: entity.getProps().query.toDate,
|
toDate: entity.getProps().query.toDate,
|
||||||
schedule: entity
|
schedule: entity
|
||||||
.getProps()
|
.getProps()
|
||||||
.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||||
day: scheduleItem.day,
|
day: scheduleItem.day,
|
||||||
time: scheduleItem.time,
|
time: scheduleItem.time,
|
||||||
margin: scheduleItem.margin,
|
margin: scheduleItem.margin,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ArgumentInvalidException } from '@mobicoop/ddd-library';
|
||||||
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 {
|
import {
|
||||||
@@ -6,7 +7,10 @@ import {
|
|||||||
} from '@modules/ad/core/domain/candidate.types';
|
} 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 { CarpoolPathItemProps } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
import { CarpoolPathItemProps } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
||||||
import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object';
|
import {
|
||||||
|
Journey,
|
||||||
|
JourneyProps,
|
||||||
|
} from '@modules/ad/core/domain/value-objects/journey.value-object';
|
||||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||||
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||||
import { StepProps } from '@modules/ad/core/domain/value-objects/step.value-object';
|
import { StepProps } from '@modules/ad/core/domain/value-objects/step.value-object';
|
||||||
@@ -374,6 +378,95 @@ describe('Candidate entity', () => {
|
|||||||
.createJourneys();
|
.createJourneys();
|
||||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
it('should create journeys for a single date without driver schedule', () => {
|
||||||
|
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||||
|
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||||
|
role: Role.PASSENGER,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
dateInterval: {
|
||||||
|
lowerDate: '2023-08-28',
|
||||||
|
higherDate: '2023-08-28',
|
||||||
|
},
|
||||||
|
driverWaypoints: waypointsSet1,
|
||||||
|
passengerWaypoints: waypointsSet2,
|
||||||
|
driverDistance: 350145,
|
||||||
|
driverDuration: 13548,
|
||||||
|
driverSchedule: undefined,
|
||||||
|
passengerSchedule: schedule2,
|
||||||
|
spacetimeDetourRatio,
|
||||||
|
})
|
||||||
|
.setCarpoolPath(carpoolPath2)
|
||||||
|
.setSteps(steps)
|
||||||
|
.createJourneys();
|
||||||
|
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||||
|
// computed driver start time should be 06:49
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
candidateEntity.getProps().journeys as JourneyProps[]
|
||||||
|
)[0].journeyItems[0].actorTimes[0].firstDatetime.getUTCMinutes(),
|
||||||
|
).toBe(49);
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
candidateEntity.getProps().journeys as JourneyProps[]
|
||||||
|
)[0].journeyItems[0].actorTimes[0].firstDatetime.getUTCHours(),
|
||||||
|
).toBe(6);
|
||||||
|
});
|
||||||
|
it('should create journeys for a single date without passenger schedule', () => {
|
||||||
|
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||||
|
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||||
|
role: Role.PASSENGER,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
dateInterval: {
|
||||||
|
lowerDate: '2023-08-28',
|
||||||
|
higherDate: '2023-08-28',
|
||||||
|
},
|
||||||
|
driverWaypoints: waypointsSet1,
|
||||||
|
passengerWaypoints: waypointsSet2,
|
||||||
|
driverDistance: 350145,
|
||||||
|
driverDuration: 13548,
|
||||||
|
driverSchedule: schedule1,
|
||||||
|
passengerSchedule: undefined,
|
||||||
|
spacetimeDetourRatio,
|
||||||
|
})
|
||||||
|
.setCarpoolPath(carpoolPath2)
|
||||||
|
.setSteps(steps)
|
||||||
|
.createJourneys();
|
||||||
|
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||||
|
// computed passenger start time should be 07:20
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
candidateEntity.getProps().journeys as JourneyProps[]
|
||||||
|
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(),
|
||||||
|
).toBe(20);
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
candidateEntity.getProps().journeys as JourneyProps[]
|
||||||
|
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCHours(),
|
||||||
|
).toBe(7);
|
||||||
|
});
|
||||||
|
it('should throw without driver and passenger schedule', () => {
|
||||||
|
expect(() =>
|
||||||
|
CandidateEntity.create({
|
||||||
|
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||||
|
role: Role.PASSENGER,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
dateInterval: {
|
||||||
|
lowerDate: '2023-08-28',
|
||||||
|
higherDate: '2023-08-28',
|
||||||
|
},
|
||||||
|
driverWaypoints: waypointsSet1,
|
||||||
|
passengerWaypoints: waypointsSet2,
|
||||||
|
driverDistance: 350145,
|
||||||
|
driverDuration: 13548,
|
||||||
|
driverSchedule: undefined,
|
||||||
|
passengerSchedule: undefined,
|
||||||
|
spacetimeDetourRatio,
|
||||||
|
})
|
||||||
|
.setCarpoolPath(carpoolPath2)
|
||||||
|
.setSteps(steps)
|
||||||
|
.createJourneys(),
|
||||||
|
).toThrow(ArgumentInvalidException);
|
||||||
|
});
|
||||||
it('should create journeys for multiple dates', () => {
|
it('should create journeys for multiple dates', () => {
|
||||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('Match Query value object', () => {
|
|||||||
expect(matchQueryVO.frequency).toBe(Frequency.PUNCTUAL);
|
expect(matchQueryVO.frequency).toBe(Frequency.PUNCTUAL);
|
||||||
expect(matchQueryVO.fromDate).toBe('2023-09-01');
|
expect(matchQueryVO.fromDate).toBe('2023-09-01');
|
||||||
expect(matchQueryVO.toDate).toBe('2023-09-01');
|
expect(matchQueryVO.toDate).toBe('2023-09-01');
|
||||||
expect(matchQueryVO.schedule.length).toBe(1);
|
expect(matchQueryVO.schedule?.length).toBe(1);
|
||||||
expect(matchQueryVO.seatsProposed).toBe(3);
|
expect(matchQueryVO.seatsProposed).toBe(3);
|
||||||
expect(matchQueryVO.seatsRequested).toBe(1);
|
expect(matchQueryVO.seatsRequested).toBe(1);
|
||||||
expect(matchQueryVO.strict).toBe(false);
|
expect(matchQueryVO.strict).toBe(false);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
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,
|
||||||
|
ScheduleItem,
|
||||||
|
} 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 +61,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 +73,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 +86,6 @@ const mockRouteProvider: RouteProviderPort = {
|
|||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}),
|
}),
|
||||||
getDetailed: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Match Query', () => {
|
describe('Match Query', () => {
|
||||||
@@ -156,9 +138,9 @@ describe('Match Query', () => {
|
|||||||
expect(matchQuery.maxDetourDurationRatio).toBe(0.3);
|
expect(matchQuery.maxDetourDurationRatio).toBe(0.3);
|
||||||
expect(matchQuery.fromDate).toBe('2023-08-27');
|
expect(matchQuery.fromDate).toBe('2023-08-27');
|
||||||
expect(matchQuery.toDate).toBe('2023-08-27');
|
expect(matchQuery.toDate).toBe('2023-08-27');
|
||||||
expect(matchQuery.schedule[0].day).toBe(0);
|
expect((matchQuery.schedule as ScheduleItem[])[0].day).toBe(0);
|
||||||
expect(matchQuery.schedule[0].time).toBe('23:05');
|
expect((matchQuery.schedule as ScheduleItem[])[0].time).toBe('23:05');
|
||||||
expect(matchQuery.schedule[0].margin).toBe(900);
|
expect((matchQuery.schedule as ScheduleItem[])[0].margin).toBe(900);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set good values for seats', async () => {
|
it('should set good values for seats', async () => {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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[] = [
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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()],
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
16
src/modules/ad/tests/unit/georouter.mock.ts
Normal file
16
src/modules/ad/tests/unit/georouter.mock.ts
Normal 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: [],
|
||||||
|
})),
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
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';
|
||||||
@@ -15,8 +13,8 @@ import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/matc
|
|||||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
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,
|
||||||
@@ -56,9 +54,7 @@ const recurrentMatchRequestDto: MatchRequestDto = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockQueryBus = {
|
const mockQueryBus = {
|
||||||
execute: jest
|
execute: jest.fn().mockImplementationOnce(
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(
|
|
||||||
() =>
|
() =>
|
||||||
<MatchingResult>{
|
<MatchingResult>{
|
||||||
id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
|
id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
|
||||||
@@ -177,15 +173,7 @@ const mockQueryBus = {
|
|||||||
],
|
],
|
||||||
total: 1,
|
total: 1,
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error();
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockRouteProvider: RouteProviderPort = {
|
|
||||||
getBasic: jest.fn(),
|
|
||||||
getDetailed: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockMatchMapper = {
|
const mockMatchMapper = {
|
||||||
@@ -286,7 +274,7 @@ describe('Match Grpc Controller', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AD_ROUTE_PROVIDER,
|
provide: AD_ROUTE_PROVIDER,
|
||||||
useValue: mockRouteProvider,
|
useValue: bareMockGeorouter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: MatchMapper,
|
provide: MatchMapper,
|
||||||
@@ -322,16 +310,4 @@ describe('Match Grpc Controller', () => {
|
|||||||
expect(matchingPaginatedResponseDto.perPage).toBe(10);
|
expect(matchingPaginatedResponseDto.perPage).toBe(10);
|
||||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw a generic RpcException', async () => {
|
|
||||||
jest.spyOn(mockQueryBus, 'execute');
|
|
||||||
expect.assertions(3);
|
|
||||||
try {
|
|
||||||
await matchGrpcController.match(recurrentMatchRequestDto);
|
|
||||||
} catch (e: any) {
|
|
||||||
expect(e).toBeInstanceOf(RpcException);
|
|
||||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
|
||||||
}
|
|
||||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,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;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export type GeorouterSettings = {
|
|
||||||
points: boolean;
|
|
||||||
detailedDuration: boolean;
|
|
||||||
detailedDistance: boolean;
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -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',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Point } from '@modules/geography/core/domain/route.types';
|
|
||||||
|
|
||||||
export type GetRouteRequestDto = {
|
|
||||||
waypoints: Point[];
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
|
"lib": ["es2022"],
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@libs/*": ["src/libs/*"],
|
"@libs/*": ["src/libs/*"],
|
||||||
"@modules/*": ["src/modules/*"],
|
"@modules/*": ["src/modules/*"],
|
||||||
"@src/*": ["src/*"]
|
"@src/*": ["src/*"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user