mirror of
https://gitlab.com/mobicoop/v3/service/matcher.git
synced 2026-01-01 02:22:40 +00:00
Compare commits
55 Commits
v1.3.0
...
amqp-geogr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fd15430a1 | ||
|
|
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 | ||
|
|
75bb82094d | ||
|
|
8d2964659b | ||
|
|
8eb1b51457 | ||
|
|
a1ff6f9f45 | ||
|
|
c09b1b9863 | ||
|
|
337c28370d | ||
|
|
14c43afb34 | ||
|
|
73ed3a948e | ||
|
|
b0718e83df | ||
|
|
4547dcf655 | ||
|
|
c5e58db5a6 | ||
|
|
d4412a0cf1 | ||
|
|
5ca5200f1a | ||
|
|
62e4015ea7 | ||
|
|
c3e03e179c | ||
|
|
07e44a259b |
@@ -8,7 +8,7 @@ HEALTH_SERVICE_PORT=6005
|
||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"
|
||||
|
||||
# MESSAGE BROKER
|
||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
|
||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
include:
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
|
||||
##############
|
||||
# TEST STAGE #
|
||||
##############
|
||||
|
||||
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
|
||||
coverage
|
||||
.prettierrc.json
|
||||
.gitlab
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# BUILD FOR LOCAL DEVELOPMENT
|
||||
###################
|
||||
|
||||
FROM node:18-alpine3.16 As development
|
||||
FROM docker.io/node:lts-hydrogen As development
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
@@ -29,7 +29,7 @@ USER node
|
||||
# BUILD FOR PRODUCTION
|
||||
###################
|
||||
|
||||
FROM node:18-alpine3.16 As build
|
||||
FROM docker.io/node:lts-hydrogen As build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -63,7 +63,7 @@ USER node
|
||||
# PRODUCTION
|
||||
###################
|
||||
|
||||
FROM node:18-alpine3.16 As production
|
||||
FROM docker.io/node:lts-hydrogen As production
|
||||
|
||||
# Copy package.json to be able to execute migration command
|
||||
COPY --chown=node:node package*.json ./
|
||||
|
||||
@@ -181,7 +181,6 @@ If the matching is successful, you will get a result, containing :
|
||||
- **page**: the number of the page that is returned (may be different than the number of the required page, if number of results does not match with perPage parameter)
|
||||
- **perPage**: the number of results per page (as it may not be specified in the request)
|
||||
- **data**: an array of the results themselves, each including:
|
||||
- **id**: an id for the result
|
||||
- **adId**: the id of the ad that matches
|
||||
- **role**: the role of the ad owner in that match
|
||||
- **distance**: the distance in metres of the resulting carpool
|
||||
|
||||
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"
|
||||
|
||||
# MESSAGE BROKER
|
||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
|
||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||
|
||||
# REDIS
|
||||
@@ -15,9 +15,9 @@ REDIS_PASSWORD=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# IMAGES
|
||||
BROKER_IMAGE=rabbitmq:3-alpine
|
||||
REDIS_IMAGE=redis:7.0-alpine
|
||||
POSTGRES_IMAGE=postgis/postgis:15-3.3
|
||||
BROKER_IMAGE=docker.io/rabbitmq:3-alpine
|
||||
REDIS_IMAGE=docker.io/redis:7.0-alpine
|
||||
POSTGRES_IMAGE=docker.io/postgis/postgis:15-3.3
|
||||
|
||||
# DEFAULT CONFIGURATION
|
||||
|
||||
@@ -54,6 +54,3 @@ MAX_DETOUR_DURATION_RATIO=0.3
|
||||
GEOROUTER_TYPE=graphhopper
|
||||
# georouter url
|
||||
GEOROUTER_URL=http://localhost:8989
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# BUILD FOR CI TESTING
|
||||
###################
|
||||
|
||||
FROM node:18-alpine3.16
|
||||
FROM docker.io/node:lts-hydrogen
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
4344
package-lock.json
generated
4344
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
80
package.json
80
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mobicoop/matcher",
|
||||
"version": "1.3.0",
|
||||
"version": "1.5.5",
|
||||
"description": "Mobicoop V3 Matcher",
|
||||
"author": "sbriat",
|
||||
"private": true,
|
||||
@@ -24,69 +24,69 @@
|
||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
||||
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",
|
||||
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate deploy'",
|
||||
"migrate:dev": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",
|
||||
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
||||
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
||||
"migrate:deploy": "npx prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.9.9",
|
||||
"@grpc/grpc-js": "^1.9.14",
|
||||
"@grpc/proto-loader": "^0.7.10",
|
||||
"@songkeys/nestjs-redis": "^10.0.0",
|
||||
"@mobicoop/configuration-module": "^7.2.1",
|
||||
"@mobicoop/ddd-library": "^2.1.1",
|
||||
"@mobicoop/health-module": "^2.3.1",
|
||||
"@mobicoop/message-broker-module": "^2.1.1",
|
||||
"@mobicoop/configuration-module": "^8.0.0",
|
||||
"@mobicoop/ddd-library": "^2.4.3",
|
||||
"@mobicoop/health-module": "^2.3.2",
|
||||
"@mobicoop/message-broker-module": "^2.1.2",
|
||||
"@nestjs/axios": "^3.0.1",
|
||||
"@nestjs/cache-manager": "^2.1.0",
|
||||
"@nestjs/common": "^10.2.7",
|
||||
"@nestjs/cache-manager": "^2.2.0",
|
||||
"@nestjs/common": "^10.3.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.2.7",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/cqrs": "^10.2.6",
|
||||
"@nestjs/event-emitter": "^2.0.2",
|
||||
"@nestjs/microservices": "^10.2.7",
|
||||
"@nestjs/platform-express": "^10.2.7",
|
||||
"@nestjs/terminus": "^10.1.1",
|
||||
"@prisma/client": "^5.5.2",
|
||||
"axios": "^1.6.0",
|
||||
"cache-manager": "^5.2.4",
|
||||
"@nestjs/event-emitter": "^2.0.3",
|
||||
"@nestjs/microservices": "^10.3.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@nestjs/terminus": "^10.2.0",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"axios": "^1.6.5",
|
||||
"cache-manager": "^5.3.2",
|
||||
"cache-manager-ioredis-yet": "^1.2.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"geo-tz": "^7.0.7",
|
||||
"class-validator": "^0.14.1",
|
||||
"geo-tz": "^8.0.0",
|
||||
"geographiclib-geodesic": "^2.0.0",
|
||||
"got": "^13.0.0",
|
||||
"got": "^14.0.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"nestjs-request-context": "^3.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"timezonecomplete": "^5.12.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.2.1",
|
||||
"@nestjs/schematics": "^10.0.3",
|
||||
"@nestjs/testing": "^10.2.7",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/jest": "29.5.7",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/supertest": "^2.0.15",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||
"@typescript-eslint/parser": "^6.9.1",
|
||||
"@nestjs/cli": "^10.3.0",
|
||||
"@nestjs/schematics": "^10.1.0",
|
||||
"@nestjs/testing": "^10.3.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "29.7.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.5.2",
|
||||
"prettier": "^3.2.3",
|
||||
"prisma": "^5.8.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"supertest": "^6.3.4",
|
||||
"ts-jest": "29.1.1",
|
||||
"ts-loader": "^9.5.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
||||
@@ -3,24 +3,18 @@ export const SERVICE_NAME = 'matcher';
|
||||
|
||||
// grpc
|
||||
export const GRPC_PACKAGE_NAME = 'matcher';
|
||||
export const GRPC_GEOGRAPHY_PACKAGE_NAME = 'geography';
|
||||
export const GRPC_GEOROUTER_SERVICE_NAME = 'GeorouterService';
|
||||
|
||||
// messaging
|
||||
// messaging output
|
||||
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
|
||||
export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
||||
'matcher-ad.creation-failed';
|
||||
|
||||
// messaging input
|
||||
export const AD_CREATED_MESSAGE_HANDLER = 'adCreated';
|
||||
export const AD_CREATED_ROUTING_KEY = 'ad.created';
|
||||
export const AD_CREATED_QUEUE = 'matcher-ad-created';
|
||||
export const AD_UPDATED_MESSAGE_HANDLER = 'adUpdated';
|
||||
export const AD_UPDATED_ROUTING_KEY = 'ad.updated';
|
||||
export const AD_UPDATED_QUEUE = 'matcher-ad-updated';
|
||||
export const AD_DELETED_MESSAGE_HANDLER = 'adDeleted';
|
||||
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
|
||||
export const AD_DELETED_QUEUE = 'matcher-ad-deleted';
|
||||
|
||||
// configuration
|
||||
export const SERVICE_CONFIGURATION_SET_QUEUE = 'matcher-configuration-set';
|
||||
export const SERVICE_CONFIGURATION_DELETE_QUEUE =
|
||||
'matcher-configuration-delete';
|
||||
export const SERVICE_CONFIGURATION_PROPAGATE_QUEUE =
|
||||
'matcher-configuration-propagate';
|
||||
export const AD_CREATED_QUEUE = 'matcher.ad.created';
|
||||
|
||||
// health
|
||||
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
|
||||
import { HealthModuleOptions } from '@mobicoop/health-module/dist/core/domain/types/health.types';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { GeographyModule } from '@modules/geography/geography.module';
|
||||
import producerServicesConfig from './config/producer-services.config';
|
||||
import {
|
||||
HEALTH_AD_REPOSITORY,
|
||||
HEALTH_CRITICAL_LOGGING_KEY,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
ConfigModule.forRoot({ isGlobal: true, load: [producerServicesConfig] }),
|
||||
EventEmitterModule.forRoot(),
|
||||
RequestContextModule,
|
||||
HealthModule.forRootAsync({
|
||||
|
||||
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,
|
||||
}));
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
ConfigurationDomainGet,
|
||||
ConfigurationType,
|
||||
} from '@mobicoop/configuration-module';
|
||||
import { KeyType, Type } from '@mobicoop/configuration-module';
|
||||
|
||||
export const CARPOOL_CONFIG_ROLE = 'role';
|
||||
export const CARPOOL_CONFIG_SEATS_PROPOSED = 'seatsProposed';
|
||||
@@ -9,25 +6,25 @@ export const CARPOOL_CONFIG_SEATS_REQUESTED = 'seatsRequested';
|
||||
export const CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN = 'departureTimeMargin';
|
||||
export const CARPOOL_CONFIG_STRICT_FREQUENCY = 'strictFrequency';
|
||||
|
||||
export const CarpoolConfig: ConfigurationDomainGet[] = [
|
||||
export const CarpoolKeyTypes: KeyType[] = [
|
||||
{
|
||||
key: CARPOOL_CONFIG_ROLE,
|
||||
type: ConfigurationType.STRING,
|
||||
type: Type.STRING,
|
||||
},
|
||||
{
|
||||
key: CARPOOL_CONFIG_SEATS_PROPOSED,
|
||||
type: ConfigurationType.INT,
|
||||
type: Type.INT,
|
||||
},
|
||||
{
|
||||
key: CARPOOL_CONFIG_SEATS_REQUESTED,
|
||||
type: ConfigurationType.INT,
|
||||
type: Type.INT,
|
||||
},
|
||||
{
|
||||
key: CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN,
|
||||
type: ConfigurationType.INT,
|
||||
type: Type.INT,
|
||||
},
|
||||
{
|
||||
key: CARPOOL_CONFIG_STRICT_FREQUENCY,
|
||||
type: ConfigurationType.BOOLEAN,
|
||||
type: Type.BOOLEAN,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -18,3 +18,5 @@ export const OUTPUT_DATETIME_TRANSFORMER = Symbol(
|
||||
export const AD_CONFIGURATION_REPOSITORY = Symbol(
|
||||
'AD_CONFIGURATION_REPOSITORY',
|
||||
);
|
||||
export const GEOGRAPHY_PACKAGE = Symbol('GEOGRAPHY_PACKAGE');
|
||||
export const GEOGRAPHY_SERVICE = Symbol('GEOGRAPHY_SERVICE');
|
||||
|
||||
@@ -5,14 +5,14 @@ import {
|
||||
AD_REPOSITORY,
|
||||
AD_DIRECTION_ENCODER,
|
||||
AD_ROUTE_PROVIDER,
|
||||
AD_GET_BASIC_ROUTE_CONTROLLER,
|
||||
TIMEZONE_FINDER,
|
||||
TIME_CONVERTER,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
AD_GET_DETAILED_ROUTE_CONTROLLER,
|
||||
OUTPUT_DATETIME_TRANSFORMER,
|
||||
MATCHING_REPOSITORY,
|
||||
AD_CONFIGURATION_REPOSITORY,
|
||||
GEOGRAPHY_PACKAGE,
|
||||
GEOGRAPHY_SERVICE,
|
||||
} from './ad.di-tokens';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { AdRepository } from './infrastructure/ad.repository';
|
||||
@@ -20,8 +20,6 @@ import { PrismaService } from './infrastructure/prisma.service';
|
||||
import { AdMapper } from './ad.mapper';
|
||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||
import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller';
|
||||
import { RouteProvider } from './infrastructure/route-provider';
|
||||
import { GeographyModule } from '@modules/geography/geography.module';
|
||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
||||
@@ -29,7 +27,6 @@ import { MatchQueryHandler } from './core/application/queries/match/match.query-
|
||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||
import { TimeConverter } from './infrastructure/time-converter';
|
||||
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller';
|
||||
import { MatchMapper } from './match.mapper';
|
||||
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
||||
import { MatchingRepository } from './infrastructure/matching.repository';
|
||||
@@ -43,9 +40,45 @@ import {
|
||||
RedisModuleOptions,
|
||||
} from '@songkeys/nestjs-redis';
|
||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
||||
import { Georouter } from './infrastructure/georouter';
|
||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||
import { GRPC_GEOGRAPHY_PACKAGE_NAME } from '@src/app.constants';
|
||||
import { join } from 'path';
|
||||
|
||||
const imports = [
|
||||
CqrsModule,
|
||||
ClientsModule.registerAsync([
|
||||
{
|
||||
name: GEOGRAPHY_PACKAGE,
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
transport: Transport.GRPC,
|
||||
options: {
|
||||
package: GRPC_GEOGRAPHY_PACKAGE_NAME,
|
||||
protoPath: join(__dirname, '/infrastructure/georouter.proto'),
|
||||
url: `${configService.get<string>(
|
||||
'producerServices.geographyUrl',
|
||||
)}:${configService.get<string>('producerServices.geographyPort')}`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
]),
|
||||
ClientsModule.register([
|
||||
{
|
||||
name: GEOGRAPHY_SERVICE,
|
||||
transport: Transport.RMQ,
|
||||
options: {
|
||||
//TODO read from config
|
||||
urls: [`${process.env.MESSAGE_BROKER_URI}`],
|
||||
queue: 'geography',
|
||||
queueOptions: {
|
||||
durable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
CacheModule.registerAsync<RedisClientOptions>({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
@@ -80,6 +113,10 @@ const grpcControllers = [MatchGrpcController];
|
||||
|
||||
const messageHandlers = [AdCreatedMessageHandler];
|
||||
|
||||
const eventHandlers: Provider[] = [
|
||||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [CreateAdService];
|
||||
|
||||
const queryHandlers: Provider[] = [MatchQueryHandler];
|
||||
@@ -117,15 +154,7 @@ const adapters: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: AD_ROUTE_PROVIDER,
|
||||
useClass: RouteProvider,
|
||||
},
|
||||
{
|
||||
provide: AD_GET_BASIC_ROUTE_CONTROLLER,
|
||||
useClass: GetBasicRouteController,
|
||||
},
|
||||
{
|
||||
provide: AD_GET_DETAILED_ROUTE_CONTROLLER,
|
||||
useClass: GetDetailedRouteController,
|
||||
useClass: Georouter,
|
||||
},
|
||||
{
|
||||
provide: TIMEZONE_FINDER,
|
||||
@@ -150,6 +179,7 @@ const adapters: Provider[] = [
|
||||
controllers: [...grpcControllers],
|
||||
providers: [
|
||||
...messageHandlers,
|
||||
...eventHandlers,
|
||||
...commandHandlers,
|
||||
...queryHandlers,
|
||||
...mappers,
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { CreateAdCommand } from './create-ad.command';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AggregateID,
|
||||
ConflictException,
|
||||
MessagePublisherPort,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||
import { RouteProviderPort } from '../../ports/route-provider.port';
|
||||
import { Role } from '@modules/ad/core/domain/ad.types';
|
||||
import {
|
||||
Path,
|
||||
@@ -17,14 +24,19 @@ import {
|
||||
import { Waypoint } from '../../types/waypoint.type';
|
||||
import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
import { Point } from '@modules/geography/core/domain/route.types';
|
||||
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event';
|
||||
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { GeorouterPort } from '../../ports/georouter.port';
|
||||
|
||||
@CommandHandler(CreateAdCommand)
|
||||
export class CreateAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
@Inject(AD_ROUTE_PROVIDER)
|
||||
private readonly routeProvider: RouteProviderPort,
|
||||
private readonly routeProvider: GeorouterPort,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||
@@ -44,17 +56,6 @@ export class CreateAdService implements ICommandHandler {
|
||||
);
|
||||
|
||||
let typedRoutes: TypedRoute[];
|
||||
try {
|
||||
typedRoutes = await Promise.all(
|
||||
pathCreator.getBasePaths().map(async (path: Path) => ({
|
||||
type: path.type,
|
||||
route: await this.routeProvider.getBasic(path.waypoints),
|
||||
})),
|
||||
);
|
||||
} catch (e: any) {
|
||||
throw new Error('Unable to find a route for given waypoints');
|
||||
}
|
||||
|
||||
let driverDistance: number | undefined;
|
||||
let driverDuration: number | undefined;
|
||||
let passengerDistance: number | undefined;
|
||||
@@ -62,25 +63,26 @@ export class CreateAdService implements ICommandHandler {
|
||||
let points: PointValueObject[] | undefined;
|
||||
let fwdAzimuth: number | undefined;
|
||||
let backAzimuth: number | undefined;
|
||||
|
||||
try {
|
||||
typedRoutes.forEach((typedRoute: TypedRoute) => {
|
||||
if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) {
|
||||
driverDistance = typedRoute.route.distance;
|
||||
driverDuration = typedRoute.route.duration;
|
||||
points = typedRoute.route.points.map(
|
||||
(point: Point) =>
|
||||
new PointValueObject({
|
||||
lon: point.lon,
|
||||
lat: point.lat,
|
||||
}),
|
||||
);
|
||||
fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||
backAzimuth = typedRoute.route.backAzimuth;
|
||||
}
|
||||
if ([PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)) {
|
||||
passengerDistance = typedRoute.route.distance;
|
||||
passengerDuration = typedRoute.route.duration;
|
||||
if (!points)
|
||||
try {
|
||||
typedRoutes = await Promise.all(
|
||||
pathCreator.getBasePaths().map(async (path: Path) => ({
|
||||
type: path.type,
|
||||
route: await this.routeProvider.getRoute({
|
||||
waypoints: path.waypoints,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
} catch (e: any) {
|
||||
throw new Error('Unable to find a route for given waypoints');
|
||||
}
|
||||
|
||||
try {
|
||||
typedRoutes.forEach((typedRoute: TypedRoute) => {
|
||||
if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) {
|
||||
driverDistance = typedRoute.route.distance;
|
||||
driverDuration = typedRoute.route.duration;
|
||||
points = typedRoute.route.points.map(
|
||||
(point: Point) =>
|
||||
new PointValueObject({
|
||||
@@ -88,40 +90,74 @@ export class CreateAdService implements ICommandHandler {
|
||||
lat: point.lat,
|
||||
}),
|
||||
);
|
||||
if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||
if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth;
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new Error('Invalid route');
|
||||
}
|
||||
const ad = AdEntity.create({
|
||||
id: command.id,
|
||||
driver: command.driver,
|
||||
passenger: command.passenger,
|
||||
frequency: command.frequency,
|
||||
fromDate: command.fromDate,
|
||||
toDate: command.toDate,
|
||||
schedule: command.schedule,
|
||||
seatsProposed: command.seatsProposed,
|
||||
seatsRequested: command.seatsRequested,
|
||||
strict: command.strict,
|
||||
waypoints: command.waypoints,
|
||||
points: points as PointValueObject[],
|
||||
driverDistance,
|
||||
driverDuration,
|
||||
passengerDistance,
|
||||
passengerDuration,
|
||||
fwdAzimuth: fwdAzimuth as number,
|
||||
backAzimuth: backAzimuth as number,
|
||||
});
|
||||
try {
|
||||
await this.repository.insertExtra(ad, 'ad');
|
||||
return ad.id;
|
||||
} catch (error: any) {
|
||||
if (error instanceof ConflictException) {
|
||||
throw new AdAlreadyExistsException(error);
|
||||
fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||
backAzimuth = typedRoute.route.backAzimuth;
|
||||
}
|
||||
if (
|
||||
[PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)
|
||||
) {
|
||||
passengerDistance = typedRoute.route.distance;
|
||||
passengerDuration = typedRoute.route.duration;
|
||||
if (!points)
|
||||
points = typedRoute.route.points.map(
|
||||
(point: Point) =>
|
||||
new PointValueObject({
|
||||
lon: point.lon,
|
||||
lat: point.lat,
|
||||
}),
|
||||
);
|
||||
if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||
if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth;
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new Error('Invalid route');
|
||||
}
|
||||
|
||||
const ad = AdEntity.create({
|
||||
id: command.id,
|
||||
driver: command.driver,
|
||||
passenger: command.passenger,
|
||||
frequency: command.frequency,
|
||||
fromDate: command.fromDate,
|
||||
toDate: command.toDate,
|
||||
schedule: command.schedule,
|
||||
seatsProposed: command.seatsProposed,
|
||||
seatsRequested: command.seatsRequested,
|
||||
strict: command.strict,
|
||||
waypoints: command.waypoints,
|
||||
points: points as PointValueObject[],
|
||||
driverDistance,
|
||||
driverDuration,
|
||||
passengerDistance,
|
||||
passengerDuration,
|
||||
fwdAzimuth: fwdAzimuth as number,
|
||||
backAzimuth: backAzimuth as number,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.repository.insertExtra(ad, 'ad');
|
||||
return ad.id;
|
||||
} catch (error: any) {
|
||||
if (error instanceof ConflictException) {
|
||||
throw new AdAlreadyExistsException(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) {
|
||||
this.candidates = await processor.execute(this.candidates);
|
||||
}
|
||||
// console.log(JSON.stringify(this.candidates, null, 2));
|
||||
return this.candidates.map((candidate: CandidateEntity) =>
|
||||
MatchEntity.create({
|
||||
adId: candidate.id,
|
||||
|
||||
@@ -33,7 +33,7 @@ export class PassengerOrientedCarpoolPathCompleter extends Completer {
|
||||
}),
|
||||
),
|
||||
candidate.getProps().role == Role.PASSENGER
|
||||
? candidate.getProps().driverWaypoints.map(
|
||||
? candidate.getProps().passengerWaypoints.map(
|
||||
(waypoint: PointProps) =>
|
||||
new Point({
|
||||
lon: waypoint.lon,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { Completer } from './completer.abstract';
|
||||
import { MatchQuery } from '../match.query';
|
||||
import { Step } from '../../../types/step.type';
|
||||
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
||||
import { RouteResponse } from '../../../ports/georouter.port';
|
||||
import { Step } from '../../../types/step.type';
|
||||
import { MatchQuery } from '../match.query';
|
||||
import { Completer } from './completer.abstract';
|
||||
|
||||
export class RouteCompleter extends Completer {
|
||||
protected readonly type: RouteCompleterType;
|
||||
@@ -18,23 +19,20 @@ export class RouteCompleter extends Completer {
|
||||
candidates.map(async (candidate: CandidateEntity) => {
|
||||
switch (this.type) {
|
||||
case RouteCompleterType.BASIC:
|
||||
const basicCandidateRoute = await this.query.routeProvider.getBasic(
|
||||
(candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
|
||||
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
|
||||
),
|
||||
);
|
||||
const basicCandidateRoute = await this._getRoute(candidate, {
|
||||
points: true,
|
||||
steps: false,
|
||||
});
|
||||
candidate.setMetrics(
|
||||
basicCandidateRoute.distance,
|
||||
basicCandidateRoute.duration,
|
||||
);
|
||||
break;
|
||||
case RouteCompleterType.DETAILED:
|
||||
const detailedCandidateRoute =
|
||||
await this.query.routeProvider.getDetailed(
|
||||
(candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
|
||||
(carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
|
||||
),
|
||||
);
|
||||
const detailedCandidateRoute = await this._getRoute(candidate, {
|
||||
points: true,
|
||||
steps: true,
|
||||
});
|
||||
candidate.setSteps(detailedCandidateRoute.steps as Step[]);
|
||||
break;
|
||||
}
|
||||
@@ -43,6 +41,20 @@ export class RouteCompleter extends Completer {
|
||||
);
|
||||
return candidates;
|
||||
};
|
||||
|
||||
_getRoute = async (
|
||||
candidate: CandidateEntity,
|
||||
detailsSettings: { points: boolean; steps: boolean },
|
||||
): Promise<RouteResponse> =>
|
||||
this.query.routeProvider.getRoute({
|
||||
waypoints: (candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
|
||||
(carpoolPathItem: CarpoolPathItem) => ({
|
||||
lon: carpoolPathItem.lon,
|
||||
lat: carpoolPathItem.lat,
|
||||
}),
|
||||
),
|
||||
detailsSettings: detailsSettings,
|
||||
});
|
||||
}
|
||||
|
||||
export enum RouteCompleterType {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Paginator } from '@mobicoop/ddd-library';
|
||||
import { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
|
||||
import { MatchingRepositoryPort } from '../../ports/matching.repository.port';
|
||||
import {
|
||||
ConfigurationDomain,
|
||||
Domain,
|
||||
Configurator,
|
||||
GetConfigurationRepositoryPort,
|
||||
} from '@mobicoop/configuration-module';
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
CARPOOL_CONFIG_SEATS_PROPOSED,
|
||||
CARPOOL_CONFIG_SEATS_REQUESTED,
|
||||
CARPOOL_CONFIG_STRICT_FREQUENCY,
|
||||
CarpoolConfig,
|
||||
CarpoolKeyTypes,
|
||||
} from '@modules/ad/ad.constants';
|
||||
import {
|
||||
MATCH_CONFIG_ALGORITHM,
|
||||
@@ -57,18 +57,12 @@ export class MatchQueryHandler implements IQueryHandler {
|
||||
|
||||
execute = async (query: MatchQuery): Promise<MatchingResult> => {
|
||||
const carpoolConfigurator: Configurator =
|
||||
await this.configurationRepository.mget(
|
||||
ConfigurationDomain.CARPOOL,
|
||||
CarpoolConfig,
|
||||
);
|
||||
await this.configurationRepository.mget(Domain.CARPOOL, CarpoolKeyTypes);
|
||||
const matchConfigurator: Configurator =
|
||||
await this.configurationRepository.mget(
|
||||
ConfigurationDomain.MATCH,
|
||||
MatchConfig,
|
||||
);
|
||||
await this.configurationRepository.mget(Domain.MATCH, MatchConfig);
|
||||
const paginationConfigurator: Configurator =
|
||||
await this.configurationRepository.mget(
|
||||
ConfigurationDomain.PAGINATION,
|
||||
Domain.PAGINATION,
|
||||
PaginationConfig,
|
||||
);
|
||||
query
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { QueryBase } from '@mobicoop/ddd-library';
|
||||
import { AlgorithmType } from '../../types/algorithm.types';
|
||||
import { Waypoint } from '../../types/waypoint.type';
|
||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
|
||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||
import { RouteProviderPort } from '../../ports/route-provider.port';
|
||||
import {
|
||||
Path,
|
||||
PathCreator,
|
||||
@@ -12,7 +7,12 @@ import {
|
||||
TypedRoute,
|
||||
} from '@modules/ad/core/domain/path-creator.service';
|
||||
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
|
||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||
import { GeorouterPort } from '../../ports/georouter.port';
|
||||
import { AlgorithmType } from '../../types/algorithm.types';
|
||||
import { Route } from '../../types/route.type';
|
||||
import { Waypoint } from '../../types/waypoint.type';
|
||||
|
||||
export class MatchQuery extends QueryBase {
|
||||
id?: string;
|
||||
@@ -40,9 +40,10 @@ export class MatchQuery extends QueryBase {
|
||||
passengerRoute?: Route;
|
||||
backAzimuth?: number;
|
||||
private readonly originWaypoint: Waypoint;
|
||||
routeProvider: RouteProviderPort;
|
||||
routeProvider: GeorouterPort;
|
||||
|
||||
constructor(props: MatchRequestDto, routeProvider: RouteProviderPort) {
|
||||
// TODO: remove MatchRequestDto depency (here core domain depends on interface /!\)
|
||||
constructor(props: MatchRequestDto, routeProvider: GeorouterPort) {
|
||||
super();
|
||||
this.id = props.id;
|
||||
this.driver = props.driver;
|
||||
@@ -207,7 +208,12 @@ export class MatchQuery extends QueryBase {
|
||||
await Promise.all(
|
||||
pathCreator.getBasePaths().map(async (path: Path) => ({
|
||||
type: path.type,
|
||||
route: await this.routeProvider.getBasic(path.waypoints),
|
||||
route: await this.routeProvider.getRoute({
|
||||
waypoints: path.waypoints.map((p) => ({
|
||||
lon: p.lon,
|
||||
lat: p.lat,
|
||||
})),
|
||||
}),
|
||||
})),
|
||||
)
|
||||
).forEach((typedRoute: TypedRoute) => {
|
||||
@@ -222,6 +228,7 @@ export class MatchQuery extends QueryBase {
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.log(e.stack || e);
|
||||
throw new Error('Unable to find a route for given waypoints');
|
||||
}
|
||||
return this;
|
||||
|
||||
@@ -174,7 +174,7 @@ export class PassengerOrientedSelector extends Selector {
|
||||
private _whereSchedule = (role: Role): string => {
|
||||
const schedule: string[] = [];
|
||||
// we need full dates to compare times, because margins can lead to compare on previous or next day
|
||||
// -first we establish a base calendar (up to a week)
|
||||
// - first we establish a base calendar (up to a week)
|
||||
const scheduleDates: Date[] = this._datesBetweenBoundaries(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AdProps, CreateAdProps } from './ad.types';
|
||||
import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event';
|
||||
|
||||
export class AdEntity extends AggregateRoot<AdProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
|
||||
static create = (create: CreateAdProps): AdEntity => {
|
||||
const props: AdProps = { ...create };
|
||||
return new AdEntity({ id: create.id, props });
|
||||
const ad = new AdEntity({ id: create.id, props });
|
||||
ad.addEvent(
|
||||
new MatcherAdCreatedDomainEvent({
|
||||
metadata: {
|
||||
correlationId: create.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
aggregateId: create.id,
|
||||
driverDistance: create.driverDistance,
|
||||
driverDuration: create.driverDuration,
|
||||
passengerDistance: create.passengerDistance,
|
||||
passengerDuration: create.passengerDuration,
|
||||
fwdAzimuth: create.fwdAzimuth,
|
||||
backAzimuth: create.backAzimuth,
|
||||
}),
|
||||
);
|
||||
return ad;
|
||||
};
|
||||
|
||||
validate(): void {
|
||||
|
||||
@@ -53,13 +53,18 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||
* This is a tedious process : additional information can be found in deeper methods !
|
||||
*/
|
||||
createJourneys = (): CandidateEntity => {
|
||||
this.props.journeys = this.props.driverSchedule
|
||||
// first we create the journeys
|
||||
.map((driverScheduleItem: ScheduleItem) =>
|
||||
this._createJourney(driverScheduleItem),
|
||||
)
|
||||
// then we filter the ones with invalid pickups
|
||||
.filter((journey: Journey) => journey.hasValidPickUp());
|
||||
try {
|
||||
this.props.journeys = this.props.driverSchedule
|
||||
// first we create the journeys
|
||||
.map((driverScheduleItem: ScheduleItem) =>
|
||||
this._createJourney(driverScheduleItem),
|
||||
)
|
||||
// then we filter the ones with invalid pickups
|
||||
.filter((journey: Journey) => journey.hasValidPickUp());
|
||||
} catch (e) {
|
||||
// irrelevant journeys fall here
|
||||
// eg. no available day for the given date range
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
@@ -235,8 +235,8 @@ export class CarpoolPathCreator {
|
||||
index == 0
|
||||
? Target.START
|
||||
: index == waypoints.length - 1
|
||||
? Target.FINISH
|
||||
: Target.INTERMEDIATE;
|
||||
? Target.FINISH
|
||||
: Target.INTERMEDIATE;
|
||||
|
||||
/**
|
||||
* Consolidate carpoolPath by removing duplicate actors (eg. driver with neutral and start or finish target)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Role } from './ad.types';
|
||||
import { Point } from './value-objects/point.value-object';
|
||||
import { PathCreatorException } from './match.errors';
|
||||
import { Route } from '../application/types/route.type';
|
||||
import { Role } from './ad.types';
|
||||
import { PathCreatorException } from './match.errors';
|
||||
import { Point } from './value-objects/point.value-object';
|
||||
|
||||
export class PathCreator {
|
||||
constructor(
|
||||
|
||||
@@ -38,24 +38,22 @@ export class Journey extends ValueObject<JourneyProps> {
|
||||
actorTime.target == Target.START,
|
||||
) as ActorTime,
|
||||
) as JourneyItem;
|
||||
const passengerDepartureActorTime =
|
||||
const passengerDepartureActorTime: ActorTime =
|
||||
passengerDepartureJourneyItem.actorTimes.find(
|
||||
(actorTime: ActorTime) =>
|
||||
actorTime.role == Role.PASSENGER && actorTime.target == Target.START,
|
||||
) as ActorTime;
|
||||
const driverNeutralActorTime =
|
||||
passengerDepartureJourneyItem.actorTimes.find(
|
||||
(actorTime: ActorTime) =>
|
||||
actorTime.role == Role.DRIVER && actorTime.target == Target.NEUTRAL,
|
||||
) as ActorTime;
|
||||
const driverActorTime = passengerDepartureJourneyItem.actorTimes.find(
|
||||
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
||||
) as ActorTime;
|
||||
return (
|
||||
(passengerDepartureActorTime.firstMinDatetime <=
|
||||
driverNeutralActorTime.firstMaxDatetime &&
|
||||
driverNeutralActorTime.firstMaxDatetime <=
|
||||
driverActorTime.firstMaxDatetime &&
|
||||
driverActorTime.firstMaxDatetime <=
|
||||
passengerDepartureActorTime.firstMaxDatetime) ||
|
||||
(passengerDepartureActorTime.firstMinDatetime <=
|
||||
driverNeutralActorTime.firstMinDatetime &&
|
||||
driverNeutralActorTime.firstMinDatetime <=
|
||||
driverActorTime.firstMinDatetime &&
|
||||
driverActorTime.firstMinDatetime <=
|
||||
passengerDepartureActorTime.firstMaxDatetime)
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
26
src/modules/ad/infrastructure/georouter.ts
Normal file
26
src/modules/ad/infrastructure/georouter.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ClientProxy } from '@nestjs/microservices';
|
||||
import { Observable, lastValueFrom } from 'rxjs';
|
||||
import { GEOGRAPHY_SERVICE } from '../ad.di-tokens';
|
||||
import {
|
||||
GeorouterPort,
|
||||
RouteRequest,
|
||||
RouteResponse,
|
||||
} from '../core/application/ports/georouter.port';
|
||||
|
||||
interface GeorouterService {
|
||||
getRoute(request: RouteRequest): Observable<RouteResponse>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class Georouter implements GeorouterPort {
|
||||
private georouterService: GeorouterService;
|
||||
|
||||
constructor(
|
||||
@Inject(GEOGRAPHY_SERVICE) private readonly client: ClientProxy,
|
||||
) {}
|
||||
|
||||
getRoute = async (request: RouteRequest): Promise<RouteResponse> => {
|
||||
return lastValueFrom(this.client.send('getRoute', request));
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDecimal,
|
||||
IsEnum,
|
||||
IsISO8601,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
Max,
|
||||
@@ -93,7 +93,7 @@ export class MatchRequestDto {
|
||||
useProportion?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsDecimal()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
proportion?: number;
|
||||
@@ -109,13 +109,13 @@ export class MatchRequestDto {
|
||||
azimuthMargin?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDecimal()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
maxDetourDistanceRatio?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDecimal()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
maxDetourDurationRatio?: number;
|
||||
|
||||
@@ -8,10 +8,10 @@ import { MatchRequestDto } from './dtos/match.request.dto';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
|
||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
@@ -24,7 +24,7 @@ export class MatchGrpcController {
|
||||
constructor(
|
||||
private readonly queryBus: QueryBus,
|
||||
@Inject(AD_ROUTE_PROVIDER)
|
||||
private readonly routeProvider: RouteProviderPort,
|
||||
private readonly routeProvider: GeorouterPort,
|
||||
private readonly matchMapper: MatchMapper,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -19,14 +19,13 @@ message MatchRequest {
|
||||
AlgorithmType algorithmType = 10;
|
||||
int32 remoteness = 11;
|
||||
bool useProportion = 12;
|
||||
int32 proportion = 13;
|
||||
float proportion = 13;
|
||||
bool useAzimuth = 14;
|
||||
int32 azimuthMargin = 15;
|
||||
float maxDetourDistanceRatio = 16;
|
||||
float maxDetourDurationRatio = 17;
|
||||
int32 identifier = 18;
|
||||
optional int32 page = 19;
|
||||
optional int32 perPage = 20;
|
||||
optional int32 page = 18;
|
||||
optional int32 perPage = 19;
|
||||
}
|
||||
|
||||
message ScheduleItem {
|
||||
@@ -57,18 +56,17 @@ enum AlgorithmType {
|
||||
}
|
||||
|
||||
message Match {
|
||||
string id = 1;
|
||||
string adId = 2;
|
||||
string role = 3;
|
||||
int32 distance = 4;
|
||||
int32 duration = 5;
|
||||
int32 initialDistance = 6;
|
||||
int32 initialDuration = 7;
|
||||
int32 distanceDetour = 8;
|
||||
int32 durationDetour = 9;
|
||||
double distanceDetourPercentage = 10;
|
||||
double durationDetourPercentage = 11;
|
||||
repeated Journey journeys = 12;
|
||||
string adId = 1;
|
||||
string role = 2;
|
||||
int32 distance = 3;
|
||||
int32 duration = 4;
|
||||
int32 initialDistance = 5;
|
||||
int32 initialDuration = 6;
|
||||
int32 distanceDetour = 7;
|
||||
int32 durationDetour = 8;
|
||||
double distanceDetourPercentage = 9;
|
||||
double durationDetourPercentage = 10;
|
||||
repeated Journey journeys = 11;
|
||||
}
|
||||
|
||||
message Journey {
|
||||
|
||||
@@ -3,7 +3,10 @@ import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||
import { Ad } from './ad.types';
|
||||
import { AD_CREATED_MESSAGE_HANDLER } from '@src/app.constants';
|
||||
import {
|
||||
AD_CREATED_MESSAGE_HANDLER,
|
||||
AD_CREATED_ROUTING_KEY,
|
||||
} from '@src/app.constants';
|
||||
|
||||
@Injectable()
|
||||
export class AdCreatedMessageHandler {
|
||||
@@ -11,6 +14,7 @@ export class AdCreatedMessageHandler {
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: AD_CREATED_MESSAGE_HANDLER,
|
||||
routingKey: AD_CREATED_ROUTING_KEY,
|
||||
})
|
||||
public async adCreated(message: string) {
|
||||
try {
|
||||
@@ -30,8 +34,9 @@ export class AdCreatedMessageHandler {
|
||||
waypoints: createdAd.waypoints,
|
||||
}),
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
} catch (error: any) {
|
||||
// do not throw error to acknowledge incoming message
|
||||
// error handling should be done in the command handler, if relevant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
ConfigurationDomainGet,
|
||||
ConfigurationType,
|
||||
} from '@mobicoop/configuration-module';
|
||||
import { KeyType, Type } from '@mobicoop/configuration-module';
|
||||
|
||||
export const MATCH_CONFIG_ALGORITHM = 'algorithm';
|
||||
export const MATCH_CONFIG_REMOTENESS = 'remoteness';
|
||||
@@ -13,44 +10,44 @@ export const MATCH_CONFIG_MAX_DETOUR_DISTANCE_RATIO = 'maxDetourDistanceRatio';
|
||||
export const MATCH_CONFIG_MAX_DETOUR_DURATION_RATIO = 'maxDetourDurationRatio';
|
||||
export const PAGINATION_CONFIG_PER_PAGE = 'perPage';
|
||||
|
||||
export const MatchConfig: ConfigurationDomainGet[] = [
|
||||
export const MatchConfig: KeyType[] = [
|
||||
{
|
||||
key: MATCH_CONFIG_ALGORITHM,
|
||||
type: ConfigurationType.STRING,
|
||||
type: Type.STRING,
|
||||
},
|
||||
{
|
||||
key: MATCH_CONFIG_REMOTENESS,
|
||||
type: ConfigurationType.INT,
|
||||
type: Type.INT,
|
||||
},
|
||||
{
|
||||
key: MATCH_CONFIG_USE_PROPORTION,
|
||||
type: ConfigurationType.BOOLEAN,
|
||||
type: Type.BOOLEAN,
|
||||
},
|
||||
{
|
||||
key: MATCH_CONFIG_PROPORTION,
|
||||
type: ConfigurationType.FLOAT,
|
||||
type: Type.FLOAT,
|
||||
},
|
||||
{
|
||||
key: MATCH_CONFIG_USE_AZIMUTH,
|
||||
type: ConfigurationType.BOOLEAN,
|
||||
type: Type.BOOLEAN,
|
||||
},
|
||||
{
|
||||
key: MATCH_CONFIG_AZIMUTH_MARGIN,
|
||||
type: ConfigurationType.INT,
|
||||
type: Type.INT,
|
||||
},
|
||||
{
|
||||
key: MATCH_CONFIG_MAX_DETOUR_DISTANCE_RATIO,
|
||||
type: ConfigurationType.FLOAT,
|
||||
type: Type.FLOAT,
|
||||
},
|
||||
{
|
||||
key: MATCH_CONFIG_MAX_DETOUR_DURATION_RATIO,
|
||||
type: ConfigurationType.FLOAT,
|
||||
type: Type.FLOAT,
|
||||
},
|
||||
];
|
||||
|
||||
export const PaginationConfig: ConfigurationDomainGet[] = [
|
||||
export const PaginationConfig: KeyType[] = [
|
||||
{
|
||||
key: PAGINATION_CONFIG_PER_PAGE,
|
||||
type: ConfigurationType.INT,
|
||||
type: Type.INT,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import {
|
||||
Algorithm,
|
||||
Selector,
|
||||
@@ -9,6 +8,7 @@ import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -29,11 +29,6 @@ const destinationWaypoint: Waypoint = {
|
||||
country: 'France',
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
const matchQuery = new MatchQuery(
|
||||
{
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
@@ -46,13 +41,15 @@ const matchQuery = new MatchQuery(
|
||||
],
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
mockRouteProvider,
|
||||
bareMockGeorouter,
|
||||
);
|
||||
|
||||
const mockAdRepository: AdRepositoryPort = {
|
||||
insertExtra: jest.fn(),
|
||||
findOneById: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findAllByIds: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateWhere: jest.fn(),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { ConflictException } from '@mobicoop/ddd-library';
|
||||
@@ -7,8 +11,8 @@ import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
||||
|
||||
const originWaypoint: PointProps = {
|
||||
lat: 48.689445,
|
||||
@@ -58,8 +62,8 @@ const mockAdRepository = {
|
||||
}),
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest
|
||||
const mockRouteProvider: GeorouterPort = {
|
||||
getRoute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
@@ -93,7 +97,10 @@ const mockRouteProvider: RouteProviderPort = {
|
||||
},
|
||||
],
|
||||
})),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('create-ad.service', () => {
|
||||
@@ -110,6 +117,10 @@ describe('create-ad.service', () => {
|
||||
provide: AD_ROUTE_PROVIDER,
|
||||
useValue: mockRouteProvider,
|
||||
},
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
CreateAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { Target } from '@modules/ad/core/domain/candidate.types';
|
||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
|
||||
import { simpleMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -42,24 +43,7 @@ const matchQuery = new MatchQuery(
|
||||
strict: false,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn().mockImplementation(() => ({
|
||||
distance: 350101,
|
||||
duration: 14422,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
})),
|
||||
getDetailed: jest.fn().mockImplementation(() => ({
|
||||
distance: 350102,
|
||||
duration: 14423,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
})),
|
||||
},
|
||||
simpleMockGeorouter,
|
||||
);
|
||||
|
||||
const candidate: CandidateEntity = CandidateEntity.create({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
|
||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
|
||||
strict: false,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
},
|
||||
bareMockGeorouter,
|
||||
);
|
||||
|
||||
const candidate: CandidateEntity = CandidateEntity.create({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
ConfigurationDomain,
|
||||
ConfigurationDomainGet,
|
||||
Domain,
|
||||
KeyType,
|
||||
Configurator,
|
||||
GetConfigurationRepositoryPort,
|
||||
} from '@mobicoop/configuration-module';
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||
import { MatchingRepositoryPort } from '@modules/ad/core/application/ports/matching.repository.port';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import {
|
||||
MatchQueryHandler,
|
||||
@@ -43,6 +42,7 @@ import {
|
||||
PAGINATION_CONFIG_PER_PAGE,
|
||||
} from '@modules/ad/match.constants';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { simpleMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -258,83 +258,83 @@ const mockConfigurationRepository: GetConfigurationRepositoryPort = {
|
||||
get: jest.fn(),
|
||||
mget: jest.fn().mockImplementation(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(domain: ConfigurationDomain, configs: ConfigurationDomainGet[]) => {
|
||||
(domain: Domain, keyTypes: KeyType[]) => {
|
||||
switch (domain) {
|
||||
case ConfigurationDomain.CARPOOL:
|
||||
return new Configurator(ConfigurationDomain.CARPOOL, [
|
||||
case Domain.CARPOOL:
|
||||
return new Configurator(Domain.CARPOOL, [
|
||||
{
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
domain: Domain.CARPOOL,
|
||||
key: CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN,
|
||||
value: 900,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
domain: Domain.CARPOOL,
|
||||
key: CARPOOL_CONFIG_ROLE,
|
||||
value: 'passenger',
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
domain: Domain.CARPOOL,
|
||||
key: CARPOOL_CONFIG_SEATS_PROPOSED,
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
domain: Domain.CARPOOL,
|
||||
key: CARPOOL_CONFIG_SEATS_REQUESTED,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.CARPOOL,
|
||||
domain: Domain.CARPOOL,
|
||||
key: CARPOOL_CONFIG_STRICT_FREQUENCY,
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
case ConfigurationDomain.MATCH:
|
||||
return new Configurator(ConfigurationDomain.MATCH, [
|
||||
case Domain.MATCH:
|
||||
return new Configurator(Domain.MATCH, [
|
||||
{
|
||||
domain: ConfigurationDomain.MATCH,
|
||||
domain: Domain.MATCH,
|
||||
key: MATCH_CONFIG_ALGORITHM,
|
||||
value: 'PASSENGER_ORIENTED',
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.MATCH,
|
||||
domain: Domain.MATCH,
|
||||
key: MATCH_CONFIG_REMOTENESS,
|
||||
value: 15000,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.MATCH,
|
||||
domain: Domain.MATCH,
|
||||
key: MATCH_CONFIG_USE_PROPORTION,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.MATCH,
|
||||
domain: Domain.MATCH,
|
||||
key: MATCH_CONFIG_PROPORTION,
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.MATCH,
|
||||
domain: Domain.MATCH,
|
||||
key: MATCH_CONFIG_USE_AZIMUTH,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.MATCH,
|
||||
domain: Domain.MATCH,
|
||||
key: MATCH_CONFIG_AZIMUTH_MARGIN,
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.MATCH,
|
||||
domain: Domain.MATCH,
|
||||
key: MATCH_CONFIG_MAX_DETOUR_DISTANCE_RATIO,
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
domain: ConfigurationDomain.MATCH,
|
||||
domain: Domain.MATCH,
|
||||
key: MATCH_CONFIG_MAX_DETOUR_DURATION_RATIO,
|
||||
value: 0.3,
|
||||
},
|
||||
]);
|
||||
case ConfigurationDomain.PAGINATION:
|
||||
return new Configurator(ConfigurationDomain.PAGINATION, [
|
||||
case Domain.PAGINATION:
|
||||
return new Configurator(Domain.PAGINATION, [
|
||||
{
|
||||
domain: ConfigurationDomain.PAGINATION,
|
||||
domain: Domain.PAGINATION,
|
||||
key: PAGINATION_CONFIG_PER_PAGE,
|
||||
value: 10,
|
||||
},
|
||||
@@ -351,17 +351,7 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
||||
time: jest.fn(),
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest.fn().mockImplementation(() => ({
|
||||
distance: 350101,
|
||||
duration: 14422,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
})),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
const mockRouteProvider = simpleMockGeorouter;
|
||||
|
||||
describe('Match Query Handler', () => {
|
||||
let matchQueryHandler: MatchQueryHandler;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { GeorouterPort } from '@modules/ad/core/application/ports/georouter.port';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { simpleMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -57,17 +58,10 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
||||
time: jest.fn().mockImplementation(() => '23:05'),
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest
|
||||
const mockRouteProvider: GeorouterPort = {
|
||||
getRoute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({
|
||||
distance: 350101,
|
||||
duration: 14422,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
}))
|
||||
.mockImplementationOnce(simpleMockGeorouter.getRoute)
|
||||
.mockImplementationOnce(() => ({
|
||||
distance: 340102,
|
||||
duration: 13423,
|
||||
@@ -76,22 +70,8 @@ const mockRouteProvider: RouteProviderPort = {
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
}))
|
||||
.mockImplementationOnce(() => ({
|
||||
distance: 350101,
|
||||
duration: 14422,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
}))
|
||||
.mockImplementationOnce(() => ({
|
||||
distance: 350101,
|
||||
duration: 14422,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
}))
|
||||
.mockImplementationOnce(simpleMockGeorouter.getRoute)
|
||||
.mockImplementationOnce(simpleMockGeorouter.getRoute)
|
||||
.mockImplementationOnce(() => ({
|
||||
distance: 340102,
|
||||
duration: 13423,
|
||||
@@ -103,7 +83,6 @@ const mockRouteProvider: RouteProviderPort = {
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Match Query', () => {
|
||||
|
||||
@@ -42,11 +42,10 @@ const matchQuery = new MatchQuery(
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn().mockImplementation(() => ({
|
||||
getRoute: jest.fn().mockImplementation(() => ({
|
||||
duration: 6500,
|
||||
distance: 89745,
|
||||
})),
|
||||
getDetailed: jest.fn(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -54,6 +53,8 @@ const mockMatcherRepository: AdRepositoryPort = {
|
||||
insertExtra: jest.fn(),
|
||||
findOneById: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findAllByIds: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
update: 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 { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
|
||||
strict: false,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
},
|
||||
bareMockGeorouter,
|
||||
);
|
||||
|
||||
const candidates: CandidateEntity[] = [
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
|
||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -40,10 +41,7 @@ const matchQuery = new MatchQuery(
|
||||
strict: false,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
},
|
||||
bareMockGeorouter,
|
||||
);
|
||||
|
||||
const candidate: CandidateEntity = CandidateEntity.create({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.type
|
||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -47,10 +48,7 @@ const matchQuery = new MatchQuery(
|
||||
strict: false,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
},
|
||||
bareMockGeorouter,
|
||||
);
|
||||
matchQuery.driverRoute = {
|
||||
distance: 150120,
|
||||
@@ -99,6 +97,8 @@ const mockMatcherRepository: AdRepositoryPort = {
|
||||
insertExtra: jest.fn(),
|
||||
findOneById: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findAllByIds: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
update: 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 {
|
||||
RouteCompleter,
|
||||
RouteCompleterType,
|
||||
@@ -9,6 +13,8 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { Target } from '@modules/ad/core/domain/candidate.types';
|
||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
|
||||
import { Step } from '@modules/geography/core/domain/route.types';
|
||||
import { simpleMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
@@ -46,23 +52,16 @@ const matchQuery = new MatchQuery(
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn().mockImplementation(() => ({
|
||||
distance: 350101,
|
||||
duration: 14422,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
})),
|
||||
getDetailed: jest.fn().mockImplementation(() => ({
|
||||
distance: 350101,
|
||||
duration: 14422,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [],
|
||||
steps: [jest.fn(), jest.fn(), jest.fn(), jest.fn()],
|
||||
})),
|
||||
getRoute: jest
|
||||
.fn()
|
||||
.mockImplementation(async (req: RouteRequest): Promise<RouteResponse> => {
|
||||
const response = await simpleMockGeorouter.getRoute(req);
|
||||
if (req.detailsSettings?.steps) {
|
||||
const step: Step = { lon: 0, lat: 0, duration: 0 };
|
||||
response.steps = [step, step, step, step];
|
||||
}
|
||||
return response;
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
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,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||
@@ -12,6 +11,7 @@ import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
@@ -73,11 +73,6 @@ const mockDirectionEncoder: DirectionEncoderPort = {
|
||||
]),
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
const mockPrismaService = {
|
||||
$queryRawUnsafe: jest
|
||||
.fn()
|
||||
@@ -239,7 +234,7 @@ describe('Ad repository', () => {
|
||||
},
|
||||
{
|
||||
provide: AD_ROUTE_PROVIDER,
|
||||
useValue: mockRouteProvider,
|
||||
useValue: bareMockGeorouter,
|
||||
},
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
|
||||
@@ -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,5 @@
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
@@ -17,6 +16,7 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: WaypointDto = {
|
||||
position: 0,
|
||||
@@ -183,11 +183,6 @@ const mockQueryBus = {
|
||||
}),
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMatchMapper = {
|
||||
toResponse: jest.fn().mockImplementation(() => ({
|
||||
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
||||
@@ -195,8 +190,15 @@ const mockMatchMapper = {
|
||||
frequency: 'RECURRENT',
|
||||
distance: 356041,
|
||||
duration: 12647,
|
||||
initialDistance: 349251,
|
||||
initialDuration: 12103,
|
||||
distanceDetour: 6790,
|
||||
durationDetour: 544,
|
||||
distanceDetourPercentage: 4.1,
|
||||
durationDetourPercentage: 3.8,
|
||||
journeys: [
|
||||
{
|
||||
day: 5,
|
||||
firstDate: '2023-09-01',
|
||||
lastDate: '2024-08-30',
|
||||
journeyItems: [
|
||||
@@ -205,16 +207,11 @@ const mockMatchMapper = {
|
||||
lon: 6.17651,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
time: '07:00',
|
||||
actorTimes: [
|
||||
{
|
||||
role: 'DRIVER',
|
||||
target: 'START',
|
||||
firstDatetime: '2023-09-01 07:00',
|
||||
firstMinDatetime: '2023-09-01 06:45',
|
||||
firstMaxDatetime: '2023-09-01 07:15',
|
||||
lastDatetime: '2024-08-30 07:00',
|
||||
lastMinDatetime: '2024-08-30 06:45',
|
||||
lastMaxDatetime: '2024-08-30 07:15',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -223,26 +220,15 @@ const mockMatchMapper = {
|
||||
lon: 6.67487,
|
||||
duration: 2100,
|
||||
distance: 56878,
|
||||
time: '07:35',
|
||||
actorTimes: [
|
||||
{
|
||||
role: 'DRIVER',
|
||||
target: 'NEUTRAL',
|
||||
firstDatetime: '2023-09-01 07:35',
|
||||
firstMinDatetime: '2023-09-01 07:20',
|
||||
firstMaxDatetime: '2023-09-01 07:50',
|
||||
lastDatetime: '2024-08-30 07:35',
|
||||
lastMinDatetime: '2024-08-30 07:20',
|
||||
lastMaxDatetime: '2024-08-30 07:50',
|
||||
},
|
||||
{
|
||||
role: 'PASSENGER',
|
||||
target: 'START',
|
||||
firstDatetime: '2023-09-01 07:32',
|
||||
firstMinDatetime: '2023-09-01 07:17',
|
||||
firstMaxDatetime: '2023-09-01 07:47',
|
||||
lastDatetime: '2024-08-30 07:32',
|
||||
lastMinDatetime: '2024-08-30 07:17',
|
||||
lastMaxDatetime: '2024-08-30 07:47',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -251,26 +237,15 @@ const mockMatchMapper = {
|
||||
lon: 6.9427,
|
||||
duration: 3840,
|
||||
distance: 76491,
|
||||
time: '08:04',
|
||||
actorTimes: [
|
||||
{
|
||||
role: 'DRIVER',
|
||||
target: 'NEUTRAL',
|
||||
firstDatetime: '2023-09-01 08:04',
|
||||
firstMinDatetime: '2023-09-01 07:51',
|
||||
firstMaxDatetime: '2023-09-01 08:19',
|
||||
lastDatetime: '2024-08-30 08:04',
|
||||
lastMinDatetime: '2024-08-30 07:51',
|
||||
lastMaxDatetime: '2024-08-30 08:19',
|
||||
},
|
||||
{
|
||||
role: 'PASSENGER',
|
||||
target: 'FINISH',
|
||||
firstDatetime: '2023-09-01 08:01',
|
||||
firstMinDatetime: '2023-09-01 07:46',
|
||||
firstMaxDatetime: '2023-09-01 08:16',
|
||||
lastDatetime: '2024-08-30 08:01',
|
||||
lastMinDatetime: '2024-08-30 07:46',
|
||||
lastMaxDatetime: '2024-08-30 08:16',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -279,16 +254,11 @@ const mockMatchMapper = {
|
||||
lon: 7.02154,
|
||||
duration: 4980,
|
||||
distance: 96475,
|
||||
time: '08:23',
|
||||
actorTimes: [
|
||||
{
|
||||
role: 'DRIVER',
|
||||
target: 'FINISH',
|
||||
firstDatetime: '2023-09-01 08:23',
|
||||
firstMinDatetime: '2023-09-01 08:08',
|
||||
firstMaxDatetime: '2023-09-01 08:38',
|
||||
lastDatetime: '2024-08-30 08:23',
|
||||
lastMinDatetime: '2024-08-30 08:08',
|
||||
lastMaxDatetime: '2024-08-30 08:38',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -311,7 +281,7 @@ describe('Match Grpc Controller', () => {
|
||||
},
|
||||
{
|
||||
provide: AD_ROUTE_PROVIDER,
|
||||
useValue: mockRouteProvider,
|
||||
useValue: bareMockGeorouter,
|
||||
},
|
||||
{
|
||||
provide: MatchMapper,
|
||||
|
||||
@@ -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
|
||||
export type Route = {
|
||||
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 GEOROUTER = Symbol('GEOROUTER');
|
||||
export const GEODESIC = Symbol('GEODESIC');
|
||||
export const GEOGRAPHY_CONFIGURATION_REPOSITORY = Symbol(
|
||||
'GEOGRAPHY_CONFIGURATION_REPOSITORY',
|
||||
);
|
||||
|
||||
@@ -2,24 +2,12 @@ import { Module, Provider } from '@nestjs/common';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import {
|
||||
DIRECTION_ENCODER,
|
||||
GEODESIC,
|
||||
GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
||||
GEOROUTER,
|
||||
} from './geography.di-tokens';
|
||||
import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder';
|
||||
import { GetBasicRouteController } from './interface/controllers/get-basic-route.controller';
|
||||
import { RouteMapper } from './route.mapper';
|
||||
import { Geodesic } from './infrastructure/geodesic';
|
||||
import { GraphhopperGeorouter } from './infrastructure/graphhopper-georouter';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { GetRouteQueryHandler } from './core/application/queries/get-route/get-route.query-handler';
|
||||
import { GetDetailedRouteController } from './interface/controllers/get-detailed-route.controller';
|
||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||
|
||||
const queryHandlers: Provider[] = [GetRouteQueryHandler];
|
||||
|
||||
const mappers: Provider[] = [RouteMapper];
|
||||
|
||||
const adapters: Provider[] = [
|
||||
{
|
||||
provide: GEOGRAPHY_CONFIGURATION_REPOSITORY,
|
||||
@@ -29,26 +17,11 @@ const adapters: Provider[] = [
|
||||
provide: DIRECTION_ENCODER,
|
||||
useClass: PostgresDirectionEncoder,
|
||||
},
|
||||
{
|
||||
provide: GEOROUTER,
|
||||
useClass: GraphhopperGeorouter,
|
||||
},
|
||||
{
|
||||
provide: GEODESIC,
|
||||
useClass: Geodesic,
|
||||
},
|
||||
GetBasicRouteController,
|
||||
GetDetailedRouteController,
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [CqrsModule, HttpModule],
|
||||
providers: [...queryHandlers, ...mappers, ...adapters],
|
||||
exports: [
|
||||
RouteMapper,
|
||||
DIRECTION_ENCODER,
|
||||
GetBasicRouteController,
|
||||
GetDetailedRouteController,
|
||||
],
|
||||
providers: [...adapters],
|
||||
exports: [DIRECTION_ENCODER],
|
||||
})
|
||||
export class GeographyModule {}
|
||||
|
||||
@@ -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_QUEUE,
|
||||
AD_CREATED_ROUTING_KEY,
|
||||
AD_DELETED_MESSAGE_HANDLER,
|
||||
AD_DELETED_QUEUE,
|
||||
AD_DELETED_ROUTING_KEY,
|
||||
AD_UPDATED_MESSAGE_HANDLER,
|
||||
AD_UPDATED_QUEUE,
|
||||
AD_UPDATED_ROUTING_KEY,
|
||||
SERVICE_NAME,
|
||||
} from '@src/app.constants';
|
||||
|
||||
@@ -39,14 +33,6 @@ const imports = [
|
||||
routingKey: AD_CREATED_ROUTING_KEY,
|
||||
queue: AD_CREATED_QUEUE,
|
||||
},
|
||||
[AD_UPDATED_MESSAGE_HANDLER]: {
|
||||
routingKey: AD_UPDATED_ROUTING_KEY,
|
||||
queue: AD_UPDATED_QUEUE,
|
||||
},
|
||||
[AD_DELETED_MESSAGE_HANDLER]: {
|
||||
routingKey: AD_DELETED_ROUTING_KEY,
|
||||
queue: AD_DELETED_QUEUE,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"paths": {
|
||||
"@libs/*": ["src/libs/*"],
|
||||
"@modules/*": ["src/modules/*"],
|
||||
"@src/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
"@src/*": ["src/*"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user